diff --git a/.github/workflows/bazel_linux.yml b/.github/workflows/bazel_linux.yml index 3487f21d6d..9e8f27c53a 100644 --- a/.github/workflows/bazel_linux.yml +++ b/.github/workflows/bazel_linux.yml @@ -30,6 +30,16 @@ jobs: - name: Check Bazel run: bazel version - name: Build - run: bazel build -c opt --cxxopt=-std=c++17 --subcommands=true //ortools/... //examples/... + run: > + bazel build + -c opt + --action_env=BAZEL_CXXOPTS="-std=c++17" + --subcommands=true + ortools/... examples/... - name: Test - run: bazel test -c opt --cxxopt=-std=c++17 --test_output=errors //ortools/... //examples/... + run: > + bazel test + -c opt + --action_env=BAZEL_CXXOPTS="-std=c++17" + --test_output=errors + ortools/... examples/... diff --git a/WORKSPACE b/WORKSPACE index 181f710ddd..ec81b824ce 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -233,7 +233,7 @@ JUNIT_JUPITER_VERSION = "5.9.2" load("@rules_jvm_external//:defs.bzl", "maven_install") maven_install( artifacts = [ - "net.java.dev.jna:jna:aar:5.12.1", + "net.java.dev.jna:jna:aar:5.13.0", "com.google.truth:truth:0.32", "org.junit.platform:junit-platform-launcher:%s" % JUNIT_PLATFORM_VERSION, "org.junit.platform:junit-platform-reporting:%s" % JUNIT_PLATFORM_VERSION, diff --git a/ortools/algorithms/java/CMakeLists.txt b/ortools/algorithms/java/CMakeLists.txt index 0131dd05eb..368b8c7f10 100644 --- a/ortools/algorithms/java/CMakeLists.txt +++ b/ortools/algorithms/java/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE knapsack_solver.i PROPERTY CPLUSPLUS ON) set_property(SOURCE knapsack_solver.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE knapsack_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE knapsack_solver.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE knapsack_solver.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.algorithms) swig_add_library(jnialgorithms diff --git a/ortools/algorithms/python/CMakeLists.txt b/ortools/algorithms/python/CMakeLists.txt index 7230bf9221..bd10034b43 100644 --- a/ortools/algorithms/python/CMakeLists.txt +++ b/ortools/algorithms/python/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE knapsack_solver.i PROPERTY CPLUSPLUS ON) set_property(SOURCE knapsack_solver.i PROPERTY SWIG_MODULE_NAME pywrapknapsack_solver) -set_property(SOURCE knapsack_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE knapsack_solver.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) swig_add_library(pywrapknapsack_solver TYPE MODULE LANGUAGE python diff --git a/ortools/constraint_solver/csharp/CMakeLists.txt b/ortools/constraint_solver/csharp/CMakeLists.txt index 3301317b6c..b270bf1927 100644 --- a/ortools/constraint_solver/csharp/CMakeLists.txt +++ b/ortools/constraint_solver/csharp/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME operations_research_constraint_solver) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS -namespace ${DOTNET_PROJECT}.ConstraintSolver -dllimport google-ortools-native) diff --git a/ortools/constraint_solver/docs/ROUTING.md b/ortools/constraint_solver/docs/ROUTING.md index 033c7ec9ce..f692090934 100644 --- a/ortools/constraint_solver/docs/ROUTING.md +++ b/ortools/constraint_solver/docs/ROUTING.md @@ -21,6 +21,7 @@ and .Net. Each language have different requirements for the code samples. ```cpp #include #include +#include #include #include "ortools/constraint_solver/routing.h" diff --git a/ortools/constraint_solver/java/CMakeLists.txt b/ortools/constraint_solver/java/CMakeLists.txt index 19223e8381..cabd5dfcf7 100644 --- a/ortools/constraint_solver/java/CMakeLists.txt +++ b/ortools/constraint_solver/java/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.constraintsolver) swig_add_library(jniconstraint_solver diff --git a/ortools/constraint_solver/python/CMakeLists.txt b/ortools/constraint_solver/python/CMakeLists.txt index 42aff8fa46..4606db3d41 100644 --- a/ortools/constraint_solver/python/CMakeLists.txt +++ b/ortools/constraint_solver/python/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME pywrapcp) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS -nofastunpack) swig_add_library(pywrapcp TYPE MODULE diff --git a/ortools/constraint_solver/samples/cvrptw_break.py b/ortools/constraint_solver/samples/cvrptw_break.py index 5f76cd3e09..0429eba09c 100755 --- a/ortools/constraint_solver/samples/cvrptw_break.py +++ b/ortools/constraint_solver/samples/cvrptw_break.py @@ -202,18 +202,17 @@ def add_time_window_constraints(routing, manager, data, time_evaluator_index): # [START solution_printer] def print_solution(data, manager, routing, assignment): # pylint:disable=too-many-locals """Prints assignment on console.""" - print('Objective: {}'.format(assignment.ObjectiveValue())) + print(f'Objective: {assignment.ObjectiveValue()}') print('Breaks:') intervals = assignment.IntervalVarContainer() for i in range(intervals.Size()): brk = intervals.Element(i) if brk.PerformedValue() == 1: - print('{}: Start({}) Duration({})'.format(brk.Var().Name(), - brk.StartValue(), - brk.DurationValue())) + print(f'{brk.Var().Name()}:' + f' Start({brk.StartValue()}) Duration({brk.DurationValue()})') else: - print('{}: Unperformed'.format(brk.Var().Name())) + print(f'{brk.Var().Name()}: Unperformed') total_distance = 0 total_load = 0 @@ -222,37 +221,40 @@ def print_solution(data, manager, routing, assignment): # pylint:disable=too-ma time_dimension = routing.GetDimensionOrDie('Time') for vehicle_id in range(data['num_vehicles']): index = routing.Start(vehicle_id) - plan_output = 'Route for vehicle {}:\n'.format(vehicle_id) + plan_output = f'Route for vehicle {vehicle_id}:\n' distance = 0 while not routing.IsEnd(index): load_var = capacity_dimension.CumulVar(index) time_var = time_dimension.CumulVar(index) slack_var = time_dimension.SlackVar(index) - plan_output += ' {0} Load({1}) Time({2},{3}) Slack({4},{5}) ->'.format( - manager.IndexToNode(index), assignment.Value(load_var), - assignment.Min(time_var), assignment.Max(time_var), - assignment.Min(slack_var), assignment.Max(slack_var)) + node = manager.IndexToNode(index) + plan_output += ( + f' {node}' + f' Load({assignment.Value(load_var)})' + f' Time({assignment.Min(time_var)}, {assignment.Max(time_var)})' + f' Slack({assignment.Min(slack_var)}, {assignment.Max(slack_var)})' + ' ->') previous_index = index index = assignment.Value(routing.NextVar(index)) distance += routing.GetArcCostForVehicle(previous_index, index, vehicle_id) load_var = capacity_dimension.CumulVar(index) time_var = time_dimension.CumulVar(index) - plan_output += ' {0} Load({1}) Time({2},{3})\n'.format( - manager.IndexToNode(index), assignment.Value(load_var), - assignment.Min(time_var), assignment.Max(time_var)) - plan_output += 'Distance of the route: {0}m\n'.format(distance) - plan_output += 'Load of the route: {}\n'.format( - assignment.Value(load_var)) - plan_output += 'Time of the route: {}\n'.format( - assignment.Value(time_var)) + node = manager.IndexToNode(index) + plan_output += ( + f' {node}' + f' Load({assignment.Value(load_var)})' + f' Time({assignment.Min(time_var)}, {assignment.Max(time_var)})\n') + plan_output += f'Distance of the route: {distance}m\n' + plan_output += f'Load of the route: {assignment.Value(load_var)}\n' + plan_output += f'Time of the route: {assignment.Value(time_var)}\n' print(plan_output) total_distance += distance total_load += assignment.Value(load_var) total_time += assignment.Value(time_var) - print('Total Distance of all routes: {0}m'.format(total_distance)) - print('Total Load of all routes: {}'.format(total_load)) - print('Total Time of all routes: {0}min'.format(total_time)) + print(f'Total Distance of all routes: {total_distance}m') + print(f'Total Load of all routes: {total_load}') + print(f'Total Time of all routes: {total_time}min') # [END solution_printer] diff --git a/ortools/constraint_solver/samples/minimal_jobshop_cp.cc b/ortools/constraint_solver/samples/minimal_jobshop_cp.cc index f0170137ef..8ab2d86755 100644 --- a/ortools/constraint_solver/samples/minimal_jobshop_cp.cc +++ b/ortools/constraint_solver/samples/minimal_jobshop_cp.cc @@ -198,7 +198,7 @@ void SolveJobShopExample() { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); - absl::SetFlag(&FLAGS_logtostderr, true); + absl::SetFlag(&FLAGS_stderrthreshold, 0); operations_research::SolveJobShopExample(); return EXIT_SUCCESS; } diff --git a/ortools/constraint_solver/samples/nurses_cp.cc b/ortools/constraint_solver/samples/nurses_cp.cc index 17d712638d..df808e1293 100644 --- a/ortools/constraint_solver/samples/nurses_cp.cc +++ b/ortools/constraint_solver/samples/nurses_cp.cc @@ -205,7 +205,7 @@ void SolveNursesExample() { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); - absl::SetFlag(&FLAGS_logtostderr, true); + absl::SetFlag(&FLAGS_stderrthreshold, 0); operations_research::SolveNursesExample(); return EXIT_SUCCESS; } diff --git a/ortools/constraint_solver/samples/rabbits_and_pheasants_cp.cc b/ortools/constraint_solver/samples/rabbits_and_pheasants_cp.cc index 810dac5fc4..7707d34fe5 100644 --- a/ortools/constraint_solver/samples/rabbits_and_pheasants_cp.cc +++ b/ortools/constraint_solver/samples/rabbits_and_pheasants_cp.cc @@ -60,7 +60,7 @@ void RunConstraintProgrammingExample() { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); - absl::SetFlag(&FLAGS_logtostderr, true); + absl::SetFlag(&FLAGS_stderrthreshold, 0); operations_research::RunConstraintProgrammingExample(); return EXIT_SUCCESS; } diff --git a/ortools/constraint_solver/samples/simple_cp_program.py b/ortools/constraint_solver/samples/simple_cp_program.py index c3d802a306..ac402bdfbd 100755 --- a/ortools/constraint_solver/samples/simple_cp_program.py +++ b/ortools/constraint_solver/samples/simple_cp_program.py @@ -53,18 +53,18 @@ def main(): solver.NewSearch(decision_builder) while solver.NextSolution(): count += 1 - solution = 'Solution {}:\n'.format(count) + solution = f'Solution {count}:\n' for var in [x, y, z]: - solution += ' {} = {}'.format(var.Name(), var.Value()) + solution += f' {var.Name()} = {var.Value()}' print(solution) solver.EndSearch() - print('Number of solutions found: ', count) + print(f'Number of solutions found: {count}') # [END print_solution] # [START advanced] print('Advanced usage:') - print('Problem solved in ', solver.WallTime(), 'ms') - print('Memory usage: ', pywrapcp.Solver.MemoryUsage(), 'bytes') + print(f'Problem solved in {solver.WallTime()}ms') + print(f'Memory usage: {pywrapcp.Solver.MemoryUsage()}bytes') # [END advanced] diff --git a/ortools/constraint_solver/samples/simple_ls_program.cc b/ortools/constraint_solver/samples/simple_ls_program.cc index cea8e7c7c9..893d0e55d6 100644 --- a/ortools/constraint_solver/samples/simple_ls_program.cc +++ b/ortools/constraint_solver/samples/simple_ls_program.cc @@ -29,13 +29,12 @@ class OneVarLns : public BaseLns { explicit OneVarLns(const std::vector& vars) : BaseLns(vars), index_(0) {} - ~OneVarLns() override {} + ~OneVarLns() override = default; void InitFragments() override { index_ = 0; } bool NextFragment() override { - const int size = Size(); - if (index_ < size) { + if (index_ < Size()) { AppendToFragment(index_); ++index_; return true; @@ -55,7 +54,7 @@ class MoveOneVar : public IntVarLocalSearchOperator { variable_index_(0), move_up_(false) {} - ~MoveOneVar() override {} + ~MoveOneVar() override = default; protected: // Make a neighbor assigning one variable to its target value. @@ -88,7 +87,7 @@ class SumFilter : public IntVarLocalSearchFilter { explicit SumFilter(const std::vector& vars) : IntVarLocalSearchFilter(vars), sum_(0) {} - ~SumFilter() override {} + ~SumFilter() override = default; void OnSynchronize(const Assignment* delta) override { sum_ = 0; @@ -200,7 +199,7 @@ void SolveProblem(SolveType solve_type) { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); - absl::SetFlag(&FLAGS_logtostderr, true); + absl::SetFlag(&FLAGS_stderrthreshold, 0); operations_research::SolveProblem(operations_research::LNS); operations_research::SolveProblem(operations_research::LS); operations_research::SolveProblem(operations_research::LS_WITH_FILTER); diff --git a/ortools/constraint_solver/samples/simple_routing_program.cc b/ortools/constraint_solver/samples/simple_routing_program.cc index 0d582d1bc4..29a492003a 100644 --- a/ortools/constraint_solver/samples/simple_routing_program.cc +++ b/ortools/constraint_solver/samples/simple_routing_program.cc @@ -15,6 +15,7 @@ // [START import] #include #include +#include #include #include "ortools/constraint_solver/routing.h" @@ -73,11 +74,11 @@ void SimpleRoutingProgram() { // Inspect solution. int64_t index = routing.Start(0); LOG(INFO) << "Route for Vehicle 0:"; - int64_t route_distance{0}; + int64_t route_distance = 0; std::ostringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution->Value(routing.NextVar(index)); route_distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{0}); diff --git a/ortools/constraint_solver/samples/simple_routing_program.py b/ortools/constraint_solver/samples/simple_routing_program.py index a8d98c973c..a3259dec11 100755 --- a/ortools/constraint_solver/samples/simple_routing_program.py +++ b/ortools/constraint_solver/samples/simple_routing_program.py @@ -72,17 +72,17 @@ def main(): # Print solution on console. # [START print_solution] - print('Objective: {}'.format(assignment.ObjectiveValue())) + print(f'Objective: {assignment.ObjectiveValue()}') index = routing.Start(0) plan_output = 'Route for vehicle 0:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += '{} -> '.format(manager.IndexToNode(index)) + plan_output += f'{manager.IndexToNode(index)} -> ' previous_index = index index = assignment.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += '{}\n'.format(manager.IndexToNode(index)) - plan_output += 'Distance of the route: {}m\n'.format(route_distance) + plan_output += f'{manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' print(plan_output) # [END print_solution] diff --git a/ortools/constraint_solver/samples/tsp.cc b/ortools/constraint_solver/samples/tsp.cc index e2df3990dc..f9e5a7d4b0 100644 --- a/ortools/constraint_solver/samples/tsp.cc +++ b/ortools/constraint_solver/samples/tsp.cc @@ -15,6 +15,7 @@ // [START import] #include #include +#include #include #include @@ -53,12 +54,12 @@ std::vector> GenerateManhattanDistanceMatrix( std::vector> distances = std::vector>( locations.size(), std::vector(locations.size(), int64_t{0})); - for (int fromNode = 0; fromNode < locations.size(); fromNode++) { - for (int toNode = 0; toNode < locations.size(); toNode++) { - if (fromNode != toNode) - distances[fromNode][toNode] = - int64_t{std::abs(locations[toNode][0] - locations[fromNode][0]) + - std::abs(locations[toNode][1] - locations[fromNode][1])}; + for (int from_node = 0; from_node < locations.size(); from_node++) { + for (int to_node = 0; to_node < locations.size(); to_node++) { + if (from_node != to_node) + distances[from_node][to_node] = + int64_t{std::abs(locations[to_node][0] - locations[from_node][0]) + + std::abs(locations[to_node][1] - locations[from_node][1])}; } } return distances; @@ -78,9 +79,9 @@ void PrintSolution(const RoutingIndexManager& manager, LOG(INFO) << "Route for Vehicle 0:"; int64_t distance{0}; std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution.Value(routing.NextVar(index)); distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{0}); } @@ -113,8 +114,8 @@ void Tsp() { // [START transit_callback] const auto distance_matrix = GenerateManhattanDistanceMatrix(data.locations); const int transit_callback_index = routing.RegisterTransitCallback( - [&distance_matrix, &manager](int64_t from_index, - int64_t to_index) -> int64_t { + [&distance_matrix, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/tsp.py b/ortools/constraint_solver/samples/tsp.py index 20e4576a6f..ee2e14c414 100755 --- a/ortools/constraint_solver/samples/tsp.py +++ b/ortools/constraint_solver/samples/tsp.py @@ -78,17 +78,17 @@ def create_distance_callback(data, manager): # [START solution_printer] def print_solution(manager, routing, assignment): """Prints assignment on console.""" - print('Objective: {}'.format(assignment.ObjectiveValue())) + print(f'Objective: {assignment.ObjectiveValue()}') index = routing.Start(0) plan_output = 'Route for vehicle 0:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += ' {} ->'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)} ->' previous_index = index index = assignment.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += ' {}\n'.format(manager.IndexToNode(index)) - plan_output += 'Distance of the route: {}m\n'.format(route_distance) + plan_output += f' {manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' print(plan_output) # [END solution_printer] diff --git a/ortools/constraint_solver/samples/tsp_circuit_board.cc b/ortools/constraint_solver/samples/tsp_circuit_board.cc index df4fcf6d51..67f1c623d6 100644 --- a/ortools/constraint_solver/samples/tsp_circuit_board.cc +++ b/ortools/constraint_solver/samples/tsp_circuit_board.cc @@ -88,12 +88,12 @@ std::vector> ComputeEuclideanDistanceMatrix( std::vector> distances = std::vector>( locations.size(), std::vector(locations.size(), int64_t{0})); - for (int fromNode = 0; fromNode < locations.size(); fromNode++) { - for (int toNode = 0; toNode < locations.size(); toNode++) { - if (fromNode != toNode) - distances[fromNode][toNode] = static_cast( - std::hypot((locations[toNode][0] - locations[fromNode][0]), - (locations[toNode][1] - locations[fromNode][1]))); + for (int from_node = 0; from_node < locations.size(); from_node++) { + for (int to_node = 0; to_node < locations.size(); to_node++) { + if (from_node != to_node) + distances[from_node][to_node] = static_cast( + std::hypot((locations[to_node][0] - locations[from_node][0]), + (locations[to_node][1] - locations[from_node][1]))); } } return distances; @@ -113,9 +113,9 @@ void PrintSolution(const RoutingIndexManager& manager, LOG(INFO) << "Route:"; int64_t distance{0}; std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution.Value(routing.NextVar(index)); distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{0}); } @@ -147,8 +147,8 @@ void Tsp() { // [START transit_callback] const auto distance_matrix = ComputeEuclideanDistanceMatrix(data.locations); const int transit_callback_index = routing.RegisterTransitCallback( - [&distance_matrix, &manager](int64_t from_index, - int64_t to_index) -> int64_t { + [&distance_matrix, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/tsp_circuit_board.py b/ortools/constraint_solver/samples/tsp_circuit_board.py index 9fc1f0ce88..d6079900da 100755 --- a/ortools/constraint_solver/samples/tsp_circuit_board.py +++ b/ortools/constraint_solver/samples/tsp_circuit_board.py @@ -103,18 +103,18 @@ def compute_euclidean_distance_matrix(locations): # [START solution_printer] def print_solution(manager, routing, solution): """Prints solution on console.""" - print('Objective: {}'.format(solution.ObjectiveValue())) + print(f'Objective: {solution.ObjectiveValue()}') index = routing.Start(0) plan_output = 'Route:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += ' {} ->'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)} ->' previous_index = index index = solution.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += ' {}\n'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)}\n' print(plan_output) - plan_output += 'Objective: {}m\n'.format(route_distance) + plan_output += f'Objective: {route_distance}m\n' # [END solution_printer] diff --git a/ortools/constraint_solver/samples/tsp_cities.cc b/ortools/constraint_solver/samples/tsp_cities.cc index d025288478..12311ec12b 100644 --- a/ortools/constraint_solver/samples/tsp_cities.cc +++ b/ortools/constraint_solver/samples/tsp_cities.cc @@ -60,9 +60,9 @@ void PrintSolution(const RoutingIndexManager& manager, LOG(INFO) << "Route:"; int64_t distance{0}; std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution.Value(routing.NextVar(index)); distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{0}); } @@ -93,7 +93,8 @@ void Tsp() { // [START transit_callback] const int transit_callback_index = routing.RegisterTransitCallback( - [&data, &manager](int64_t from_index, int64_t to_index) -> int64_t { + [&data, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/tsp_cities.py b/ortools/constraint_solver/samples/tsp_cities.py index 43e18b7924..4d5273a459 100755 --- a/ortools/constraint_solver/samples/tsp_cities.py +++ b/ortools/constraint_solver/samples/tsp_cities.py @@ -49,18 +49,18 @@ def create_data_model(): # [START solution_printer] def print_solution(manager, routing, solution): """Prints solution on console.""" - print('Objective: {} miles'.format(solution.ObjectiveValue())) + print(f'Objective: {solution.ObjectiveValue()} miles') index = routing.Start(0) plan_output = 'Route for vehicle 0:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += ' {} ->'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)} ->' previous_index = index index = solution.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += ' {}\n'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)}\n' print(plan_output) - plan_output += 'Route distance: {}miles\n'.format(route_distance) + plan_output += f'Route distance: {route_distance}miles\n' # [END solution_printer] diff --git a/ortools/constraint_solver/samples/tsp_cities_routes.cc b/ortools/constraint_solver/samples/tsp_cities_routes.cc index 05ad923309..ecbc6df7cd 100644 --- a/ortools/constraint_solver/samples/tsp_cities_routes.cc +++ b/ortools/constraint_solver/samples/tsp_cities_routes.cc @@ -92,7 +92,8 @@ void Tsp() { // Define cost of each arc. // [START arc_cost] const int transit_callback_index = routing.RegisterTransitCallback( - [&data, &manager](int64_t from_index, int64_t to_index) -> int64_t { + [&data, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/tsp_distance_matrix.cc b/ortools/constraint_solver/samples/tsp_distance_matrix.cc index 6465370c66..3044fcef9a 100644 --- a/ortools/constraint_solver/samples/tsp_distance_matrix.cc +++ b/ortools/constraint_solver/samples/tsp_distance_matrix.cc @@ -80,9 +80,9 @@ void PrintSolution(const RoutingIndexManager& manager, LOG(INFO) << "Route for Vehicle 0:"; int64_t distance{0}; std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution.Value(routing.NextVar(index)); distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{0}); } @@ -114,7 +114,8 @@ void Tsp() { // Create and register a transit callback. // [START transit_callback] const int transit_callback_index = routing.RegisterTransitCallback( - [&data, &manager](int64_t from_index, int64_t to_index) -> int64_t { + [&data, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/tsp_distance_matrix.py b/ortools/constraint_solver/samples/tsp_distance_matrix.py index 6452c39f29..e5876cc323 100755 --- a/ortools/constraint_solver/samples/tsp_distance_matrix.py +++ b/ortools/constraint_solver/samples/tsp_distance_matrix.py @@ -104,17 +104,17 @@ def create_data_model(): # [START solution_printer] def print_solution(manager, routing, solution): """Prints solution on console.""" - print('Objective: {}'.format(solution.ObjectiveValue())) + print(f'Objective: {solution.ObjectiveValue()}') index = routing.Start(0) plan_output = 'Route for vehicle 0:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += ' {} ->'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)} ->' previous_index = index index = solution.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += ' {}\n'.format(manager.IndexToNode(index)) - plan_output += 'Distance of the route: {}m\n'.format(route_distance) + plan_output += f' {manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' print(plan_output) # [END solution_printer] diff --git a/ortools/constraint_solver/samples/vrp.cc b/ortools/constraint_solver/samples/vrp.cc index f2c03e6bc3..aa8ad528d8 100644 --- a/ortools/constraint_solver/samples/vrp.cc +++ b/ortools/constraint_solver/samples/vrp.cc @@ -82,9 +82,9 @@ void PrintSolution(const DataModel& data, const RoutingIndexManager& manager, LOG(INFO) << "Route for Vehicle " << vehicle_id << ":"; int64_t distance{0}; std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { route << manager.IndexToNode(index).value() << " -> "; - int64_t previous_index = index; + const int64_t previous_index = index; index = solution.Value(routing.NextVar(index)); distance += routing.GetArcCostForVehicle(previous_index, index, int64_t{vehicle_id}); @@ -120,7 +120,8 @@ void Vrp() { // Create and register a transit callback. // [START transit_callback] const int transit_callback_index = routing.RegisterTransitCallback( - [&data, &manager](int64_t from_index, int64_t to_index) -> int64_t { + [&data, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. auto from_node = manager.IndexToNode(from_index).value(); auto to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/constraint_solver/samples/vrp.py b/ortools/constraint_solver/samples/vrp.py index 4d412b423f..04d0570f14 100755 --- a/ortools/constraint_solver/samples/vrp.py +++ b/ortools/constraint_solver/samples/vrp.py @@ -116,19 +116,19 @@ def print_solution(data, manager, routing, solution): total_distance = 0 for vehicle_id in range(data['num_vehicles']): index = routing.Start(vehicle_id) - plan_output = 'Route for vehicle {}:\n'.format(vehicle_id) + plan_output = f'Route for vehicle {vehicle_id}:\n' route_distance = 0 while not routing.IsEnd(index): - plan_output += ' {} ->'.format(manager.IndexToNode(index)) + plan_output += f' {manager.IndexToNode(index)} ->' previous_index = index index = solution.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle( previous_index, index, vehicle_id) - plan_output += ' {}\n'.format(manager.IndexToNode(index)) - plan_output += 'Distance of the route: {}m\n'.format(route_distance) + plan_output += f' {manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' print(plan_output) total_distance += route_distance - print('Total Distance of all routes: {}m'.format(total_distance)) + print(f'Total Distance of all routes: {total_distance}m') # [END solution_printer] diff --git a/ortools/constraint_solver/samples/vrp_breaks.cc b/ortools/constraint_solver/samples/vrp_breaks.cc index 8172119ffc..d8d286b9ec 100644 --- a/ortools/constraint_solver/samples/vrp_breaks.cc +++ b/ortools/constraint_solver/samples/vrp_breaks.cc @@ -89,7 +89,7 @@ void PrintSolution(const RoutingIndexManager& manager, LOG(INFO) << "Route for Vehicle " << vehicle_id << ":"; int64_t index = routing.Start(vehicle_id); std::stringstream route; - while (routing.IsEnd(index) == false) { + while (!routing.IsEnd(index)) { const IntVar* time_var = time_dimension.CumulVar(index); route << manager.IndexToNode(index).value() << " Time(" << solution.Value(time_var) << ") -> "; @@ -128,7 +128,8 @@ void VrpBreaks() { // Create and register a transit callback. // [START transit_callback] const int transit_callback_index = routing.RegisterTransitCallback( - [&data, &manager](int64_t from_index, int64_t to_index) -> int64_t { + [&data, &manager](const int64_t from_index, + const int64_t to_index) -> int64_t { // Convert from routing variable Index to distance matrix NodeIndex. int from_node = manager.IndexToNode(from_index).value(); int to_node = manager.IndexToNode(to_index).value(); diff --git a/ortools/graph/README.md b/ortools/graph/README.md index aa07036e5a..174837fd01 100644 --- a/ortools/graph/README.md +++ b/ortools/graph/README.md @@ -4,62 +4,65 @@ This directory contains data structures and algorithms for graph and network flow problems. It contains in particular: -* a compact and efficient graph representation - ([EbertGraph](https://dl.acm.org/doi/abs/10.1145/214762.214769)), +* a compact and efficient graph representation, + [`EbertGraph`](https://dl.acm.org/doi/abs/10.1145/214762.214769), which is used for most of the algorithms herein, unless specified otherwise. -* well-tuned algorithms (for example shortest paths and - [Hamiltonian paths](https://en.wikipedia.org/wiki/Hamiltonian_path).) -* hard-to-find algorithms (Hamiltonian paths, push-relabel flow algorithms.) -* other, more common algorithm, that are useful to use with EbertGraph. +* well-tuned algorithms (for example shortest, paths and + [Hamiltonian paths](https://en.wikipedia.org/wiki/Hamiltonian_path)). +* hard-to-find algorithms (Hamiltonian paths, push-relabel flow algorithms). +* other, more common algorithm, that are useful to use with `EbertGraph`. Graph representations: -* [ebert_graph.h](./ebert_graph.h): Entry point for a directed graph class. - -* [digraph.h](./digraph.h): Entry point for a directed graph class. +* [ebert_graph.h](./ebert_graph.h): entry point for a directed graph class. +* [digraph.h](./digraph.h): entry point for a directed graph class. To be deprecated by `ebert_graph.h`. Paths: -* [shortestpaths.h](./shortestpaths.h): Entry point for shortest path computations. +* [shortestpaths.h](./shortestpaths.h): entry point for shortest paths. Includes [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) and - [Bellman-Ford](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) algorithms. - -* [hamiltonian_path.h](./hamiltonian_path.h): Entry point for computing minimum + [Bellman-Ford](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) + algorithms. These implementations are being deprecated. +* [hamiltonian_path.h](./hamiltonian_path.h): entry point for computing minimum [Hamiltonian paths](https://en.wikipedia.org/wiki/Hamiltonian_path) and cycles on directed graphs with costs on arcs, using a dynamic-programming - algorithm (Does not need `ebert_graph.h` or `digraph.h`.) + algorithm. (It does not need `ebert_graph.h` or `digraph.h`.) Graph decompositions: -* [connected_components.h](./connected_components.h): Entry point for computing connected - components in an undirected graph. (Does not need `ebert_graph.h` or `digraph.h`.) +* [connected_components.h](./connected_components.h): entry point for computing + connected components in an undirected graph. (It does not need `ebert_graph.h` + or `digraph.h`.) -* [strongly_connected_components.h](./strongly_connected_components.h): Entry point for - computing the strongly connected components of a directed graph, based on an algorithm of Tarjan. +* [strongly_connected_components.h](./strongly_connected_components.h): entry + point for computing the strongly connected components of a directed graph, + based on Tarjan's algorithm. -* [cliques.h](./cliques.h): Entry point for computing maximum cliques and clique covers in a directed graph, - based on the Bron-Kerbosch algorithm. (Does not need `ebert_graph.h` or `digraph.h`.) +* [cliques.h](./cliques.h): entry point for computing maximum cliques and + clique covers in a directed graph, based on the Bron-Kerbosch algorithm. + (It does not need `ebert_graph.h` or `digraph.h`.) Flow algorithms: -* [linear_assignment.h](./linear_assignment.h): Entry point for solving linear sum assignment problems - (classical assignment problems where the total cost is the sum of the costs - of each arc used) on directed graphs with arc costs, based on a push-relabel - algorithm of Goldberg and Kennedy. +* [linear_assignment.h](./linear_assignment.h): entry point for solving linear + sum assignment problems (classical assignment problems where the total cost is + the sum of the costs of each arc used) on directed graphs with arc costs, + based on the Goldberg-Kennedy push-relabel algorithm. -* [max_flow.h](./max_flow.h): Entry point for computing maximum flows on directed graphs with - arc capacities, based on a push-relabel algorithm of Goldberg and Tarjan. +* [max_flow.h](./max_flow.h): entry point for computing maximum flows on + directed graphs with arc capacities, based on the Goldberg-Tarjan + push-relabel algorithm. -* [min_cost_flow.h](./min_cost_flow.h): Entry point for computing minimum-cost flows on directed - graphs with arc capacities, arc costs, and supplies/demands at nodes, based on - a push-relabel algorithm of Goldberg and Tarjan. +* [min_cost_flow.h](./min_cost_flow.h): entry point for computing minimum-cost + flows on directed graphs with arc capacities, arc costs, and supplies/demands + at nodes, based on the Goldberg-Tarjan push-relabel algorithm. ## Wrappers -* [python](python): the SWIG code that makes the wrapper available in Python, +* [python](python): the SWIG code that makes the wrapper available in Python and its unit tests. -* [java](java): the SWIG code that makes the wrapper available in Java, +* [java](java): the SWIG code that makes the wrapper available in Java and its unit tests. -* [csharp](csharp): the SWIG code that makes the wrapper available in C#, +* [csharp](csharp): the SWIG code that makes the wrapper available in C# and its unit tests. ## Samples diff --git a/ortools/graph/csharp/CMakeLists.txt b/ortools/graph/csharp/CMakeLists.txt index c405891135..0be8558743 100644 --- a/ortools/graph/csharp/CMakeLists.txt +++ b/ortools/graph/csharp/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE graph.i PROPERTY CPLUSPLUS ON) set_property(SOURCE graph.i PROPERTY SWIG_MODULE_NAME operations_research_graph) -set_property(SOURCE graph.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE graph.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE graph.i PROPERTY COMPILE_OPTIONS -namespace ${DOTNET_PROJECT}.Graph -dllimport google-ortools-native) diff --git a/ortools/graph/java/CMakeLists.txt b/ortools/graph/java/CMakeLists.txt index 73ea360230..73a019f33b 100644 --- a/ortools/graph/java/CMakeLists.txt +++ b/ortools/graph/java/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE graph.i PROPERTY CPLUSPLUS ON) set_property(SOURCE graph.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE graph.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE graph.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT) set_property(SOURCE graph.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.graph) swig_add_library(jnigraph diff --git a/ortools/graph/samples/code_samples.bzl b/ortools/graph/samples/code_samples.bzl index e9da788f2c..daa944e496 100644 --- a/ortools/graph/samples/code_samples.bzl +++ b/ortools/graph/samples/code_samples.bzl @@ -26,7 +26,6 @@ def code_sample_cc(name): "//ortools/graph:linear_assignment", "//ortools/graph:max_flow", "//ortools/graph:min_cost_flow", - "//ortools/graph:shortestpaths", ], ) @@ -42,7 +41,6 @@ def code_sample_cc(name): "//ortools/graph:linear_assignment", "//ortools/graph:max_flow", "//ortools/graph:min_cost_flow", - "//ortools/graph:shortestpaths", ], ) diff --git a/ortools/init/csharp/CMakeLists.txt b/ortools/init/csharp/CMakeLists.txt index d248085ec6..2b71324eb6 100644 --- a/ortools/init/csharp/CMakeLists.txt +++ b/ortools/init/csharp/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE init.i PROPERTY CPLUSPLUS ON) set_property(SOURCE init.i PROPERTY SWIG_MODULE_NAME operations_research_init) -set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE init.i PROPERTY COMPILE_OPTIONS -namespace ${DOTNET_PROJECT}.Init -dllimport google-ortools-native) diff --git a/ortools/init/java/CMakeLists.txt b/ortools/init/java/CMakeLists.txt index 44e29afb54..01e191c663 100644 --- a/ortools/init/java/CMakeLists.txt +++ b/ortools/init/java/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE init.i PROPERTY CPLUSPLUS ON) set_property(SOURCE init.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE init.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.init) swig_add_library(jniinit diff --git a/ortools/init/python/CMakeLists.txt b/ortools/init/python/CMakeLists.txt index bb12a80c83..4669ca0885 100644 --- a/ortools/init/python/CMakeLists.txt +++ b/ortools/init/python/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE init.i PROPERTY CPLUSPLUS ON) set_property(SOURCE init.i PROPERTY SWIG_MODULE_NAME pywrapinit) -set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE init.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) swig_add_library(pywrapinit TYPE MODULE LANGUAGE python diff --git a/ortools/java/README.md b/ortools/java/README.md index 1f834b448b..b6daef51ee 100644 --- a/ortools/java/README.md +++ b/ortools/java/README.md @@ -110,7 +110,7 @@ Here some dev-note concerning this `POM.xml`. net.java.dev.jna jna-platform - 5.12.1 + 5.13.0 ``` diff --git a/ortools/java/com/google/ortools/Loader.java b/ortools/java/com/google/ortools/Loader.java index f160bf99cb..ca8f38902f 100644 --- a/ortools/java/com/google/ortools/Loader.java +++ b/ortools/java/com/google/ortools/Loader.java @@ -120,6 +120,7 @@ public class Loader { // Load the native library System.load(tempPath.resolve(RESOURCE_PATH) .resolve(System.mapLibraryName("jniortools")) + .toAbsolutePath() .toString()); loaded = true; } catch (IOException e) { diff --git a/ortools/java/pom-full.xml.in b/ortools/java/pom-full.xml.in index c3b0de61c4..4c0472f4ce 100644 --- a/ortools/java/pom-full.xml.in +++ b/ortools/java/pom-full.xml.in @@ -104,7 +104,7 @@ net.java.dev.jna jna-platform - 5.12.1 + 5.13.0 com.google.protobuf diff --git a/ortools/java/pom-local.xml.in b/ortools/java/pom-local.xml.in index a2b3e50017..84005b2e92 100644 --- a/ortools/java/pom-local.xml.in +++ b/ortools/java/pom-local.xml.in @@ -76,7 +76,7 @@ net.java.dev.jna jna-platform - 5.12.1 + 5.13.0 com.google.protobuf diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index 6e7d4b8344..68c115510e 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -58,8 +58,8 @@ cc_proto_library( py_proto_library( name = "linear_solver_py_pb2", - deps = [":linear_solver_proto"], visibility = ["//visibility:public"], + deps = [":linear_solver_proto"], ) # You can include the interfaces to different solvers by invoking '--define' @@ -77,8 +77,8 @@ cc_library( "gurobi_interface.cc", "highs_interface.cc", "linear_expr.cc", - "linear_solver_callback.cc", "linear_solver.cc", + "linear_solver_callback.cc", "lpi_glop.cpp", "pdlp_interface.cc", "sat_interface.cc", diff --git a/ortools/linear_solver/csharp/CMakeLists.txt b/ortools/linear_solver/csharp/CMakeLists.txt index c8aa796f05..dd45f7fd63 100644 --- a/ortools/linear_solver/csharp/CMakeLists.txt +++ b/ortools/linear_solver/csharp/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE linear_solver.i PROPERTY CPLUSPLUS ON) set_property(SOURCE linear_solver.i PROPERTY SWIG_MODULE_NAME operations_research_linear_solver) -set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE linear_solver.i PROPERTY COMPILE_OPTIONS -namespace ${DOTNET_PROJECT}.LinearSolver -dllimport google-ortools-native) diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index abd6032df1..8179bf2934 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -43,6 +43,7 @@ // #include +#include #include #include #include diff --git a/ortools/linear_solver/java/CMakeLists.txt b/ortools/linear_solver/java/CMakeLists.txt index 4cfed5923e..2f59438ce9 100644 --- a/ortools/linear_solver/java/CMakeLists.txt +++ b/ortools/linear_solver/java/CMakeLists.txt @@ -13,7 +13,8 @@ set_property(SOURCE linear_solver.i PROPERTY CPLUSPLUS ON) set_property(SOURCE linear_solver.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE linear_solver.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.linearsolver) swig_add_library(jnilinear_solver @@ -30,7 +31,8 @@ target_link_libraries(jnilinear_solver PRIVATE ortools::ortools) set_property(SOURCE modelbuilder.i PROPERTY CPLUSPLUS ON) set_property(SOURCE modelbuilder.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE modelbuilder.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS}) +set_property(SOURCE modelbuilder.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) set_property(SOURCE modelbuilder.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.modelbuilder) swig_add_library(jnimodelbuilder diff --git a/ortools/linear_solver/python/linear_solver.i b/ortools/linear_solver/python/linear_solver.i index 321c37834e..838a3c4f8c 100644 --- a/ortools/linear_solver/python/linear_solver.i +++ b/ortools/linear_solver/python/linear_solver.i @@ -203,7 +203,6 @@ from ortools.linear_solver.linear_solver_natural_api import VariableExpr } } - static double Infinity() { return operations_research::MPSolver::infinity(); } void SetTimeLimit(int64_t x) { $self->set_time_limit(x); } int64_t WallTime() const { return $self->wall_time(); } diff --git a/ortools/linear_solver/samples/assignment_mb.py b/ortools/linear_solver/samples/assignment_mb.py index 5b516b72fb..1ed8c19613 100644 --- a/ortools/linear_solver/samples/assignment_mb.py +++ b/ortools/linear_solver/samples/assignment_mb.py @@ -42,7 +42,7 @@ def main(): # [START variables] # x[i, j] is an array of 0-1 variables, which will be 1 # if worker i is assigned to task j. - x = model.new_bool_var_array(shape=[num_workers, num_tasks], name='x') + x = model.new_bool_var_array(shape=[num_workers, num_tasks], name='x') # pytype: disable=wrong-arg-types # numpy-scalars # [END variables] # Constraints diff --git a/ortools/lp_data/sparse_vector.h b/ortools/lp_data/sparse_vector.h index 14ba23f179..eafec2c92d 100644 --- a/ortools/lp_data/sparse_vector.h +++ b/ortools/lp_data/sparse_vector.h @@ -31,6 +31,7 @@ #define OR_TOOLS_LP_DATA_SPARSE_VECTOR_H_ #include +#include #include #include #include diff --git a/ortools/math_opt/BUILD.bazel b/ortools/math_opt/BUILD.bazel index 0df7be2557..fa2acf085c 100644 --- a/ortools/math_opt/BUILD.bazel +++ b/ortools/math_opt/BUILD.bazel @@ -88,6 +88,7 @@ proto_library( deps = [ "//ortools/glop:parameters_proto", "//ortools/gscip:gscip_proto", + "//ortools/math_opt/solvers:glpk_proto", "//ortools/math_opt/solvers:gurobi_proto", "//ortools/sat:sat_parameters_proto", "@com_google_protobuf//:duration_proto", @@ -148,3 +149,14 @@ cc_proto_library( ":sparse_containers_proto", ], ) + +cc_proto_library( + name = "infeasible_subsystem_cc_proto", + deps = [":infeasible_subsystem_proto"], +) + +proto_library( + name = "infeasible_subsystem_proto", + srcs = ["infeasible_subsystem.proto"], + deps = [":result_proto"], +) diff --git a/ortools/math_opt/constraints/indicator/BUILD.bazel b/ortools/math_opt/constraints/indicator/BUILD.bazel index 006569abc7..60e7b94755 100644 --- a/ortools/math_opt/constraints/indicator/BUILD.bazel +++ b/ortools/math_opt/constraints/indicator/BUILD.bazel @@ -20,7 +20,6 @@ cc_library( deps = [ "//ortools/base:intops", "//ortools/math_opt/constraints/util:model_util", - "//ortools/math_opt/cpp:id_map", "//ortools/math_opt/cpp:variable_and_expressions", "//ortools/math_opt/storage:model_storage", "@com_google_absl//absl/strings", diff --git a/ortools/math_opt/constraints/indicator/indicator_constraint.cc b/ortools/math_opt/constraints/indicator/indicator_constraint.cc index 196b3b68c9..1b96f5efd4 100644 --- a/ortools/math_opt/constraints/indicator/indicator_constraint.cc +++ b/ortools/math_opt/constraints/indicator/indicator_constraint.cc @@ -18,6 +18,7 @@ #include #include +#include "absl/strings/string_view.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/util/model_util.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" @@ -27,7 +28,10 @@ namespace operations_research::math_opt { BoundedLinearExpression IndicatorConstraint::ImpliedConstraint() const { const IndicatorConstraintData& data = storage()->constraint_data(id_); - LinearExpression expr = ToLinearExpression(*storage_, data.linear_terms, 0.0); + // NOTE: The following makes a copy of `data.linear_terms`. This can be made + // more efficient if the need arises. + LinearExpression expr = ToLinearExpression( + *storage_, {.coeffs = data.linear_terms, .offset = 0.0}); return data.lower_bound <= std::move(expr) <= data.upper_bound; } diff --git a/ortools/math_opt/constraints/indicator/indicator_constraint.h b/ortools/math_opt/constraints/indicator/indicator_constraint.h index df6476bba7..a1b785d236 100644 --- a/ortools/math_opt/constraints/indicator/indicator_constraint.h +++ b/ortools/math_opt/constraints/indicator/indicator_constraint.h @@ -25,7 +25,6 @@ #include "absl/strings/string_view.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/util/model_util.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" @@ -79,11 +78,6 @@ class IndicatorConstraint { IndicatorConstraintId id_; }; -// Implements the API of std::unordered_map, but forbids -// IndicatorConstraints from different models in the same map. -template -using IndicatorConstraintMap = IdMap; - // Streams the name of the constraint, as registered upon constraint creation, // or a short default if none was provided. inline std::ostream& operator<<(std::ostream& ostr, diff --git a/ortools/math_opt/constraints/quadratic/BUILD.bazel b/ortools/math_opt/constraints/quadratic/BUILD.bazel index d6cfeb205e..474d79f973 100644 --- a/ortools/math_opt/constraints/quadratic/BUILD.bazel +++ b/ortools/math_opt/constraints/quadratic/BUILD.bazel @@ -18,16 +18,15 @@ cc_library( srcs = ["quadratic_constraint.cc"], hdrs = ["quadratic_constraint.h"], deps = [ - "//ortools/base", "//ortools/base:intops", "//ortools/math_opt/constraints/util:model_util", - "//ortools/math_opt/cpp:id_map", "//ortools/math_opt/cpp:key_types", "//ortools/math_opt/cpp:variable_and_expressions", "//ortools/math_opt/storage:model_storage", - "//ortools/math_opt/storage:model_storage_types", "//ortools/math_opt/storage:sparse_coefficient_map", "//ortools/math_opt/storage:sparse_matrix", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", ], ) diff --git a/ortools/math_opt/constraints/quadratic/quadratic_constraint.h b/ortools/math_opt/constraints/quadratic/quadratic_constraint.h index abbc73849c..c8305b3390 100644 --- a/ortools/math_opt/constraints/quadratic/quadratic_constraint.h +++ b/ortools/math_opt/constraints/quadratic/quadratic_constraint.h @@ -24,11 +24,10 @@ #include #include -#include "absl/strings/string_view.h" #include "absl/log/check.h" +#include "absl/strings/string_view.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/util/model_util.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/key_types.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" @@ -93,11 +92,6 @@ class QuadraticConstraint { QuadraticConstraintId id_; }; -// Implements the API of std::unordered_map, but forbids -// QuadraticConstraints from different models in the same map. -template -using QuadraticConstraintMap = IdMap; - // Streams the name of the constraint, as registered upon constraint creation, // or a short default if none was provided. inline std::ostream& operator<<(std::ostream& ostr, diff --git a/ortools/math_opt/constraints/second_order_cone/BUILD.bazel b/ortools/math_opt/constraints/second_order_cone/BUILD.bazel new file mode 100644 index 0000000000..c6b9d7aeaf --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/BUILD.bazel @@ -0,0 +1,62 @@ +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//ortools/math_opt:__subpackages__"]) + +cc_library( + name = "validator", + srcs = ["validator.cc"], + hdrs = ["validator.h"], + deps = [ + "//ortools/base:status_macros", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:model_summary", + "//ortools/math_opt/core:sparse_vector_view", + "//ortools/math_opt/validators:linear_expression_validator", + "@com_google_absl//absl/status", + ], +) + +cc_library( + name = "storage", + srcs = ["storage.cc"], + hdrs = ["storage.h"], + deps = [ + "//ortools/base:intops", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_update_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/storage:atomic_constraint_storage", + "//ortools/math_opt/storage:linear_expression_data", + "//ortools/math_opt/storage:model_storage_types", + "//ortools/math_opt/storage:sorted", + "//ortools/math_opt/storage:sparse_coefficient_map", + "@com_google_absl//absl/container:flat_hash_set", + ], +) + +cc_library( + name = "second_order_cone_constraint", + srcs = ["second_order_cone_constraint.cc"], + hdrs = ["second_order_cone_constraint.h"], + deps = [ + ":storage", + "//ortools/base:intops", + "//ortools/math_opt/constraints/util:model_util", + "//ortools/math_opt/cpp:variable_and_expressions", + "//ortools/math_opt/storage:linear_expression_data", + "//ortools/math_opt/storage:model_storage", + "@com_google_absl//absl/strings", + ], +) diff --git a/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.cc b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.cc new file mode 100644 index 0000000000..65d6ba90e8 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.cc @@ -0,0 +1,67 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/constraints/second_order_cone/storage.h" +#include "ortools/math_opt/constraints/util/model_util.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research::math_opt { + +LinearExpression SecondOrderConeConstraint::UpperBound() const { + return ToLinearExpression(*storage_, + storage()->constraint_data(id_).upper_bound); +} + +std::vector SecondOrderConeConstraint::ArgumentsToNorm() + const { + const SecondOrderConeConstraintData& data = storage()->constraint_data(id_); + std::vector args; + args.reserve(data.arguments_to_norm.size()); + for (const LinearExpressionData& arg_data : data.arguments_to_norm) { + args.push_back(ToLinearExpression(*storage_, arg_data)); + } + return args; +} + +std::string SecondOrderConeConstraint::ToString() const { + if (!storage()->has_constraint(id_)) { + return std::string(kDeletedConstraintDefaultDescription); + } + const SecondOrderConeConstraintData& data = storage()->constraint_data(id_); + std::stringstream str; + str << "||{"; + bool leading_comma = false; + for (const LinearExpressionData& arg_data : data.arguments_to_norm) { + if (leading_comma) { + str << ", "; + } + leading_comma = true; + str << ToLinearExpression(*storage_, arg_data); + } + str << "}||₂ ≤ " << ToLinearExpression(*storage_, data.upper_bound); + return str.str(); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h new file mode 100644 index 0000000000..6420e188c6 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h @@ -0,0 +1,143 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h" +// IWYU pragma: friend "ortools/math_opt/cpp/.*" +#ifndef OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_SECOND_ORDER_CONE_CONSTRAINT_H_ +#define OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_SECOND_ORDER_CONE_CONSTRAINT_H_ + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/constraints/util/model_util.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research::math_opt { + +// A value type that references a second-order cone constraint from +// ModelStorage. Usually this type is passed by copy. +class SecondOrderConeConstraint { + public: + // The typed integer used for ids. + using IdType = SecondOrderConeConstraintId; + + inline SecondOrderConeConstraint(const ModelStorage* storage, + SecondOrderConeConstraintId id); + + inline int64_t id() const; + + inline SecondOrderConeConstraintId typed_id() const; + inline const ModelStorage* storage() const; + + inline absl::string_view name() const; + + // Returns "upper_bound" with respect to a constraint of the form + // ||arguments_to_norm||₂ ≤ upper_bound. + LinearExpression UpperBound() const; + + // Returns "arguments_to_norm" with respect to a constraint of the form + // ||arguments_to_norm||₂ ≤ upper_bound. + std::vector ArgumentsToNorm() const; + + // Returns all variables that appear in the second-order cone constraint with + // a nonzero coefficient. Order is not defined. + inline std::vector NonzeroVariables() const; + + // Returns a detailed string description of the contents of the constraint + // (not its name, use `<<` for that instead). + std::string ToString() const; + + friend inline bool operator==(const SecondOrderConeConstraint& lhs, + const SecondOrderConeConstraint& rhs); + friend inline bool operator!=(const SecondOrderConeConstraint& lhs, + const SecondOrderConeConstraint& rhs); + template + friend H AbslHashValue(H h, const SecondOrderConeConstraint& constraint); + friend std::ostream& operator<<(std::ostream& ostr, + const SecondOrderConeConstraint& constraint); + + private: + const ModelStorage* storage_; + SecondOrderConeConstraintId id_; +}; + +// Streams the name of the constraint, as registered upon constraint creation, +// or a short default if none was provided. +inline std::ostream& operator<<(std::ostream& ostr, + const SecondOrderConeConstraint& constraint); + +//////////////////////////////////////////////////////////////////////////////// +// Inline function implementations +//////////////////////////////////////////////////////////////////////////////// + +int64_t SecondOrderConeConstraint::id() const { return id_.value(); } + +SecondOrderConeConstraintId SecondOrderConeConstraint::typed_id() const { + return id_; +} + +const ModelStorage* SecondOrderConeConstraint::storage() const { + return storage_; +} + +absl::string_view SecondOrderConeConstraint::name() const { + if (storage_->has_constraint(id_)) { + return storage_->constraint_data(id_).name; + } + return kDeletedConstraintDefaultDescription; +} + +std::vector SecondOrderConeConstraint::NonzeroVariables() const { + return AtomicConstraintNonzeroVariables(*storage_, id_); +} + +bool operator==(const SecondOrderConeConstraint& lhs, + const SecondOrderConeConstraint& rhs) { + return lhs.id_ == rhs.id_ && lhs.storage_ == rhs.storage_; +} + +bool operator!=(const SecondOrderConeConstraint& lhs, + const SecondOrderConeConstraint& rhs) { + return !(lhs == rhs); +} + +template +H AbslHashValue(H h, const SecondOrderConeConstraint& constraint) { + return H::combine(std::move(h), constraint.id_.value(), constraint.storage_); +} + +std::ostream& operator<<(std::ostream& ostr, + const SecondOrderConeConstraint& constraint) { + // TODO(b/170992529): handle quoting of invalid characters in the name. + const absl::string_view name = constraint.name(); + if (name.empty()) { + ostr << "__soc_con#" << constraint.id() << "__"; + } else { + ostr << name; + } + return ostr; +} + +SecondOrderConeConstraint::SecondOrderConeConstraint( + const ModelStorage* const storage, const SecondOrderConeConstraintId id) + : storage_(storage), id_(id) {} + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_SECOND_ORDER_CONE_CONSTRAINT_H_ diff --git a/ortools/math_opt/constraints/second_order_cone/storage.cc b/ortools/math_opt/constraints/second_order_cone/storage.cc new file mode 100644 index 0000000000..fbfc5bf7a2 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/storage.cc @@ -0,0 +1,75 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/constraints/second_order_cone/storage.h" + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/linear_expression_data.h" +#include "ortools/math_opt/storage/model_storage_types.h" +#include "ortools/math_opt/storage/sorted.h" +#include "ortools/math_opt/storage/sparse_coefficient_map.h" + +namespace operations_research::math_opt { + +SecondOrderConeConstraintData SecondOrderConeConstraintData::FromProto( + const ProtoType& in_proto) { + SecondOrderConeConstraintData data; + data.upper_bound = LinearExpressionData::FromProto(in_proto.upper_bound()); + data.arguments_to_norm.reserve(in_proto.arguments_to_norm_size()); + for (const LinearExpressionProto& expr_proto : in_proto.arguments_to_norm()) { + data.arguments_to_norm.push_back( + LinearExpressionData::FromProto(expr_proto)); + } + data.name = in_proto.name(); + return data; +} + +typename SecondOrderConeConstraintData::ProtoType +SecondOrderConeConstraintData::Proto() const { + ProtoType constraint; + *constraint.mutable_upper_bound() = upper_bound.Proto(); + for (const LinearExpressionData& expr : arguments_to_norm) { + *constraint.add_arguments_to_norm() = expr.Proto(); + } + constraint.set_name(name); + return constraint; +} + +std::vector SecondOrderConeConstraintData::RelatedVariables() + const { + absl::flat_hash_set vars; + for (const auto& [var, unused] : upper_bound.coeffs.terms()) { + vars.insert(var); + } + for (const LinearExpressionData& expr : arguments_to_norm) { + for (const auto& [var, unused] : expr.coeffs.terms()) { + vars.insert(var); + } + } + return std::vector(vars.begin(), vars.end()); +} + +void SecondOrderConeConstraintData::DeleteVariable(const VariableId var) { + upper_bound.coeffs.set(var, 0.0); + for (LinearExpressionData& expr : arguments_to_norm) { + expr.coeffs.set(var, 0.0); + } +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/constraints/second_order_cone/storage.h b/ortools/math_opt/constraints/second_order_cone/storage.h new file mode 100644 index 0000000000..09d1950652 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/storage.h @@ -0,0 +1,57 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_STORAGE_H_ +#define OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_STORAGE_H_ + +#include +#include +#include + +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/storage/atomic_constraint_storage.h" +#include "ortools/math_opt/storage/linear_expression_data.h" +#include "ortools/math_opt/storage/model_storage_types.h" + +namespace operations_research::math_opt { + +// Internal storage representation for a single second-order cone constraint. +// +// Implements the interface specified for the `ConstraintData` parameter of +// `AtomicConstraintStorage`. +struct SecondOrderConeConstraintData { + using IdType = SecondOrderConeConstraintId; + using ProtoType = SecondOrderConeConstraintProto; + using UpdatesProtoType = SecondOrderConeConstraintUpdatesProto; + + // The `in_proto` must be in a valid state; see the inline comments on + // `SecondOrderConeConstraintProto` for details. + static SecondOrderConeConstraintData FromProto(const ProtoType& in_proto); + ProtoType Proto() const; + std::vector RelatedVariables() const; + void DeleteVariable(VariableId var); + + LinearExpressionData upper_bound; + std::vector arguments_to_norm; + std::string name; +}; + +template <> +struct AtomicConstraintTraits { + using ConstraintData = SecondOrderConeConstraintData; +}; + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_STORAGE_H_ diff --git a/ortools/math_opt/constraints/second_order_cone/validator.cc b/ortools/math_opt/constraints/second_order_cone/validator.cc new file mode 100644 index 0000000000..01593898b8 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/validator.cc @@ -0,0 +1,43 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/constraints/second_order_cone/validator.h" + +#include + +#include "absl/status/status.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/validators/linear_expression_validator.h" + +namespace operations_research::math_opt { + +absl::Status ValidateConstraint( + const SecondOrderConeConstraintProto& constraint, + const IdNameBiMap& variable_universe) { + RETURN_IF_ERROR( + ValidateLinearExpression(constraint.upper_bound(), variable_universe)) + << "invalid `upper_bound`"; + for (int i = 0; i < constraint.arguments_to_norm_size(); ++i) { + const LinearExpressionProto& expression = constraint.arguments_to_norm(i); + RETURN_IF_ERROR(ValidateLinearExpression(expression, variable_universe)) + << "invalid `arguments_to_norm` at index: " << i; + } + return absl::OkStatus(); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/constraints/second_order_cone/validator.h b/ortools/math_opt/constraints/second_order_cone/validator.h new file mode 100644 index 0000000000..6244df8eef --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/validator.h @@ -0,0 +1,29 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_VALIDATOR_H_ +#define OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_VALIDATOR_H_ + +#include "absl/status/status.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/model.pb.h" + +namespace operations_research::math_opt { + +absl::Status ValidateConstraint( + const SecondOrderConeConstraintProto& constraint, + const IdNameBiMap& variable_universe); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CONSTRAINTS_SECOND_ORDER_CONE_VALIDATOR_H_ diff --git a/ortools/math_opt/constraints/sos/BUILD.bazel b/ortools/math_opt/constraints/sos/BUILD.bazel index 1eed5f1e97..60fcfe17c5 100644 --- a/ortools/math_opt/constraints/sos/BUILD.bazel +++ b/ortools/math_opt/constraints/sos/BUILD.bazel @@ -33,16 +33,14 @@ cc_library( name = "storage", hdrs = ["storage.h"], deps = [ - "//ortools/base", "//ortools/base:intops", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", - "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/storage:atomic_constraint_storage", - "//ortools/math_opt/storage:model_storage_types", - "//ortools/math_opt/storage:sorted", - "@com_google_absl//absl/container:flat_hash_map", + "//ortools/math_opt/storage:linear_expression_data", + "//ortools/math_opt/storage:sparse_coefficient_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:check", ], ) @@ -64,9 +62,10 @@ cc_library( ":util", "//ortools/base:intops", "//ortools/math_opt/constraints/util:model_util", - "//ortools/math_opt/cpp:id_map", "//ortools/math_opt/cpp:variable_and_expressions", + "//ortools/math_opt/storage:linear_expression_data", "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:sparse_coefficient_map", "@com_google_absl//absl/strings", ], ) @@ -79,9 +78,10 @@ cc_library( ":util", "//ortools/base:intops", "//ortools/math_opt/constraints/util:model_util", - "//ortools/math_opt/cpp:id_map", "//ortools/math_opt/cpp:variable_and_expressions", + "//ortools/math_opt/storage:linear_expression_data", "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:sparse_coefficient_map", "@com_google_absl//absl/strings", ], ) diff --git a/ortools/math_opt/constraints/sos/sos1_constraint.cc b/ortools/math_opt/constraints/sos/sos1_constraint.cc index 6dd9f0216d..87eba963ca 100644 --- a/ortools/math_opt/constraints/sos/sos1_constraint.cc +++ b/ortools/math_opt/constraints/sos/sos1_constraint.cc @@ -15,15 +15,17 @@ #include "ortools/base/strong_int.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" #include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/sparse_coefficient_map.h" namespace operations_research::math_opt { LinearExpression Sos1Constraint::Expression(int index) const { - const Sos1ConstraintData::LinearExpression storage_expr = + const LinearExpressionData& storage_expr = storage_->constraint_data(id_).expression(index); LinearExpression out_expr = storage_expr.offset; - for (const auto [var_id, coeff] : storage_expr.terms) { + for (const auto [var_id, coeff] : storage_expr.coeffs.terms()) { out_expr += coeff * Variable(storage_, var_id); } return out_expr; diff --git a/ortools/math_opt/constraints/sos/sos1_constraint.h b/ortools/math_opt/constraints/sos/sos1_constraint.h index d1c9d550e9..934a02af77 100644 --- a/ortools/math_opt/constraints/sos/sos1_constraint.h +++ b/ortools/math_opt/constraints/sos/sos1_constraint.h @@ -25,7 +25,6 @@ #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/sos/util.h" #include "ortools/math_opt/constraints/util/model_util.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" @@ -72,11 +71,6 @@ class Sos1Constraint { Sos1ConstraintId id_; }; -// Implements the API of std::unordered_map, but forbids -// Sos1Constraints from different models in the same map. -template -using Sos1ConstraintMap = IdMap; - // Streams the name of the constraint, as registered upon constraint creation, // or a short default if none was provided. inline std::ostream& operator<<(std::ostream& ostr, diff --git a/ortools/math_opt/constraints/sos/sos2_constraint.cc b/ortools/math_opt/constraints/sos/sos2_constraint.cc index ae6c78ceb5..b209d153b1 100644 --- a/ortools/math_opt/constraints/sos/sos2_constraint.cc +++ b/ortools/math_opt/constraints/sos/sos2_constraint.cc @@ -15,15 +15,17 @@ #include "ortools/base/strong_int.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" #include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/sparse_coefficient_map.h" namespace operations_research::math_opt { LinearExpression Sos2Constraint::Expression(int index) const { - const Sos2ConstraintData::LinearExpression storage_expr = + const LinearExpressionData& storage_expr = storage_->constraint_data(id_).expression(index); LinearExpression out_expr = storage_expr.offset; - for (const auto [var_id, coeff] : storage_expr.terms) { + for (const auto [var_id, coeff] : storage_expr.coeffs.terms()) { out_expr += coeff * Variable(storage_, var_id); } return out_expr; diff --git a/ortools/math_opt/constraints/sos/sos2_constraint.h b/ortools/math_opt/constraints/sos/sos2_constraint.h index c5d386c7a6..4b0ba14fcd 100644 --- a/ortools/math_opt/constraints/sos/sos2_constraint.h +++ b/ortools/math_opt/constraints/sos/sos2_constraint.h @@ -25,7 +25,6 @@ #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/sos/util.h" #include "ortools/math_opt/constraints/util/model_util.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" @@ -73,11 +72,6 @@ class Sos2Constraint { Sos2ConstraintId id_; }; -// Implements the API of std::unordered_map, but forbids -// Sos2Constraints from different models in the same map. -template -using Sos2ConstraintMap = IdMap; - // Streams the name of the constraint, as registered upon constraint creation, // or a short default if none was provided. inline std::ostream& operator<<(std::ostream& ostr, diff --git a/ortools/math_opt/constraints/sos/storage.h b/ortools/math_opt/constraints/sos/storage.h index 48639e2a1e..17a6c67090 100644 --- a/ortools/math_opt/constraints/sos/storage.h +++ b/ortools/math_opt/constraints/sos/storage.h @@ -21,16 +21,14 @@ #include #include -#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" -#include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/storage/atomic_constraint_storage.h" -#include "ortools/math_opt/storage/model_storage_types.h" -#include "ortools/math_opt/storage/sorted.h" +#include "ortools/math_opt/storage/linear_expression_data.h" +#include "ortools/math_opt/storage/sparse_coefficient_map.h" namespace operations_research::math_opt { namespace internal { @@ -51,14 +49,9 @@ class SosConstraintData { std::is_same>, "ID type may only be Sos1ConstraintId or Sos2ConstraintId"); - struct LinearExpression { - absl::flat_hash_map terms; - double offset = 0.0; - }; - // `weights` must either be empty or the same length as `expressions`. If it // is empty, default weights of 1, 2, ... will be used. - SosConstraintData(std::vector expressions, + SosConstraintData(std::vector expressions, std::vector weights, std::string name) : expressions_(std::move(expressions)), name_(std::move(name)) { if (!weights.empty()) { @@ -80,7 +73,7 @@ class SosConstraintData { AssertInbounds(index); return weights_.has_value() ? (*weights_)[index] : index + 1; } - const LinearExpression& expression(const int index) const { + const LinearExpressionData& expression(const int index) const { AssertInbounds(index); return expressions_[index]; } @@ -96,7 +89,7 @@ class SosConstraintData { // If present, length must be the same as that of `expressions_`. // If absent, default weights of 1, 2, ... are used. std::optional> weights_; - std::vector expressions_; + std::vector expressions_; std::string name_; }; @@ -128,13 +121,8 @@ SosConstraintData SosConstraintData::FromProto( SosConstraintData data; data.name_ = in_proto.name(); for (int i = 0; i < num_expressions; ++i) { - LinearExpression& expression = data.expressions_.emplace_back(); - const LinearExpressionProto& proto_expression = in_proto.expressions(i); - expression.offset = proto_expression.offset(); - for (int j = 0; j < proto_expression.ids_size(); ++j) { - expression.terms.insert({VariableId(proto_expression.ids(j)), - proto_expression.coefficients(j)}); - } + data.expressions_.push_back( + LinearExpressionData::FromProto(in_proto.expressions(i))); } // Otherwise proto has default weights, so leave data.weights_ as unset. if (!in_proto.weights().empty()) { @@ -152,13 +140,7 @@ SosConstraintData::Proto() const { ProtoType constraint; constraint.set_name(name()); for (int i = 0; i < num_expressions(); ++i) { - const LinearExpression& expr = expression(i); - LinearExpressionProto& proto_expr = *constraint.add_expressions(); - proto_expr.set_offset(expr.offset); - for (const VariableId id : SortedMapKeys(expr.terms)) { - proto_expr.add_ids(id.value()); - proto_expr.add_coefficients(expr.terms.at(id)); - } + *constraint.add_expressions() = expression(i).Proto(); } if (weights_.has_value()) { for (int i = 0; i < num_expressions(); ++i) { @@ -172,8 +154,8 @@ template std::vector SosConstraintData::RelatedVariables() const { absl::flat_hash_set vars; - for (const LinearExpression& expression : expressions_) { - for (const auto [var, _] : expression.terms) { + for (const LinearExpressionData& expression : expressions_) { + for (const auto [var, _] : expression.coeffs.terms()) { vars.insert(var); } } @@ -182,8 +164,8 @@ std::vector SosConstraintData::RelatedVariables() template void SosConstraintData::DeleteVariable(const VariableId var) { - for (LinearExpression& expression : expressions_) { - expression.terms.erase(var); + for (LinearExpressionData& expression : expressions_) { + expression.coeffs.erase(var); } } diff --git a/ortools/math_opt/constraints/util/BUILD.bazel b/ortools/math_opt/constraints/util/BUILD.bazel index ab76bf126d..f0bc59d1cd 100644 --- a/ortools/math_opt/constraints/util/BUILD.bazel +++ b/ortools/math_opt/constraints/util/BUILD.bazel @@ -18,7 +18,9 @@ cc_library( srcs = ["model_util.cc"], hdrs = ["model_util.h"], deps = [ + "//ortools/base:intops", "//ortools/math_opt/cpp:variable_and_expressions", + "//ortools/math_opt/storage:linear_expression_data", "//ortools/math_opt/storage:model_storage", "//ortools/math_opt/storage:sparse_coefficient_map", "@com_google_absl//absl/algorithm:container", diff --git a/ortools/math_opt/constraints/util/model_util.cc b/ortools/math_opt/constraints/util/model_util.cc index 515c0bf1ef..2e2262bcf2 100644 --- a/ortools/math_opt/constraints/util/model_util.cc +++ b/ortools/math_opt/constraints/util/model_util.cc @@ -15,28 +15,28 @@ #include +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/math_opt/storage/sparse_coefficient_map.h" namespace operations_research::math_opt { LinearExpression ToLinearExpression(const ModelStorage& storage, - const SparseCoefficientMap& coeffs, - const double offset) { - LinearExpression expr = offset; - for (const auto [var_id, coeff] : coeffs.terms()) { + const LinearExpressionData& expr_data) { + LinearExpression expr = expr_data.offset; + for (const auto [var_id, coeff] : expr_data.coeffs.terms()) { expr += coeff * Variable(&storage, var_id); } return expr; } -std::pair FromLinearExpression( - const LinearExpression& expression) { +LinearExpressionData FromLinearExpression(const LinearExpression& expression) { SparseCoefficientMap coeffs; for (const auto [var, coeff] : expression.terms()) { coeffs.set(var.typed_id(), coeff); } - return {coeffs, expression.offset()}; + return {.coeffs = std::move(coeffs), .offset = expression.offset()}; } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/constraints/util/model_util.h b/ortools/math_opt/constraints/util/model_util.h index 49cb2aa7a2..5a21700d6a 100644 --- a/ortools/math_opt/constraints/util/model_util.h +++ b/ortools/math_opt/constraints/util/model_util.h @@ -14,14 +14,14 @@ #ifndef OR_TOOLS_MATH_OPT_CONSTRAINTS_UTIL_MODEL_UTIL_H_ #define OR_TOOLS_MATH_OPT_CONSTRAINTS_UTIL_MODEL_UTIL_H_ -#include #include #include "absl/algorithm/container.h" #include "absl/strings/string_view.h" +#include "ortools/base/strong_int.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" #include "ortools/math_opt/storage/model_storage.h" -#include "ortools/math_opt/storage/sparse_coefficient_map.h" namespace operations_research::math_opt { @@ -33,12 +33,10 @@ constexpr absl::string_view kDeletedConstraintDefaultDescription = // Converts data from "raw ID" format to a LinearExpression, in the C++ API, // associated with `storage`. LinearExpression ToLinearExpression(const ModelStorage& storage, - const SparseCoefficientMap& coeffs, - double offset); + const LinearExpressionData& expr_data); // Converts a `LinearExpression` to the associated "raw ID" format. -std::pair FromLinearExpression( - const LinearExpression& expression); +LinearExpressionData FromLinearExpression(const LinearExpression& expression); template std::vector AtomicConstraintNonzeroVariables( diff --git a/ortools/math_opt/core/BUILD.bazel b/ortools/math_opt/core/BUILD.bazel index fd46ef2578..1b5cbd099b 100644 --- a/ortools/math_opt/core/BUILD.bazel +++ b/ortools/math_opt/core/BUILD.bazel @@ -23,10 +23,14 @@ cc_library( "//ortools/base:status_macros", "//ortools/math_opt:callback_cc_proto", "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", ], @@ -52,13 +56,14 @@ cc_library( srcs = ["model_summary.cc"], hdrs = ["model_summary.h"], deps = [ - "//ortools/base", "//ortools/base:linked_hash_map", + "//ortools/base:status_macros", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", "//ortools/util:status_macros", "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -73,10 +78,10 @@ cc_library( deps = [ ":non_streamable_solver_init_arguments", ":solve_interrupter", - "//ortools/base", "//ortools/base:map_util", "//ortools/base:status_macros", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -85,7 +90,7 @@ cc_library( "//ortools/port:proto_utils", "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/status", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/synchronization", @@ -103,27 +108,26 @@ cc_library( ":solve_interrupter", ":solver_debug", ":solver_interface", - "//ortools/base", "//ortools/base:status_macros", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", "//ortools/math_opt:parameters_cc_proto", "//ortools/math_opt:result_cc_proto", "//ortools/math_opt/validators:callback_validator", + "//ortools/math_opt/validators:infeasible_subsystem_validator", "//ortools/math_opt/validators:model_parameters_validator", "//ortools/math_opt/validators:model_validator", "//ortools/math_opt/validators:result_validator", "//ortools/math_opt/validators:solve_parameters_validator", "//ortools/port:proto_utils", - "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_absl//absl/synchronization", - "@com_google_absl//absl/types:span", ], ) @@ -206,11 +210,21 @@ cc_library( srcs = ["concurrent_calls_guard.cc"], hdrs = ["concurrent_calls_guard.h"], deps = [ - "@com_google_absl//absl/log:check", "@com_google_absl//absl/base:core_headers", - #"@com_google_absl//absl/log:check", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/synchronization", ], ) + +cc_library( + name = "empty_bounds", + srcs = ["empty_bounds.cc"], + hdrs = ["empty_bounds.h"], + deps = [ + "//ortools/math_opt:result_cc_proto", + "//ortools/util:fp_roundtrip_conv", + "@com_google_absl//absl/strings", + ], +) diff --git a/ortools/math_opt/core/concurrent_calls_guard.cc b/ortools/math_opt/core/concurrent_calls_guard.cc index 27160c3639..b255fd5b01 100644 --- a/ortools/math_opt/core/concurrent_calls_guard.cc +++ b/ortools/math_opt/core/concurrent_calls_guard.cc @@ -14,10 +14,10 @@ #include "ortools/math_opt/core/concurrent_calls_guard.h" #include "absl/base/thread_annotations.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/synchronization/mutex.h" -#include "absl/log/check.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/core/empty_bounds.cc b/ortools/math_opt/core/empty_bounds.cc new file mode 100644 index 0000000000..140d498d89 --- /dev/null +++ b/ortools/math_opt/core/empty_bounds.cc @@ -0,0 +1,46 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/core/empty_bounds.h" + +#include + +#include "absl/strings/str_cat.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/util/fp_roundtrip_conv.h" + +namespace operations_research::math_opt { + +constexpr double kInf = std::numeric_limits::infinity(); + +SolveResultProto ResultForIntegerInfeasible(const bool is_maximize, + const int64_t bad_variable_id, + const double lb, const double ub) { + SolveResultProto result; + result.mutable_termination()->set_reason(TERMINATION_REASON_INFEASIBLE); + result.mutable_termination()->set_detail(absl::StrCat( + "Problem had one or more integer variables with no integers " + "in domain, e.g. integer variable with id: ", + bad_variable_id, " had bounds: [", RoundTripDoubleFormat::ToString(lb), + ", ", RoundTripDoubleFormat::ToString(ub), "].")); + result.mutable_solve_stats()->mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_INFEASIBLE); + result.mutable_solve_stats()->mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_UNDETERMINED); + const double objective_value = is_maximize ? -kInf : kInf; + result.mutable_solve_stats()->set_best_primal_bound(objective_value); + result.mutable_solve_stats()->set_best_dual_bound(-objective_value); + return result; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/core/empty_bounds.h b/ortools/math_opt/core/empty_bounds.h new file mode 100644 index 0000000000..357fef2c49 --- /dev/null +++ b/ortools/math_opt/core/empty_bounds.h @@ -0,0 +1,34 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_CORE_EMPTY_BOUNDS_H_ +#define OR_TOOLS_MATH_OPT_CORE_EMPTY_BOUNDS_H_ + +#include + +#include "ortools/math_opt/result.pb.h" + +namespace operations_research::math_opt { + +// Returns an "infeasible" result for models where the infeasibility is caused +// by an integer variable whose bounds are nonempty but contain no integers. +// +// Callers should make sure to set the SolveResultProto.solve_stats.solve_time +// field before returning the result. +SolveResultProto ResultForIntegerInfeasible(bool is_maximize, + int64_t bad_variable_id, double lb, + double ub); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CORE_EMPTY_BOUNDS_H_ diff --git a/ortools/math_opt/core/math_opt_proto_utils.cc b/ortools/math_opt/core/math_opt_proto_utils.cc index bdca1533c7..a19359c2f9 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.cc +++ b/ortools/math_opt/core/math_opt_proto_utils.cc @@ -19,16 +19,18 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/base/logging.h" #include "ortools/base/status_builder.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" namespace operations_research { @@ -69,6 +71,42 @@ SparseVectorFilterPredicate::SparseVectorFilterPredicate( } } +SparseDoubleVectorProto FilterSparseVector( + const SparseDoubleVectorProto& input, + const SparseVectorFilterProto& filter) { + SparseDoubleVectorProto result; + SparseVectorFilterPredicate predicate(filter); + for (const auto [id, val] : MakeView(input)) { + if (predicate.AcceptsAndUpdate(id, val)) { + result.add_ids(id); + result.add_values(val); + } + } + return result; +} + +void ApplyAllFilters(const ModelSolveParametersProto& model_solve_params, + SolutionProto& solution) { + if (model_solve_params.has_variable_values_filter() && + solution.has_primal_solution()) { + *solution.mutable_primal_solution()->mutable_variable_values() = + FilterSparseVector(solution.primal_solution().variable_values(), + model_solve_params.variable_values_filter()); + } + if (model_solve_params.has_dual_values_filter() && + solution.has_dual_solution()) { + *solution.mutable_dual_solution()->mutable_dual_values() = + FilterSparseVector(solution.dual_solution().dual_values(), + model_solve_params.dual_values_filter()); + } + if (model_solve_params.has_reduced_costs_filter() && + solution.has_dual_solution()) { + *solution.mutable_dual_solution()->mutable_reduced_costs() = + FilterSparseVector(solution.dual_solution().reduced_costs(), + model_solve_params.reduced_costs_filter()); + } +} + absl::flat_hash_set EventSet( const CallbackRegistrationProto& callback_registration) { // Here we don't use for-range loop since for repeated enum fields, the type @@ -145,7 +183,7 @@ absl::Status ModelIsSupported(const ModelProto& model, if (const SupportType support = support_menu.multi_objectives; support != SupportType::kSupported) { if (!model.auxiliary_objectives().empty()) { - return error_status("multi objectives", support); + return error_status("multiple objectives", support); } } if (const SupportType support = support_menu.quadratic_objectives; @@ -165,6 +203,12 @@ absl::Status ModelIsSupported(const ModelProto& model, return error_status("quadratic constraints", support); } } + if (const SupportType support = support_menu.second_order_cone_constraints; + support != SupportType::kSupported) { + if (!model.second_order_cone_constraints().empty()) { + return error_status("second-order cone constraints", support); + } + } if (const SupportType support = support_menu.sos1_constraints; support != SupportType::kSupported) { if (!model.sos1_constraints().empty()) { @@ -243,6 +287,12 @@ bool UpdateIsSupported(const ModelUpdateProto& update, return false; } } + if (support_menu.second_order_cone_constraints != SupportType::kSupported) { + if (contains_new_or_deleted_constraints( + update.second_order_cone_constraint_updates())) { + return false; + } + } if (support_menu.sos1_constraints != SupportType::kSupported) { if (contains_new_or_deleted_constraints(update.sos1_constraint_updates())) { return false; diff --git a/ortools/math_opt/core/math_opt_proto_utils.h b/ortools/math_opt/core/math_opt_proto_utils.h index 7c8369b0e1..30c767985d 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.h +++ b/ortools/math_opt/core/math_opt_proto_utils.h @@ -19,17 +19,18 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" -namespace operations_research { -namespace math_opt { +namespace operations_research::math_opt { inline int NumVariables(const VariablesProto& variables) { return variables.ids_size(); @@ -89,7 +90,7 @@ class SparseVectorFilterPredicate { // non-optimized builds it will CHECK that this is the case. It updates an // internal counter when filtering by ids. template - bool AcceptsAndUpdate(const int64_t id, const Value& value); + bool AcceptsAndUpdate(int64_t id, const Value& value); private: const SparseVectorFilterProto& filter_; @@ -105,19 +106,39 @@ class SparseVectorFilterPredicate { #endif // NDEBUG }; +// Applies filter to each element of input and returns the elements that remain. +// +// TODO(b/261603235): this function is not very efficient, decide if this +// matters. +SparseDoubleVectorProto FilterSparseVector( + const SparseDoubleVectorProto& input, + const SparseVectorFilterProto& filter); + +// Applies the primal, dual and reduced costs filters from model_solve_params +// to the primal solution variable values, dual solution dual values, and dual +// solution reduced costs, respectively, and overwriting these values with +// the results. +// +// Warning: solution is modified in place. +// +// TODO(b/261603235): this function is not very efficient, decide if this +// matters. +void ApplyAllFilters(const ModelSolveParametersProto& model_solve_params, + SolutionProto& solution); + // Returns the callback_registration.request_registration as a set of enums. absl::flat_hash_set EventSet( const CallbackRegistrationProto& callback_registration); // Sets the reason to TERMINATION_REASON_FEASIBLE if feasible = true and // TERMINATION_REASON_NO_SOLUTION_FOUND otherwise. -TerminationProto TerminateForLimit(const LimitProto limit, bool feasible, +TerminationProto TerminateForLimit(LimitProto limit, bool feasible, absl::string_view detail = {}); -TerminationProto FeasibleTermination(const LimitProto limit, +TerminationProto FeasibleTermination(LimitProto limit, absl::string_view detail = {}); -TerminationProto NoSolutionFoundTermination(const LimitProto limit, +TerminationProto NoSolutionFoundTermination(LimitProto limit, absl::string_view detail = {}); TerminationProto TerminateForReason(TerminationReasonProto reason, @@ -134,6 +155,7 @@ struct SupportedProblemStructures { SupportType multi_objectives = SupportType::kNotSupported; SupportType quadratic_objectives = SupportType::kNotSupported; SupportType quadratic_constraints = SupportType::kNotSupported; + SupportType second_order_cone_constraints = SupportType::kNotSupported; SupportType sos1_constraints = SupportType::kNotSupported; SupportType sos2_constraints = SupportType::kNotSupported; SupportType indicator_constraints = SupportType::kNotSupported; @@ -195,7 +217,6 @@ bool SparseVectorFilterPredicate::AcceptsAndUpdate(const int64_t id, return id == filter_.filtered_ids(next_filtered_id_index_); } -} // namespace math_opt -} // namespace operations_research +} // namespace operations_research::math_opt #endif // OR_TOOLS_MATH_OPT_CORE_MATH_OPT_PROTO_UTILS_H_ diff --git a/ortools/math_opt/core/model_summary.cc b/ortools/math_opt/core/model_summary.cc index f7982d3b37..0eff520108 100644 --- a/ortools/math_opt/core/model_summary.cc +++ b/ortools/math_opt/core/model_summary.cc @@ -21,11 +21,11 @@ #include #include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "absl/log/check.h" #include "ortools/base/linked_hash_map.h" #include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" @@ -122,6 +122,7 @@ ModelSummary::ModelSummary(const bool check_names) auxiliary_objectives(check_names), linear_constraints(check_names), quadratic_constraints(check_names), + second_order_cone_constraints(check_names), sos1_constraints(check_names), sos2_constraints(check_names), indicator_constraints(check_names) {} @@ -149,6 +150,10 @@ absl::StatusOr ModelSummary::Create(const ModelProto& model, RETURN_IF_ERROR(internal::UpdateBiMapFromMappedData( {}, model.quadratic_constraints(), summary.quadratic_constraints)) << "ModelProto.quadratic_constraints are invalid"; + RETURN_IF_ERROR(internal::UpdateBiMapFromMappedData( + {}, model.second_order_cone_constraints(), + summary.second_order_cone_constraints)) + << "ModelProto.second_order_cone_constraints are invalid"; RETURN_IF_ERROR(internal::UpdateBiMapFromMappedData( {}, model.sos1_constraints(), summary.sos1_constraints)) << "ModelProto.sos1_constraints are invalid"; @@ -185,6 +190,12 @@ absl::Status ModelSummary::Update(const ModelUpdateProto& model_update) { model_update.quadratic_constraint_updates().new_constraints(), quadratic_constraints)) << "invalid quadratic constraints"; + RETURN_IF_ERROR(internal::UpdateBiMapFromMappedData( + model_update.second_order_cone_constraint_updates() + .deleted_constraint_ids(), + model_update.second_order_cone_constraint_updates().new_constraints(), + second_order_cone_constraints)) + << "invalid second-order cone constraints"; RETURN_IF_ERROR(internal::UpdateBiMapFromMappedData( model_update.sos1_constraint_updates().deleted_constraint_ids(), model_update.sos1_constraint_updates().new_constraints(), diff --git a/ortools/math_opt/core/model_summary.h b/ortools/math_opt/core/model_summary.h index 4f3bd54b60..49f19eded6 100644 --- a/ortools/math_opt/core/model_summary.h +++ b/ortools/math_opt/core/model_summary.h @@ -25,11 +25,11 @@ #include "absl/algorithm/container.h" #include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "absl/log/check.h" #include "ortools/base/linked_hash_map.h" #include "ortools/base/status_macros.h" #include "ortools/math_opt/model.pb.h" @@ -125,8 +125,6 @@ class IdNameBiMap { nonempty_name_to_id_; }; -// TODO(b/232619901): In the guide for how to add new constraints, include how -// this class must updated. struct ModelSummary { explicit ModelSummary(bool check_names = true); static absl::StatusOr Create(const ModelProto& model, @@ -138,6 +136,7 @@ struct ModelSummary { IdNameBiMap auxiliary_objectives; IdNameBiMap linear_constraints; IdNameBiMap quadratic_constraints; + IdNameBiMap second_order_cone_constraints; IdNameBiMap sos1_constraints; IdNameBiMap sos2_constraints; IdNameBiMap indicator_constraints; diff --git a/ortools/math_opt/core/solver.cc b/ortools/math_opt/core/solver.cc index 0a62cec326..b8cd0c04fa 100644 --- a/ortools/math_opt/core/solver.cc +++ b/ortools/math_opt/core/solver.cc @@ -13,22 +13,17 @@ #include "ortools/math_opt/core/solver.h" -#include - +#include #include #include #include #include -#include "absl/base/thread_annotations.h" +#include "absl/log/check.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" -#include "absl/synchronization/mutex.h" -#include "absl/types/span.h" -#include "ortools/base/integral_types.h" -#include "ortools/base/logging.h" #include "ortools/base/status_macros.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/concurrent_calls_guard.h" @@ -36,11 +31,13 @@ #include "ortools/math_opt/core/non_streamable_solver_init_arguments.h" #include "ortools/math_opt/core/solver_debug.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/result.pb.h" #include "ortools/math_opt/validators/callback_validator.h" +#include "ortools/math_opt/validators/infeasible_subsystem_validator.h" #include "ortools/math_opt/validators/model_parameters_validator.h" #include "ortools/math_opt/validators/model_validator.h" #include "ortools/math_opt/validators/result_validator.h" @@ -66,8 +63,8 @@ absl::Status ToInternalError(const absl::Status original) { // previous call to one of them failed. absl::Status PreviousFatalFailureOccurred() { return absl::InvalidArgumentError( - "a previous call to Solve() or Update() failed, the Solver can't be used " - "anymore"); + "a previous call to Solve(), InfeasibleSubsystem(), or Update() failed, " + "the Solver can't be used anymore"); } } // namespace @@ -187,6 +184,48 @@ absl::StatusOr Solver::Update(const ModelUpdateProto& model_update) { return true; } +absl::StatusOr Solver::InfeasibleSubsystem( + const InfeasibleSubsystemArgs& infeasible_subsystem_args) { + ASSIGN_OR_RETURN(const auto guard, + ConcurrentCallsGuard::TryAcquire(concurrent_calls_tracker_)); + + if (fatal_failure_occurred_) { + return PreviousFatalFailureOccurred(); + } + CHECK(underlying_solver_ != nullptr); + + // We will reset it in code paths where no error occur. + fatal_failure_occurred_ = true; + + RETURN_IF_ERROR(ValidateSolveParameters(infeasible_subsystem_args.parameters)) + << "invalid parameters"; + + ASSIGN_OR_RETURN(const InfeasibleSubsystemResultProto result, + underlying_solver_->InfeasibleSubsystem( + infeasible_subsystem_args.parameters, + infeasible_subsystem_args.message_callback, + infeasible_subsystem_args.interrupter)); + + // We consider errors in `result` to be internal errors, but + // `ValidateInfeasibleSubsystemResult()` will return an InvalidArgumentError. + // So here we convert the error. + RETURN_IF_ERROR(ToInternalError( + ValidateInfeasibleSubsystemResult(result, model_summary_))); + + fatal_failure_occurred_ = false; + return result; +} + +absl::StatusOr +Solver::NonIncrementalInfeasibleSubsystem( + const ModelProto& model, const SolverTypeProto solver_type, + const InitArgs& init_args, + const InfeasibleSubsystemArgs& infeasible_subsystem_args) { + ASSIGN_OR_RETURN(std::unique_ptr solver, + Solver::New(solver_type, model, init_args)); + return solver->InfeasibleSubsystem(infeasible_subsystem_args); +} + namespace internal { absl::Status ValidateInitArgs(const Solver::InitArgs& init_args, diff --git a/ortools/math_opt/core/solver.h b/ortools/math_opt/core/solver.h index ede9b6c652..efd7b17b43 100644 --- a/ortools/math_opt/core/solver.h +++ b/ortools/math_opt/core/solver.h @@ -19,12 +19,12 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/synchronization/mutex.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/concurrent_calls_guard.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -102,6 +102,22 @@ class Solver { SolveInterrupter* interrupter = nullptr; }; + // Arguments used when calling InfeasibleSubsystem(). + struct InfeasibleSubsystemArgs { + SolveParametersProto parameters; + + // An optional callback for messages emitted by the solver. + // + // When set it enables the solver messages and ignores the `enable_output` + // in solve parameters; messages are redirected to the callback and not + // printed on stdout/stderr/logs anymore. + MessageCallback message_callback = nullptr; + + // An optional interrupter that the solver can use to interrupt the solve + // early. + SolveInterrupter* interrupter = nullptr; + }; + // A shortcut for calling Solver::New() and then Solver::Solve(). static absl::StatusOr NonIncrementalSolve( const ModelProto& model, SolverTypeProto solver_type, @@ -134,6 +150,18 @@ class Solver { // license). absl::StatusOr Update(const ModelUpdateProto& model_update); + // Computes an infeasible subsystem of `model`. + absl::StatusOr InfeasibleSubsystem( + const InfeasibleSubsystemArgs& infeasible_subsystem_args); + + // A shortcut for calling Solver::New() and then + // Solver()::InfeasibleSubsystem() + static absl::StatusOr + NonIncrementalInfeasibleSubsystem( + const ModelProto& model, SolverTypeProto solver_type, + const InitArgs& init_args, + const InfeasibleSubsystemArgs& infeasible_subsystem_args); + private: Solver(std::unique_ptr underlying_solver, ModelSummary model_summary); diff --git a/ortools/math_opt/core/solver_interface.cc b/ortools/math_opt/core/solver_interface.cc index 56eb470514..20b35a0f08 100644 --- a/ortools/math_opt/core/solver_interface.cc +++ b/ortools/math_opt/core/solver_interface.cc @@ -20,12 +20,13 @@ #include #include "absl/container/flat_hash_map.h" -#include "absl/status/status.h" +#include "absl/log/check.h" #include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/synchronization/mutex.h" -#include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/status_builder.h" #include "ortools/math_opt/model.pb.h" @@ -34,6 +35,7 @@ namespace operations_research { namespace math_opt { +namespace {} // namespace AllSolversRegistry* AllSolversRegistry::Instance() { static AllSolversRegistry* const instance = new AllSolversRegistry; @@ -66,7 +68,8 @@ absl::StatusOr> AllSolversRegistry::Create( name = absl::StrCat("unknown(", static_cast(solver_type), ")"); } return util::InvalidArgumentErrorBuilder() - << "solver type " << name << " is not registered"; + << "solver type " << name << " is not registered" + << ", support for this solver has not been compiled"; } return (*factory)(model, init_args); } diff --git a/ortools/math_opt/core/solver_interface.h b/ortools/math_opt/core/solver_interface.h index 7f3a4a01e6..757c4a87d7 100644 --- a/ortools/math_opt/core/solver_interface.h +++ b/ortools/math_opt/core/solver_interface.h @@ -21,13 +21,13 @@ #include "absl/base/attributes.h" #include "absl/container/flat_hash_map.h" -#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/synchronization/mutex.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/non_streamable_solver_init_arguments.h" #include "ortools/math_opt/core/solve_interrupter.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -135,6 +135,24 @@ class SolverInterface { // The implementation should assume the input ModelUpdate is valid and is free // to assert if this is not the case. virtual absl::StatusOr Update(const ModelUpdateProto& model_update) = 0; + + // Computes a infeasible subsystem of the model (including all updates). + // + // All input arguments are ensured (by solver.cc) to be valid. Furthermore, + // since all parameters are references or functions (which could be a lambda + // expression), the implementation should not keep a reference or copy of + // them, as they may become invalid reference after the invocation if this + // function. + // + // The parameters `message_cb` and `interrupter` are optional. They are + // nullptr when not set. + // + // When parameter `message_cb` is not null and the underlying solver does not + // supports message callbacks, it must return an InvalidArgumentError with the + // message internal::kMessageCallbackNotSupported. + virtual absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) = 0; }; class AllSolversRegistry { diff --git a/ortools/math_opt/core/sparse_collection_matchers.cc b/ortools/math_opt/core/sparse_collection_matchers.cc deleted file mode 100644 index 9d8afa69da..0000000000 --- a/ortools/math_opt/core/sparse_collection_matchers.cc +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/core/sparse_collection_matchers.h" - -#include -#include -#include -#include - -#include "ortools/math_opt/sparse_containers.pb.h" - -namespace operations_research { -namespace math_opt { - -SparseDoubleVectorProto MakeSparseDoubleVector( - std::initializer_list> pairs) { - SparseDoubleVectorProto ret; - for (const auto [id, value] : pairs) { - ret.add_ids(id); - ret.add_values(value); - } - return ret; -} - -SparseBoolVectorProto MakeSparseBoolVector( - std::initializer_list> pairs) { - SparseBoolVectorProto ret; - for (const auto [id, value] : pairs) { - ret.add_ids(id); - ret.add_values(value); - } - return ret; -} - -SparseDoubleMatrixProto MakeSparseDoubleMatrix( - std::initializer_list> values) { - SparseDoubleMatrixProto ret; - for (const auto [row, col, coefficient] : values) { - ret.add_row_ids(row); - ret.add_column_ids(col); - ret.add_coefficients(coefficient); - } - return ret; -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/core/sparse_collection_matchers.h b/ortools/math_opt/core/sparse_collection_matchers.h deleted file mode 100644 index b35e297773..0000000000 --- a/ortools/math_opt/core/sparse_collection_matchers.h +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_MATH_OPT_CORE_SPARSE_COLLECTION_MATCHERS_H_ -#define OR_TOOLS_MATH_OPT_CORE_SPARSE_COLLECTION_MATCHERS_H_ - -#include -#include -#include -#include -#include -#include - -#include "gmock/gmock.h" -#include "gtest/gtest.h" -#include "ortools/math_opt/core/sparse_vector_view.h" -#include "ortools/math_opt/sparse_containers.pb.h" - -namespace operations_research { -namespace math_opt { - -SparseDoubleVectorProto MakeSparseDoubleVector( - std::initializer_list> pairs); - -SparseBoolVectorProto MakeSparseBoolVector( - std::initializer_list> pairs); - -SparseDoubleMatrixProto MakeSparseDoubleMatrix( - std::initializer_list> values); - -// Type of the argument of SparseVectorMatcher. -template -using Pairs = std::initializer_list>; - -// Here `pairs` must be a Pairs. -// -// Usage: -// EXPECT_THAT(v, SparseVectorMatcher(Pairs{})); -// EXPECT_THAT(v, SparseVectorMatcher(Pairs{{2, 3.0}, {3, 2.0}})); -MATCHER_P(SparseVectorMatcher, pairs, "") { - const auto iterable = MakeView(arg); - const std::vector v(iterable.begin(), iterable.end()); - const std::vector expected(pairs.begin(), - pairs.end()); - - return ::testing::ExplainMatchResult(::testing::ContainerEq(expected), v, - result_listener); -} - -// Type of the argument of SparseDoubleMatrixMatcher. -using Coefficient = std::tuple; -using Coefficients = std::initializer_list; - -// Here `coefficients` must be a Coefficients. -// -// Usage: -// EXPECT_THAT(v, SparseDoubleMatrixMatcher(Coefficients{})); -// EXPECT_THAT(v, SparseDoubleMatrixMatcher(Coefficients{{2, 1, 3.0}, {3, -// 0, 2.0}})); -MATCHER_P(SparseDoubleMatrixMatcher, coefficients, "") { - std::vector v; - for (int i = 0; i < arg.row_ids_size(); ++i) { - v.emplace_back(arg.row_ids(i), arg.column_ids(i), arg.coefficients(i)); - } - const std::vector expected(coefficients.begin(), - coefficients.end()); - - return ::testing::ExplainMatchResult(::testing::ContainerEq(expected), v, - result_listener); -} - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_CORE_SPARSE_COLLECTION_MATCHERS_H_ diff --git a/ortools/math_opt/core/sparse_vector_view.h b/ortools/math_opt/core/sparse_vector_view.h index 7def9dc18f..9ba122143a 100644 --- a/ortools/math_opt/core/sparse_vector_view.h +++ b/ortools/math_opt/core/sparse_vector_view.h @@ -128,9 +128,13 @@ class SparseVectorView { int values_size() const { return values_.size(); } const T& values(int index) const { return values_[index]; } - // It should be possible to construct an IndexType from an integer - template - absl::flat_hash_map as_map(); + // Returns the map corresponding to this sparse vector. + // + // It should be possible to construct KeyType::IdType from an int64_t and + // KeyType from a Storage pointer and the build id. See cpp/key_types.h for + // details. + template + absl::flat_hash_map as_map(const Storage* storage); private: absl::Span ids_; @@ -233,13 +237,15 @@ typename SparseVectorView::const_iterator SparseVectorView::end() const { } template -template -absl::flat_hash_map SparseVectorView::as_map() { - absl::flat_hash_map result; +template +absl::flat_hash_map SparseVectorView::as_map( + const Storage* storage) { + absl::flat_hash_map result; CHECK_EQ(ids_size(), values_size()); result.reserve(ids_size()); for (const auto& [id, value] : *this) { - gtl::InsertOrDie(&result, IndexType(id), value); + gtl::InsertOrDie(&result, KeyType(storage, typename KeyType::IdType(id)), + value); } return result; } diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index 3e1efb6058..4a7677b3c1 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -44,6 +44,7 @@ cc_library( deps = [ ":basis_status", ":linear_constraint", + ":objective", ":variable_and_expressions", "//ortools/base", "//ortools/base:status_macros", @@ -54,9 +55,11 @@ cc_library( "//ortools/math_opt/validators:ids_validator", "//ortools/math_opt/validators:sparse_vector_validator", "//ortools/util:status_macros", + "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/strings", "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", ], ) @@ -67,58 +70,50 @@ cc_library( deps = [ ":key_types", ":linear_constraint", + ":objective", ":update_tracker", ":variable_and_expressions", - "//ortools/base", "//ortools/base:intops", "//ortools/base:status_macros", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", "//ortools/math_opt/constraints/indicator:indicator_constraint", "//ortools/math_opt/constraints/quadratic:quadratic_constraint", + "//ortools/math_opt/constraints/second_order_cone:second_order_cone_constraint", "//ortools/math_opt/constraints/sos:sos1_constraint", "//ortools/math_opt/constraints/sos:sos2_constraint", "//ortools/math_opt/constraints/util:model_util", + "//ortools/math_opt/storage:linear_expression_data", "//ortools/math_opt/storage:model_storage", "//ortools/math_opt/storage:model_storage_types", "//ortools/math_opt/storage:sparse_coefficient_map", "//ortools/math_opt/storage:sparse_matrix", + "//ortools/util:fp_roundtrip_conv", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], ) -cc_library( - name = "id_map", - hdrs = ["id_map.h"], - deps = [ - ":key_types", - "//ortools/base", - "//ortools/base:intops", - "//ortools/math_opt/core:arrow_operator_proxy", - "//ortools/math_opt/storage:model_storage", - "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/container:flat_hash_set", - "@com_google_absl//absl/types:span", - ], -) - cc_library( name = "variable_and_expressions", srcs = ["variable_and_expressions.cc"], hdrs = ["variable_and_expressions.h"], deps = [ ":formatters", - ":id_map", ":key_types", "//ortools/base", "//ortools/base:intops", "//ortools/base:map_util", "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_types", "//ortools/util:fp_roundtrip_conv", "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", ], ) @@ -126,13 +121,14 @@ cc_library( name = "linear_constraint", hdrs = ["linear_constraint.h"], deps = [ - ":id_map", + ":key_types", ":variable_and_expressions", - "//ortools/base", "//ortools/base:intops", "//ortools/math_opt/constraints/util:model_util", "//ortools/math_opt/storage:model_storage", "//ortools/math_opt/storage:model_storage_types", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", ], ) @@ -144,9 +140,9 @@ cc_library( ":basis_status", ":enums", ":linear_constraint", + ":objective", ":sparse_containers", ":variable_and_expressions", - "//ortools/base", "//ortools/base:status_macros", "//ortools/math_opt:result_cc_proto", "//ortools/math_opt:solution_cc_proto", @@ -157,8 +153,10 @@ cc_library( "//ortools/math_opt/validators:sparse_vector_validator", "//ortools/util:status_macros", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_absl//absl/types:optional", "@com_google_absl//absl/types:span", ], ) @@ -194,10 +192,11 @@ cc_library( name = "map_filter", hdrs = ["map_filter.h"], deps = [ - ":id_set", - "//ortools/base:intops", + ":key_types", + "//ortools/base:status_macros", "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/storage:model_storage", + "@com_google_absl//absl/algorithm:container", ], ) @@ -219,6 +218,7 @@ cc_library( "//ortools/math_opt/core:sparse_vector_view", "//ortools/math_opt/storage:model_storage", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", @@ -230,21 +230,12 @@ cc_library( name = "key_types", hdrs = ["key_types.h"], deps = [ - "//ortools/base", "//ortools/math_opt/storage:model_storage", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", - ], -) - -cc_library( - name = "id_set", - hdrs = ["id_set.h"], - deps = [ - ":key_types", - "//ortools/base", - "//ortools/math_opt/core:arrow_operator_proxy", - "//ortools/math_opt/storage:model_storage", - "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/types:span", ], ) @@ -255,12 +246,18 @@ cc_library( deps = [ ":linear_constraint", ":map_filter", + ":model", ":solution", + ":sparse_containers", ":variable_and_expressions", + "//ortools/base:status_macros", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/storage:model_storage", + "//ortools/util:status_macros", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", "@com_google_protobuf//:protobuf", ], ) @@ -322,7 +319,11 @@ cc_library( ":message_callback", ":model_solve_parameters", ":parameters", + "//ortools/base:status_macros", "//ortools/math_opt/core:solve_interrupter", + "//ortools/math_opt/storage:model_storage", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", ], ) @@ -331,22 +332,30 @@ cc_library( srcs = ["solve.cc"], hdrs = ["solve.h"], deps = [ + ":callback", + ":enums", + ":infeasible_subsystem_arguments", + ":infeasible_subsystem_result", ":model", + ":model_solve_parameters", ":parameters", ":solve_arguments", ":solve_result", ":solver_init_arguments", + ":streamable_solver_init_arguments", ":update_result", - "//ortools/base", + ":update_tracker", "//ortools/base:status_macros", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:parameters_cc_proto", "//ortools/math_opt/core:solver", "//ortools/math_opt/storage:model_storage", "//ortools/util:status_macros", - "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/memory", + "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/synchronization", ], ) @@ -367,17 +376,19 @@ cc_library( hdrs = ["parameters.h"], deps = [ ":enums", - "//ortools/base", "//ortools/base:linked_hash_map", "//ortools/base:protoutil", "//ortools/base:status_macros", "//ortools/glop:parameters_cc_proto", "//ortools/gscip:gscip_cc_proto", "//ortools/math_opt:parameters_cc_proto", + "//ortools/math_opt/solvers:glpk_cc_proto", "//ortools/math_opt/solvers:gurobi_cc_proto", "//ortools/port:proto_utils", "//ortools/sat:sat_parameters_cc_proto", "//ortools/util:status_macros", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", @@ -389,7 +400,7 @@ cc_library( name = "enums", hdrs = ["enums.h"], deps = [ - "//ortools/base", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/strings", "@com_google_absl//absl/types:span", ], @@ -416,3 +427,59 @@ cc_library( hdrs = ["update_result.h"], deps = ["//ortools/math_opt:model_update_cc_proto"], ) + +cc_library( + name = "objective", + srcs = ["objective.cc"], + hdrs = ["objective.h"], + deps = [ + ":key_types", + ":variable_and_expressions", + "//ortools/base:intops", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_types", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + ], +) + +cc_library( + name = "infeasible_subsystem_result", + srcs = ["infeasible_subsystem_result.cc"], + hdrs = ["infeasible_subsystem_result.h"], + deps = [ + ":enums", + ":key_types", + ":linear_constraint", + ":solve_result", + ":variable_and_expressions", + "//ortools/base:status_macros", + "//ortools/math_opt:infeasible_subsystem_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt/constraints/indicator:indicator_constraint", + "//ortools/math_opt/constraints/quadratic:quadratic_constraint", + "//ortools/math_opt/constraints/second_order_cone:second_order_cone_constraint", + "//ortools/math_opt/constraints/sos:sos1_constraint", + "//ortools/math_opt/constraints/sos:sos2_constraint", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/validators:infeasible_subsystem_validator", + "//ortools/util:status_macros", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) + +cc_library( + name = "infeasible_subsystem_arguments", + hdrs = ["infeasible_subsystem_arguments.h"], + deps = [ + ":message_callback", + ":parameters", + "//ortools/math_opt/core:solve_interrupter", + ], +) diff --git a/ortools/math_opt/cpp/callback.cc b/ortools/math_opt/cpp/callback.cc index 2344ddf05a..b0fe9f0fa7 100644 --- a/ortools/math_opt/cpp/callback.cc +++ b/ortools/math_opt/cpp/callback.cc @@ -91,13 +91,9 @@ CallbackData::CallbackData(const ModelStorage* storage, absl::Status CallbackRegistration::CheckModelStorage( const ModelStorage* const expected_storage) const { - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/mip_node_filter.storage(), - /*expected_storage=*/expected_storage)) + RETURN_IF_ERROR(mip_node_filter.CheckModelStorage(expected_storage)) << "invalid mip_node_filter"; - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/mip_solution_filter.storage(), - /*expected_storage=*/expected_storage)) + RETURN_IF_ERROR(mip_solution_filter.CheckModelStorage(expected_storage)) << "invalid mip_solution_filter"; return absl::OkStatus(); } @@ -125,9 +121,11 @@ absl::Status CallbackResult::CheckModelStorage( << "invalid new_constraints"; } for (const VariableMap& solution : suggested_solutions) { - RETURN_IF_ERROR(internal::CheckModelStorage( - /*storage=*/solution.storage(), /*expected_storage=*/expected_storage)) - << "invalid suggested_solutions"; + for (const auto& [v, _] : solution) { + RETURN_IF_ERROR(internal::CheckModelStorage( + /*storage=*/v.storage(), /*expected_storage=*/expected_storage)) + << "invalid variable " << v << " in suggested_solutions"; + } } return absl::OkStatus(); } diff --git a/ortools/math_opt/cpp/enums.h b/ortools/math_opt/cpp/enums.h index 85b135b54d..d51038557e 100644 --- a/ortools/math_opt/cpp/enums.h +++ b/ortools/math_opt/cpp/enums.h @@ -83,9 +83,9 @@ #include #include +#include "absl/log/check.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "absl/log/check.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/cpp/id_map.h b/ortools/math_opt/cpp/id_map.h deleted file mode 100644 index 47108ea9e8..0000000000 --- a/ortools/math_opt/cpp/id_map.h +++ /dev/null @@ -1,659 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h" -// IWYU pragma: friend "ortools/math_opt/cpp/.*" - -// A faster version of flat_hash_map for Variable and LinearConstraint keys. -#ifndef OR_TOOLS_MATH_OPT_CPP_ID_MAP_H_ -#define OR_TOOLS_MATH_OPT_CPP_ID_MAP_H_ - -#include -#include -#include -#include -#include - -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/types/span.h" -#include "absl/log/check.h" -#include "ortools/base/strong_int.h" -#include "ortools/math_opt/core/arrow_operator_proxy.h" // IWYU pragma: export -#include "ortools/math_opt/cpp/key_types.h" -#include "ortools/math_opt/storage/model_storage.h" - -namespace operations_research { -namespace math_opt { - -// Similar to a absl::flat_hash_map for K as Variable or LinearConstraint. -// -// Important differences: -// * The storage is more efficient, as we store the underlying ids directly. -// * The consequence of that is that the keys are usually returned by value in -// situations where the flat_hash_map would return references. -// * You cannot mix variables/constraints from multiple models in these maps. -// Doing so results in a CHECK failure. -// -// Implementation notes: -// * Emptying the map (with clear() or erase()) resets the underlying model to -// nullptr, enabling reusing the same instance with a different model. -// * Operator= and swap() support operating with different models by -// respectively replacing or swapping it. -// * For details requirements on K, see key_types.h. -// -// See also IdSet for the equivalent class for sets. -template -class IdMap { - public: - using IdType = typename K::IdType; - using StorageType = absl::flat_hash_map; - using key_type = K; - using mapped_type = V; - using value_type = std::pair; - using size_type = typename StorageType::size_type; - using difference_type = typename StorageType::difference_type; - using reference = std::pair; - using const_reference = std::pair; - using pointer = void; - using const_pointer = void; - - class iterator { - public: - using value_type = IdMap::value_type; - using reference = IdMap::reference; - using pointer = IdMap::pointer; - using difference_type = IdMap::difference_type; - using iterator_category = std::forward_iterator_tag; - - iterator() = default; - - inline reference operator*() const; - inline internal::ArrowOperatorProxy operator->() const; - inline iterator& operator++(); - inline iterator operator++(int); - - friend bool operator==(const iterator& lhs, const iterator& rhs) { - return lhs.storage_iterator_ == rhs.storage_iterator_; - } - friend bool operator!=(const iterator& lhs, const iterator& rhs) { - return lhs.storage_iterator_ != rhs.storage_iterator_; - } - - private: - friend class IdMap; - - inline iterator(const IdMap* map, - typename StorageType::iterator storage_iterator); - - const IdMap* map_ = nullptr; - typename StorageType::iterator storage_iterator_; - }; - - class const_iterator { - public: - using value_type = IdMap::value_type; - using reference = IdMap::const_reference; - using pointer = IdMap::const_pointer; - using difference_type = IdMap::difference_type; - using iterator_category = std::forward_iterator_tag; - - const_iterator() = default; - inline const_iterator(const iterator& non_const_iterator); // NOLINT - - inline reference operator*() const; - inline internal::ArrowOperatorProxy operator->() const; - inline const_iterator& operator++(); - inline const_iterator operator++(int); - - friend bool operator==(const const_iterator& lhs, - const const_iterator& rhs) { - return lhs.storage_iterator_ == rhs.storage_iterator_; - } - friend bool operator!=(const const_iterator& lhs, - const const_iterator& rhs) { - return lhs.storage_iterator_ != rhs.storage_iterator_; - } - - private: - friend class IdMap; - - inline const_iterator( - const IdMap* map, - typename StorageType::const_iterator storage_iterator); - - const IdMap* map_ = nullptr; - typename StorageType::const_iterator storage_iterator_; - }; - - IdMap() = default; - template - inline IdMap(InputIt first, InputIt last); - inline IdMap(std::initializer_list ilist); - - // Typically for internal use only. - inline IdMap(const ModelStorage* storage, StorageType values); - - inline const_iterator cbegin() const; - inline const_iterator begin() const; - inline iterator begin(); - - inline const_iterator cend() const; - inline const_iterator end() const; - inline iterator end(); - - bool empty() const { return map_.empty(); } - size_type size() const { return map_.size(); } - inline void clear(); - void reserve(size_type count) { map_.reserve(count); } - - inline std::pair insert(std::pair k_v); - template - inline void insert(InputIt first, InputIt last); - inline void insert(std::initializer_list ilist); - - template - inline std::pair insert_or_assign(const K& k, M&& v); - - inline std::pair emplace(const K& k, V v); - template - inline std::pair try_emplace(const K& k, Args&&... args); - - // Returns the number of elements erased (zero or one). - inline size_type erase(const K& k); - // In STL erase(const_iterator) and erase(iterator) both return an - // iterator. But flat_hash_map instead has void return types. So here we also - // use void. - // - // In flat_hash_map, both erase(const_iterator) and erase(iterator) are - // defined since there is also the erase(const K&) that exists and that - // would be used. Since we don't have this overload, we can rely on the - // automatic cast of the iterator in const_iterator. - inline void erase(const_iterator pos); - inline iterator erase(const_iterator first, const_iterator last); - - inline void swap(IdMap& other); - - inline const V& at(const K& k) const; - inline V& at(const K& k); - inline V& operator[](const K& k); - inline size_type count(const K& k) const; - inline bool contains(const K& k) const; - inline iterator find(const K& k); - inline const_iterator find(const K& k) const; - inline std::pair equal_range(const K& k); - inline std::pair equal_range( - const K& k) const; - - // Updates the values in this map by adding the value of the corresponding - // keys in the other map. For keys only in the other map, insert their value. - // - // This function is only available when type V supports operator+=. - // - // This is equivalent to (but is more efficient than): - // for (const auto pair : other) { - // (*this)[pair.first] += pair.second; - // } - // - // This function CHECK that all the keys in the two maps have the same model. - inline void Add(const IdMap& other); - - // Updates the values in this map by subtracting the value of the - // corresponding keys in the other map. For keys only in the other map, insert - // the opposite of their value. - // - // This function is only available when type V supports operator-=. - // - // This is equivalent to (but is more efficient than): - // for (const auto pair : other) { - // (*this)[pair.first] -= pair.second; - // } - // - // This function CHECK that all the keys in the two maps have the same model. - inline void Subtract(const IdMap& other); - - inline std::vector Values(absl::Span keys) const; - inline absl::flat_hash_map Values( - const absl::flat_hash_set& keys) const; - - inline std::vector SortedKeys() const; - - // Returns the values in sorted KEY order. - inline std::vector SortedValues() const; - - const StorageType& raw_map() const { return map_; } - const ModelStorage* storage() const { return storage_; } - - friend bool operator==(const IdMap& lhs, const IdMap& rhs) { - return lhs.storage_ == rhs.storage_ && lhs.map_ == rhs.map_; - } - friend bool operator!=(const IdMap& lhs, const IdMap& rhs) { - return !(lhs == rhs); - } - - private: - inline std::vector SortedIds() const; - // CHECKs that storage_ and k.storage() matches when this map is not empty - // (i.e. its storage_ is not null). When it is empty, simply check that - // k.storage() is not null. - inline void CheckModel(const K& k) const; - // Sets storage_ to k.storage() if this map is empty (i.e. its storage_ is - // null). Else CHECK that it has the same model. It also CHECK that - // k.storage() is not null. - inline void CheckOrSetModel(const K& k); - // Sets storage_ to other.storage_ if this map is empty (i.e. its storage_ is - // null). Else if the other map is not empty, CHECK that it has the same - // model. - inline void CheckOrSetModel(const IdMap& other); - - // Invariant: storage == nullptr if and only if map_.empty(). - const ModelStorage* storage_ = nullptr; - StorageType map_; -}; - -// Calls a.swap(b). -// -// This function is used for making MapId "swappable". -// Ref: https://en.cppreference.com/w/cpp/named_req/Swappable. -template -void swap(IdMap& a, IdMap& b) { - a.swap(b); -} - -//////////////////////////////////////////////////////////////////////////////// -// Inline implementations -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -// IdMap::iterator -//////////////////////////////////////////////////////////////////////////////// - -template -typename IdMap::reference IdMap::iterator::operator*() const { - return reference(K(map_->storage_, storage_iterator_->first), - storage_iterator_->second); -} - -template -internal::ArrowOperatorProxy::iterator::reference> -IdMap::iterator::operator->() const { - return internal::ArrowOperatorProxy(**this); -} - -template -typename IdMap::iterator& IdMap::iterator::operator++() { - ++storage_iterator_; - return *this; -} - -template -typename IdMap::iterator IdMap::iterator::operator++(int) { - iterator ret = *this; - ++(*this); - return ret; -} - -template -IdMap::iterator::iterator(const IdMap* map, - typename StorageType::iterator storage_iterator) - : map_(map), storage_iterator_(std::move(storage_iterator)) {} - -//////////////////////////////////////////////////////////////////////////////// -// IdMap::const_iterator -//////////////////////////////////////////////////////////////////////////////// - -template -IdMap::const_iterator::const_iterator(const iterator& non_const_iterator) - : map_(non_const_iterator.map_), - storage_iterator_(non_const_iterator.storage_iterator_) {} - -template -typename IdMap::const_iterator::reference -IdMap::const_iterator::operator*() const { - return reference(K(map_->storage_, storage_iterator_->first), - storage_iterator_->second); -} - -template -internal::ArrowOperatorProxy::const_iterator::reference> -IdMap::const_iterator::operator->() const { - return internal::ArrowOperatorProxy(**this); -} - -template -typename IdMap::const_iterator& -IdMap::const_iterator::operator++() { - ++storage_iterator_; - return *this; -} - -template -typename IdMap::const_iterator IdMap::const_iterator::operator++( - int) { - const_iterator ret = *this; - ++(*this); - return ret; -} - -template -IdMap::const_iterator::const_iterator( - const IdMap* map, typename StorageType::const_iterator storage_iterator) - : map_(map), storage_iterator_(std::move(storage_iterator)) {} - -//////////////////////////////////////////////////////////////////////////////// -// IdMap -//////////////////////////////////////////////////////////////////////////////// - -template -IdMap::IdMap(const ModelStorage* storage, StorageType values) - : storage_(values.empty() ? nullptr : storage), map_(std::move(values)) { - if (!map_.empty()) { - CHECK(storage_ != nullptr); - } -} - -template -template -IdMap::IdMap(InputIt first, InputIt last) { - insert(first, last); -} - -template -IdMap::IdMap(std::initializer_list ilist) { - insert(ilist); -} - -template -typename IdMap::const_iterator IdMap::cbegin() const { - return const_iterator(this, map_.cbegin()); -} - -template -typename IdMap::const_iterator IdMap::begin() const { - return cbegin(); -} - -template -typename IdMap::iterator IdMap::begin() { - return iterator(this, map_.begin()); -} - -template -typename IdMap::const_iterator IdMap::cend() const { - return const_iterator(this, map_.cend()); -} - -template -typename IdMap::const_iterator IdMap::end() const { - return cend(); -} - -template -typename IdMap::iterator IdMap::end() { - return iterator(this, map_.end()); -} - -template -void IdMap::clear() { - storage_ = nullptr; - map_.clear(); -} - -template -std::pair::iterator, bool> IdMap::insert( - std::pair k_v) { - return emplace(k_v.first, std::move(k_v.second)); -} - -template -template -void IdMap::insert(const InputIt first, const InputIt last) { - for (InputIt it = first; it != last; ++it) { - insert(*it); - } -} - -template -void IdMap::insert(std::initializer_list ilist) { - insert(ilist.begin(), ilist.end()); -} - -template -template -std::pair::iterator, bool> IdMap::insert_or_assign( - const K& k, M&& v) { - CheckOrSetModel(k); - auto initial_ret = map_.insert_or_assign(k.typed_id(), std::forward(v)); - return std::make_pair(iterator(this, std::move(initial_ret.first)), - initial_ret.second); -} - -template -std::pair::iterator, bool> IdMap::emplace(const K& k, - V v) { - CheckOrSetModel(k); - auto initial_ret = map_.emplace(k.typed_id(), std::move(v)); - return std::make_pair(iterator(this, std::move(initial_ret.first)), - initial_ret.second); -} - -template -template -std::pair::iterator, bool> IdMap::try_emplace( - const K& k, Args&&... args) { - CheckOrSetModel(k); - auto initial_ret = - map_.try_emplace(k.typed_id(), std::forward(args)...); - return std::make_pair(iterator(this, std::move(initial_ret.first)), - initial_ret.second); -} - -template -typename IdMap::size_type IdMap::erase(const K& k) { - CheckModel(k); - const size_type ret = map_.erase(k.typed_id()); - if (map_.empty()) { - storage_ = nullptr; - } - return ret; -} - -template -void IdMap::erase(const const_iterator pos) { - map_.erase(pos.storage_iterator_); - if (map_.empty()) { - storage_ = nullptr; - } -} - -template -typename IdMap::iterator IdMap::erase(const const_iterator first, - const const_iterator last) { - auto ret = map_.erase(first.storage_iterator_, last.storage_iterator_); - if (map_.empty()) { - storage_ = nullptr; - } - return iterator(this, std::move(ret)); -} - -template -void IdMap::swap(IdMap& other) { - using std::swap; - swap(storage_, other.storage_); - swap(map_, other.map_); -} - -template -const V& IdMap::at(const K& k) const { - CheckModel(k); - return map_.at(k.typed_id()); -} - -template -V& IdMap::at(const K& k) { - CheckModel(k); - return map_.at(k.typed_id()); -} - -template -V& IdMap::operator[](const K& k) { - CheckOrSetModel(k); - return map_[k.typed_id()]; -} - -template -typename IdMap::size_type IdMap::count(const K& k) const { - CheckModel(k); - return map_.count(k.typed_id()); -} - -template -bool IdMap::contains(const K& k) const { - CheckModel(k); - return map_.contains(k.typed_id()); -} - -template -typename IdMap::iterator IdMap::find(const K& k) { - CheckModel(k); - return iterator(this, map_.find(k.typed_id())); -} - -template -typename IdMap::const_iterator IdMap::find(const K& k) const { - CheckModel(k); - return const_iterator(this, map_.find(k.typed_id())); -} - -template -std::pair::iterator, typename IdMap::iterator> -IdMap::equal_range(const K& k) { - const auto it = find(k); - if (it == end()) { - return {it, it}; - } - return {it, std::next(it)}; -} - -template -std::pair::const_iterator, - typename IdMap::const_iterator> -IdMap::equal_range(const K& k) const { - const auto it = find(k); - if (it == end()) { - return {it, it}; - } - return {it, std::next(it)}; -} - -template -void IdMap::Add(const IdMap& other) { - CheckOrSetModel(other); - for (const auto& pair : other.map_) { - map_[pair.first] += pair.second; - } -} - -template -void IdMap::Subtract(const IdMap& other) { - CheckOrSetModel(other); - for (const auto& pair : other.map_) { - map_[pair.first] -= pair.second; - } -} - -template -std::vector IdMap::Values(const absl::Span keys) const { - std::vector result; - result.reserve(keys.size()); - for (const K key : keys) { - result.push_back(at(key)); - } - return result; -} - -template -absl::flat_hash_map IdMap::Values( - const absl::flat_hash_set& keys) const { - absl::flat_hash_map result; - for (const K key : keys) { - result[key] = at(key); - } - return result; -} - -template -std::vector IdMap::SortedKeys() const { - std::vector result; - result.reserve(map_.size()); - for (const IdType id : SortedIds()) { - result.push_back(K(storage_, id)); - } - return result; -} - -template -std::vector IdMap::SortedValues() const { - std::vector result; - result.reserve(map_.size()); - for (const IdType id : SortedIds()) { - result.push_back(map_.at(id)); - } - return result; -} - -template -std::vector IdMap::SortedIds() const { - std::vector result; - result.reserve(map_.size()); - for (const auto& [id, _] : map_) { - result.push_back(id); - } - std::sort(result.begin(), result.end()); - return result; -} - -template -void IdMap::CheckModel(const K& k) const { - CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage; - CHECK(storage_ == nullptr || storage_ == k.storage()) - << internal::kObjectsFromOtherModelStorage; -} - -template -void IdMap::CheckOrSetModel(const K& k) { - CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage; - if (storage_ == nullptr) { - storage_ = k.storage(); - } else { - CHECK_EQ(storage_, k.storage()) << internal::kObjectsFromOtherModelStorage; - } -} - -template -void IdMap::CheckOrSetModel(const IdMap& other) { - if (storage_ == nullptr) { - storage_ = other.storage_; - } else if (other.storage_ != nullptr) { - CHECK_EQ(storage_, other.storage_) - << internal::kObjectsFromOtherModelStorage; - } else { - // By construction when other is not empty, it has a non null `storage_`. - DCHECK(other.empty()); - } -} - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_CPP_ID_MAP_H_ diff --git a/ortools/math_opt/cpp/id_set.h b/ortools/math_opt/cpp/id_set.h deleted file mode 100644 index 902b720d44..0000000000 --- a/ortools/math_opt/cpp/id_set.h +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h" -// IWYU pragma: friend "ortools/math_opt/cpp/.*" - -#ifndef OR_TOOLS_MATH_OPT_CPP_ID_SET_H_ -#define OR_TOOLS_MATH_OPT_CPP_ID_SET_H_ - -#include -#include -#include - -#include "absl/container/flat_hash_set.h" -#include "absl/log/check.h" -#include "ortools/math_opt/core/arrow_operator_proxy.h" -#include "ortools/math_opt/cpp/key_types.h" -#include "ortools/math_opt/storage/model_storage.h" - -namespace operations_research { -namespace math_opt { - -// Similar to a absl::flat_hash_set for K as Variable or LinearConstraint. -// -// Important differences: -// * The storage is more efficient, as we store the underlying ids directly. -// * The consequence of that is that the keys are usually returned by value in -// situations where the flat_hash_set would return references. -// * You cannot mix variables/constraints from multiple models in these maps. -// Doing so results in a CHECK failure. -// -// Implementation notes: -// * Emptying the set (with clear() or erase()) resets the underlying model to -// nullptr, enabling reusing the same instance with a different model. -// * Operator= and swap() support operating with different models by -// respectively replacing or swapping it. -// * For details requirements on K, see key_types.h. -// -// See also IdMap for the equivalent class for maps. -template -class IdSet { - public: - using IdType = typename K::IdType; - using StorageType = absl::flat_hash_set; - using key_type = K; - using value_type = key_type; - using size_type = typename StorageType::size_type; - using difference_type = typename StorageType::difference_type; - using reference = K; - using const_reference = const K; - using pointer = void; - using const_pointer = void; - - class const_iterator { - public: - using value_type = IdSet::value_type; - using reference = IdSet::const_reference; - using pointer = IdSet::const_pointer; - using difference_type = IdSet::difference_type; - using iterator_category = std::forward_iterator_tag; - - const_iterator() = default; - - inline const_reference operator*() const; - inline internal::ArrowOperatorProxy operator->() const; - inline const_iterator& operator++(); - inline const_iterator operator++(int); - - friend bool operator==(const const_iterator& lhs, - const const_iterator& rhs) { - return lhs.storage_iterator_ == rhs.storage_iterator_; - } - friend bool operator!=(const const_iterator& lhs, - const const_iterator& rhs) { - return lhs.storage_iterator_ != rhs.storage_iterator_; - } - - private: - friend class IdSet; - - inline const_iterator( - const IdSet* set, - typename StorageType::const_iterator storage_iterator); - - const IdSet* set_ = nullptr; - typename StorageType::const_iterator storage_iterator_; - }; - - // All iterators on sets are const; but STL still defines the `iterator` - // type. The `flat_hash_set` defines two classes the but the policy makes both - // constant. Here to simplify the code we use the same type. - using iterator = const_iterator; - - IdSet() = default; - template - inline IdSet(InputIt first, InputIt last); - inline IdSet(std::initializer_list ilist); - - // Typically for internal use only. - inline IdSet(const ModelStorage* storage, StorageType values); - - inline const_iterator cbegin() const; - inline const_iterator begin() const; - - inline const_iterator cend() const; - inline const_iterator end() const; - - bool empty() const { return set_.empty(); } - size_type size() const { return set_.size(); } - inline void clear(); - void reserve(size_type count) { set_.reserve(count); } - - inline std::pair insert(const K& k); - template - inline void insert(InputIt first, InputIt last); - inline void insert(std::initializer_list ilist); - - inline std::pair emplace(const K& k); - - // Returns the number of elements erased (zero or one). - inline size_type erase(const K& k); - // In STL erase(const_iterator) returns an iterator. But flat_hash_set instead - // has void return types. So here we also use void. - inline void erase(const_iterator pos); - inline const_iterator erase(const_iterator first, const_iterator last); - - inline void swap(IdSet& other); - - inline size_type count(const K& k) const; - inline bool contains(const K& k) const; - inline const_iterator find(const K& k) const; - inline std::pair equal_range( - const K& k) const; - - const StorageType& raw_set() const { return set_; } - const ModelStorage* storage() const { return storage_; } - - friend bool operator==(const IdSet& lhs, const IdSet& rhs) { - return lhs.storage_ == rhs.storage_ && lhs.set_ == rhs.set_; - } - friend bool operator!=(const IdSet& lhs, const IdSet& rhs) { - return !(lhs == rhs); - } - - private: - // CHECKs that storage_ and k.storage() matches when this set is not empty - // (i.e. its storage_ is not null). When it is empty, simply check that - // k.storage() is not null. - inline void CheckModel(const K& k) const; - // Sets storage_ to k.storage() if this set is empty (i.e. its storage_ is - // null). Else CHECK that it has the same storage. It also CHECK that - // k.storage() is not null. - inline void CheckOrSetModel(const K& k); - - // Invariant: storage == nullptr if and only if set_.empty(). - const ModelStorage* storage_ = nullptr; - StorageType set_; -}; - -// Calls a.swap(b). -// -// This function is used for making IdSet "swappable". -// Ref: https://en.cppreference.com/w/cpp/named_req/Swappable. -template -void swap(IdSet& a, IdSet& b) { - a.swap(b); -} - -//////////////////////////////////////////////////////////////////////////////// -// Inline implementations -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -// IdSet::const_iterator -//////////////////////////////////////////////////////////////////////////////// - -template -typename IdSet::const_iterator::reference -IdSet::const_iterator::operator*() const { - return K(set_->storage_, *storage_iterator_); -} - -template -internal::ArrowOperatorProxy::const_iterator::reference> -IdSet::const_iterator::operator->() const { - return internal::ArrowOperatorProxy(**this); -} - -template -typename IdSet::const_iterator& IdSet::const_iterator::operator++() { - ++storage_iterator_; - return *this; -} - -template -typename IdSet::const_iterator IdSet::const_iterator::operator++(int) { - const_iterator ret = *this; - ++(*this); - return ret; -} - -template -IdSet::const_iterator::const_iterator( - const IdSet* set, typename StorageType::const_iterator storage_iterator) - : set_(set), storage_iterator_(std::move(storage_iterator)) {} - -//////////////////////////////////////////////////////////////////////////////// -// IdSet -//////////////////////////////////////////////////////////////////////////////// - -template -IdSet::IdSet(const ModelStorage* storage, StorageType values) - : storage_(values.empty() ? nullptr : storage), set_(std::move(values)) { - if (!set_.empty()) { - CHECK(storage_ != nullptr); - } -} - -template -template -IdSet::IdSet(InputIt first, InputIt last) { - insert(first, last); -} - -template -IdSet::IdSet(std::initializer_list ilist) { - insert(ilist); -} - -template -typename IdSet::const_iterator IdSet::cbegin() const { - return const_iterator(this, set_.cbegin()); -} - -template -typename IdSet::const_iterator IdSet::begin() const { - return cbegin(); -} - -template -typename IdSet::const_iterator IdSet::cend() const { - return const_iterator(this, set_.cend()); -} - -template -typename IdSet::const_iterator IdSet::end() const { - return cend(); -} - -template -void IdSet::clear() { - storage_ = nullptr; - set_.clear(); -} - -template -std::pair::const_iterator, bool> IdSet::insert( - const K& k) { - return emplace(k); -} - -template -template -void IdSet::insert(const InputIt first, const InputIt last) { - for (InputIt it = first; it != last; ++it) { - insert(*it); - } -} - -template -void IdSet::insert(std::initializer_list ilist) { - insert(ilist.begin(), ilist.end()); -} - -template -std::pair::const_iterator, bool> IdSet::emplace( - const K& k) { - CheckOrSetModel(k); - auto initial_ret = set_.emplace(k.typed_id()); - return std::make_pair(const_iterator(this, std::move(initial_ret.first)), - initial_ret.second); -} - -template -typename IdSet::size_type IdSet::erase(const K& k) { - CheckModel(k); - const size_type ret = set_.erase(k.typed_id()); - if (set_.empty()) { - storage_ = nullptr; - } - return ret; -} - -template -void IdSet::erase(const const_iterator pos) { - set_.erase(pos.storage_iterator_); - if (set_.empty()) { - storage_ = nullptr; - } -} - -template -typename IdSet::const_iterator IdSet::erase(const const_iterator first, - const const_iterator last) { - auto ret = set_.erase(first.storage_iterator_, last.storage_iterator_); - if (set_.empty()) { - storage_ = nullptr; - } - return const_iterator(this, std::move(ret)); -} - -template -void IdSet::swap(IdSet& other) { - using std::swap; - swap(storage_, other.storage_); - swap(set_, other.set_); -} - -template -typename IdSet::size_type IdSet::count(const K& k) const { - CheckModel(k); - return set_.count(k.typed_id()); -} - -template -bool IdSet::contains(const K& k) const { - CheckModel(k); - return set_.contains(k.typed_id()); -} - -template -typename IdSet::const_iterator IdSet::find(const K& k) const { - CheckModel(k); - return const_iterator(this, set_.find(k.typed_id())); -} - -template -std::pair::const_iterator, typename IdSet::const_iterator> -IdSet::equal_range(const K& k) const { - const auto it = find(k); - if (it == end()) { - return {it, it}; - } - return {it, std::next(it)}; -} - -template -void IdSet::CheckModel(const K& k) const { - CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage; - CHECK(storage_ == nullptr || storage_ == k.storage()) - << internal::kObjectsFromOtherModelStorage; -} - -template -void IdSet::CheckOrSetModel(const K& k) { - CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage; - if (storage_ == nullptr) { - storage_ = k.storage(); - } else { - CHECK_EQ(storage_, k.storage()) << internal::kObjectsFromOtherModelStorage; - } -} - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_CPP_ID_SET_H_ diff --git a/ortools/math_opt/cpp/infeasible_subsystem_arguments.h b/ortools/math_opt/cpp/infeasible_subsystem_arguments.h new file mode 100644 index 0000000000..8fb9e660f6 --- /dev/null +++ b/ortools/math_opt/cpp/infeasible_subsystem_arguments.h @@ -0,0 +1,67 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ +#define OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ + +#include "ortools/math_opt/core/solve_interrupter.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/message_callback.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/parameters.h" // IWYU pragma: export + +namespace operations_research::math_opt { + +// Arguments passed to InfeasibleSubsystem() to control the solver. +struct InfeasibleSubsystemArguments { + // Model independent parameters, e.g. time limit. + SolveParameters parameters; + + // An optional callback for messages emitted by the solver. + // + // When set it enables the solver messages and ignores the `enable_output` in + // solve parameters; messages are redirected to the callback and not printed + // on stdout/stderr/logs anymore. + // + // See PrinterMessageCallback() for logging to stdout/stderr. + // + // Usage: + // + // // To print messages to stdout with a prefix. + // ASSIGN_OR_RETURN( + // const InfeasibleSubsystemResult result, + // InfeasibleSubsystem(model, SolverType::kGurobi, + // { .message_callback = PrinterMessageCallback(std::cout, + // "logs| "); }); + MessageCallback message_callback = nullptr; + + // An optional interrupter that the solver can use to interrupt the solve + // early. + // + // Usage: + // auto interrupter = std::make_shared(); + // + // // Use another thread to trigger the interrupter. + // RunInOtherThread([interrupter](){ + // ... wait for something that should interrupt the solve ... + // interrupter->Interrupt(); + // }); + // + // ASSIGN_OR_RETURN(const InfeasibleSubsystemResult result, + // InfeasibleSubsystem(model, SolverType::kGurobi, + // { .interrupter = interrupter.get() }); + // + SolveInterrupter* interrupter = nullptr; +}; + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ diff --git a/ortools/math_opt/cpp/infeasible_subsystem_result.cc b/ortools/math_opt/cpp/infeasible_subsystem_result.cc new file mode 100644 index 0000000000..f7e4538cfe --- /dev/null +++ b/ortools/math_opt/cpp/infeasible_subsystem_result.cc @@ -0,0 +1,305 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/cpp/infeasible_subsystem_result.h" + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/enums.h" +#include "ortools/math_opt/cpp/key_types.h" +#include "ortools/math_opt/cpp/solve_result.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/validators/infeasible_subsystem_validator.h" +#include "ortools/util/status_macros.h" + +namespace operations_research::math_opt { + +ModelSubset::Bounds ModelSubset::Bounds::FromProto( + const ModelSubsetProto::Bounds& bounds_proto) { + return {.lower = bounds_proto.lower(), .upper = bounds_proto.upper()}; +} + +ModelSubsetProto::Bounds ModelSubset::Bounds::Proto() const { + ModelSubsetProto::Bounds proto; + proto.set_lower(lower); + proto.set_upper(upper); + return proto; +} + +std::ostream& operator<<(std::ostream& out, const ModelSubset::Bounds& bounds) { + const auto bool_to_str = [](const bool b) { return b ? "true" : "false"; }; + out << "{lower: " << bool_to_str(bounds.lower) + << ", upper: " << bool_to_str(bounds.upper) << "}"; + return out; +} + +namespace { + +template +absl::Status BoundsMapProtoToCpp( + const google::protobuf::Map& source, + absl::flat_hash_map& target, + const ModelStorage* const model, + bool (ModelStorage::*const contains_strong_id)(typename K::IdType id) const, + const absl::string_view object_name) { + for (const auto& [raw_id, bounds_proto] : source) { + const typename K::IdType strong_id(raw_id); + if (!(model->*contains_strong_id)(strong_id)) { + return util::InvalidArgumentErrorBuilder() + << "no " << object_name << " with id: " << raw_id; + } + target.insert( + {K(model, strong_id), ModelSubset::Bounds::FromProto(bounds_proto)}); + } + return absl::OkStatus(); +} + +template +absl::Status RepeatedIdsProtoToCpp( + const google::protobuf::RepeatedField& source, + absl::flat_hash_set& target, const ModelStorage* const model, + bool (ModelStorage::*const contains_strong_id)(typename K::IdType id) const, + const absl::string_view object_name) { + for (const int64_t raw_id : source) { + const typename K::IdType strong_id(raw_id); + if (!(model->*contains_strong_id)(strong_id)) { + return util::InvalidArgumentErrorBuilder() + << "no " << object_name << " with id: " << raw_id; + } + target.insert(K(model, strong_id)); + } + return absl::OkStatus(); +} + +template +google::protobuf::Map BoundsMapCppToProto( + const absl::flat_hash_map source) { + google::protobuf::Map result; + for (const auto& [key, bounds] : source) { + result.insert({key.id(), bounds.Proto()}); + } + return result; +} + +template +google::protobuf::RepeatedField RepeatedIdsCppToProto( + const absl::flat_hash_set& source) { + google::protobuf::RepeatedField result; + for (const auto object : source) { + result.Add(object.id()); + } + absl::c_sort(result); + return result; +} + +} // namespace + +absl::StatusOr ModelSubset::FromProto( + const ModelStorage* const model, const ModelSubsetProto& proto) { + ModelSubset model_subset; + RETURN_IF_ERROR(BoundsMapProtoToCpp(proto.variable_bounds(), + model_subset.variable_bounds, model, + &ModelStorage::has_variable, "variable")) + << "element of variable_bounds"; + RETURN_IF_ERROR(RepeatedIdsProtoToCpp( + proto.variable_integrality(), model_subset.variable_integrality, model, + &ModelStorage::has_variable, "variable")) + << "element of variable_integrality"; + RETURN_IF_ERROR(BoundsMapProtoToCpp( + proto.linear_constraints(), model_subset.linear_constraints, model, + &ModelStorage::has_linear_constraint, "linear constraint")); + RETURN_IF_ERROR(BoundsMapProtoToCpp( + proto.quadratic_constraints(), model_subset.quadratic_constraints, model, + &ModelStorage::has_constraint, "quadratic constraint")); + RETURN_IF_ERROR(RepeatedIdsProtoToCpp( + proto.second_order_cone_constraints(), + model_subset.second_order_cone_constraints, model, + &ModelStorage::has_constraint, "second-order cone constraint")); + RETURN_IF_ERROR(RepeatedIdsProtoToCpp( + proto.sos1_constraints(), model_subset.sos1_constraints, model, + &ModelStorage::has_constraint, "SOS1 constraint")); + RETURN_IF_ERROR(RepeatedIdsProtoToCpp( + proto.sos2_constraints(), model_subset.sos2_constraints, model, + &ModelStorage::has_constraint, "SOS2 constraint")); + RETURN_IF_ERROR(RepeatedIdsProtoToCpp( + proto.indicator_constraints(), model_subset.indicator_constraints, model, + &ModelStorage::has_constraint, "indicator constraint")); + return model_subset; +} + +ModelSubsetProto ModelSubset::Proto() const { + ModelSubsetProto proto; + *proto.mutable_variable_bounds() = BoundsMapCppToProto(variable_bounds); + *proto.mutable_variable_integrality() = + RepeatedIdsCppToProto(variable_integrality); + *proto.mutable_linear_constraints() = BoundsMapCppToProto(linear_constraints); + *proto.mutable_quadratic_constraints() = + BoundsMapCppToProto(quadratic_constraints); + *proto.mutable_second_order_cone_constraints() = + RepeatedIdsCppToProto(second_order_cone_constraints); + *proto.mutable_sos1_constraints() = RepeatedIdsCppToProto(sos1_constraints); + *proto.mutable_sos2_constraints() = RepeatedIdsCppToProto(sos2_constraints); + *proto.mutable_indicator_constraints() = + RepeatedIdsCppToProto(indicator_constraints); + return proto; +} + +absl::Status ModelSubset::CheckModelStorage( + const ModelStorage* const expected_storage) const { + const auto validate_map_keys = + [expected_storage](const auto& map, + const absl::string_view name) -> absl::Status { + for (const auto& [key, unused] : map) { + RETURN_IF_ERROR( + internal::CheckModelStorage(key.storage(), expected_storage)) + << "invalid key " << key << " in " << name; + } + return absl::OkStatus(); + }; + const auto validate_set_elements = + [expected_storage](const auto& set, + const absl::string_view name) -> absl::Status { + for (const auto entry : set) { + RETURN_IF_ERROR( + internal::CheckModelStorage(entry.storage(), expected_storage)) + << "invalid entry " << entry << " in " << name; + } + return absl::OkStatus(); + }; + + RETURN_IF_ERROR(validate_map_keys(variable_bounds, "variable_bounds")); + RETURN_IF_ERROR( + validate_set_elements(variable_integrality, "variable_integrality")); + RETURN_IF_ERROR(validate_map_keys(linear_constraints, "linear_constraints")); + RETURN_IF_ERROR( + validate_map_keys(quadratic_constraints, "quadratic_constraints")); + RETURN_IF_ERROR(validate_set_elements(second_order_cone_constraints, + "second_order_cone_constraints")); + RETURN_IF_ERROR(validate_set_elements(sos1_constraints, "sos1_constraints")); + RETURN_IF_ERROR(validate_set_elements(sos2_constraints, "sos2_constraints")); + RETURN_IF_ERROR( + validate_set_elements(indicator_constraints, "indicator_constraints")); + return absl::OkStatus(); +} + +bool ModelSubset::empty() const { + return variable_bounds.empty() && variable_integrality.empty() && + linear_constraints.empty() && quadratic_constraints.empty() && + second_order_cone_constraints.empty() && sos1_constraints.empty() && + sos2_constraints.empty() && indicator_constraints.empty(); +} + +std::ostream& operator<<(std::ostream& out, const ModelSubset& model_subset) { + const auto stream_bounds_map = [&out](const auto& map, + const absl::string_view name) { + out << name << ": {" + << absl::StrJoin(SortedKeys(map), ", ", + [map](std::string* out, const auto& key) { + absl::StrAppendFormat( + out, "{%s, %s}", absl::FormatStreamed(key), + absl::FormatStreamed(map.at(key))); + }) + << "}"; + }; + const auto stream_set = [&out](const auto& set, + const absl::string_view name) { + out << name << ": {" + << absl::StrJoin(SortedElements(set), ", ", absl::StreamFormatter()) + << "}"; + }; + + out << "{"; + stream_bounds_map(model_subset.variable_bounds, "variable_bounds"); + out << ", "; + stream_set(model_subset.variable_integrality, "variable_integrality"); + out << ", "; + stream_bounds_map(model_subset.linear_constraints, "linear_constraints"); + out << ", "; + stream_bounds_map(model_subset.quadratic_constraints, + "quadratic_constraints"); + out << ", "; + stream_set(model_subset.second_order_cone_constraints, + "second_order_cone_constraints"); + out << ", "; + stream_set(model_subset.sos1_constraints, "sos1_constraints"); + out << ", "; + stream_set(model_subset.sos2_constraints, "sos2_constraints"); + out << ", "; + stream_set(model_subset.indicator_constraints, "indicator_constraints"); + out << "}"; + return out; +} + +absl::StatusOr InfeasibleSubsystemResult::FromProto( + const ModelStorage* const model, + const InfeasibleSubsystemResultProto& result_proto) { + InfeasibleSubsystemResult result; + const std::optional feasibility = + EnumFromProto(result_proto.feasibility()); + if (!feasibility.has_value()) { + return absl::InvalidArgumentError( + "InfeasibleSubsystemResultProto.feasibility must be specified"); + } + // We intentionally call this validator after checking `feasibility` so that + // we can return a friendlier message for UNSPECIFIED. + RETURN_IF_ERROR(ValidateInfeasibleSubsystemResultNoModel(result_proto)); + result.feasibility = *feasibility; + OR_ASSIGN_OR_RETURN3( + result.infeasible_subsystem, + ModelSubset::FromProto(model, result_proto.infeasible_subsystem()), + _ << "invalid InfeasibleSubsystemResultProto.infeasible_subsystem"); + result.is_minimal = result_proto.is_minimal(); + return result; +} + +InfeasibleSubsystemResultProto InfeasibleSubsystemResult::Proto() const { + InfeasibleSubsystemResultProto proto; + proto.set_feasibility(EnumToProto(feasibility)); + if (!infeasible_subsystem.empty()) { + *proto.mutable_infeasible_subsystem() = infeasible_subsystem.Proto(); + } + proto.set_is_minimal(is_minimal); + return proto; +} + +absl::Status InfeasibleSubsystemResult::CheckModelStorage( + const ModelStorage* const expected_storage) const { + return infeasible_subsystem.CheckModelStorage(expected_storage); +} + +std::ostream& operator<<(std::ostream& out, + const InfeasibleSubsystemResult& result) { + out << "{feasibility: " << result.feasibility + << ", infeasible_subsystem: " << result.infeasible_subsystem + << ", is_minimal: " << (result.is_minimal ? "true" : "false") << "}"; + return out; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/infeasible_subsystem_result.h b/ortools/math_opt/cpp/infeasible_subsystem_result.h new file mode 100644 index 0000000000..22e97036c2 --- /dev/null +++ b/ortools/math_opt/cpp/infeasible_subsystem_result.h @@ -0,0 +1,129 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ +#define OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ + +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/math_opt/constraints/indicator/indicator_constraint.h" +#include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" +#include "ortools/math_opt/constraints/sos/sos1_constraint.h" +#include "ortools/math_opt/constraints/sos/sos2_constraint.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/solve_result.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research::math_opt { + +// Represents a subset of the constraints (including variable bounds and +// integrality) of a `Model`. +// +// The fields contain `Variable` and Constraint objects which retain pointers +// back to their associated `Model`. Therefore, a `ModelSubset` should not +// outlive the `Model` it is in reference to. +struct ModelSubset { + struct Bounds { + static Bounds FromProto(const ModelSubsetProto::Bounds& bounds_proto); + ModelSubsetProto::Bounds Proto() const; + + bool empty() const { return !lower && !upper; } + + bool lower = false; + bool upper = false; + }; + + // Returns the `ModelSubset` equivalent to `proto`. + // + // Returns an error when `model` does not contain a variable or constraint + // associated with an index present in `proto`. + static absl::StatusOr FromProto(const ModelStorage* model, + const ModelSubsetProto& proto); + + // Returns the proto equivalent of this object. + // + // The caller should use CheckModelStorage() as this function does not check + // internal consistency of the referenced variables and constraints. + ModelSubsetProto Proto() const; + + // Returns a failure if the `Variable` and Constraints contained in the fields + // do not belong to the input expected_storage (which must not be nullptr). + absl::Status CheckModelStorage(const ModelStorage* expected_storage) const; + + // True if this object corresponds to the empty subset. + bool empty() const; + + absl::flat_hash_map variable_bounds; + absl::flat_hash_set variable_integrality; + absl::flat_hash_map linear_constraints; + absl::flat_hash_map quadratic_constraints; + absl::flat_hash_set second_order_cone_constraints; + absl::flat_hash_set sos1_constraints; + absl::flat_hash_set sos2_constraints; + absl::flat_hash_set indicator_constraints; +}; + +std::ostream& operator<<(std::ostream& out, const ModelSubset::Bounds& bounds); +std::ostream& operator<<(std::ostream& out, const ModelSubset& model_subset); + +struct InfeasibleSubsystemResult { + // Returns the `InfeasibleSubsystemResult` equivalent to `proto`. + // + // Returns an error when: + // * `model` does not contain a variable or constraint associated with an + // index present in `proto.infeasible_subsystem`. + // * ValidateInfeasibleSubsystemResultNoModel(result_proto) fails. + static absl::StatusOr FromProto( + const ModelStorage* model, + const InfeasibleSubsystemResultProto& result_proto); + + // Returns the proto equivalent of this object. + // + // The caller should use CheckModelStorage() before calling this function as + // it does not check internal consistency of the referenced variables and + // constraints. + InfeasibleSubsystemResultProto Proto() const; + + // Returns a failure if this object contains references to a model other than + // `expected_storage` (which must not be nullptr). + absl::Status CheckModelStorage(const ModelStorage* expected_storage) const; + + // The primal feasibility status of the model, as determined by the solver. + FeasibilityStatus feasibility = FeasibilityStatus::kUndetermined; + + // An infeasible subsystem of the input model. Set if `feasible` is + // kInfeasible, and empty otherwise. The IDs correspond to those constraints + // included in the infeasible subsystem. Submessages with `Bounds` values + // indicate which side of a potentially ranged constraint are included in the + // subsystem: lower bound, upper bound, or both. + ModelSubset infeasible_subsystem; + + // True if the solver has certified that the returned infeasible subsystem is + // minimal (i.e., the instance is feasible if any additional constraint is + // removed). + bool is_minimal = false; +}; + +std::ostream& operator<<(std::ostream& out, + const InfeasibleSubsystemResult& result); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ diff --git a/ortools/math_opt/cpp/key_types.h b/ortools/math_opt/cpp/key_types.h index 7ba6bd8bc3..0beb511739 100644 --- a/ortools/math_opt/cpp/key_types.h +++ b/ortools/math_opt/cpp/key_types.h @@ -17,14 +17,11 @@ // This header defines the common properties of "key types" and some related // constants. // -// MathOpt provides optimized custom collections for variables and -// constraints. This file contains implementation details for these custom -// collections and should not be needed by users. -// // Key types are types that are used as identifiers in the C++ interface where // the ModelStorage is using typed integers. They are pairs of (storage, // typed_index) where `storage` is a pointer on an ModelStorage and -// `typed_index` is the typed integer type used in ModelStorage. +// `typed_index` is the typed integer type used in ModelStorage (or a pair of +// typed integers for QuadraticTermKey). // // A key type K must match the following requirements: // - K::IdType is a value type used for indices. @@ -35,17 +32,119 @@ // It must return a non-null pointer. // - K::IdType is a valid key for absl::flat_hash_map or absl::flat_hash_set // (supports hash and ==). -// -// These requirements are met by Variable and LinearConstraint. +// - the is_key_type_v<> below should include them. #ifndef OR_TOOLS_MATH_OPT_CPP_KEY_TYPES_H_ #define OR_TOOLS_MATH_OPT_CPP_KEY_TYPES_H_ +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/math_opt/storage/model_storage.h" -namespace operations_research { -namespace math_opt { +namespace operations_research::math_opt { + +// Forward declarations of types implementing the keys interface defined at the +// top of this file. +class Variable; +class LinearConstraint; +class QuadraticConstraint; +class SecondOrderConeConstraint; +class Sos1Constraint; +class Sos2Constraint; +class IndicatorConstraint; +class QuadraticTermKey; +class Objective; + +// True for types in MathOpt that implements the keys interface defined at the +// top of this file. +// +// This is used in conjunction with std::enable_if_t<> to prevent Argument +// Dependent Lookup (ADL) from selecting some overload defined in MathOpt +// because one of the template type is in MathOpt. For example the SortedKeys() +// function below could be selected as a valid overload in another namespace if +// the values in the hash map are in the math_opt namespace. +template +constexpr inline bool is_key_type_v = + (std::is_same_v || std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || + std::is_same_v || std::is_same_v); + +// Returns the keys of the map sorted by their (storage(), type_id()). +// +// Implementation note: here we must use std::enable_if to prevent Argument +// Dependent Lookup (ADL) from selecting this overload for maps which values are +// in MathOpt but keys are not. +template >> +std::vector SortedKeys(const Map& map) { + using K = typename Map::key_type; + std::vector result; + result.reserve(map.size()); + for (const typename Map::const_reference item : map) { + result.push_back(item.first); + } + absl::c_sort(result, [](const K& lhs, const K& rhs) { + if (lhs.storage() != rhs.storage()) { + return lhs.storage() < rhs.storage(); + } + return lhs.typed_id() < rhs.typed_id(); + }); + return result; +} + +// Returns the elements of the set sorted by their (storage(), type_id()). +// +// Implementation note: here we must use std::enable_if to prevent Argument +// Dependent Lookup (ADL) from selecting this overload for maps which values are +// in MathOpt but keys are not. +template >> +std::vector SortedElements(const Set& set) { + using K = typename Set::key_type; + std::vector result; + result.reserve(set.size()); + for (const typename Set::const_reference item : set) { + result.push_back(item); + } + absl::c_sort(result, [](const K& lhs, const K& rhs) { + if (lhs.storage() != rhs.storage()) { + return lhs.storage() < rhs.storage(); + } + return lhs.typed_id() < rhs.typed_id(); + }); + return result; +} + +// Returns the values corresponding to the keys. Keys must be present in the +// input map. +// +// The keys must be in a type convertible to absl::Span. +// +// Implementation note: here we must use std::enable_if to prevent Argument +// Dependent Lookup (ADL) from selecting this overload for maps which values are +// in MathOpt but keys are not. +template >> +std::vector Values(const Map& map, + const Keys& keys) { + using K = typename Map::key_type; + const absl::Span keys_span = keys; + std::vector result; + result.reserve(keys_span.size()); + for (const K& key : keys_span) { + result.push_back(map.at(key)); + } + return result; +} + namespace internal { // The CHECK message to use when a KeyType::storage() is nullptr. @@ -79,7 +178,6 @@ inline absl::Status CheckModelStorage( } } // namespace internal -} // namespace math_opt -} // namespace operations_research +} // namespace operations_research::math_opt #endif // OR_TOOLS_MATH_OPT_CPP_KEY_TYPES_H_ diff --git a/ortools/math_opt/cpp/linear_constraint.h b/ortools/math_opt/cpp/linear_constraint.h index ea322b880a..74c9fbff79 100644 --- a/ortools/math_opt/cpp/linear_constraint.h +++ b/ortools/math_opt/cpp/linear_constraint.h @@ -19,15 +19,15 @@ #define OR_TOOLS_MATH_OPT_CPP_LINEAR_CONSTRAINT_H_ #include +#include #include #include #include -#include "absl/strings/string_view.h" #include "absl/log/check.h" +#include "absl/strings/string_view.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/util/model_util.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/key_types.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" @@ -79,10 +79,8 @@ class LinearConstraint { LinearConstraintId id_; }; -// Implements the API of std::unordered_map, but forbids -// LinearConstraints from different models in the same map. template -using LinearConstraintMap = IdMap; +using LinearConstraintMap = absl::flat_hash_map; // Streams the name of the constraint, as registered upon constraint creation, // or a short default if none was provided. diff --git a/ortools/math_opt/cpp/map_filter.h b/ortools/math_opt/cpp/map_filter.h index e3384360b7..683b8af417 100644 --- a/ortools/math_opt/cpp/map_filter.h +++ b/ortools/math_opt/cpp/map_filter.h @@ -21,7 +21,9 @@ #include #include -#include "ortools/math_opt/cpp/id_set.h" +#include "absl/algorithm/container.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/key_types.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/storage/model_storage.h" @@ -89,13 +91,17 @@ struct MapFilter { // filter.emplace(decision_vars.begin(), decision_vars.end()); // // Prefer using MakeSkipAllFilter() or MakeKeepKeysFilter() when appropriate. - std::optional> filtered_keys; + std::optional> filtered_keys; - // Returns the model of filtered keys. It returns a non-null value if and only - // if the filtered_keys is set and non-empty. - inline const ModelStorage* storage() const; + // Returns a failure if the keys don't belong to the input expected_storage + // (which must not be nullptr). + inline absl::Status CheckModelStorage( + const ModelStorage* expected_storage) const; // Returns the proto corresponding to this filter. + // + // The caller should use CheckModelStorage() as this function does not check + // internal consistency of the referenced variables and constraints. SparseVectorFilterProto Proto() const; }; @@ -159,23 +165,32 @@ MapFilter MakeKeepKeysFilter(std::initializer_list keys) { //////////////////////////////////////////////////////////////////////////////// template -const ModelStorage* MapFilter::storage() const { - return filtered_keys ? filtered_keys->storage() : nullptr; +absl::Status MapFilter::CheckModelStorage( + const ModelStorage* expected_storage) const { + if (!filtered_keys.has_value()) { + return absl::OkStatus(); + } + for (const KeyType& k : filtered_keys.value()) { + RETURN_IF_ERROR(internal::CheckModelStorage( + /*storage=*/k.storage(), + /*expected_storage=*/expected_storage)); + } + return absl::OkStatus(); } template SparseVectorFilterProto MapFilter::Proto() const { SparseVectorFilterProto ret; ret.set_skip_zero_values(skip_zero_values); - if (filtered_keys) { + if (filtered_keys.has_value()) { ret.set_filter_by_ids(true); - const auto filtered_ids = ret.mutable_filtered_ids(); - filtered_ids->Reserve(filtered_keys->size()); - for (const auto id : filtered_keys->raw_set()) { - filtered_ids->Add(id.value()); + auto& filtered_ids = *ret.mutable_filtered_ids(); + filtered_ids.Reserve(static_cast(filtered_keys.value().size())); + for (const auto k : filtered_keys.value()) { + filtered_ids.Add(k.typed_id().value()); } // Iteration on the set is random but we want the proto to be stable. - std::sort(filtered_ids->begin(), filtered_ids->end()); + absl::c_sort(filtered_ids); } return ret; } diff --git a/ortools/math_opt/cpp/matchers.cc b/ortools/math_opt/cpp/matchers.cc deleted file mode 100644 index 705de7951b..0000000000 --- a/ortools/math_opt/cpp/matchers.cc +++ /dev/null @@ -1,844 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/cpp/matchers.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/strings/str_cat.h" -#include "absl/types/span.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" -#include "ortools/base/logging.h" -#include "ortools/math_opt/cpp/math_opt.h" -#include "ortools/math_opt/cpp/variable_and_expressions.h" - -namespace operations_research { -namespace math_opt { - -namespace { - -using ::testing::AllOf; -using ::testing::AllOfArray; -using ::testing::AnyOf; -using ::testing::AnyOfArray; -using ::testing::Contains; -using ::testing::DoubleNear; -using ::testing::Eq; -using ::testing::ExplainMatchResult; -using ::testing::Field; -using ::testing::IsEmpty; -using ::testing::Matcher; -using ::testing::MatcherInterface; -using ::testing::MatchResultListener; -using ::testing::Optional; -using ::testing::PrintToString; -using ::testing::Property; -} // namespace - -//////////////////////////////////////////////////////////////////////////////// -// Printing -//////////////////////////////////////////////////////////////////////////////// - -namespace { - -template -struct Printer { - explicit Printer(const T& t) : value(t) {} - - const T& value; - - friend std::ostream& operator<<(std::ostream& os, const Printer& printer) { - os << PrintToString(printer.value); - return os; - } -}; - -template -Printer Print(const T& t) { - return Printer(t); -} - -} // namespace - -void PrintTo(const Termination& termination, std::ostream* os) { - *os << "{reason: " << termination.reason; - if (termination.limit.has_value()) { - *os << ", limit: " << *termination.limit; - } - *os << ", detail: " << Print(termination.detail) << "}"; -} - -void PrintTo(const PrimalSolution& primal_solution, std::ostream* const os) { - *os << "{variable_values: " << Print(primal_solution.variable_values) - << ", objective_value: " << Print(primal_solution.objective_value) - << ", feasibility_status: " << Print(primal_solution.feasibility_status) - << "}"; -} - -void PrintTo(const DualSolution& dual_solution, std::ostream* const os) { - *os << "{dual_values: " << Print(dual_solution.dual_values) - << ", reduced_costs: " << Print(dual_solution.reduced_costs) - << ", objective_value: " << Print(dual_solution.objective_value) - << ", feasibility_status: " << Print(dual_solution.feasibility_status) - << "}"; -} - -void PrintTo(const PrimalRay& primal_ray, std::ostream* const os) { - *os << "{variable_values: " << Print(primal_ray.variable_values) << "}"; -} - -void PrintTo(const DualRay& dual_ray, std::ostream* const os) { - *os << "{dual_values: " << Print(dual_ray.dual_values) - << ", reduced_costs: " << Print(dual_ray.reduced_costs) << "}"; -} - -void PrintTo(const Basis& basis, std::ostream* const os) { - *os << "{variable_status: " << Print(basis.variable_status) - << ", constraint_status: " << Print(basis.constraint_status) - << ", basic_dual_feasibility: " << Print(basis.basic_dual_feasibility) - << "}"; -} - -void PrintTo(const Solution& solution, std::ostream* const os) { - *os << "{primal_solution: " << Print(solution.primal_solution) - << ", dual_solution: " << Print(solution.dual_solution) - << ", basis: " << Print(solution.basis) << "}"; -} - -void PrintTo(const SolveResult& result, std::ostream* const os) { - *os << "{termination: " << Print(result.termination) - << ", solve_stats: " << Print(result.solve_stats) - << ", solutions: " << Print(result.solutions) - << ", primal_rays: " << Print(result.primal_rays) - << ", dual_rays: " << Print(result.dual_rays) << "}"; -} - -//////////////////////////////////////////////////////////////////////////////// -// IdMap Matchers -//////////////////////////////////////////////////////////////////////////////// - -namespace { - -template -class IdMapMatcher : public MatcherInterface> { - public: - IdMapMatcher(IdMap expected, const bool all_keys, - const double tolerance) - : expected_(std::move(expected)), - all_keys_(all_keys), - tolerance_(tolerance) { - for (const auto [k, v] : expected_) { - CHECK(!std::isnan(v)) << "Illegal NaN for key: " << k; - } - } - - bool MatchAndExplain(IdMap actual, - MatchResultListener* const os) const override { - for (const auto& [key, value] : expected_) { - if (!actual.contains(key)) { - *os << "expected key " << key << " not found"; - return false; - } - if (!(std::abs(value - actual.at(key)) <= tolerance_)) { - *os << "value for key " << key - << " not within tolerance, expected: " << value - << " but found: " << actual.at(key); - return false; - } - } - // Post condition: expected_ is a subset of actual. - if (all_keys_ && expected_.size() != actual.size()) { - for (const auto& [key, value] : actual) { - if (!expected_.contains(key)) { - *os << "found unexpected key " << key << " in actual"; - return false; - } - } - // expected_ subset of actual && expected_.size() != actual.size() implies - // that there is a member A of actual not in expected. When the loop above - // hits A, it will return, thus this line is unreachable. - LOG(FATAL) << "unreachable"; - } - return true; - } - - void DescribeTo(std::ostream* const os) const override { - if (all_keys_) { - *os << "has identical keys to "; - } else { - *os << "keys are contained in "; - } - PrintTo(expected_, os); - *os << " and values within " << tolerance_; - } - - void DescribeNegationTo(std::ostream* const os) const override { - if (all_keys_) { - *os << "either keys differ from "; - } else { - *os << "either has a key not in "; - } - PrintTo(expected_, os); - *os << " or a value differs by more than " << tolerance_; - } - - private: - const IdMap expected_; - const bool all_keys_; - const double tolerance_; -}; - -} // namespace - -Matcher> IsNearlySubsetOf(VariableMap expected, - double tolerance) { - return Matcher>(new IdMapMatcher( - std::move(expected), /*all_keys=*/false, tolerance)); -} - -Matcher> IsNear(VariableMap expected, - const double tolerance) { - return Matcher>(new IdMapMatcher( - std::move(expected), /*all_keys=*/true, tolerance)); -} - -Matcher> IsNearlySubsetOf( - LinearConstraintMap expected, double tolerance) { - return Matcher>( - new IdMapMatcher(std::move(expected), - /*all_keys=*/false, tolerance)); -} - -Matcher> IsNear( - LinearConstraintMap expected, const double tolerance) { - return Matcher>( - new IdMapMatcher(std::move(expected), /*all_keys=*/true, - tolerance)); -} - -template -Matcher> IsNear(IdMap expected, - const double tolerance) { - return Matcher>( - new IdMapMatcher(std::move(expected), /*all_keys=*/true, tolerance)); -} - -template -Matcher> IsNearlySubsetOf(IdMap expected, - const double tolerance) { - return Matcher>( - new IdMapMatcher(std::move(expected), /*all_keys=*/false, tolerance)); -} - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for LinearExpression and QuadraticExpression -//////////////////////////////////////////////////////////////////////////////// - -testing::Matcher IsIdentical(LinearExpression expected) { - return LinearExpressionIsNear(expected, 0.0); -} - -testing::Matcher LinearExpressionIsNear( - const LinearExpression expected, const double tolerance) { - CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset"; - return AllOf( - Property("storage", &LinearExpression::storage, Eq(expected.storage())), - Property("offset", &LinearExpression::offset, - testing::DoubleNear(expected.offset(), tolerance)), - Property("terms", &LinearExpression::terms, - IsNear(expected.terms(), tolerance))); -} - -namespace { -testing::Matcher IsNearForSign( - const BoundedLinearExpression& expected, const double tolerance) { - return AllOf(Property("upper_bound_minus_offset", - &BoundedLinearExpression::upper_bound_minus_offset, - testing::DoubleNear(expected.upper_bound_minus_offset(), - tolerance)), - Property("lower_bound_minus_offset", - &BoundedLinearExpression::lower_bound_minus_offset, - testing::DoubleNear(expected.lower_bound_minus_offset(), - tolerance)), - Field("expression", &BoundedLinearExpression::expression, - Property("terms", &LinearExpression::terms, - IsNear(expected.expression.terms(), tolerance)))); -} -} // namespace - -testing::Matcher IsNearlyEquivalent( - const BoundedLinearExpression& expected, const double tolerance) { - const BoundedLinearExpression expected_negation( - -expected.expression, /*lower_bound=*/-expected.upper_bound, - /*upper_bound=*/-expected.lower_bound); - return AnyOf(IsNearForSign(expected, tolerance), - IsNearForSign(expected_negation, tolerance)); -} - -testing::Matcher IsIdentical( - QuadraticExpression expected) { - CHECK(!std::isnan(expected.offset())) << "Illegal NaN-valued offset"; - return AllOf( - Property("storage", &QuadraticExpression::storage, - Eq(expected.storage())), - Property("offset", &QuadraticExpression::offset, - testing::Eq(expected.offset())), - Property("linear_terms", &QuadraticExpression::linear_terms, - IsNear(expected.linear_terms(), /*tolerance=*/0)), - Property("quadratic_terms", &QuadraticExpression::quadratic_terms, - IsNear(expected.quadratic_terms(), /*tolerance=*/0))); -} - -//////////////////////////////////////////////////////////////////////////////// -// Matcher helpers -//////////////////////////////////////////////////////////////////////////////// - -namespace { - -template -class RayMatcher : public MatcherInterface { - public: - RayMatcher(RayType expected, const double tolerance) - : expected_(std::move(expected)), tolerance_(tolerance) {} - void DescribeTo(std::ostream* os) const final { - *os << "after L_inf normalization, is within tolerance: " << tolerance_ - << " of expected: "; - PrintTo(expected_, os); - } - void DescribeNegationTo(std::ostream* const os) const final { - *os << "after L_inf normalization, is not within tolerance: " << tolerance_ - << " of expected: "; - PrintTo(expected_, os); - } - - protected: - const RayType expected_; - const double tolerance_; -}; - -// Alias to use the std::optional templated adaptor. -Matcher IsNear(double expected, const double tolerance) { - return DoubleNear(expected, tolerance); -} - -template -Matcher> IsNear(std::optional expected, - const double tolerance) { - if (expected.has_value()) { - return Optional(IsNear(*expected, tolerance)); - } - return testing::Eq(std::nullopt); -} - -// Custom std::optional for basis. -Matcher> BasisIs(const std::optional& expected) { - if (expected.has_value()) { - return Optional(BasisIs(*expected)); - } - return testing::Eq(std::nullopt); -} - -testing::Matcher> IsNear( - const std::vector& expected_solutions, - const SolutionMatcherOptions options) { - if (expected_solutions.empty()) { - return IsEmpty(); - } - std::vector> matchers; - for (const Solution& sol : expected_solutions) { - matchers.push_back(IsNear(sol, options)); - } - return ::testing::ElementsAreArray(matchers); -} - -} // namespace - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for Solutions -//////////////////////////////////////////////////////////////////////////////// - -Matcher IsNear(PrimalSolution expected, - const double tolerance) { - return AllOf(Field("variable_values", &PrimalSolution::variable_values, - IsNear(expected.variable_values, tolerance)), - Field("objective_value", &PrimalSolution::objective_value, - IsNear(expected.objective_value, tolerance)), - Field("feasibility_status", &PrimalSolution::feasibility_status, - expected.feasibility_status)); -} - -Matcher IsNear(DualSolution expected, const double tolerance) { - return AllOf(Field("dual_values", &DualSolution::dual_values, - IsNear(expected.dual_values, tolerance)), - Field("reduced_costs", &DualSolution::reduced_costs, - IsNear(expected.reduced_costs, tolerance)), - Field("objective_value", &DualSolution::objective_value, - IsNear(expected.objective_value, tolerance)), - Field("feasibility_status", &DualSolution::feasibility_status, - expected.feasibility_status)); -} - -Matcher BasisIs(const Basis& expected) { - return AllOf(Field("variable_status", &Basis::variable_status, - expected.variable_status), - Field("constraint_status", &Basis::constraint_status, - expected.constraint_status), - Field("basic_dual_feasibility", &Basis::basic_dual_feasibility, - expected.basic_dual_feasibility)); -} - -Matcher IsNear(Solution expected, - const SolutionMatcherOptions options) { - std::vector> to_check; - if (options.check_primal) { - to_check.push_back( - Field("primal_solution", &Solution::primal_solution, - IsNear(expected.primal_solution, options.tolerance))); - } - if (options.check_dual) { - to_check.push_back( - Field("dual_solution", &Solution::dual_solution, - IsNear(expected.dual_solution, options.tolerance))); - } - if (options.check_basis) { - to_check.push_back( - Field("basis", &Solution::basis, BasisIs(expected.basis))); - } - return AllOfArray(to_check); -} - -//////////////////////////////////////////////////////////////////////////////// -// Primal Ray Matcher -//////////////////////////////////////////////////////////////////////////////// - -namespace { - -template -double InfinityNorm(const IdMap& vector) { - double infinity_norm = 0.0; - for (auto [id, value] : vector) { - infinity_norm = std::max(infinity_norm, std::abs(value)); - } - return infinity_norm; -} - -// Returns a normalized primal ray. -// -// The normalization is done using infinity norm: -// -// ray / ||ray||_inf -// -// If the input ray norm is zero, the ray is returned unchanged. -PrimalRay NormalizePrimalRay(PrimalRay ray) { - const double norm = InfinityNorm(ray.variable_values); - if (norm != 0.0) { - for (auto entry : ray.variable_values) { - entry.second /= norm; - } - } - return ray; -} - -class PrimalRayMatcher : public RayMatcher { - public: - PrimalRayMatcher(PrimalRay expected, const double tolerance) - : RayMatcher(std::move(expected), tolerance) {} - - bool MatchAndExplain(PrimalRay actual, - MatchResultListener* const os) const override { - auto normalized_actual = NormalizePrimalRay(actual); - auto normalized_expected = NormalizePrimalRay(expected_); - if (os->IsInterested()) { - *os << "actual normalized: " << PrintToString(normalized_actual) - << ", expected normalized: " << PrintToString(normalized_expected); - } - return ExplainMatchResult( - IsNear(normalized_expected.variable_values, tolerance_), - normalized_actual.variable_values, os); - } -}; - -} // namespace - -Matcher IsNear(PrimalRay expected, const double tolerance) { - return Matcher( - new PrimalRayMatcher(std::move(expected), tolerance)); -} - -Matcher PrimalRayIsNear(VariableMap expected_var_values, - const double tolerance) { - PrimalRay expected; - expected.variable_values = std::move(expected_var_values); - return IsNear(expected, tolerance); -} - -//////////////////////////////////////////////////////////////////////////////// -// Dual Ray Matcher -//////////////////////////////////////////////////////////////////////////////// - -namespace { - -// Returns a normalized dual ray. -// -// The normalization is done using infinity norm: -// -// ray / ||ray||_inf -// -// If the input ray norm is zero, the ray is returned unchanged. -DualRay NormalizeDualRay(DualRay ray) { - const double norm = - std::max(InfinityNorm(ray.dual_values), InfinityNorm(ray.reduced_costs)); - if (norm != 0.0) { - for (auto entry : ray.dual_values) { - entry.second /= norm; - } - for (auto entry : ray.reduced_costs) { - entry.second /= norm; - } - } - return ray; -} - -class DualRayMatcher : public RayMatcher { - public: - DualRayMatcher(DualRay expected, const double tolerance) - : RayMatcher(std::move(expected), tolerance) {} - - bool MatchAndExplain(DualRay actual, MatchResultListener* os) const override { - auto normalized_actual = NormalizeDualRay(actual); - auto normalized_expected = NormalizeDualRay(expected_); - if (os->IsInterested()) { - *os << "actual normalized: " << PrintToString(normalized_actual) - << ", expected normalized: " << PrintToString(normalized_expected); - } - return ExplainMatchResult( - IsNear(normalized_expected.dual_values, tolerance_), - normalized_actual.dual_values, os) && - ExplainMatchResult( - IsNear(normalized_expected.reduced_costs, tolerance_), - normalized_actual.reduced_costs, os); - } -}; - -} // namespace - -Matcher IsNear(DualRay expected, const double tolerance) { - return Matcher(new DualRayMatcher(std::move(expected), tolerance)); -} - -//////////////////////////////////////////////////////////////////////////////// -// SolveResult termination reason matchers -//////////////////////////////////////////////////////////////////////////////// - -Matcher TerminatesWithOneOf( - const std::vector& allowed) { - return Field("termination", &SolveResult::termination, - Field("reason", &Termination::reason, AnyOfArray(allowed))); -} - -Matcher TerminatesWith(const TerminationReason expected) { - return Field("termination", &SolveResult::termination, - Field("reason", &Termination::reason, expected)); -} - -namespace { -testing::Matcher LimitIs(const Limit expected, - const bool allow_limit_undetermined) { - if (allow_limit_undetermined) { - return Field("termination", &SolveResult::termination, - Field("limit", &Termination::limit, - AnyOf(Limit::kUndetermined, expected))); - } - return Field("termination", &SolveResult::termination, - Field("limit", &Termination::limit, expected)); -} - -} // namespace - -testing::Matcher TerminatesWithLimit( - const Limit expected, const bool allow_limit_undetermined) { - std::vector> matchers; - matchers.push_back(LimitIs(expected, allow_limit_undetermined)); - matchers.push_back(TerminatesWithOneOf( - {TerminationReason::kFeasible, TerminationReason::kNoSolutionFound})); - return ::testing::AllOfArray(matchers); -} - -testing::Matcher TerminatesWithReasonFeasible( - const Limit expected, const bool allow_limit_undetermined) { - std::vector> matchers; - matchers.push_back(LimitIs(expected, allow_limit_undetermined)); - matchers.push_back(TerminatesWith(TerminationReason::kFeasible)); - return ::testing::AllOfArray(matchers); -} - -testing::Matcher TerminatesWithReasonNoSolutionFound( - const Limit expected, const bool allow_limit_undetermined) { - std::vector> matchers; - matchers.push_back(LimitIs(expected, allow_limit_undetermined)); - matchers.push_back(TerminatesWith(TerminationReason::kNoSolutionFound)); - return ::testing::AllOfArray(matchers); -} - -template -std::string MatcherToStringImpl(const MatcherType& matcher, const bool negate) { - std::ostringstream os; - if (negate) { - matcher.DescribeNegationTo(&os); - } else { - matcher.DescribeTo(&os); - } - return os.str(); -} - -template -std::string MatcherToString(const Matcher& matcher, bool negate) { - return MatcherToStringImpl(matcher, negate); -} - -// clang-format off -// Polymorphic matchers do not always define DescribeTo, -// The type may not be a matcher, but it will implement DescribeTo. -// clang-format on -template -std::string MatcherToString(const ::testing::PolymorphicMatcher& matcher, - bool negate) { - return MatcherToStringImpl(matcher.impl(), negate); -} - -MATCHER_P(FirstElementIs, first_element_matcher, - (negation - ? absl::StrCat("is empty or first element ", - MatcherToString(first_element_matcher, true)) - : absl::StrCat("has at least one element and first element ", - MatcherToString(first_element_matcher, false)))) { - return ExplainMatchResult(UnorderedElementsAre(first_element_matcher), - absl::MakeSpan(arg).subspan(0, 1), result_listener); -} - -Matcher ReasonIs(TerminationReason reason) { - return Field("reason", &Termination::reason, reason); -} - -Matcher ReasonIsOptimal() { - return ReasonIs(TerminationReason::kOptimal); -} - -Matcher IsOptimal(const std::optional expected_objective, - const double tolerance) { - std::vector> matchers; - matchers.push_back( - Field("termination", &SolveResult::termination, ReasonIsOptimal())); - if (expected_objective.has_value()) { - matchers.push_back(Field( - "solutions", &SolveResult::solutions, - FirstElementIs(Field( - "primal_solution", &Solution::primal_solution, - Optional(Field("objective_value", &PrimalSolution::objective_value, - IsNear(*expected_objective, tolerance))))))); - } - return ::testing::AllOfArray(matchers); -} - -Matcher IsOptimalWithSolution( - const double expected_objective, - const VariableMap expected_variable_values, - const double tolerance) { - return AllOf( - IsOptimal(std::make_optional(expected_objective), tolerance), - HasSolution( - PrimalSolution{.variable_values = expected_variable_values, - .objective_value = expected_objective, - .feasibility_status = SolutionStatus::kFeasible}, - tolerance)); -} - -Matcher IsOptimalWithDualSolution( - const double expected_objective, - const LinearConstraintMap expected_dual_values, - const VariableMap expected_reduced_costs, const double tolerance) { - return AllOf( - IsOptimal(std::make_optional(expected_objective), tolerance), - HasDualSolution( - DualSolution{ - .dual_values = expected_dual_values, - .reduced_costs = expected_reduced_costs, - .objective_value = std::make_optional(expected_objective), - .feasibility_status = SolutionStatus::kFeasible}, - tolerance)); -} - -Matcher HasSolution(PrimalSolution expected, - const double tolerance) { - return ::testing::Field( - "solutions", &SolveResult::solutions, - Contains(Field("primal_solution", &Solution::primal_solution, - Optional(IsNear(std::move(expected), tolerance))))); -} - -Matcher HasDualSolution(DualSolution expected, - const double tolerance) { - return ::testing::Field( - "solutions", &SolveResult::solutions, - Contains(Field("dual_solution", &Solution::dual_solution, - Optional(IsNear(std::move(expected), tolerance))))); -} - -Matcher HasPrimalRay(PrimalRay expected, const double tolerance) { - return ::testing::Field("primal_rays", &SolveResult::primal_rays, - Contains(IsNear(std::move(expected), tolerance))); -} - -Matcher HasPrimalRay(VariableMap expected_vars, - const double tolerance) { - PrimalRay ray; - ray.variable_values = std::move(expected_vars); - return HasPrimalRay(std::move(ray), tolerance); -} - -Matcher HasDualRay(DualRay expected, const double tolerance) { - return ::testing::Field("dual_rays", &SolveResult::dual_rays, - Contains(IsNear(std::move(expected), tolerance))); -} - -namespace { - -bool MightTerminateWithRays(const TerminationReason reason) { - switch (reason) { - case TerminationReason::kInfeasibleOrUnbounded: - case TerminationReason::kUnbounded: - case TerminationReason::kInfeasible: - return true; - default: - return false; - } -} - -std::vector CompatibleReasons( - const TerminationReason expected, const bool inf_or_unb_soft_match) { - if (!inf_or_unb_soft_match) { - return {expected}; - } - switch (expected) { - case TerminationReason::kUnbounded: - return {TerminationReason::kUnbounded, - TerminationReason::kInfeasibleOrUnbounded}; - case TerminationReason::kInfeasible: - return {TerminationReason::kInfeasible, - TerminationReason::kInfeasibleOrUnbounded}; - case TerminationReason::kInfeasibleOrUnbounded: - return {TerminationReason::kUnbounded, TerminationReason::kInfeasible, - TerminationReason::kInfeasibleOrUnbounded}; - default: - return {expected}; - } -} - -Matcher> CheckSolutions( - const std::vector& expected_solutions, - const SolveResultMatcherOptions& options) { - if (options.first_solution_only && !expected_solutions.empty()) { - return FirstElementIs( - IsNear(expected_solutions[0], - SolutionMatcherOptions{.tolerance = options.tolerance, - .check_primal = true, - .check_dual = options.check_dual, - .check_basis = options.check_basis})); - } - return IsNear(expected_solutions, - SolutionMatcherOptions{.tolerance = options.tolerance, - .check_primal = true, - .check_dual = options.check_dual, - .check_basis = options.check_basis}); -} - -template -Matcher> AnyRayNear( - const std::vector& expected_rays, const double tolerance) { - std::vector> matchers; - for (const RayType& ray : expected_rays) { - matchers.push_back(IsNear(ray, tolerance)); - } - return ::testing::Contains(::testing::AnyOfArray(matchers)); -} - -template -Matcher> AllRaysNear( - const std::vector& expected_rays, const double tolerance) { - std::vector> matchers; - for (const RayType& ray : expected_rays) { - matchers.push_back(IsNear(ray, tolerance)); - } - return ::testing::UnorderedElementsAreArray(matchers); -} - -template -Matcher> CheckRays( - const std::vector& expected_rays, const double tolerance, - bool check_all) { - if (expected_rays.empty()) { - return ::testing::IsEmpty(); - } - if (check_all) { - return AllRaysNear(expected_rays, tolerance); - } - return AnyRayNear(expected_rays, tolerance); -} - -} // namespace - -Matcher IsConsistentWith( - const SolveResult& expected, const SolveResultMatcherOptions& options) { - std::vector> to_check; - to_check.push_back(TerminatesWithOneOf(CompatibleReasons( - expected.termination.reason, options.inf_or_unb_soft_match))); - const bool skip_solution = - MightTerminateWithRays(expected.termination.reason) && - !options.check_solutions_if_inf_or_unbounded; - if (!skip_solution) { - to_check.push_back(Field("solutions", &SolveResult::solutions, - CheckSolutions(expected.solutions, options))); - } - if (options.check_rays) { - to_check.push_back(Field("primal_rays", &SolveResult::primal_rays, - CheckRays(expected.primal_rays, options.tolerance, - !options.first_solution_only))); - to_check.push_back(Field("dual_rays", &SolveResult::dual_rays, - CheckRays(expected.dual_rays, options.tolerance, - !options.first_solution_only))); - } - - return AllOfArray(to_check); -} - -//////////////////////////////////////////////////////////////////////////////// -// Rarely used -//////////////////////////////////////////////////////////////////////////////// - -Matcher DidUpdate() { - return ::testing::Field("did_update", &UpdateResult::did_update, - ::testing::IsTrue()); -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/cpp/matchers.h b/ortools/math_opt/cpp/matchers.h deleted file mode 100644 index 0ded82600c..0000000000 --- a/ortools/math_opt/cpp/matchers.h +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Matchers for MathOpt types, specifically SolveResult and nested fields. -// -// The matchers defined here are useful for writing unit tests checking that the -// result of Solve(), absl::StatusOr, meets expectations. We give -// some examples below. All code is assumed with the following setup: -// -// namespace operations_research::math_opt { -// using ::testing::status::IsOkAndHolds; -// -// Model model; -// const Variable x = model.AddContinuousVariable(0.0, 1.0); -// const Variable y = model.AddContinuousVariable(0.0, 1.0); -// const LinearConstraint c = model.AddLinearConstraint(x + y <= 1); -// model.Maximize(2*x + y); -// -// Example 1.a: result is OK, optimal, and objective value approximately 42. -// EXPECT_THAT(Solve(model, SOLVER_TYPE_GLOP), IsOkAndHolds(IsOptimal(42))); -// -// Example 1.b: equivalent to 1.a. -// ASSERT_OK_AND_ASSIGN(const SolveResult result, -// Solve(model, SOLVER_TYPE_GLOP)); -// EXPECT_THAT(result, IsOptimal(42)); -// -// Example 2: result is OK, optimal, and best solution is x=1, y=0. -// ASSERT_OK_AND_ASSIGN(const SolveResult result, -// Solve(model, SOLVER_TYPE_GLOP)); -// ASSERT_THAT(result, IsOptimal()); -// EXPECT_THAT(result.variable_value(), IsNear({{x, 1}, {y, 0}}); -// Note: the second ASSERT ensures that if the solution is not optimal, then -// result.variable_value() will not run (the function will crash if the solver -// didn't find a solution). Further, MathOpt guarantees there is a solution -// when the termination reason is optimal. -// -// Example 3: result is OK, check the solution without specifying termination. -// ASSERT_OK_AND_ASSIGN(const SolveResult result, -// Solve(model, SOLVER_TYPE_GLOP)); -// EXPECT_THAT(result, HasBestSolution({{x, 1}, {y, 0}})); -// -// Example 4: multiple possible termination reason, primal ray optional: -// ASSERT_OK_AND_ASSIGN(const SolveResult result, -// Solve(model, SOLVER_TYPE_GLOP)); -// ASSERT_THAT(result, TerminatesWithOneOf( -// TerminationReason::kUnbounded, -// TerminationReason::kInfeasibleOrUnbounded)); -// if(!result.primal_rays.empty()) { -// EXPECT_THAT(result.primal_rays[0], PrimalRayIsNear({{x, 1,}, {y, 0}})); -// } -// -// -// Tips on writing good tests: -// * Use ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(...)) to ensure -// the test terminates immediately if Solve() does not return OK. -// * If you ASSERT_THAT(result, IsOptimal()), you can assume that you have a -// feasible primal solution afterwards. Otherwise, make no assumptions on -// the contents of result (e.g. do not assume result contains a primal ray -// just because the termination reason was UNBOUNDED). -// * For problems that are infeasible, the termination reasons INFEASIBLE and -// DUAL_INFEASIBLE are both possible. Likewise, for unbounded problems, you -// can get both UNBOUNDED and DUAL_INFEASIBLE. See TerminatesWithOneOf() -// below to make assertions in this case. Note also that some solvers have -// solver specific parameters to ensure that DUAL_INFEASIBLE will not be -// returned (e.g. for Gurobi, use DualReductions or InfUnbdInfo). -// * The objective value and variable values should always be compared up to -// a tolerance, even if your decision variables are integer. The matchers -// defined have a configurable tolerance with default value 1e-5. -// * Primal and dual rays are unique only up to a constant scaling. The -// matchers provided rescale both expected and actual before comparing. -// * Take care on problems with multiple optimal solutions. Do not rely on a -// particular solution being returned in your test, as the test will break -// when we upgrade the solver. -// -// This file also defines functions to let gunit print various MathOpt types. -// -// To see the error messages these matchers generate, run -// blaze test experimental/users/rander/math_opt:matchers_error_messages -// which is a fork of matchers_test.cc where the assertions are all negated -// (note that every test should fail). -#ifndef OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_ -#define OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_ - -#include -#include -#include -#include - -#include "gtest/gtest.h" -#include "ortools/math_opt/cpp/linear_constraint.h" -#include "ortools/math_opt/cpp/math_opt.h" -#include "ortools/math_opt/cpp/variable_and_expressions.h" - -namespace operations_research { -namespace math_opt { - -constexpr double kMatcherDefaultTolerance = 1e-5; - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for IdMap -//////////////////////////////////////////////////////////////////////////////// - -// Checks that the maps have identical keys and values within tolerance. This -// factory will CHECK-fail if expected contains any NaN values. -testing::Matcher> IsNear( - VariableMap expected, double tolerance = kMatcherDefaultTolerance); - -// Checks that the keys of actual are a subset of the keys of expected, and that -// for all shared keys, the values are within tolerance. This factory will -// CHECK-fail if expected contains any NaN values, and any NaN values in the -// expression compared against will result in the matcher failing. -testing::Matcher> IsNearlySubsetOf( - VariableMap expected, double tolerance = kMatcherDefaultTolerance); - -// Checks that the maps have identical keys and values within tolerance. This -// factory will CHECK-fail if expected contains any NaN values, and any NaN -// values in the expression compared against will result in the matcher failing. -testing::Matcher> IsNear( - LinearConstraintMap expected, - double tolerance = kMatcherDefaultTolerance); - -// Checks that the keys of actual are a subset of the keys of expected, and that -// for all shared keys, the values are within tolerance. This factory will -// CHECK-fail if expected contains any NaN values, and any NaN values in the -// expression compared against will result in the matcher failing. -testing::Matcher> IsNearlySubsetOf( - LinearConstraintMap expected, - double tolerance = kMatcherDefaultTolerance); - -// Checks that the maps have identical keys and values within tolerance. Works -// for VariableMap, LinearConstraintMap, among other realizations of IdMap. This -// factory will CHECK-fail if expected contains any NaN values, and any NaN -// values in the expression compared against will result in the matcher failing. -template -testing::Matcher> IsNear( - IdMap expected, - const double tolerance = kMatcherDefaultTolerance); - -// Checks that the keys of actual are a subset of the keys of expected, and that -// for all shared keys, the values are within tolerance. Works for VariableMap, -// LinearConstraintMap, among other realizations of IdMap. This factory will -// CHECK-fail if expected contains any NaN values, and any NaN values in the -// expression compared against will result in the matcher failing. -template -testing::Matcher> IsNearlySubsetOf( - IdMap expected, - const double tolerance = kMatcherDefaultTolerance); - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for various Variable expressions (e.g. LinearExpression) -//////////////////////////////////////////////////////////////////////////////// - -// Checks that the expressions are structurally identical (i.e., internal maps -// have the same keys and storage, coefficients are exactly equal). This factory -// will CHECK-fail if expected contains any NaN values, and any NaN values in -// the expression compared against will result in the matcher failing. -testing::Matcher IsIdentical(LinearExpression expected); - -testing::Matcher LinearExpressionIsNear( - LinearExpression expected, double tolerance = kMatcherDefaultTolerance); - -// Checks that the bounded linear expression is equivalent to expected, where -// equivalence is maintained by: -// * adding alpha to the lower bound, the linear expression and upper bound -// * multiplying the lower bound, linear expression, by -1 (and flipping the -// inequalities). -// Note that, as implemented, we do not allow for arbitrary multiplicative -// rescalings (this makes additive tolerance complicated). -testing::Matcher IsNearlyEquivalent( - const BoundedLinearExpression& expected, - double tolerance = kMatcherDefaultTolerance); - -// Checks that the expressions are structurally identical (i.e., internal maps -// have the same keys and storage, coefficients are exactly equal). This factory -// will CHECK-fail if expected contains any NaN values, and any NaN values in -// the expression compared against will result in the matcher failing. -testing::Matcher IsIdentical(QuadraticExpression expected); - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for solutions -//////////////////////////////////////////////////////////////////////////////// - -// Options for IsNear(Solution). -struct SolutionMatcherOptions { - double tolerance = kMatcherDefaultTolerance; - bool check_primal = true; - bool check_dual = true; - bool check_basis = true; -}; - -testing::Matcher IsNear(Solution expected, - SolutionMatcherOptions options = {}); - -// Checks variables match and variable/objective values are within tolerance and -// feasibility statuses are identical. -testing::Matcher IsNear( - PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); - -// Checks dual variables, reduced costs and objective are within tolerance and -// feasibility statuses are identical. -testing::Matcher IsNear( - DualSolution expected, double tolerance = kMatcherDefaultTolerance); - -testing::Matcher BasisIs(const Basis& expected); - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for a Rays -//////////////////////////////////////////////////////////////////////////////// - -// Checks variables match and that after rescaling, variable values are within -// tolerance. -testing::Matcher IsNear(PrimalRay expected, - double tolerance = kMatcherDefaultTolerance); - -// Checks variables match and that after rescaling, variable values are within -// tolerance. -testing::Matcher PrimalRayIsNear( - VariableMap expected_var_values, - double tolerance = kMatcherDefaultTolerance); - -// Checks that dual variables and reduced costs are defined for the same -// set of Variables/LinearConstraints, and that their rescaled values are within -// tolerance. -testing::Matcher IsNear(DualRay expected, - double tolerance = kMatcherDefaultTolerance); - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for a Termination -//////////////////////////////////////////////////////////////////////////////// - -testing::Matcher ReasonIs(TerminationReason reason); - -testing::Matcher ReasonIsOptimal(); - -//////////////////////////////////////////////////////////////////////////////// -// Matchers for a SolveResult -//////////////////////////////////////////////////////////////////////////////// - -// Checks the following: -// * The termination reason is optimal. -// * If expected_objective contains a value, there is at least one feasible -// solution and that solution has an objective value within tolerance of -// expected_objective. -testing::Matcher IsOptimal( - std::optional expected_objective = std::nullopt, - double tolerance = kMatcherDefaultTolerance); - -testing::Matcher IsOptimalWithSolution( - double expected_objective, VariableMap expected_variable_values, - double tolerance = kMatcherDefaultTolerance); - -testing::Matcher IsOptimalWithDualSolution( - double expected_objective, LinearConstraintMap expected_dual_values, - VariableMap expected_reduced_costs, - double tolerance = kMatcherDefaultTolerance); - -// Checks the following: -// * The result has the expected termination reason. -testing::Matcher TerminatesWith(TerminationReason expected); - -// Checks that the result has one of the allowed termination reasons. -testing::Matcher TerminatesWithOneOf( - const std::vector& allowed); - -// Checks the following: -// * The result has termination reason kFeasible or kNoSolutionFound. -// * The limit is expected, or is kUndetermined if allow_limit_undetermined. -testing::Matcher TerminatesWithLimit( - Limit expected, bool allow_limit_undetermined = false); - -// Checks the following: -// * The result has termination reason kFeasible. -// * The limit is expected, or is kUndetermined if allow_limit_undetermined. -testing::Matcher TerminatesWithReasonFeasible( - Limit expected, bool allow_limit_undetermined = false); - -// Checks the following: -// * The result has termination reason kNoSolutionFound. -// * The limit is expected, or is kUndetermined if allow_limit_undetermined. -testing::Matcher TerminatesWithReasonNoSolutionFound( - Limit expected, bool allow_limit_undetermined = false); - -// SolveResult has a primal solution matching expected within tolerance. -testing::Matcher HasSolution( - PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); - -// SolveResult has a dual solution matching expected within -// tolerance. -testing::Matcher HasDualSolution( - DualSolution expected, double tolerance = kMatcherDefaultTolerance); - -// Actual SolveResult contains a primal ray that matches expected within -// tolerance. -testing::Matcher HasPrimalRay( - PrimalRay expected, double tolerance = kMatcherDefaultTolerance); - -// Actual SolveResult contains a primal ray with variable values equivalent to -// (under L_inf scaling) expected_vars up to tolerance. -testing::Matcher HasPrimalRay( - VariableMap expected_vars, - double tolerance = kMatcherDefaultTolerance); - -// Actual SolveResult contains a dual ray that matches expected within -// tolerance. -testing::Matcher HasDualRay( - DualRay expected, double tolerance = kMatcherDefaultTolerance); - -// Configures SolveResult matcher IsConsistentWith() below. -struct SolveResultMatcherOptions { - double tolerance = 1e-5; - bool first_solution_only = true; - bool check_dual = true; - bool check_rays = true; - - // If the expected result has termination reason kInfeasible, kUnbounded, or - // kDualInfeasasible, the primal solution, dual solution, and basis are - // ignored unless check_solutions_if_inf_or_unbounded is true. - // - // TODO(b/201099290): this is perhaps not a good default. Gurobi as - // implemented is returning primal solutions for both unbounded and - // infeasible problems. We need to add unit tests that inspect this value - // and turn them on one solver at a time with a new parameter on - // SimpleLpTestParameters. - bool check_solutions_if_inf_or_unbounded = false; - bool check_basis = false; - - // In linear programming, the following outcomes are all possible - // - // Primal LP | Dual LP | Possible MathOpt Termination Reasons - // ----------------------------------------------------------------- - // 1. Infeasible | Unbounded | kInfeasible - // 2. Optimal | Optimal | kOptimal - // 3. Unbounded | Infeasible | kUnbounded, kInfeasibleOrUnbounded - // 4. Infeasible | Infeasible | kInfeasible, kInfeasibleOrUnbounded - // - // (Above "Optimal" means that an optimal solution exists. This is a statement - // about the existence of optimal solutions and certificates of - // infeasibility/unboundedness, not about the outcome of applying any - // particular algorithm.) - // - // When writing your unit test, you can typically tell which case of 1-4 you - // are in, but in cases 3-4 you do not know which termination reason will be - // returned. In some situations, it may not be clear if you are in case 1 or - // case 4 as well. - // - // When inf_or_unb_soft_match=false, the matcher must exactly specify the - // status returned by the solver. For cases 3-4, this is implementation - // dependent and we do not recommend this. When - // inf_or_unb_soft_match=true: - // * kInfeasible can also match kInfeasibleOrUnbounded - // * kUnbounded can also match kInfeasibleOrUnbounded - // * kInfeasibleOrUnbounded can also match kInfeasible and kUnbounded. - // For case 2, inf_or_unb_soft_match has no effect. - // - // To build the strongest possible matcher (accepting the minimal set of - // termination reasons): - // * If you know you are in case 1, se inf_or_unb_soft_match=false - // (soft_match=true over-matches) - // * For case 3, use inf_or_unb_soft_match=false and - // termination_reason=kUnbounded (kInfeasibleOrUnbounded over-matches). - // * For case 4 (or if you are unsure of case 1 vs case 4), use - // inf_or_unb_soft_match=true and - // termination_reason=kInfeasible (kInfeasibleOrUnbounded over-matches). - // * If you cannot tell if you are in case 3 or case 4, use - // inf_or_unb_soft_match=true and termination reason - // kInfeasibleOrUnbounded. - // - // If the above is too complicated, always setting - // inf_or_unb_soft_match=true and using any of the expected MathOpt - // termination reasons from the above table will give a matcher that is - // slightly too lenient. - bool inf_or_unb_soft_match = true; -}; - -// Tests that two SolveResults are equivalent. Basic use: -// -// SolveResult expected; -// // Fill in expected... -// ASSERT_OK_AND_ASSIGN(SolveResult actual, Solve(model, solver_type)); -// EXPECT_THAT(actual, IsConsistentWith(expected)); -// -// Equivalence is defined as follows: -// * The termination reasons are the same. -// - For infeasible and unbounded problems, see -// options.inf_or_unb_soft_match. -// * The solve stats are ignored. -// * For both primal and dual solutions, either expected and actual are -// both empty, or their first entries satisfy IsNear() at options.tolerance. -// - Not checked if options.check_solutions_if_inf_or_unbounded and the -// problem is infeasible or unbounded (default). -// - If options.first_solution_only is false, check the entire list of -// solutions matches in the same order. -// - Dual solution is not checked if options.check_dual=false -// * For both the primal and dual rays, either expected and actual are both -// empty, or any ray in expected IsNear() any ray in actual (which is up -// to a rescaling) at options.tolerance. -// - Not checked if options.check_rays=false -// - If options.first_solution_only is false, check the entire list of -// solutions matches in the same order. -// * The basis is not checked by default. If enabled, checked with BasisIs(). -// - Enable with options.check_basis -// -// This function is symmetric in that: -// EXPECT_THAT(actual, IsConsistentWith(expected)); -// EXPECT_THAT(expected, IsConsistentWith(actual)); -// agree on matching, they only differ in strings produced. Per gmock -// conventions, prefer the former. -// -// For problems with either primal or dual infeasibility, see -// SolveResultMatcherOptions::inf_or_unb_soft_match for guidance on how to -// best set the termination reason and inf_or_unb_soft_match. -testing::Matcher IsConsistentWith( - const SolveResult& expected, const SolveResultMatcherOptions& options = {}); - -//////////////////////////////////////////////////////////////////////////////// -// Rarely used -//////////////////////////////////////////////////////////////////////////////// - -// Actual UpdateResult.did_update is true. -testing::Matcher DidUpdate(); - -//////////////////////////////////////////////////////////////////////////////// -// Implementation details -//////////////////////////////////////////////////////////////////////////////// - -// TODO(b/200835670): use the << operator on Termination instead once it -// supports quoting/escaping on termination.detail. -void PrintTo(const Termination& termination, std::ostream* os); -void PrintTo(const PrimalSolution& primal_solution, std::ostream* os); -void PrintTo(const DualSolution& dual_solution, std::ostream* os); -void PrintTo(const PrimalRay& primal_ray, std::ostream* os); -void PrintTo(const DualRay& dual_ray, std::ostream* os); -void PrintTo(const Basis& basis, std::ostream* os); -void PrintTo(const Solution& solution, std::ostream* os); -void PrintTo(const SolveResult& result, std::ostream* os); - -// We do not want to rely on ::testing::internal::ContainerPrinter because we -// want to sort the keys. -template -void PrintTo(const IdMap& id_map, std::ostream* const os) { - constexpr int kMaxPrint = 10; - int num_added = 0; - *os << "{"; - for (const K k : id_map.SortedKeys()) { - if (num_added > 0) { - *os << ", "; - } - if (num_added >= kMaxPrint) { - *os << "...(size=" << id_map.size() << ")"; - break; - } - *os << "{" << k << ", " << ::testing::PrintToString(id_map.at(k)) << "}"; - ++num_added; - } - *os << "}"; -} - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_ diff --git a/ortools/math_opt/cpp/message_callback.cc b/ortools/math_opt/cpp/message_callback.cc index 6ce3025e34..54a17d39ad 100644 --- a/ortools/math_opt/cpp/message_callback.cc +++ b/ortools/math_opt/cpp/message_callback.cc @@ -47,6 +47,21 @@ class PrinterMessageCallbackImpl { const std::string prefix_; }; +class VectorMessageCallbackImpl { + public: + explicit VectorMessageCallbackImpl(std::vector* const sink) + : sink_(ABSL_DIE_IF_NULL(sink)) {} + + void Call(const std::vector& messages) { + const absl::MutexLock lock(&mutex_); + sink_->insert(sink_->end(), messages.begin(), messages.end()); + } + + private: + absl::Mutex mutex_; + std::vector* const sink_; +}; + } // namespace MessageCallback PrinterMessageCallback(std::ostream& output_stream, @@ -60,4 +75,14 @@ MessageCallback PrinterMessageCallback(std::ostream& output_stream, [=](const std::vector& messages) { impl->Call(messages); }; } +MessageCallback VectorMessageCallback(std::vector* sink) { + CHECK(sink != nullptr); + // Here we must use an std::shared_ptr since std::function requires that its + // input is copyable. And VectorMessageCallbackImpl can't be copyable since it + // uses an absl::Mutex that is not. + const auto impl = std::make_shared(sink); + return + [=](const std::vector& messages) { impl->Call(messages); }; +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/message_callback.h b/ortools/math_opt/cpp/message_callback.h index 6a6273100a..05e617e888 100644 --- a/ortools/math_opt/cpp/message_callback.h +++ b/ortools/math_opt/cpp/message_callback.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -49,6 +50,16 @@ using MessageCallback = std::function&)>; MessageCallback PrinterMessageCallback(std::ostream& output_stream = std::cout, absl::string_view prefix = ""); +// Returns a message callback function that aggregates all messages in the +// provided vector. +// +// Usage: +// +// std::vector msgs; +// SolveArguments args; +// args.message_callback = VectorMessageCallback(&msgs); +MessageCallback VectorMessageCallback(std::vector* sink); + } // namespace operations_research::math_opt #endif // OR_TOOLS_MATH_OPT_CPP_MESSAGE_CALLBACK_H_ diff --git a/ortools/math_opt/cpp/model.cc b/ortools/math_opt/cpp/model.cc index 7cb83a3aa4..48bdd0b5ce 100644 --- a/ortools/math_opt/cpp/model.cc +++ b/ortools/math_opt/cpp/model.cc @@ -22,24 +22,27 @@ #include #include +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/base/status_macros.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/indicator/indicator_constraint.h" #include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" #include "ortools/math_opt/constraints/sos/sos1_constraint.h" #include "ortools/math_opt/constraints/sos/sos2_constraint.h" #include "ortools/math_opt/constraints/util/model_util.h" #include "ortools/math_opt/cpp/linear_constraint.h" #include "ortools/math_opt/cpp/update_tracker.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/linear_expression_data.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/math_opt/storage/model_storage_types.h" #include "ortools/math_opt/storage/sparse_coefficient_map.h" #include "ortools/math_opt/storage/sparse_matrix.h" +#include "ortools/util/fp_roundtrip_conv.h" namespace operations_research { namespace math_opt { @@ -71,8 +74,9 @@ LinearConstraint Model::AddLinearConstraint( const LinearConstraintId constraint = storage()->AddLinearConstraint( bounded_expr.lower_bound_minus_offset(), bounded_expr.upper_bound_minus_offset(), name); - for (auto [variable, coef] : bounded_expr.expression.raw_terms()) { - storage()->set_linear_constraint_coefficient(constraint, variable, coef); + for (const auto& [variable, coef] : bounded_expr.expression.terms()) { + storage()->set_linear_constraint_coefficient(constraint, + variable.typed_id(), coef); } return LinearConstraint(storage(), constraint); } @@ -138,78 +142,135 @@ std::vector Model::SortedLinearConstraints() const { void Model::SetObjective(const LinearExpression& objective, const bool is_maximize) { CheckOptionalModel(objective.storage()); - storage()->clear_objective(); - storage()->set_is_maximize(is_maximize); - storage()->set_objective_offset(objective.offset()); - for (auto [var, coef] : objective.raw_terms()) { - storage()->set_linear_objective_coefficient(var, coef); + storage()->clear_objective(kPrimaryObjectiveId); + storage()->set_is_maximize(kPrimaryObjectiveId, is_maximize); + storage()->set_objective_offset(kPrimaryObjectiveId, objective.offset()); + for (const auto& [var, coef] : objective.terms()) { + storage()->set_linear_objective_coefficient(kPrimaryObjectiveId, + var.typed_id(), coef); } } void Model::SetObjective(const QuadraticExpression& objective, const bool is_maximize) { CheckOptionalModel(objective.storage()); - storage()->clear_objective(); - storage()->set_is_maximize(is_maximize); - storage()->set_objective_offset(objective.offset()); - for (auto [var, coef] : objective.raw_linear_terms()) { - storage()->set_linear_objective_coefficient(var, coef); + storage()->clear_objective(kPrimaryObjectiveId); + storage()->set_is_maximize(kPrimaryObjectiveId, is_maximize); + storage()->set_objective_offset(kPrimaryObjectiveId, objective.offset()); + for (const auto& [var, coef] : objective.linear_terms()) { + storage()->set_linear_objective_coefficient(kPrimaryObjectiveId, + var.typed_id(), coef); } - for (auto [vars, coef] : objective.raw_quadratic_terms()) { - storage()->set_quadratic_objective_coefficient(vars.first, vars.second, - coef); + for (const auto& [vars, coef] : objective.quadratic_terms()) { + storage()->set_quadratic_objective_coefficient( + kPrimaryObjectiveId, vars.typed_id().first, vars.typed_id().second, + coef); } } void Model::AddToObjective(const LinearExpression& objective_terms) { CheckOptionalModel(objective_terms.storage()); - storage()->set_objective_offset(objective_terms.offset() + - storage()->objective_offset()); - for (auto [var, coef] : objective_terms.raw_terms()) { + storage()->set_objective_offset( + kPrimaryObjectiveId, + objective_terms.offset() + + storage()->objective_offset(kPrimaryObjectiveId)); + for (const auto& [var, coef] : objective_terms.terms()) { storage()->set_linear_objective_coefficient( - var, coef + storage()->linear_objective_coefficient(var)); + kPrimaryObjectiveId, var.typed_id(), + coef + storage()->linear_objective_coefficient(kPrimaryObjectiveId, + var.typed_id())); } } void Model::AddToObjective(const QuadraticExpression& objective_terms) { CheckOptionalModel(objective_terms.storage()); - storage()->set_objective_offset(objective_terms.offset() + - storage()->objective_offset()); - for (auto [var, coef] : objective_terms.raw_linear_terms()) { + storage()->set_objective_offset( + kPrimaryObjectiveId, + objective_terms.offset() + + storage()->objective_offset(kPrimaryObjectiveId)); + for (const auto& [var, coef] : objective_terms.linear_terms()) { storage()->set_linear_objective_coefficient( - var, coef + storage()->linear_objective_coefficient(var)); + kPrimaryObjectiveId, var.typed_id(), + coef + storage()->linear_objective_coefficient(kPrimaryObjectiveId, + var.typed_id())); } - for (auto [vars, coef] : objective_terms.raw_quadratic_terms()) { + for (const auto& [vars, coef] : objective_terms.quadratic_terms()) { storage()->set_quadratic_objective_coefficient( - vars.first, vars.second, - coef + storage()->quadratic_objective_coefficient(vars.first, - vars.second)); + kPrimaryObjectiveId, vars.typed_id().first, vars.typed_id().second, + coef + storage()->quadratic_objective_coefficient( + kPrimaryObjectiveId, vars.typed_id().first, + vars.typed_id().second)); } } LinearExpression Model::ObjectiveAsLinearExpression() const { - CHECK_EQ(storage()->num_quadratic_objective_terms(), 0) + CHECK_EQ(storage()->num_quadratic_objective_terms(kPrimaryObjectiveId), 0) << "The objective function contains quadratic terms and cannot be " "represented as a LinearExpression"; - LinearExpression result = storage()->objective_offset(); - for (const auto& [v, coef] : storage()->linear_objective()) { + LinearExpression result = storage()->objective_offset(kPrimaryObjectiveId); + for (const auto& [v, coef] : + storage()->linear_objective(kPrimaryObjectiveId)) { result += Variable(storage(), v) * coef; } return result; } QuadraticExpression Model::ObjectiveAsQuadraticExpression() const { - QuadraticExpression result = storage()->objective_offset(); - for (const auto& [v, coef] : storage()->linear_objective()) { + QuadraticExpression result = storage()->objective_offset(kPrimaryObjectiveId); + for (const auto& [v, coef] : + storage()->linear_objective(kPrimaryObjectiveId)) { result += Variable(storage(), v) * coef; } - for (const auto& [v1, v2, coef] : storage()->quadratic_objective_terms()) { + for (const auto& [v1, v2, coef] : + storage()->quadratic_objective_terms(kPrimaryObjectiveId)) { result += QuadraticTerm(Variable(storage(), v1), Variable(storage(), v2), coef); } return result; } +std::vector Model::AuxiliaryObjectives() const { + std::vector result; + result.reserve(num_auxiliary_objectives()); + for (const AuxiliaryObjectiveId id : storage()->AuxiliaryObjectives()) { + result.push_back(auxiliary_objective(id)); + } + return result; +} + +std::vector Model::SortedAuxiliaryObjectives() const { + std::vector result = AuxiliaryObjectives(); + std::sort(result.begin(), result.end(), + [](const Objective& l, const Objective& r) { + return l.typed_id() < r.typed_id(); + }); + return result; +} + +void Model::SetObjective(const Objective objective, + const LinearExpression& expression, + const bool is_maximize) { + CheckModel(objective.storage()); + CheckOptionalModel(expression.storage()); + storage()->clear_objective(objective.typed_id()); + set_is_maximize(objective, is_maximize); + set_objective_offset(objective, expression.offset()); + for (const auto [var, coef] : expression.terms()) { + set_objective_coefficient(objective, var, coef); + } +} + +void Model::AddToObjective(Objective objective, + const LinearExpression& expression) { + CheckModel(objective.storage()); + CheckOptionalModel(expression.storage()); + set_objective_offset(objective, objective.offset() + expression.offset()); + for (const auto [var, coef] : expression.terms()) { + set_objective_coefficient(objective, var, + objective.coefficient(var) + coef); + } +} + ModelProto Model::ExportModel() const { return storage()->ExportModel(); } std::unique_ptr Model::NewUpdateTracker() { @@ -225,9 +286,22 @@ std::ostream& operator<<(std::ostream& ostr, const Model& model) { if (!model.name().empty()) ostr << " " << model.name(); ostr << ":\n"; - ostr << " Objective:\n" - << (model.is_maximize() ? " maximize " : " minimize ") - << model.ObjectiveAsQuadraticExpression() << "\n"; + if (model.num_auxiliary_objectives() == 0) { + ostr << " Objective:\n" + << (model.is_maximize() ? " maximize " : " minimize ") + << model.ObjectiveAsQuadraticExpression() << "\n"; + } else { + ostr << " Objectives:\n"; + const auto stream_objective = [](std::ostream& ostr, const Objective obj) { + ostr << " " << obj << " (priority " << obj.priority() + << "): " << (obj.maximize() ? "maximize " : "minimize ") + << obj.AsQuadraticExpression() << "\n"; + }; + stream_objective(ostr, model.primary_objective()); + for (const Objective obj : model.SortedAuxiliaryObjectives()) { + stream_objective(ostr, obj); + } + } ostr << " Linear constraints:\n"; for (const LinearConstraint constraint : model.SortedLinearConstraints()) { @@ -244,6 +318,14 @@ std::ostream& operator<<(std::ostream& ostr, const Model& model) { } } + if (model.num_second_order_cone_constraints() > 0) { + ostr << " Second-order cone constraints:\n"; + for (const SecondOrderConeConstraint constraint : + model.SortedSecondOrderConeConstraints()) { + ostr << " " << constraint << ": " << constraint.ToString() << "\n"; + } + } + if (model.num_sos1_constraints() > 0) { ostr << " SOS1 constraints:\n"; for (const Sos1Constraint constraint : model.SortedSos1Constraints()) { @@ -305,8 +387,9 @@ QuadraticConstraint Model::AddQuadraticConstraint( } SparseSymmetricMatrix quadratic_terms; for (const auto& [var_ids, coeff] : - bounded_expr.expression.raw_quadratic_terms()) { - quadratic_terms.set(var_ids.first, var_ids.second, coeff); + bounded_expr.expression.quadratic_terms()) { + quadratic_terms.set(var_ids.typed_id().first, var_ids.typed_id().second, + coeff); } const QuadraticConstraintId id = storage()->AddAtomicConstraint(QuadraticConstraintData{ @@ -319,6 +402,27 @@ QuadraticConstraint Model::AddQuadraticConstraint( return QuadraticConstraint(storage(), id); } +// --------------------- Second-order cone constraints ------------------------- + +SecondOrderConeConstraint Model::AddSecondOrderConeConstraint( + const std::vector& arguments_to_norm, + const LinearExpression& upper_bound, const absl::string_view name) { + CheckOptionalModel(upper_bound.storage()); + std::vector arguments_to_norm_data; + arguments_to_norm_data.reserve(arguments_to_norm.size()); + for (const LinearExpression& expr : arguments_to_norm) { + CheckOptionalModel(expr.storage()); + arguments_to_norm_data.push_back(FromLinearExpression(expr)); + } + const SecondOrderConeConstraintId id = + storage()->AddAtomicConstraint(SecondOrderConeConstraintData{ + .upper_bound = FromLinearExpression(upper_bound), + .arguments_to_norm = std::move(arguments_to_norm_data), + .name = std::string(name), + }); + return SecondOrderConeConstraint(storage(), id); +} + // --------------------------- SOS1 constraints -------------------------------- namespace { @@ -326,14 +430,13 @@ namespace { template SosData MakeSosData(const std::vector& expressions, std::vector weights, const absl::string_view name) { - std::vector storage_expressions; + std::vector storage_expressions; storage_expressions.reserve(expressions.size()); for (const LinearExpression& expr : expressions) { - typename SosData::LinearExpression& storage_expr = - storage_expressions.emplace_back(); + LinearExpressionData& storage_expr = storage_expressions.emplace_back(); storage_expr.offset = expr.offset(); - for (const auto [var, coeff] : expr.raw_terms()) { - storage_expr.terms[var] = coeff; + for (const auto& [var, coeff] : expr.terms()) { + storage_expr.coeffs.set(var.typed_id(), coeff); } } return SosData(std::move(storage_expressions), std::move(weights), diff --git a/ortools/math_opt/cpp/model.h b/ortools/math_opt/cpp/model.h index 3556f2b3e5..7b0bb423f8 100644 --- a/ortools/math_opt/cpp/model.h +++ b/ortools/math_opt/cpp/model.h @@ -24,19 +24,21 @@ #include #include +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/base/status_builder.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/indicator/indicator_constraint.h" // IWYU pragma: export #include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" // IWYU pragma: export +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" // IWYU pragma: export #include "ortools/math_opt/constraints/sos/sos1_constraint.h" // IWYU pragma: export #include "ortools/math_opt/constraints/sos/sos2_constraint.h" // IWYU pragma: export #include "ortools/math_opt/constraints/util/model_util.h" #include "ortools/math_opt/cpp/key_types.h" #include "ortools/math_opt/cpp/linear_constraint.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/objective.h" // IWYU pragma: export #include "ortools/math_opt/cpp/update_tracker.h" // IWYU pragma: export #include "ortools/math_opt/cpp/variable_and_expressions.h" // IWYU pragma: export #include "ortools/math_opt/model.pb.h" // IWYU pragma: export @@ -410,6 +412,64 @@ class Model { // the model sorted by id. inline std::vector SortedQuadraticConstraints() const; + ////////////////////////////////////////////////////////////////////////////// + // SecondOrderConeConstraint methods + ////////////////////////////////////////////////////////////////////////////// + + // Adds a second-order cone constraint to the model of the form + // ||arguments_to_norm||₂ ≤ upper_bound. + // + // Usage: + // Model model = ...; + // const Variable x = ...; + // const Variable y = ...; + // model.AddSecondOrderConeConstraint({x, y}, 1.0, "soc"); + // model.AddSecondOrderConeConstraint({1.0, 3 * y - x}, 2 * x); + SecondOrderConeConstraint AddSecondOrderConeConstraint( + const std::vector& arguments_to_norm, + const LinearExpression& upper_bound, absl::string_view name = ""); + + // Removes a second-order cone constraint from the model. + // + // It is an error to use any reference to this second-order cone constraint + // after this operation. Runs in O(#linear terms appearing in constraint). + inline void DeleteSecondOrderConeConstraint( + SecondOrderConeConstraint constraint); + + // The number of second-order cone constraints in the model. + // + // Equal to the number of second-order cone constraints created minus the + // number of second-order cone constraints deleted. + inline int64_t num_second_order_cone_constraints() const; + + // The returned id of the next call to AddSecondOrderConeConstraint. + inline int64_t next_second_order_cone_constraint_id() const; + + // Returns true if this id has been created and not yet deleted. + inline bool has_second_order_cone_constraint(int64_t id) const; + + // Returns true if this id has been created and not yet deleted. + inline bool has_second_order_cone_constraint( + SecondOrderConeConstraintId id) const; + + // Will CHECK if has_second_order_cone_constraint(id) is false. + inline SecondOrderConeConstraint second_order_cone_constraint( + int64_t id) const; + + // Will CHECK if has_second_order_cone_constraint(id) is false. + inline SecondOrderConeConstraint second_order_cone_constraint( + SecondOrderConeConstraintId id) const; + + // Returns all the existing (created and not deleted) second-order cone + // constraints in the model in an arbitrary order. + inline std::vector SecondOrderConeConstraints() + const; + + // Returns all the existing (created and not deleted) second-order cone + // constraints in the model sorted by id. + inline std::vector + SortedSecondOrderConeConstraints() const; + ////////////////////////////////////////////////////////////////////////////// // Sos1Constraint methods ////////////////////////////////////////////////////////////////////////////// @@ -642,6 +702,11 @@ class Model { LinearExpression ObjectiveAsLinearExpression() const; QuadraticExpression ObjectiveAsQuadraticExpression() const; + // Returns an object referring to the primary objective in the model. Can be + // used with the multi-objective API in the same way that an auxiliary + // objective can be. + inline Objective primary_objective() const; + // Returns 0.0 if this variable has no linear objective coefficient. inline double objective_coefficient(Variable variable) const; @@ -661,8 +726,9 @@ class Model { inline void set_objective_coefficient(Variable first_variable, Variable second_variable, double value); - // Equivalent to calling set_linear_coefficient(v, 0.0) for every variable - // with nonzero objective coefficient. + // Sets the objective offset, linear terms, and quadratic terms of the + // objective to zero. The name, direction, and priority are unchanged. + // Equivalent to SetObjective(0.0, is_maximize()). // // Runs in O(#linear and quadratic objective terms with nonzero coefficient). inline void clear_objective(); @@ -683,6 +749,103 @@ class Model { // Prefer set_maximize() and set_minimize() above for more readable code. inline void set_is_maximize(bool is_maximize); + ////////////////////////////////////////////////////////////////////////////// + // Auxiliary objective methods + // + // This is an API for creating and deleting auxiliary objectives. To modify + // them, use the multi-objective API below. + ////////////////////////////////////////////////////////////////////////////// + + // Adds an empty (== 0) auxiliary minimization objective to the model. + inline Objective AddAuxiliaryObjective(int64_t priority, + absl::string_view name = {}); + // Adds `expression` as an auxiliary objective to the model. + inline Objective AddAuxiliaryObjective(const LinearExpression& expression, + bool is_maximize, int64_t priority, + absl::string_view name = {}); + // Adds `expression` as an auxiliary maximization objective to the model. + inline Objective AddMaximizationObjective(const LinearExpression& expression, + int64_t priority, + absl::string_view name = {}); + // Adds `expression` as an auxiliary minimization objective to the model. + inline Objective AddMinimizationObjective(const LinearExpression& expression, + int64_t priority, + absl::string_view name = {}); + + // Removes an auxiliary objective from the model. + // + // It is an error to use any reference to this auxiliary objective after this + // operation. Runs in O(1) time. + // + // Will CHECK-fail if `objective` is from another model, has already been + // deleted, or is a primary objective. + inline void DeleteAuxiliaryObjective(Objective objective); + + // The number of auxiliary objectives in the model. + // + // Equal to the number of auxiliary objectives created minus the number of + // auxiliary objectives deleted. + inline int64_t num_auxiliary_objectives() const; + + // The returned id of the next call to AddAuxiliaryObjectve. + inline int64_t next_auxiliary_objective_id() const; + + // Returns true if this id has been created and not yet deleted. + inline bool has_auxiliary_objective(int64_t id) const; + + // Returns true if this id has been created and not yet deleted. + inline bool has_auxiliary_objective(AuxiliaryObjectiveId id) const; + + // Will CHECK if has_auxiliary_objective(id) is false. + inline Objective auxiliary_objective(int64_t id) const; + + // Will CHECK if has_auxiliary_objective(id) is false. + inline Objective auxiliary_objective(AuxiliaryObjectiveId id) const; + + // Returns all the existing (created and not deleted) auxiliary objectives in + // the model in an arbitrary order. + std::vector AuxiliaryObjectives() const; + + // Returns all the existing (created and not deleted) auxiliary objectives in + // the model sorted by id. + std::vector SortedAuxiliaryObjectives() const; + + ////////////////////////////////////////////////////////////////////////////// + // Multi-objective methods + // + // This is an API for setting objective properties (for either primary or + // auxiliary objectives). Only linear objectives are supported through this + // API. To query objective properties, use the methods on `Objective`. + ////////////////////////////////////////////////////////////////////////////// + + // Sets `objective` to be maximizing `expression`. + inline void Maximize(Objective objective, const LinearExpression& expression); + // Sets `objective` to be minimizing `expression`. + inline void Minimize(Objective objective, const LinearExpression& expression); + // Sets the objective to optimize the provided expression. + void SetObjective(Objective objective, const LinearExpression& expression, + bool is_maximize); + + // Adds the provided expression terms to the objective. + void AddToObjective(Objective objective, const LinearExpression& expression); + + // Sets the priority for an objective (lower is more important). `priority` + // must be nonnegative. + inline void set_objective_priority(Objective objective, int64_t priority); + + // Setting a value to 0.0 will delete the variable from the underlying sparse + // representation (and has no effect if the variable is not present). + inline void set_objective_coefficient(Objective objective, Variable variable, + double value); + + inline void set_objective_offset(Objective objective, double value); + + inline void set_maximize(Objective objective); + inline void set_minimize(Objective objective); + + // Prefer set_maximize() and set_minimize() above for more readable code. + inline void set_is_maximize(Objective objective, bool is_maximize); + // Returns a proto representation of the optimization model. // // See FromModelProto() to build a Model from a proto. @@ -1027,6 +1190,53 @@ std::vector Model::SortedQuadraticConstraints() const { return SortedAtomicConstraints(*storage()); } +// --------------------- Second-order cone constraints ------------------------- + +void Model::DeleteSecondOrderConeConstraint( + const SecondOrderConeConstraint constraint) { + CheckModel(constraint.storage()); + storage()->DeleteAtomicConstraint(constraint.typed_id()); +} + +int64_t Model::num_second_order_cone_constraints() const { + return storage()->num_constraints(); +} + +int64_t Model::next_second_order_cone_constraint_id() const { + return storage()->next_constraint_id().value(); +} + +bool Model::has_second_order_cone_constraint(const int64_t id) const { + return has_second_order_cone_constraint(SecondOrderConeConstraintId(id)); +} + +bool Model::has_second_order_cone_constraint( + const SecondOrderConeConstraintId id) const { + return storage()->has_constraint(id); +} + +SecondOrderConeConstraint Model::second_order_cone_constraint( + const int64_t id) const { + return second_order_cone_constraint(SecondOrderConeConstraintId(id)); +} + +SecondOrderConeConstraint Model::second_order_cone_constraint( + const SecondOrderConeConstraintId id) const { + CHECK(has_second_order_cone_constraint(id)) + << "No second-order cone constraint with id: " << id.value(); + return SecondOrderConeConstraint(storage(), id); +} + +std::vector Model::SecondOrderConeConstraints() + const { + return AtomicConstraints(*storage()); +} + +std::vector Model::SortedSecondOrderConeConstraints() + const { + return SortedAtomicConstraints(*storage()); +} + // --------------------------- SOS1 constraints -------------------------------- void Model::DeleteSos1Constraint(const Sos1Constraint constraint) { @@ -1205,23 +1415,30 @@ void Model::AddToObjective(const LinearTerm objective) { AddToObjective(LinearExpression(objective)); } +Objective Model::primary_objective() const { + return Objective::Primary(storage()); +} + double Model::objective_coefficient(const Variable variable) const { CheckModel(variable.storage()); - return storage()->linear_objective_coefficient(variable.typed_id()); + return storage()->linear_objective_coefficient(kPrimaryObjectiveId, + variable.typed_id()); } double Model::objective_coefficient(const Variable first_variable, const Variable second_variable) const { CheckModel(first_variable.storage()); CheckModel(second_variable.storage()); - return storage()->quadratic_objective_coefficient(first_variable.typed_id(), + return storage()->quadratic_objective_coefficient(kPrimaryObjectiveId, + first_variable.typed_id(), second_variable.typed_id()); } void Model::set_objective_coefficient(const Variable variable, const double value) { CheckModel(variable.storage()); - storage()->set_linear_objective_coefficient(variable.typed_id(), value); + storage()->set_linear_objective_coefficient(kPrimaryObjectiveId, + variable.typed_id(), value); } void Model::set_objective_coefficient(const Variable first_variable, @@ -1230,15 +1447,18 @@ void Model::set_objective_coefficient(const Variable first_variable, CheckModel(first_variable.storage()); CheckModel(second_variable.storage()); storage()->set_quadratic_objective_coefficient( - first_variable.typed_id(), second_variable.typed_id(), value); + kPrimaryObjectiveId, first_variable.typed_id(), + second_variable.typed_id(), value); } -void Model::clear_objective() { storage()->clear_objective(); } +void Model::clear_objective() { + storage()->clear_objective(kPrimaryObjectiveId); +} bool Model::is_objective_coefficient_nonzero(const Variable variable) const { CheckModel(variable.storage()); return storage()->is_linear_objective_coefficient_nonzero( - variable.typed_id()); + kPrimaryObjectiveId, variable.typed_id()); } bool Model::is_objective_coefficient_nonzero( @@ -1246,23 +1466,140 @@ bool Model::is_objective_coefficient_nonzero( CheckModel(first_variable.storage()); CheckModel(second_variable.storage()); return storage()->is_quadratic_objective_coefficient_nonzero( - first_variable.typed_id(), second_variable.typed_id()); + kPrimaryObjectiveId, first_variable.typed_id(), + second_variable.typed_id()); } -double Model::objective_offset() const { return storage()->objective_offset(); } +double Model::objective_offset() const { + return storage()->objective_offset(kPrimaryObjectiveId); +} void Model::set_objective_offset(const double value) { - storage()->set_objective_offset(value); + storage()->set_objective_offset(kPrimaryObjectiveId, value); } -bool Model::is_maximize() const { return storage()->is_maximize(); } +bool Model::is_maximize() const { + return storage()->is_maximize(kPrimaryObjectiveId); +} -void Model::set_maximize() { storage()->set_maximize(); } +void Model::set_maximize() { storage()->set_maximize(kPrimaryObjectiveId); } -void Model::set_minimize() { storage()->set_minimize(); } +void Model::set_minimize() { storage()->set_minimize(kPrimaryObjectiveId); } void Model::set_is_maximize(const bool is_maximize) { - storage()->set_is_maximize(is_maximize); + storage()->set_is_maximize(kPrimaryObjectiveId, is_maximize); +} + +// -------------------------- Auxiliary objectives ----------------------------- + +Objective Model::AddAuxiliaryObjective(const int64_t priority, + const absl::string_view name) { + return Objective::Auxiliary(storage(), + storage()->AddAuxiliaryObjective(priority, name)); +} + +Objective Model::AddAuxiliaryObjective(const LinearExpression& expression, + const bool is_maximize, + const int64_t priority, + const absl::string_view name) { + const Objective obj = AddAuxiliaryObjective(priority, name); + SetObjective(obj, expression, is_maximize); + return obj; +} + +Objective Model::AddMaximizationObjective(const LinearExpression& expression, + const int64_t priority, + const absl::string_view name) { + return AddAuxiliaryObjective(expression, /*is_maximize=*/true, priority, + name); +} + +Objective Model::AddMinimizationObjective(const LinearExpression& expression, + const int64_t priority, + const absl::string_view name) { + return AddAuxiliaryObjective(expression, /*is_maximize=*/false, priority, + name); +} + +void Model::DeleteAuxiliaryObjective(const Objective objective) { + CheckModel(objective.storage()); + CHECK(!objective.is_primary()) << "cannot delete primary objective"; + const AuxiliaryObjectiveId id = *objective.typed_id(); + CHECK(storage()->has_auxiliary_objective(id)) + << "cannot delete unrecognized auxiliary objective id: " << id; + storage()->DeleteAuxiliaryObjective(id); +} + +int64_t Model::num_auxiliary_objectives() const { + return storage()->num_auxiliary_objectives(); +} + +int64_t Model::next_auxiliary_objective_id() const { + return storage()->next_auxiliary_objective_id().value(); +} + +bool Model::has_auxiliary_objective(const int64_t id) const { + return has_auxiliary_objective(AuxiliaryObjectiveId(id)); +} + +bool Model::has_auxiliary_objective(const AuxiliaryObjectiveId id) const { + return storage()->has_auxiliary_objective(id); +} + +Objective Model::auxiliary_objective(const int64_t id) const { + return auxiliary_objective(AuxiliaryObjectiveId(id)); +} + +Objective Model::auxiliary_objective(const AuxiliaryObjectiveId id) const { + CHECK(has_auxiliary_objective(id)) + << "unrecognized auxiliary objective id: " << id; + return Objective::Auxiliary(storage(), id); +} + +// ---------------------------- Multi-objective -------------------------------- + +void Model::Maximize(const Objective objective, + const LinearExpression& expression) { + SetObjective(objective, expression, /*is_maximize=*/true); +} + +void Model::Minimize(const Objective objective, + const LinearExpression& expression) { + SetObjective(objective, expression, /*is_maximize=*/false); +} + +void Model::set_objective_priority(const Objective objective, + const int64_t priority) { + CheckModel(objective.storage()); + storage()->set_objective_priority(objective.typed_id(), priority); +} + +void Model::set_objective_coefficient(const Objective objective, + const Variable variable, + const double value) { + CheckModel(objective.storage()); + CheckModel(variable.storage()); + storage()->set_linear_objective_coefficient(objective.typed_id(), + variable.typed_id(), value); +} + +void Model::set_objective_offset(const Objective objective, + const double value) { + CheckModel(objective.storage()); + storage()->set_objective_offset(objective.typed_id(), value); +} + +void Model::set_maximize(const Objective objective) { + set_is_maximize(objective, /*is_maximize=*/true); +} + +void Model::set_minimize(const Objective objective) { + set_is_maximize(objective, /*is_maximize=*/false); +} + +void Model::set_is_maximize(const Objective objective, const bool is_maximize) { + CheckModel(objective.storage()); + storage()->set_is_maximize(objective.typed_id(), is_maximize); } void Model::CheckOptionalModel(const ModelStorage* const other_storage) const { diff --git a/ortools/math_opt/cpp/model_solve_parameters.cc b/ortools/math_opt/cpp/model_solve_parameters.cc index 0f34d061da..bc16280c64 100644 --- a/ortools/math_opt/cpp/model_solve_parameters.cc +++ b/ortools/math_opt/cpp/model_solve_parameters.cc @@ -23,11 +23,13 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/cpp/linear_constraint.h" #include "ortools/math_opt/cpp/solution.h" +#include "ortools/math_opt/cpp/sparse_containers.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/storage/model_storage.h" +#include "ortools/util/status_macros.h" namespace operations_research { namespace math_opt { @@ -49,30 +51,63 @@ ModelSolveParameters ModelSolveParameters::OnlySomePrimalVariables( absl::Status ModelSolveParameters::CheckModelStorage( const ModelStorage* const expected_storage) const { for (const SolutionHint& hint : solution_hints) { - RETURN_IF_ERROR(internal::CheckModelStorage( - /*storage=*/hint.variable_values.storage(), - /*expected_storage=*/expected_storage)) - << "invalid solution_hints"; + RETURN_IF_ERROR(hint.CheckModelStorage(expected_storage)) + << "invalid hint in solution_hints"; } if (initial_basis.has_value()) { RETURN_IF_ERROR(initial_basis->CheckModelStorage(expected_storage)) << "invalid initial_basis"; } - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/variable_values_filter.storage(), - /*expected_storage=*/expected_storage)) + RETURN_IF_ERROR(variable_values_filter.CheckModelStorage(expected_storage)) << "invalid variable_values_filter"; - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/dual_values_filter.storage(), - /*expected_storage=*/expected_storage)) + RETURN_IF_ERROR(dual_values_filter.CheckModelStorage(expected_storage)) << "invalid dual_values_filter"; - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/reduced_costs_filter.storage(), - /*expected_storage=*/expected_storage)) + RETURN_IF_ERROR(reduced_costs_filter.CheckModelStorage(expected_storage)) << "invalid reduced_costs_filter"; return absl::OkStatus(); } +absl::Status ModelSolveParameters::SolutionHint::CheckModelStorage( + const ModelStorage* expected_storage) const { + for (const auto& [v, _] : variable_values) { + RETURN_IF_ERROR(internal::CheckModelStorage( + /*storage=*/v.storage(), + /*expected_storage=*/expected_storage)) + << "invalid variable " << v << " in variable_values"; + } + for (const auto& [c, _] : dual_values) { + RETURN_IF_ERROR(internal::CheckModelStorage( + /*storage=*/c.storage(), + /*expected_storage=*/expected_storage)) + << "invalid constraint " << c << " in dual_values"; + } + return absl::OkStatus(); +} + +SolutionHintProto ModelSolveParameters::SolutionHint::Proto() const { + SolutionHintProto hint; + *hint.mutable_variable_values() = VariableValuesToProto(variable_values); + *hint.mutable_dual_values() = LinearConstraintValuesToProto(dual_values); + return hint; +} + +absl::StatusOr +ModelSolveParameters::SolutionHint::FromProto( + const Model& model, const SolutionHintProto& hint_proto) { + OR_ASSIGN_OR_RETURN3( + VariableMap variable_values, + VariableValuesFromProto(model.storage(), hint_proto.variable_values()), + _ << "failed to parse SolutionHintProto.variable_values"); + OR_ASSIGN_OR_RETURN3(LinearConstraintMap dual_values, + LinearConstraintValuesFromProto( + model.storage(), hint_proto.dual_values()), + _ << "failed to parse SolutionHintProto.dual_values"); + return SolutionHint{ + .variable_values = std::move(variable_values), + .dual_values = std::move(dual_values), + }; +} + ModelSolveParametersProto ModelSolveParameters::Proto() const { ModelSolveParametersProto ret; *ret.mutable_variable_values_filter() = variable_values_filter.Proto(); @@ -91,7 +126,7 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const { constraint_status_ids->Reserve(initial_basis->constraint_status.size()); constraint_status_values->Reserve(initial_basis->constraint_status.size()); for (const LinearConstraint& key : - initial_basis->constraint_status.SortedKeys()) { + SortedKeys(initial_basis->constraint_status)) { constraint_status_ids->Add(key.id()); constraint_status_values->Add( EnumToProto(initial_basis->constraint_status.at(key))); @@ -104,24 +139,14 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const { ->mutable_values(); variable_status_ids->Reserve(initial_basis->variable_status.size()); variable_status_values->Reserve(initial_basis->variable_status.size()); - for (const Variable& key : initial_basis->variable_status.SortedKeys()) { + for (const Variable& key : SortedKeys(initial_basis->variable_status)) { variable_status_ids->Add(key.id()); variable_status_values->Add( EnumToProto(initial_basis->variable_status.at(key))); } } for (const SolutionHint& solution_hint : solution_hints) { - SolutionHintProto& hint = *ret.add_solution_hints(); - RepeatedField* const variable_ids = - hint.mutable_variable_values()->mutable_ids(); - RepeatedField* const variable_values = - hint.mutable_variable_values()->mutable_values(); - variable_ids->Reserve(solution_hint.variable_values.size()); - variable_values->Reserve(solution_hint.variable_values.size()); - for (const Variable& key : solution_hint.variable_values.SortedKeys()) { - variable_ids->Add(key.id()); - variable_values->Add(solution_hint.variable_values.at(key)); - } + *ret.add_solution_hints() = solution_hint.Proto(); } if (!branching_priorities.empty()) { RepeatedField* const variable_ids = @@ -130,7 +155,7 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const { ret.mutable_branching_priorities()->mutable_values(); variable_ids->Reserve(branching_priorities.size()); variable_values->Reserve(branching_priorities.size()); - for (const Variable& key : branching_priorities.SortedKeys()) { + for (const Variable& key : SortedKeys(branching_priorities)) { variable_ids->Add(key.id()); variable_values->Add(branching_priorities.at(key)); } diff --git a/ortools/math_opt/cpp/model_solve_parameters.h b/ortools/math_opt/cpp/model_solve_parameters.h index bd4882e0cf..11f3b97723 100644 --- a/ortools/math_opt/cpp/model_solve_parameters.h +++ b/ortools/math_opt/cpp/model_solve_parameters.h @@ -24,8 +24,10 @@ #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "ortools/math_opt/cpp/linear_constraint.h" #include "ortools/math_opt/cpp/map_filter.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/model.h" #include "ortools/math_opt/cpp/solution.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/model_parameters.pb.h" @@ -95,6 +97,24 @@ struct ModelSolveParameters { // solution_hints for details. struct SolutionHint { VariableMap variable_values; + LinearConstraintMap dual_values; + + // Returns a failure if the referenced variables and constraints don't + // belong to the input expected_storage (which must not be nullptr). + absl::Status CheckModelStorage(const ModelStorage* expected_storage) const; + + // Returns the proto equivalent of this object. + // + // The caller should use CheckModelStorage() as this function does not check + // internal consistency of the referenced variables and constraints. + SolutionHintProto Proto() const; + + // Returns the hint corresponding to this proto and the given model. + // + // This can be useful when loading a hint from another format, e.g. with + // MPModelProtoSolutionHintToMathOptHint(). + static absl::StatusOr FromProto( + const Model& model, const SolutionHintProto& hint_proto); }; // Optional solution hints. If set, they are expected to consist of diff --git a/ortools/math_opt/cpp/objective.cc b/ortools/math_opt/cpp/objective.cc new file mode 100644 index 0000000000..99842db4ea --- /dev/null +++ b/ortools/math_opt/cpp/objective.cc @@ -0,0 +1,76 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/cpp/objective.h" + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/string_view.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/model_storage_types.h" + +namespace operations_research::math_opt { + +LinearExpression Objective::AsLinearExpression() const { + CHECK_EQ(storage()->num_quadratic_objective_terms(id_), 0) + << "The objective function contains quadratic terms and cannot be " + "represented as a LinearExpression"; + LinearExpression objective = offset(); + for (const auto [raw_var_id, coeff] : storage_->linear_objective(id_)) { + objective += coeff * Variable(storage_, raw_var_id); + } + return objective; +} + +QuadraticExpression Objective::AsQuadraticExpression() const { + QuadraticExpression result = offset(); + for (const auto& [v, coef] : storage_->linear_objective(id_)) { + result += coef * Variable(storage(), v); + } + for (const auto& [v1, v2, coef] : storage_->quadratic_objective_terms(id_)) { + result += + QuadraticTerm(Variable(storage(), v1), Variable(storage(), v2), coef); + } + return result; +} + +std::string Objective::ToString() const { + if (!is_primary() && !storage()->has_auxiliary_objective(*id_)) { + return std::string(kDeletedObjectiveDefaultDescription); + } + std::stringstream str; + str << AsQuadraticExpression(); + return str.str(); +} + +std::ostream& operator<<(std::ostream& ostr, const Objective& objective) { + // TODO(b/170992529): handle quoting of invalid characters in the name. + const absl::string_view name = objective.name(); + if (name.empty()) { + if (objective.is_primary()) { + ostr << "__primary_obj__"; + } else { + ostr << "__aux_obj#" << *objective.id() << "__"; + } + } else { + ostr << name; + } + return ostr; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/objective.h b/ortools/math_opt/cpp/objective.h new file mode 100644 index 0000000000..672d7985fe --- /dev/null +++ b/ortools/math_opt/cpp/objective.h @@ -0,0 +1,213 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h" +// IWYU pragma: friend "ortools/math_opt/cpp/.*" +// +// An object oriented wrapper for objectives in ModelStorage. +#ifndef OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_ +#define OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_ + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/string_view.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/cpp/key_types.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/model_storage_types.h" + +namespace operations_research::math_opt { + +constexpr absl::string_view kDeletedObjectiveDefaultDescription = + "[objective deleted from model]"; + +// A value type that references an objective (either primary or auxiliary) from +// ModelStorage. Usually this type is passed by copy. +class Objective { + public: + // The type used for ids. + using IdType = AuxiliaryObjectiveId; + + // Returns an object that refers to the primary objective of the model. + inline static Objective Primary(const ModelStorage* storage); + // Returns an object that refers to an auxiliary objective of the model. + inline static Objective Auxiliary(const ModelStorage* storage, + AuxiliaryObjectiveId id); + + // Returns the raw integer ID associated with the objective: nullopt for the + // primary objective, a nonnegative int64_t for an auxiliary objective. + inline std::optional id() const; + // Returns the strong int ID associated with the objective: nullopt for the + // primary objective, an AuxiliaryObjectiveId for an auxiliary objective. + inline ObjectiveId typed_id() const; + // Returns a const-pointer to the underlying storage object for the model. + inline const ModelStorage* storage() const; + + // Returns true if the ID corresponds to the primary objective, and false if + // it is an auxiliary objective. + inline bool is_primary() const; + + // Returns true if the objective is the maximization sense. + inline bool maximize() const; + + // Returns the priority (lower is more important) of the objective. + inline int64_t priority() const; + + // Returns the name of the objective. + inline absl::string_view name() const; + + // Returns the constant offset of the objective. + inline double offset() const; + + // Returns the linear coefficient for the variable in the model. + inline double coefficient(Variable variable) const; + // Returns the quadratic coefficient for the pair of variables in the model. + inline double coefficient(Variable first_variable, + Variable second_variable) const; + + // Returns true if the variable has a nonzero linear coefficient in the model. + inline bool is_coefficient_nonzero(Variable variable) const; + // Returns true if the pair of variables has a nonzero quadratic coefficient + // in the model. + inline bool is_coefficient_nonzero(Variable first_variable, + Variable second_variable) const; + + // Returns a representation of the objective as a LinearExpression. + // NOTE: This will CHECK fail if the objective has quadratic terms. + LinearExpression AsLinearExpression() const; + // Returns a representation of the objective as a QuadraticExpression. + QuadraticExpression AsQuadraticExpression() const; + + // Returns a detailed string description of the contents of the objective + // (not its name, use `<<` for that instead). + std::string ToString() const; + + friend inline bool operator==(const Objective& lhs, const Objective& rhs); + friend inline bool operator!=(const Objective& lhs, const Objective& rhs); + template + friend H AbslHashValue(H h, const Objective& objective); + friend std::ostream& operator<<(std::ostream& ostr, + const Objective& objective); + + private: + inline Objective(const ModelStorage* storage, ObjectiveId id); + + const ModelStorage* storage_; + ObjectiveId id_; +}; + +template +using ObjectiveMap = absl::flat_hash_map; + +// Streams the name of the objective, as registered upon objective creation, or +// a short default if none was provided. +std::ostream& operator<<(std::ostream& ostr, const Objective& objective); + +//////////////////////////////////////////////////////////////////////////////// +// Inline function implementations +//////////////////////////////////////////////////////////////////////////////// + +std::optional Objective::id() const { + if (is_primary()) { + return std::nullopt; + } + return id_->value(); +} + +ObjectiveId Objective::typed_id() const { return id_; } + +const ModelStorage* Objective::storage() const { return storage_; } + +bool Objective::is_primary() const { return id_ == kPrimaryObjectiveId; } + +int64_t Objective::priority() const { + return storage_->objective_priority(id_); +} + +bool Objective::maximize() const { return storage_->is_maximize(id_); } + +absl::string_view Objective::name() const { + if (is_primary() || storage_->has_auxiliary_objective(*id_)) { + return storage_->objective_name(id_); + } + return kDeletedObjectiveDefaultDescription; +} + +double Objective::offset() const { return storage_->objective_offset(id_); } + +double Objective::coefficient(const Variable variable) const { + CHECK_EQ(variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + return storage_->linear_objective_coefficient(id_, variable.typed_id()); +} + +double Objective::coefficient(const Variable first_variable, + const Variable second_variable) const { + CHECK_EQ(first_variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + CHECK_EQ(second_variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + return storage_->quadratic_objective_coefficient( + id_, first_variable.typed_id(), second_variable.typed_id()); +} + +bool Objective::is_coefficient_nonzero(const Variable variable) const { + CHECK_EQ(variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + return storage_->is_linear_objective_coefficient_nonzero(id_, + variable.typed_id()); +} + +bool Objective::is_coefficient_nonzero(const Variable first_variable, + const Variable second_variable) const { + CHECK_EQ(first_variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + CHECK_EQ(second_variable.storage(), storage_) + << internal::kObjectsFromOtherModelStorage; + return storage_->is_quadratic_objective_coefficient_nonzero( + id_, first_variable.typed_id(), second_variable.typed_id()); +} + +bool operator==(const Objective& lhs, const Objective& rhs) { + return lhs.id_ == rhs.id_ && lhs.storage_ == rhs.storage_; +} + +bool operator!=(const Objective& lhs, const Objective& rhs) { + return !(lhs == rhs); +} + +template +H AbslHashValue(H h, const Objective& objective) { + return H::combine(std::move(h), objective.id_, objective.storage_); +} + +Objective::Objective(const ModelStorage* const storage, const ObjectiveId id) + : storage_(storage), id_(id) {} + +Objective Objective::Primary(const ModelStorage* const storage) { + return Objective(storage, kPrimaryObjectiveId); +} + +Objective Objective::Auxiliary(const ModelStorage* const storage, + const AuxiliaryObjectiveId id) { + return Objective(storage, id); +} + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_ diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc index 38ccebd3a3..e3185072cc 100644 --- a/ortools/math_opt/cpp/parameters.cc +++ b/ortools/math_opt/cpp/parameters.cc @@ -17,18 +17,21 @@ #include #include #include -#include #include +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" -#include "ortools/base/logging.h" +#include "ortools/base/linked_hash_map.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/enums.h" #include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/solvers/glpk.pb.h" #include "ortools/math_opt/solvers/gurobi.pb.h" #include "ortools/port/proto_utils.h" #include "ortools/util/status_macros.h" @@ -72,6 +75,12 @@ std::optional Enum::ToOptString( return "cp_sat"; case SolverType::kGlpk: return "glpk"; + case SolverType::kEcos: + return "ecos"; + case SolverType::kScs: + return "scs"; + case SolverType::kHighs: + return "highs"; } return std::nullopt; } @@ -79,7 +88,8 @@ std::optional Enum::ToOptString( absl::Span Enum::AllValues() { static constexpr SolverType kSolverTypeValues[] = { SolverType::kGscip, SolverType::kGurobi, SolverType::kGlop, - SolverType::kCpSat, SolverType::kGlpk, + SolverType::kCpSat, SolverType::kGlpk, SolverType::kEcos, + SolverType::kScs, SolverType::kHighs, }; return absl::MakeConstSpan(kSolverTypeValues); } @@ -102,6 +112,8 @@ std::optional Enum::ToOptString( return "dual_simplex"; case LPAlgorithm::kBarrier: return "barrier"; + case LPAlgorithm::kFirstOrder: + return "first_order"; } return std::nullopt; } @@ -111,6 +123,7 @@ absl::Span Enum::AllValues() { LPAlgorithm::kPrimalSimplex, LPAlgorithm::kDualSimplex, LPAlgorithm::kBarrier, + LPAlgorithm::kFirstOrder, }; return absl::MakeConstSpan(kLPAlgorithmValues); } @@ -176,6 +189,24 @@ GurobiParameters GurobiParameters::FromProto( return result; } +GlpkParametersProto GlpkParameters::Proto() const { + GlpkParametersProto result; + if (compute_unbound_rays_if_possible.has_value()) { + result.set_compute_unbound_rays_if_possible( + compute_unbound_rays_if_possible.value()); + } + return result; +} + +GlpkParameters GlpkParameters::FromProto(const GlpkParametersProto& proto) { + GlpkParameters result; + if (proto.has_compute_unbound_rays_if_possible()) { + result.compute_unbound_rays_if_possible = + proto.compute_unbound_rays_if_possible(); + } + return result; +} + SolveParametersProto SolveParameters::Proto() const { SolveParametersProto result; result.set_enable_output(enable_output); @@ -225,6 +256,7 @@ SolveParametersProto SolveParameters::Proto() const { *result.mutable_gurobi() = gurobi.Proto(); *result.mutable_glop() = glop; *result.mutable_cp_sat() = cp_sat; + *result.mutable_glpk() = glpk.Proto(); return result; } @@ -281,6 +313,7 @@ absl::StatusOr SolveParameters::FromProto( result.gurobi = GurobiParameters::FromProto(proto.gurobi()); result.glop = proto.glop(); result.cp_sat = proto.cp_sat(); + result.glpk = GlpkParameters::FromProto(proto.glpk()); return result; } diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 4a0d8aab17..5ff392ca97 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -36,32 +36,36 @@ namespace operations_research { namespace math_opt { -// The solvers wrapped by MathOpt. +// The solvers supported by MathOpt. enum class SolverType { - // Solving Constraint Integer Programs (SCIP) solver. + // Solving Constraint Integer Programs (SCIP) solver (third party). // - // It supports both MIPs and LPs. No dual data for LPs is returned though. To - // solve LPs, kGlop should be preferred. + // Supports LP, MIP, and nonconvex integer quadratic problems. No dual data + // for LPs is returned though. Prefer GLOP for LPs. kGscip = SOLVER_TYPE_GSCIP, - // Gurobi solver. + // Gurobi solver (third party). // - // It supports both MIPs and LPs. + // Supports LP, MIP, and nonconvex integer quadratic problems. Generally the + // fastest option, but has special licensing, see go/gurobi-google for + // details. kGurobi = SOLVER_TYPE_GUROBI, - // Google's Glop linear solver. + // Google's Glop solver. // - // It only solves LPs. + // Supports LP with primal and dual simplex methods. kGlop = SOLVER_TYPE_GLOP, // Google's CP-SAT solver. // - // It supports solving IPs and can scale MIPs to solve them as IPs. + // Supports problems where all variables are integer and bounded (or implied + // to be after presolve). Experimental support to rescale and discretize + // problems with continuous variables. kCpSat = SOLVER_TYPE_CP_SAT, - // GNU Linear Programming Kit (GLPK). + // GNU Linear Programming Kit (GLPK) (third party). // - // It supports both MIPs and LPs. + // Supports MIP and LP. // // Thread-safety: GLPK use thread-local storage for memory allocations. As a // consequence when using IncrementalSolver, the user must make sure that @@ -78,6 +82,20 @@ enum class SolverType { // for details. kGlpk = SOLVER_TYPE_GLPK, + // The Embedded Conic Solver (ECOS). + // + // Supports LP and SOCP problems. Uses interior point methods (barrier). + kEcos = SOLVER_TYPE_ECOS, + + // The Splitting Conic Solver (SCS) (third party). + // + // Supports LP and SOCP problems. Uses a first-order method. + kScs = SOLVER_TYPE_SCS, + + // The HiGHS Solver (third party). + // + // Supports LP and MIP problems (convex QPs are unimplemented). + kHighs = SOLVER_TYPE_HIGHS, }; MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED); @@ -107,7 +125,14 @@ enum class LPAlgorithm { // Can typically give both primal and dual solutions. Some implementations can // also produce rays on unbounded/infeasible problems. A basis is not given // unless the underlying solver does "crossover" and finishes with simplex. - kBarrier = LP_ALGORITHM_BARRIER + kBarrier = LP_ALGORITHM_BARRIER, + + // An algorithm based around a first-order method. These will typically + // produce both primal and dual solutions, and potentially also certificates + // of primal and/or dual infeasibility. First-order methods typically will + // provide solutions with lower accuracy, so users should take care to set + // solution quality parameters (e.g., tolerances) and to validate solutions. + kFirstOrder = LP_ALGORITHM_FIRST_ORDER, }; MATH_OPT_DEFINE_ENUM(LPAlgorithm, LP_ALGORITHM_UNSPECIFIED); @@ -126,7 +151,7 @@ std::string AbslUnparseFlag(LPAlgorithm value); // Effort level applied to an optional task while solving (see SolveParameters // for use). // -// Typically used as a std::optional. It used to configure a solver +// Typically used as a std::optional. It's used to configure a solver // feature as follows: // * If a solver doesn't support the feature, only nullopt will always be // valid, any other setting will give an invalid argument error (some solvers @@ -183,11 +208,39 @@ struct GurobiParameters { bool empty() const { return param_values.empty(); } }; +// GLPK specific parameters for solving. +// +// Fields are optional to enable capturing user intention; if the user +// explicitly sets a value, then no generic solve parameters will overwrite this +// parameter. User specified solver specific parameters have priority over +// generic parameters. +struct GlpkParameters { + // Compute the primal or dual unbound ray when the variable (structural or + // auxiliary) causing the unboundness is identified (see glp_get_unbnd_ray()). + // + // The unset value is equivalent to false. + // + // Rays are only available when solving linear programs, they are not + // available for MIPs. On top of that they are only available when using a + // simplex algorithm with the presolve disabled. + // + // A primal ray can only be built if the chosen LP algorithm is + // LPAlgorithm::kPrimalSimplex. Same for a dual ray and + // LPAlgorithm::kDualSimplex. + // + // The computation involves the basis factorization to be available which may + // lead to extra computations/errors. + std::optional compute_unbound_rays_if_possible = std::nullopt; + + GlpkParametersProto Proto() const; + static GlpkParameters FromProto(const GlpkParametersProto& proto); +}; + // Parameters to control a single solve. // -// Contains both parameters common to all solvers e.g. time_limit, and +// Contains both parameters common to all solvers, e.g. time_limit, and // parameters for a specific solver, e.g. gscip. If a value is set in both -// common and solver specific field, the solver specific setting is used. +// common and solver specific fields, the solver specific setting is used. // // The common parameters that are optional and unset indicate that the solver // default is used. @@ -253,9 +306,9 @@ struct SolveParameters { // The solver stops early after finding this many feasible solutions, with // termination reason kFeasible and limit kSolution. Must be greater than - // zero if set. It is often used get the solver to stop on the first feasible - // solution found. Note that there is no guarantee on the objective value for - // any of the returned solutions. + // zero if set. It is often used to get the solver to stop on the first + // feasible solution found. Note that there is no guarantee on the objective + // value for any of the returned solutions. // // Solvers will typically not return more solutions than the solution limit, // but this is not enforced by MathOpt, see also b/214041169. @@ -352,6 +405,8 @@ struct SolveParameters { glop::GlopParameters glop; sat::SatParameters cp_sat; + GlpkParameters glpk; + SolveParametersProto Proto() const; static absl::StatusOr FromProto( const SolveParametersProto& proto); diff --git a/ortools/math_opt/cpp/solution.cc b/ortools/math_opt/cpp/solution.cc index e3ca9f10cf..2462b20aaa 100644 --- a/ortools/math_opt/cpp/solution.cc +++ b/ortools/math_opt/cpp/solution.cc @@ -67,6 +67,11 @@ absl::StatusOr PrimalSolution::FromProto( VariableValuesFromProto(model, primal_solution_proto.variable_values()), _ << "invalid variable_values"); primal_solution.objective_value = primal_solution_proto.objective_value(); + OR_ASSIGN_OR_RETURN3( + primal_solution.auxiliary_objective_values, + AuxiliaryObjectiveValuesFromProto( + model, primal_solution_proto.auxiliary_objective_values()), + _ << "invalid auxiliary_objective_values"); const std::optional feasibility_status = EnumFromProto(primal_solution_proto.feasibility_status()); if (!feasibility_status.has_value()) { @@ -80,10 +85,26 @@ PrimalSolutionProto PrimalSolution::Proto() const { PrimalSolutionProto result; *result.mutable_variable_values() = VariableValuesToProto(variable_values); result.set_objective_value(objective_value); + *result.mutable_auxiliary_objective_values() = + AuxiliaryObjectiveValuesToProto(auxiliary_objective_values); result.set_feasibility_status(EnumToProto(feasibility_status)); return result; } +double PrimalSolution::get_objective_value(const Objective objective) const { + if (!variable_values.empty()) { + // Here we assume all keys are in the same storage. As PrimalSolution is not + // properly encapsulated, we can't maintain a ModelStorage pointer and + // iterating on all keys would have a too high cost. + CHECK_EQ(variable_values.begin()->first.storage(), objective.storage()); + } + if (!objective.is_primary()) { + CHECK(auxiliary_objective_values.contains(objective)); + return auxiliary_objective_values.at(objective); + } + return objective_value; +} + absl::StatusOr PrimalRay::FromProto( const ModelStorage* model, const PrimalRayProto& primal_ray_proto) { PrimalRay result; @@ -178,14 +199,18 @@ absl::StatusOr Basis::FromProto(const ModelStorage* model, absl::Status Basis::CheckModelStorage( const ModelStorage* const expected_storage) const { - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/variable_status.storage(), - /*expected_storage=*/expected_storage)) - << "invalid variable_status"; - RETURN_IF_ERROR( - internal::CheckModelStorage(/*storage=*/constraint_status.storage(), - /*expected_storage=*/expected_storage)) - << "invalid constraint_status"; + for (const auto& [v, _] : variable_status) { + RETURN_IF_ERROR( + internal::CheckModelStorage(/*storage=*/v.storage(), + /*expected_storage=*/expected_storage)) + << "invalid variable " << v << " in variable_status"; + } + for (const auto& [c, _] : constraint_status) { + RETURN_IF_ERROR( + internal::CheckModelStorage(/*storage=*/c.storage(), + /*expected_storage=*/expected_storage)) + << "invalid constraint " << c << " in constraint_status"; + } return absl::OkStatus(); } diff --git a/ortools/math_opt/cpp/solution.h b/ortools/math_opt/cpp/solution.h index 4f5d95dc28..4b2d84a998 100644 --- a/ortools/math_opt/cpp/solution.h +++ b/ortools/math_opt/cpp/solution.h @@ -25,6 +25,7 @@ #include "ortools/math_opt/cpp/basis_status.h" #include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export #include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/objective.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/result.pb.h" // IWYU pragma: export #include "ortools/math_opt/solution.pb.h" @@ -72,8 +73,16 @@ struct PrimalSolution { // Returns the proto equivalent of this. PrimalSolutionProto Proto() const; + // Returns the value for the given `objective`. + // + // Will CHECK-fail if `objective` has been deleted, or if it is from the is + // from the wrong model (however, if the solution has no variables, this CHECK + // will not occur due to an implementation detail of the struct). + double get_objective_value(Objective objective) const; + VariableMap variable_values; double objective_value = 0.0; + absl::flat_hash_map auxiliary_objective_values; SolutionStatus feasibility_status = SolutionStatus::kUndetermined; }; @@ -146,10 +155,10 @@ struct DualSolution { SolutionStatus feasibility_status = SolutionStatus::kUndetermined; }; -// A direction of unbounded improvement to the dual of an optimization, +// A direction of unbounded improvement to the dual of an optimization // problem; equivalently, a certificate of primal infeasibility. // -// E.g. consider the primal dual pair linear program pair: +// E.g. consider the primal dual linear program pair: // (Primal) (Dual) // min c * x max b * y // s.t. A * x >= b s.t. y * A + r = c @@ -243,8 +252,8 @@ struct Basis { // 1. MIP solvers return only a primal solution. // 2. Simplex LP solvers often return a basis and the primal and dual // solutions associated to this basis. -// 3. Other continuous solvers often return a primal and dual solution -// solution that are connected in a solver-dependent form. +// 3. Other continuous solvers often return a primal and dual solution that +// are connected in a solver-dependent form. struct Solution { // Returns the Solution equivalent of solution_proto. // diff --git a/ortools/math_opt/cpp/solve.cc b/ortools/math_opt/cpp/solve.cc index bdc8ecd12c..0f0d9bf678 100644 --- a/ortools/math_opt/cpp/solve.cc +++ b/ortools/math_opt/cpp/solve.cc @@ -18,7 +18,6 @@ #include #include -#include "absl/container/flat_hash_set.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -26,7 +25,19 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/solver.h" +#include "ortools/math_opt/cpp/callback.h" +#include "ortools/math_opt/cpp/enums.h" +#include "ortools/math_opt/cpp/infeasible_subsystem_arguments.h" +#include "ortools/math_opt/cpp/infeasible_subsystem_result.h" #include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/model_solve_parameters.h" +#include "ortools/math_opt/cpp/parameters.h" +#include "ortools/math_opt/cpp/solve_arguments.h" +#include "ortools/math_opt/cpp/solve_result.h" +#include "ortools/math_opt/cpp/solver_init_arguments.h" +#include "ortools/math_opt/cpp/streamable_solver_init_arguments.h" +#include "ortools/math_opt/cpp/update_tracker.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/util/status_macros.h" @@ -108,6 +119,21 @@ absl::StatusOr Solve(const Model& model, return CallSolve(*solver, model.storage(), solve_args); } +absl::StatusOr InfeasibleSubsystem( + const Model& model, const SolverType solver_type, + const InfeasibleSubsystemArguments& infeasible_subsystem_args, + const SolverInitArguments& init_args) { + ASSIGN_OR_RETURN( + const InfeasibleSubsystemResultProto result_proto, + Solver::NonIncrementalInfeasibleSubsystem( + model.ExportModel(), EnumToProto(solver_type), + ToSolverInitArgs(init_args), + {.parameters = infeasible_subsystem_args.parameters.Proto(), + .message_callback = infeasible_subsystem_args.message_callback, + .interrupter = infeasible_subsystem_args.interrupter})); + return InfeasibleSubsystemResult::FromProto(model.storage(), result_proto); +} + absl::StatusOr> IncrementalSolver::New( Model* const model, const SolverType solver_type, SolverInitArguments arguments) { diff --git a/ortools/math_opt/cpp/solve.h b/ortools/math_opt/cpp/solve.h index 30b97bf34e..cfa70e029c 100644 --- a/ortools/math_opt/cpp/solve.h +++ b/ortools/math_opt/cpp/solve.h @@ -28,6 +28,8 @@ #include "absl/status/statusor.h" #include "ortools/math_opt/core/solver.h" +#include "ortools/math_opt/cpp/infeasible_subsystem_arguments.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/infeasible_subsystem_result.h" // IWYU pragma: export #include "ortools/math_opt/cpp/model.h" #include "ortools/math_opt/cpp/parameters.h" // IWYU pragma: export #include "ortools/math_opt/cpp/solve_arguments.h" // IWYU pragma: export @@ -49,7 +51,7 @@ namespace math_opt { // solution was found. // // Memory model: the returned SolveResult owns its own memory (for solutions, -// solve stats, etc.), EXPECT for a pointer back to the model. As a result: +// solve stats, etc.), EXCEPT for a pointer back to the model. As a result: // * Keep the model alive to access SolveResult, // * Avoid unnecessarily copying SolveResult, // * The result is generally accessible after mutating the model, but some care @@ -63,6 +65,27 @@ absl::StatusOr Solve(const Model& model, SolverType solver_type, const SolveArguments& solve_args = {}, const SolverInitArguments& init_args = {}); +// Computes an infeasible subsystem of the input model. +// +// A Status error will be returned if the inputs are invalid or there is an +// unexpected failure in an underlying solver or for some internal math_opt +// errors. Otherwise, check InfeasibleSubsystemResult::feasibility to see if an +// infeasible subsystem was found. +// +// Memory model: the returned InfeasibleSubsystemResult owns its own memory (for +// subsystems, solve stats, etc.), EXCEPT for a pointer back to the model. As a +// result: +// * Keep the model alive to access InfeasibleSubsystemResult, +// * Avoid unnecessarily copying InfeasibleSubsystemResult, +// * The result is generally accessible after mutating the model, but some care +// is needed if variables or linear constraints are added or deleted. +// +// Thread-safety: this method is safe to call concurrently on the same Model. +absl::StatusOr InfeasibleSubsystem( + const Model& model, SolverType solver_type, + const InfeasibleSubsystemArguments& infeasible_subsystem_args = {}, + const SolverInitArguments& init_args = {}); + // Incremental solve of a model. // // This is a feature for advance users. Most users should only use the Solve() @@ -174,6 +197,10 @@ class IncrementalSolver { absl::StatusOr SolveWithoutUpdate( const SolveArguments& arguments = {}) const; + SolverType solver_type() const { return solver_type_; } + + // TODO(b/273961536): Add InfeasibleSubsystem() member function. + private: IncrementalSolver(SolverType solver_type, SolverInitArguments init_args, const ModelStorage* expected_storage, diff --git a/ortools/math_opt/cpp/solve_arguments.h b/ortools/math_opt/cpp/solve_arguments.h index bcba67c9b2..1d6538c178 100644 --- a/ortools/math_opt/cpp/solve_arguments.h +++ b/ortools/math_opt/cpp/solve_arguments.h @@ -49,7 +49,7 @@ struct SolveArguments { // // To print messages to stdout with a prefix. // ASSIGN_OR_RETURN( // const SolveResult result, - // Solve(model, SOLVER_TYPE_GLOP, + // Solve(model, SolverType::kGlop, // { .message_callback = PrinterMessageCallback(std::cout, // "logs| "); }); MessageCallback message_callback = nullptr; @@ -79,7 +79,7 @@ struct SolveArguments { // }); // // ASSIGN_OR_RETURN(const SolveResult result, - // Solve(model, SOLVER_TYPE_GLOP, + // Solve(model, SolverType::kGlop, // { .interrupter = interrupter.get() }); // SolveInterrupter* interrupter = nullptr; diff --git a/ortools/math_opt/cpp/solve_result.cc b/ortools/math_opt/cpp/solve_result.cc index 41c6ee1d92..777a734fac 100644 --- a/ortools/math_opt/cpp/solve_result.cc +++ b/ortools/math_opt/cpp/solve_result.cc @@ -379,6 +379,11 @@ bool SolveResult::has_primal_feasible_solution() const { SolutionStatus::kFeasible); } +const PrimalSolution& SolveResult::best_primal_solution() const { + CHECK(has_primal_feasible_solution()); + return *solutions.front().primal_solution; +} + double SolveResult::best_objective_bound() const { return solve_stats.best_dual_bound; } @@ -388,6 +393,11 @@ double SolveResult::objective_value() const { return solutions[0].primal_solution->objective_value; } +double SolveResult::objective_value(const Objective objective) const { + CHECK(has_primal_feasible_solution()); + return solutions[0].primal_solution->get_objective_value(objective); +} + bool SolveResult::bounded() const { return solve_stats.problem_status.primal_status == FeasibilityStatus::kFeasible && diff --git a/ortools/math_opt/cpp/solve_result.h b/ortools/math_opt/cpp/solve_result.h index d0ec26f51d..cf3ba9b7e3 100644 --- a/ortools/math_opt/cpp/solve_result.h +++ b/ortools/math_opt/cpp/solve_result.h @@ -173,8 +173,8 @@ enum class TerminationReason { // The problem was solved to one of the criteria above (Optimal, Infeasible, // Unbounded, or InfeasibleOrUnbounded), but one or more tolerances was not - // met. Some primal/dual solutions/rays be present, but either they will be - // slightly infeasible, or (if the problem was nearly optimal) their may be + // met. Some primal/dual solutions/rays may be present, but either they will + // be slightly infeasible, or (if the problem was nearly optimal) their may be // a gap between the best solution objective and best objective bound. // // Users can still query primal/dual solutions/rays and solution stats, but @@ -346,7 +346,7 @@ struct SolveResult { // Returns an error if: // * Any solution or ray cannot be read from proto (e.g. on a subfield, // ids.size != values.size). - // * termination or solve_result cannot be read from proto. + // * termination or solve_stats cannot be read from proto. // See the FromProto() functions for these types for details. // // Note: this is (intentionally) a much weaker test than ValidateResult(). The @@ -376,14 +376,21 @@ struct SolveResult { // Indicates if at least one primal feasible solution is available. // - // When termination.reason is TerminationReason::kOptimal or + // For SolveResults generated by calling Solver::Solve(), when + // termination.reason is TerminationReason::kOptimal or // TerminationReason::kFeasible, this is guaranteed to be true and need not be - // checked. + // checked. SolveResult objects generated directed from a proto need not have + // this property. bool has_primal_feasible_solution() const; + // Returns the best primal feasible solution. CHECK fails if no such solution + // is available; check this using `has_primal_feasible_solution()`. + const PrimalSolution& best_primal_solution() const; + // The objective value of the best primal feasible solution. Will CHECK fail // if there are no primal feasible solutions. double objective_value() const; + double objective_value(Objective objective) const; // A bound on the best possible objective value. double best_objective_bound() const; @@ -445,10 +452,12 @@ struct SolveResult { // Indicates if the best solution has an associated basis. bool has_basis() const; - // The constraint basis status for the best solution. + // The constraint basis status for the best solution. Will CHECK fail if the + // best solution does not have an associated basis. const LinearConstraintMap& constraint_status() const; - // The variable basis status for the best solution. + // The variable basis status for the best solution. Will CHECK fail if the + // best solution does not have an associated basis. const VariableMap& variable_status() const; }; diff --git a/ortools/math_opt/cpp/sparse_containers.cc b/ortools/math_opt/cpp/sparse_containers.cc index dbd92369d2..312f01bfbb 100644 --- a/ortools/math_opt/cpp/sparse_containers.cc +++ b/ortools/math_opt/cpp/sparse_containers.cc @@ -13,8 +13,14 @@ #include "ortools/math_opt/cpp/sparse_containers.h" +#include #include +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "google/protobuf/map.h" +#include "ortools/base/status_builder.h" + namespace operations_research::math_opt { namespace { @@ -27,12 +33,12 @@ absl::Status CheckSparseVectorProto(const SparseVectorProtoType& vec) { } template -absl::StatusOr> BasisVectorFromProto( +absl::StatusOr> BasisVectorFromProto( const ModelStorage* const model, const SparseBasisStatusVector& basis_proto) { using IdType = typename Key::IdType; - absl::flat_hash_map raw_map; - raw_map.reserve(basis_proto.ids_size()); + absl::flat_hash_map map; + map.reserve(basis_proto.ids_size()); for (const auto& [id, basis_status_proto_int] : MakeView(basis_proto)) { const auto basis_status_proto = static_cast(basis_status_proto_int); @@ -42,18 +48,22 @@ absl::StatusOr> BasisVectorFromProto( return util::InvalidArgumentErrorBuilder() << "basis status not specified for id " << id; } - raw_map[IdType(id)] = *basis_status; + map[Key(model, IdType(id))] = *basis_status; } - return IdMap(model, std::move(raw_map)); + return map; } template -SparseDoubleVectorProto IdMapToProto(const IdMap& id_map) { +SparseDoubleVectorProto MapToProto( + const absl::flat_hash_map& id_map) { using IdType = typename Key::IdType; + std::vector> sorted_entries; + sorted_entries.reserve(id_map.size()); + for (const auto& [k, v] : id_map) { + sorted_entries.emplace_back(k.typed_id(), v); + } + absl::c_sort(sorted_entries); SparseDoubleVectorProto result; - std::vector> sorted_entries( - id_map.raw_map().begin(), id_map.raw_map().end()); - std::sort(sorted_entries.begin(), sorted_entries.end()); for (const auto& [id, val] : sorted_entries) { result.add_ids(id.value()); result.add_values(val); @@ -62,13 +72,16 @@ SparseDoubleVectorProto IdMapToProto(const IdMap& id_map) { } template -SparseBasisStatusVector BasisIdMapToProto( - const IdMap& basis_map) { +SparseBasisStatusVector BasisMapToProto( + const absl::flat_hash_map& basis_map) { using IdType = typename Key::IdType; + std::vector> sorted_entries; + sorted_entries.reserve(basis_map.size()); + for (const auto& [k, v] : basis_map) { + sorted_entries.emplace_back(k.typed_id(), v); + } + absl::c_sort(sorted_entries); SparseBasisStatusVector result; - std::vector> sorted_entries( - basis_map.raw_map().begin(), basis_map.raw_map().end()); - std::sort(sorted_entries.begin(), sorted_entries.end()); for (const auto& [id, val] : sorted_entries) { result.add_ids(id.value()); result.add_values(EnumToProto(val)); @@ -105,12 +118,39 @@ absl::StatusOr> VariableValuesFromProto( const SparseDoubleVectorProto& vars_proto) { RETURN_IF_ERROR(CheckSparseVectorProto(vars_proto)); RETURN_IF_ERROR(VariableIdsExist(model, vars_proto.ids())); - return VariableMap(model, MakeView(vars_proto).as_map()); + return MakeView(vars_proto).as_map(model); } SparseDoubleVectorProto VariableValuesToProto( const VariableMap& variable_values) { - return IdMapToProto(variable_values); + return MapToProto(variable_values); +} + +absl::StatusOr> +AuxiliaryObjectiveValuesFromProto( + const ModelStorage* const model, + const google::protobuf::Map& aux_obj_proto) { + absl::flat_hash_map result; + for (const auto [raw_id, value] : aux_obj_proto) { + const AuxiliaryObjectiveId id(raw_id); + if (!model->has_auxiliary_objective(id)) { + return util::InvalidArgumentErrorBuilder() + << "no auxiliary objective with id " << raw_id << " exists"; + } + result[Objective::Auxiliary(model, id)] = value; + } + return result; +} + +google::protobuf::Map AuxiliaryObjectiveValuesToProto( + const absl::flat_hash_map& aux_obj_values) { + google::protobuf::Map result; + for (const auto& [objective, value] : aux_obj_values) { + CHECK(objective.id().has_value()) + << "encountered primary objective in auxiliary objective value map"; + result[objective.id().value()] = value; + } + return result; } absl::StatusOr> LinearConstraintValuesFromProto( @@ -118,13 +158,12 @@ absl::StatusOr> LinearConstraintValuesFromProto( const SparseDoubleVectorProto& lin_cons_proto) { RETURN_IF_ERROR(CheckSparseVectorProto(lin_cons_proto)); RETURN_IF_ERROR(LinearConstraintIdsExist(model, lin_cons_proto.ids())); - return LinearConstraintMap( - model, MakeView(lin_cons_proto).as_map()); + return MakeView(lin_cons_proto).as_map(model); } SparseDoubleVectorProto LinearConstraintValuesToProto( const LinearConstraintMap& linear_constraint_values) { - return IdMapToProto(linear_constraint_values); + return MapToProto(linear_constraint_values); } absl::StatusOr> VariableBasisFromProto( @@ -137,7 +176,7 @@ absl::StatusOr> VariableBasisFromProto( SparseBasisStatusVector VariableBasisToProto( const VariableMap& basis_values) { - return BasisIdMapToProto(basis_values); + return BasisMapToProto(basis_values); } absl::StatusOr> LinearConstraintBasisFromProto( @@ -150,7 +189,7 @@ absl::StatusOr> LinearConstraintBasisFromProto( SparseBasisStatusVector LinearConstraintBasisToProto( const LinearConstraintMap& basis_values) { - return BasisIdMapToProto(basis_values); + return BasisMapToProto(basis_values); } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/sparse_containers.h b/ortools/math_opt/cpp/sparse_containers.h index 3453dd7a50..3d17b60ca6 100644 --- a/ortools/math_opt/cpp/sparse_containers.h +++ b/ortools/math_opt/cpp/sparse_containers.h @@ -17,6 +17,8 @@ #ifndef OR_TOOLS_MATH_OPT_CPP_SPARSE_CONTAINERS_H_ #define OR_TOOLS_MATH_OPT_CPP_SPARSE_CONTAINERS_H_ +#include + #include "absl/container/flat_hash_map.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" @@ -26,6 +28,7 @@ #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/cpp/basis_status.h" #include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/objective.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" @@ -46,12 +49,31 @@ namespace operations_research::math_opt { // // Note that the values of vars_proto.values are not checked (it may have NaNs). absl::StatusOr> VariableValuesFromProto( - const ModelStorage* const model, const SparseDoubleVectorProto& vars_proto); + const ModelStorage* model, const SparseDoubleVectorProto& vars_proto); // Returns the proto equivalent of variable_values. SparseDoubleVectorProto VariableValuesToProto( const VariableMap& variable_values); +// Returns an absl::flat_hash_map equivalent to +// `aux_obj_proto`. +// +// Requires that (or returns a status error): +// * The keys of `aux_obj_proto` correspond to objectives in `model`. +// +// Note that the values of `aux_obj_proto` are not checked (it may have NaNs). +absl::StatusOr> +AuxiliaryObjectiveValuesFromProto( + const ModelStorage* model, + const google::protobuf::Map& aux_obj_proto); + +// Returns the proto equivalent of auxiliary_obj_values. +// +// Requires that (or will CHECK-fail): +// * The keys of `aux_obj_values` all correspond to auxiliary objectives. +google::protobuf::Map AuxiliaryObjectiveValuesToProto( + const absl::flat_hash_map& aux_obj_values); + // Returns the LinearConstraintMap equivalent to `lin_cons_proto`. // // Requires that (or returns a status error): @@ -63,8 +85,7 @@ SparseDoubleVectorProto VariableValuesToProto( // Note that the values of lin_cons_proto.values are not checked (it may have // NaNs). absl::StatusOr> LinearConstraintValuesFromProto( - const ModelStorage* const model, - const SparseDoubleVectorProto& lin_cons_proto); + const ModelStorage* model, const SparseDoubleVectorProto& lin_cons_proto); // Returns the proto equivalent of linear_constraint_values. SparseDoubleVectorProto LinearConstraintValuesToProto( @@ -79,8 +100,7 @@ SparseDoubleVectorProto LinearConstraintValuesToProto( // * basis_proto.ids has elements that are variables in `model`. // * basis_proto.values does not contain UNSPECIFIED and has valid enum values. absl::StatusOr> VariableBasisFromProto( - const ModelStorage* const model, - const SparseBasisStatusVector& basis_proto); + const ModelStorage* model, const SparseBasisStatusVector& basis_proto); // Returns the proto equivalent of basis_values. SparseBasisStatusVector VariableBasisToProto( @@ -95,8 +115,7 @@ SparseBasisStatusVector VariableBasisToProto( // * basis_proto.ids has elements that are linear constraints in `model`. // * basis_proto.values does not contain UNSPECIFIED and has valid enum values. absl::StatusOr> LinearConstraintBasisFromProto( - const ModelStorage* const model, - const SparseBasisStatusVector& basis_proto); + const ModelStorage* model, const SparseBasisStatusVector& basis_proto); // Returns the proto equivalent of basis_values. SparseBasisStatusVector LinearConstraintBasisToProto( diff --git a/ortools/math_opt/cpp/statistics.cc b/ortools/math_opt/cpp/statistics.cc index f0a3f35bf1..3b215f2d47 100644 --- a/ortools/math_opt/cpp/statistics.cc +++ b/ortools/math_opt/cpp/statistics.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include #include diff --git a/ortools/math_opt/cpp/variable_and_expressions.cc b/ortools/math_opt/cpp/variable_and_expressions.cc index 9a7eb12694..ec1cf12ac4 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.cc +++ b/ortools/math_opt/cpp/variable_and_expressions.cc @@ -35,22 +35,10 @@ constexpr double kInf = std::numeric_limits::infinity(); LinearExpression::LinearExpression() { ++num_calls_default_constructor_; } LinearExpression::LinearExpression(const LinearExpression& other) - : terms_(other.terms_), offset_(other.offset_) { + : storage_(other.storage_), terms_(other.terms_), offset_(other.offset_) { ++num_calls_copy_constructor_; } -LinearExpression::LinearExpression(LinearExpression&& other) - : terms_(std::move(other.terms_)), - offset_(std::exchange(other.offset_, 0.0)) { - ++num_calls_move_constructor_; -} - -LinearExpression& LinearExpression::operator=(const LinearExpression& other) { - terms_ = other.terms_; - offset_ = other.offset_; - return *this; -} - ABSL_CONST_INIT thread_local int LinearExpression::num_calls_default_constructor_ = 0; ABSL_CONST_INIT thread_local int LinearExpression::num_calls_copy_constructor_ = @@ -70,29 +58,22 @@ void LinearExpression::ResetCounters() { double LinearExpression::Evaluate( const VariableMap& variable_values) const { - if (variable_values.storage() != nullptr && storage() != nullptr) { - CHECK_EQ(variable_values.storage(), storage()) - << internal::kObjectsFromOtherModelStorage; - } double result = offset_; - for (const auto& variable : terms_.SortedKeys()) { - result += terms_.raw_map().at(variable.typed_id()) * - variable_values.raw_map().at(variable.typed_id()); + for (const auto& variable : SortedKeys(terms_)) { + const auto found = variable_values.find(variable); + CHECK(found != variable_values.end()) + << internal::kObjectsFromOtherModelStorage; + result += terms_.at(variable) * found->second; } return result; } double LinearExpression::EvaluateWithDefaultZero( const VariableMap& variable_values) const { - if (variable_values.storage() != nullptr && storage() != nullptr) { - CHECK_EQ(variable_values.storage(), storage()) - << internal::kObjectsFromOtherModelStorage; - } double result = offset_; - for (const auto& variable : terms_.SortedKeys()) { + for (const auto& variable : SortedKeys(terms_)) { result += - terms_.raw_map().at(variable.typed_id()) * - gtl::FindWithDefault(variable_values.raw_map(), variable.typed_id()); + terms_.at(variable) * gtl::FindWithDefault(variable_values, variable); } return result; } @@ -103,7 +84,7 @@ std::ostream& operator<<(std::ostream& ostr, // - make sure to quote the variable name so that we support: // * variable names contains +, -, ... // * variable names resembling anonymous variable names. - const std::vector sorted_variables = expression.terms_.SortedKeys(); + const std::vector sorted_variables = SortedKeys(expression.terms_); bool first = true; for (const auto v : sorted_variables) { const double coeff = expression.terms_.at(v); @@ -136,41 +117,37 @@ std::ostream& operator<<(std::ostream& ostr, double QuadraticExpression::Evaluate( const VariableMap& variable_values) const { - if (variable_values.storage() != nullptr && storage() != nullptr) { - CHECK_EQ(variable_values.storage(), storage()) - << internal::kObjectsFromOtherModelStorage; - } double result = offset(); - for (const auto& variable : linear_terms_.SortedKeys()) { - result += linear_terms_.raw_map().at(variable.typed_id()) * - variable_values.raw_map().at(variable.typed_id()); + for (const auto& variable : SortedKeys(linear_terms_)) { + const auto found = variable_values.find(variable); + CHECK(found != variable_values.end()) + << internal::kObjectsFromOtherModelStorage; + result += linear_terms_.at(variable) * found->second; } - for (const auto& variables : quadratic_terms_.SortedKeys()) { - result += quadratic_terms_.raw_map().at(variables.typed_id()) * - variable_values.raw_map().at(variables.typed_id().first) * - variable_values.raw_map().at(variables.typed_id().second); + for (const auto& variables : SortedKeys(quadratic_terms_)) { + const auto found_first = variable_values.find(variables.first()); + CHECK(found_first != variable_values.end()) + << internal::kObjectsFromOtherModelStorage; + const auto found_second = variable_values.find(variables.second()); + CHECK(found_second != variable_values.end()) + << internal::kObjectsFromOtherModelStorage; + result += quadratic_terms_.at(variables) * found_first->second * + found_second->second; } return result; } double QuadraticExpression::EvaluateWithDefaultZero( const VariableMap& variable_values) const { - if (variable_values.storage() != nullptr && storage() != nullptr) { - CHECK_EQ(variable_values.storage(), storage()) - << internal::kObjectsFromOtherModelStorage; - } double result = offset(); - for (const auto& variable : linear_terms_.SortedKeys()) { - result += - linear_terms_.raw_map().at(variable.typed_id()) * - gtl::FindWithDefault(variable_values.raw_map(), variable.typed_id()); + for (const auto& variable : SortedKeys(linear_terms_)) { + result += linear_terms_.at(variable) * + gtl::FindWithDefault(variable_values, variable); } - for (const auto& variables : quadratic_terms_.SortedKeys()) { - result += quadratic_terms_.raw_map().at(variables.typed_id()) * - gtl::FindWithDefault(variable_values.raw_map(), - variables.typed_id().first) * - gtl::FindWithDefault(variable_values.raw_map(), - variables.typed_id().second); + for (const auto& variables : SortedKeys(quadratic_terms_)) { + result += quadratic_terms_.at(variables) * + gtl::FindWithDefault(variable_values, variables.first()) * + gtl::FindWithDefault(variable_values, variables.second()); } return result; } @@ -180,23 +157,21 @@ std::ostream& operator<<(std::ostream& ostr, const QuadraticExpression& expr) { // for desired improvements for LinearExpression streaming which are also // applicable here. bool first = true; - for (const auto v : expr.quadratic_terms().SortedKeys()) { - const double coeff = expr.quadratic_terms().at(v); + for (const auto vs : SortedKeys(expr.quadratic_terms())) { + const double coeff = expr.quadratic_terms().at(vs); if (coeff != 0) { ostr << LeadingCoefficientFormatter(coeff, first); first = false; } - const Variable first_variable(expr.quadratic_terms().storage(), - v.typed_id().first); - const Variable second_variable(expr.quadratic_terms().storage(), - v.typed_id().second); + const Variable first_variable = vs.first(); + const Variable second_variable = vs.second(); if (first_variable == second_variable) { ostr << first_variable << "²"; } else { ostr << first_variable << "*" << second_variable; } } - for (const auto v : expr.linear_terms().SortedKeys()) { + for (const auto v : SortedKeys(expr.linear_terms())) { const double coeff = expr.linear_terms().at(v); if (coeff != 0) { ostr << LeadingCoefficientFormatter(coeff, first) << v; @@ -228,27 +203,13 @@ std::ostream& operator<<(std::ostream& ostr, QuadraticExpression::QuadraticExpression() { ++num_calls_default_constructor_; } QuadraticExpression::QuadraticExpression(const QuadraticExpression& other) - : quadratic_terms_(other.quadratic_terms_), + : storage_(other.storage_), + quadratic_terms_(other.quadratic_terms_), linear_terms_(other.linear_terms_), offset_(other.offset_) { ++num_calls_copy_constructor_; } -QuadraticExpression::QuadraticExpression(QuadraticExpression&& other) - : quadratic_terms_(std::move(other.quadratic_terms_)), - linear_terms_(std::move(other.linear_terms_)), - offset_(std::exchange(other.offset_, 0.0)) { - ++num_calls_move_constructor_; -} - -QuadraticExpression& QuadraticExpression::operator=( - const QuadraticExpression& other) { - quadratic_terms_ = other.quadratic_terms_; - linear_terms_ = other.linear_terms_; - offset_ = other.offset_; - return *this; -} - ABSL_CONST_INIT thread_local int QuadraticExpression::num_calls_default_constructor_ = 0; ABSL_CONST_INIT thread_local int diff --git a/ortools/math_opt/cpp/variable_and_expressions.h b/ortools/math_opt/cpp/variable_and_expressions.h index c49326328f..fe701fecc0 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.h +++ b/ortools/math_opt/cpp/variable_and_expressions.h @@ -102,11 +102,10 @@ #include #include "absl/container/flat_hash_map.h" -#include "absl/strings/string_view.h" #include "absl/log/check.h" +#include "absl/strings/string_view.h" #include "ortools/base/logging.h" #include "ortools/base/strong_int.h" -#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export #include "ortools/math_opt/cpp/key_types.h" // IWYU pragma: export #include "ortools/math_opt/storage/model_storage.h" #include "ortools/math_opt/storage/model_storage_types.h" @@ -152,10 +151,8 @@ class Variable { VariableId id_; }; -// Implements the API of std::unordered_map, but forbids Variables -// from different models in the same map. template -using VariableMap = IdMap; +using VariableMap = absl::flat_hash_map; inline std::ostream& operator<<(std::ostream& ostr, const Variable& variable); @@ -200,15 +197,18 @@ class QuadraticExpression; class LinearExpression { public: // For unit testing purpose, we define optional counters. We have to - // explicitly define default constructors in that case. + // explicitly define the default constructor, copy constructor and assignment + // operators in that case. Else we use the defaults. #ifndef MATH_OPT_USE_EXPRESSION_COUNTERS LinearExpression() = default; + LinearExpression(const LinearExpression& other) = default; #else // MATH_OPT_USE_EXPRESSION_COUNTERS LinearExpression(); LinearExpression(const LinearExpression& other); - LinearExpression(LinearExpression&& other); - LinearExpression& operator=(const LinearExpression& other); #endif // MATH_OPT_USE_EXPRESSION_COUNTERS + // We have to define a custom move constructor as we need to reset storage_ to + // nullptr. + inline LinearExpression(LinearExpression&& other); // Usually users should use the overloads of operators to build linear // expressions. For example, assuming `x` and `y` are Variable, then `x + 2*y // + 5` will build a LinearExpression automatically. @@ -217,6 +217,10 @@ class LinearExpression { inline LinearExpression(double offset); // NOLINT inline LinearExpression(Variable variable); // NOLINT inline LinearExpression(const LinearTerm& term); // NOLINT + LinearExpression& operator=(const LinearExpression& other) = default; + // We have to define a custom move assignment operator as we need to reset + // storage_ to nullptr. + inline LinearExpression& operator=(LinearExpression&& other); inline LinearExpression& operator+=(const LinearExpression& other); inline LinearExpression& operator+=(const LinearTerm& term); @@ -320,19 +324,18 @@ class LinearExpression { // Compute the numeric value of this expression when variables are substituted // by their values in variable_values. // - // Will CHECK fail the underlying model storage is different or if a variable - // in terms() is missing from variables_values. + // Will CHECK fail if a variable in terms() is missing from variables_values. double Evaluate(const VariableMap& variable_values) const; // Compute the numeric value of this expression when variables are substituted // by their values in variable_values, or zero if missing from the map. // - // Will CHECK fail the underlying model storage is different. + // This function won't check that the variables in the input map are indeed in + // the same model as the ones of the expression. double EvaluateWithDefaultZero( const VariableMap& variable_values) const; inline const ModelStorage* storage() const; - inline const absl::flat_hash_map& raw_terms() const; #ifdef MATH_OPT_USE_EXPRESSION_COUNTERS static thread_local int num_calls_default_constructor_; @@ -349,6 +352,14 @@ class LinearExpression { const LinearExpression& expression); friend QuadraticExpression; + // Sets the storage_ to the input value if nullptr, else CHECKs that it is + // equal. Also CHECKs that the input value is not nullptr. + inline void SetOrCheckStorage(const ModelStorage* storage); + + // Invariants: + // * nullptr, if terms_ is empty + // * equal to Variable::storage() of each key of terms_, else + const ModelStorage* storage_ = nullptr; VariableMap terms_; double offset_ = 0.0; }; @@ -711,10 +722,8 @@ inline QuadraticTerm operator*(LinearTerm lhs, LinearTerm rhs); inline QuadraticTerm operator*(QuadraticTerm lhs, double rhs); inline QuadraticTerm operator/(QuadraticTerm lhs, double rhs); -// Implements the API of std::unordered_map, but forbids -// QuadraticTermKeys from different models in the same map. template -using QuadraticTermMap = IdMap; +using QuadraticTermMap = absl::flat_hash_map; // This class represents a sum of quadratic terms, linear terms, and constant // offset. For example: "3*x*y + 2*x + 1". @@ -732,14 +741,19 @@ using QuadraticTermMap = IdMap; // invariant in any class or friend method. class QuadraticExpression { public: + // For unit testing purpose, we define optional counters. We have to + // explicitly define the default constructor, copy constructor and assignment + // operators in that case. Else we use the defaults. #ifndef MATH_OPT_USE_EXPRESSION_COUNTERS QuadraticExpression() = default; + QuadraticExpression(const QuadraticExpression& other) = default; #else // MATH_OPT_USE_EXPRESSION_COUNTERS QuadraticExpression(); QuadraticExpression(const QuadraticExpression& other); - QuadraticExpression(QuadraticExpression&& other); - QuadraticExpression& operator=(const QuadraticExpression& other); #endif // MATH_OPT_USE_EXPRESSION_COUNTERS + // We have to define a custom move constructor as we need to reset storage_ to + // nullptr. + inline QuadraticExpression(QuadraticExpression&& other); // Users should prefer the default constructor and operator overloads to build // expressions. inline QuadraticExpression( @@ -750,16 +764,15 @@ class QuadraticExpression { inline QuadraticExpression(const LinearTerm& term); // NOLINT inline QuadraticExpression(LinearExpression expr); // NOLINT inline QuadraticExpression(const QuadraticTerm& term); // NOLINT + QuadraticExpression& operator=(const QuadraticExpression& other) = default; + // We have to define a custom move assignment operator as we need to reset + // storage_ to nullptr. + inline QuadraticExpression& operator=(QuadraticExpression&& other); inline double offset() const; inline const VariableMap& linear_terms() const; inline const QuadraticTermMap& quadratic_terms() const; - inline const absl::flat_hash_map& raw_linear_terms() - const; - inline const absl::flat_hash_map& - raw_quadratic_terms() const; - inline QuadraticExpression& operator+=(double value); inline QuadraticExpression& operator+=(Variable variable); inline QuadraticExpression& operator+=(const LinearTerm& term); @@ -903,15 +916,15 @@ class QuadraticExpression { // Compute the numeric value of this expression when variables are substituted // by their values in variable_values. // - // Will CHECK fail if the underlying model storage is different, or if a - // variable in linear_terms() or quadratic_terms() is missing from - // variables_values. + // Will CHECK fail if a variable in linear_terms() or quadratic_terms() is + // missing from variables_values. double Evaluate(const VariableMap& variable_values) const; // Compute the numeric value of this expression when variables are substituted // by their values in variable_values, or zero if missing from the map. // - // Will CHECK fail the underlying model storage is different. + // This function won't check that the variables in the input map are indeed in + // the same model as the ones of the expression. double EvaluateWithDefaultZero( const VariableMap& variable_values) const; @@ -931,8 +944,16 @@ class QuadraticExpression { friend QuadraticExpression operator-(QuadraticExpression expr); friend std::ostream& operator<<(std::ostream& ostr, const QuadraticExpression& expr); - inline void CheckModelsAgree(); + // Sets the storage_ to the input value if nullptr, else CHECKs that it is + // equal. Also CHECKs that the input value is not nullptr. + inline void SetOrCheckStorage(const ModelStorage* storage); + + // Invariants: + // * nullptr, if both quadratic_terms_ and linear_terms_ are empty + // * equal to Variable::storage() of each key of linear_terms_ and + // QuadraticTermKey::storage() of each key of quadratic_terms_, else + const ModelStorage* storage_ = nullptr; QuadraticTermMap quadratic_terms_; VariableMap linear_terms_; double offset_ = 0.0; @@ -1342,6 +1363,33 @@ LinearTerm operator/(Variable variable, const double coefficient) { // LinearExpression //////////////////////////////////////////////////////////////////////////////// +void LinearExpression::SetOrCheckStorage(const ModelStorage* const storage) { + CHECK(storage != nullptr) << internal::kKeyHasNullModelStorage; + if (storage_ == nullptr) { + storage_ = storage; + return; + } + CHECK_EQ(storage, storage_) << internal::kObjectsFromOtherModelStorage; +} + +LinearExpression::LinearExpression(LinearExpression&& other) + : storage_(std::exchange(other.storage_, nullptr)), + terms_(std::move(other.terms_)), + offset_(std::exchange(other.offset_, 0.0)) { + other.terms_.clear(); +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS + ++num_calls_move_constructor_; +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS +} + +LinearExpression& LinearExpression::operator=(LinearExpression&& other) { + storage_ = std::exchange(other.storage_, nullptr); + terms_ = std::move(other.terms_); + other.terms_.clear(); + offset_ = std::exchange(other.offset_, 0.0); + return *this; +} + LinearExpression::LinearExpression(std::initializer_list terms, const double offset) : offset_(offset) { @@ -1349,6 +1397,7 @@ LinearExpression::LinearExpression(std::initializer_list terms, ++num_calls_initializer_list_constructor_; #endif // MATH_OPT_USE_EXPRESSION_COUNTERS for (const auto& term : terms) { + SetOrCheckStorage(term.variable.storage()); // The same variable may appear multiple times in the input list; we must // accumulate the coefficients. terms_[term.variable] += term.coefficient; @@ -1366,7 +1415,7 @@ LinearExpression::LinearExpression(const LinearTerm& term) LinearExpression operator-(LinearExpression expr) { expr.offset_ = -expr.offset_; - for (auto term : expr.terms_) { + for (auto& term : expr.terms_) { term.second = -term.second; } return expr; @@ -1520,17 +1569,27 @@ LinearExpression operator/(LinearExpression lhs, const double rhs) { } LinearExpression& LinearExpression::operator+=(const LinearExpression& other) { - terms_.Add(other.terms_); + // Here we know that each key in other.terms_ has already been checked and + // thus we don't need to compare in the loop. Of course this only applies if + // the other has terms. + if (!other.terms_.empty()) { + SetOrCheckStorage(other.storage()); + for (const auto& [v, coeff] : other.terms_) { + terms_[v] += coeff; + } + } offset_ += other.offset_; return *this; } LinearExpression& LinearExpression::operator+=(const LinearTerm& term) { + SetOrCheckStorage(term.variable.storage()); terms_[term.variable] += term.coefficient; return *this; } LinearExpression& LinearExpression::operator+=(const Variable variable) { + SetOrCheckStorage(variable.storage()); return *this += LinearTerm(variable, 1.0); } @@ -1540,17 +1599,25 @@ LinearExpression& LinearExpression::operator+=(const double value) { } LinearExpression& LinearExpression::operator-=(const LinearExpression& other) { - terms_.Subtract(other.terms_); + // See operator+=. + if (!other.terms_.empty()) { + SetOrCheckStorage(other.storage()); + for (const auto& [v, coeff] : other.terms_) { + terms_[v] -= coeff; + } + } offset_ -= other.offset_; return *this; } LinearExpression& LinearExpression::operator-=(const LinearTerm& term) { + SetOrCheckStorage(term.variable.storage()); terms_[term.variable] -= term.coefficient; return *this; } LinearExpression& LinearExpression::operator-=(const Variable variable) { + SetOrCheckStorage(variable.storage()); return *this -= LinearTerm(variable, 1.0); } @@ -1561,7 +1628,7 @@ LinearExpression& LinearExpression::operator-=(const double value) { LinearExpression& LinearExpression::operator*=(const double value) { offset_ *= value; - for (auto term : terms_) { + for (auto& term : terms_) { term.second *= value; } return *this; @@ -1569,7 +1636,7 @@ LinearExpression& LinearExpression::operator*=(const double value) { LinearExpression& LinearExpression::operator/=(const double value) { offset_ /= value; - for (auto term : terms_) { + for (auto& term : terms_) { term.second /= value; } return *this; @@ -1640,14 +1707,7 @@ const VariableMap& LinearExpression::terms() const { return terms_; } double LinearExpression::offset() const { return offset_; } -const ModelStorage* LinearExpression::storage() const { - return terms_.storage(); -} - -const absl::flat_hash_map& LinearExpression::raw_terms() - const { - return terms_.raw_map(); -} +const ModelStorage* LinearExpression::storage() const { return storage_; } //////////////////////////////////////////////////////////////////////////////// // VariablesEquality @@ -2057,6 +2117,38 @@ QuadraticTermKey QuadraticTerm::GetKey() const { // QuadraticExpression (no arithmetic) //////////////////////////////////////////////////////////////////////////////// +void QuadraticExpression::SetOrCheckStorage(const ModelStorage* const storage) { + CHECK(storage != nullptr) << internal::kKeyHasNullModelStorage; + if (storage_ == nullptr) { + storage_ = storage; + return; + } + CHECK_EQ(storage, storage_) << internal::kObjectsFromOtherModelStorage; +} + +QuadraticExpression::QuadraticExpression(QuadraticExpression&& other) + : storage_(std::exchange(other.storage_, nullptr)), + quadratic_terms_(std::move(other.quadratic_terms_)), + linear_terms_(std::move(other.linear_terms_)), + offset_(std::exchange(other.offset_, 0.0)) { + other.quadratic_terms_.clear(); + other.linear_terms_.clear(); +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS + ++num_calls_move_constructor_; +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS +} + +QuadraticExpression& QuadraticExpression::operator=( + QuadraticExpression&& other) { + storage_ = std::exchange(other.storage_, nullptr); + quadratic_terms_ = std::move(other.quadratic_terms_); + other.quadratic_terms_.clear(); + linear_terms_ = std::move(other.linear_terms_); + other.linear_terms_.clear(); + offset_ = std::exchange(other.offset_, 0.0); + return *this; +} + QuadraticExpression::QuadraticExpression( const std::initializer_list quadratic_terms, const std::initializer_list linear_terms, const double offset) @@ -2065,12 +2157,14 @@ QuadraticExpression::QuadraticExpression( ++num_calls_initializer_list_constructor_; #endif // MATH_OPT_USE_EXPRESSION_COUNTERS for (const LinearTerm& term : linear_terms) { + SetOrCheckStorage(term.variable.storage()); linear_terms_[term.variable] += term.coefficient; } for (const QuadraticTerm& term : quadratic_terms) { - quadratic_terms_[term.GetKey()] += term.coefficient(); + const QuadraticTermKey key = term.GetKey(); + SetOrCheckStorage(key.storage()); + quadratic_terms_[key] += term.coefficient(); } - CheckModelsAgree(); } QuadraticExpression::QuadraticExpression(const double offset) @@ -2083,7 +2177,8 @@ QuadraticExpression::QuadraticExpression(const LinearTerm& term) : QuadraticExpression({}, {term}, 0.0) {} QuadraticExpression::QuadraticExpression(LinearExpression expr) - : linear_terms_(std::move(expr.terms_)), + : storage_(std::exchange(expr.storage_, nullptr)), + linear_terms_(std::move(expr.terms_)), offset_(std::exchange(expr.offset_, 0.0)) { #ifdef MATH_OPT_USE_EXPRESSION_COUNTERS ++num_calls_linear_expression_constructor_; @@ -2093,22 +2188,7 @@ QuadraticExpression::QuadraticExpression(LinearExpression expr) QuadraticExpression::QuadraticExpression(const QuadraticTerm& term) : QuadraticExpression({term}, {}, 0.0) {} -void QuadraticExpression::CheckModelsAgree() { - const ModelStorage* const quadratic_model = quadratic_terms_.storage(); - const ModelStorage* const linear_model = linear_terms_.storage(); - if ((linear_model != nullptr) && (quadratic_model != nullptr) && - (quadratic_model != linear_model)) { - LOG(FATAL) << internal::kObjectsFromOtherModelStorage; - } -} - -const ModelStorage* QuadraticExpression::storage() const { - if (quadratic_terms().storage()) { - return quadratic_terms().storage(); - } else { - return linear_terms().storage(); - } -} +const ModelStorage* QuadraticExpression::storage() const { return storage_; } double QuadraticExpression::offset() const { return offset_; } @@ -2120,16 +2200,6 @@ const QuadraticTermMap& QuadraticExpression::quadratic_terms() const { return quadratic_terms_; } -const absl::flat_hash_map& -QuadraticExpression::raw_linear_terms() const { - return linear_terms_.raw_map(); -} - -const absl::flat_hash_map& -QuadraticExpression::raw_quadratic_terms() const { - return quadratic_terms_.raw_map(); -} - //////////////////////////////////////////////////////////////////////////////// // Arithmetic operators (non-member). // @@ -2253,10 +2323,10 @@ QuadraticTerm operator-(QuadraticTerm term) { // NOTE: A friend of QuadraticExpression, but does not touch variables QuadraticExpression operator-(QuadraticExpression expr) { expr.offset_ = -expr.offset_; - for (auto term : expr.linear_terms_) { + for (auto& term : expr.linear_terms_) { term.second = -term.second; } - for (auto term : expr.quadratic_terms_) { + for (auto& term : expr.quadratic_terms_) { term.second = -term.second; } return expr; @@ -2505,38 +2575,51 @@ QuadraticExpression& QuadraticExpression::operator+=(const double value) { } QuadraticExpression& QuadraticExpression::operator+=(const Variable variable) { + SetOrCheckStorage(variable.storage()); linear_terms_[variable] += 1; - CheckModelsAgree(); return *this; } QuadraticExpression& QuadraticExpression::operator+=(const LinearTerm& term) { + SetOrCheckStorage(term.variable.storage()); linear_terms_[term.variable] += term.coefficient; - CheckModelsAgree(); return *this; } QuadraticExpression& QuadraticExpression::operator+=( const LinearExpression& expr) { offset_ += expr.offset(); - linear_terms_.Add(expr.terms()); - CheckModelsAgree(); + // See comment in LinearExpression::operator+=. + if (!expr.terms().empty()) { + SetOrCheckStorage(expr.storage()); + for (const auto& [v, coeff] : expr.terms()) { + linear_terms_[v] += coeff; + } + } return *this; } QuadraticExpression& QuadraticExpression::operator+=( const QuadraticTerm& term) { - quadratic_terms_[term.GetKey()] += term.coefficient(); - CheckModelsAgree(); + const QuadraticTermKey key = term.GetKey(); + SetOrCheckStorage(key.storage()); + quadratic_terms_[key] += term.coefficient(); return *this; } QuadraticExpression& QuadraticExpression::operator+=( const QuadraticExpression& expr) { offset_ += expr.offset(); - linear_terms_.Add(expr.linear_terms()); - quadratic_terms_.Add(expr.quadratic_terms()); - CheckModelsAgree(); + // See comment in LinearExpression::operator+=. + if (!expr.linear_terms().empty() || !expr.quadratic_terms().empty()) { + SetOrCheckStorage(expr.storage()); + for (const auto& [v, coeff] : expr.linear_terms()) { + linear_terms_[v] += coeff; + } + for (const auto& [k, coeff] : expr.quadratic_terms()) { + quadratic_terms_[k] += coeff; + } + } return *this; } @@ -2547,38 +2630,51 @@ QuadraticExpression& QuadraticExpression::operator-=(const double value) { } QuadraticExpression& QuadraticExpression::operator-=(const Variable variable) { + SetOrCheckStorage(variable.storage()); linear_terms_[variable] -= 1; - CheckModelsAgree(); return *this; } QuadraticExpression& QuadraticExpression::operator-=(const LinearTerm& term) { + SetOrCheckStorage(term.variable.storage()); linear_terms_[term.variable] -= term.coefficient; - CheckModelsAgree(); return *this; } QuadraticExpression& QuadraticExpression::operator-=( const LinearExpression& expr) { offset_ -= expr.offset(); - linear_terms_.Subtract(expr.terms()); - CheckModelsAgree(); + // See comment in LinearExpression::operator+=. + if (!expr.terms().empty()) { + SetOrCheckStorage(expr.storage()); + for (const auto& [v, coeff] : expr.terms()) { + linear_terms_[v] -= coeff; + } + } return *this; } QuadraticExpression& QuadraticExpression::operator-=( const QuadraticTerm& term) { - quadratic_terms_[term.GetKey()] -= term.coefficient(); - CheckModelsAgree(); + const QuadraticTermKey key = term.GetKey(); + SetOrCheckStorage(key.storage()); + quadratic_terms_[key] -= term.coefficient(); return *this; } QuadraticExpression& QuadraticExpression::operator-=( const QuadraticExpression& expr) { offset_ -= expr.offset(); - linear_terms_.Subtract(expr.linear_terms()); - quadratic_terms_.Subtract(expr.quadratic_terms()); - CheckModelsAgree(); + // See comment in LinearExpression::operator+=. + if (!expr.linear_terms().empty() || !expr.quadratic_terms().empty()) { + SetOrCheckStorage(expr.storage()); + for (const auto& [v, coeff] : expr.linear_terms()) { + linear_terms_[v] -= coeff; + } + for (const auto& [k, coeff] : expr.quadratic_terms()) { + quadratic_terms_[k] -= coeff; + } + } return *this; } @@ -2591,10 +2687,10 @@ QuadraticTerm& QuadraticTerm::operator*=(const double value) { QuadraticExpression& QuadraticExpression::operator*=(const double value) { offset_ *= value; - for (auto term : linear_terms_) { + for (auto& term : linear_terms_) { term.second *= value; } - for (auto term : quadratic_terms_) { + for (auto& term : quadratic_terms_) { term.second *= value; } // NOTE: Not adding/removing/altering variables in expression, just modifying @@ -2611,10 +2707,10 @@ QuadraticTerm& QuadraticTerm::operator/=(const double value) { QuadraticExpression& QuadraticExpression::operator/=(const double value) { offset_ /= value; - for (auto term : linear_terms_) { + for (auto& term : linear_terms_) { term.second /= value; } - for (auto term : quadratic_terms_) { + for (auto& term : quadratic_terms_) { term.second /= value; } // NOTE: Not adding/removing/altering variables in expression, just modifying diff --git a/ortools/math_opt/infeasible_subsystem.proto b/ortools/math_opt/infeasible_subsystem.proto new file mode 100644 index 0000000000..8e4d06803f --- /dev/null +++ b/ortools/math_opt/infeasible_subsystem.proto @@ -0,0 +1,82 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Messages for representing subsets of a models constraints, and for computing +// infeasible subsystems of a model. Cf. "Irreducible Inconsistent subsystems" +// (IIS), which are useful for debugging/diagnosing model infeasibility. +syntax = "proto3"; + +package operations_research.math_opt; + +import "ortools/math_opt/result.proto"; + +option java_package = "com.google.ortools.mathopt"; +option java_multiple_files = true; + +// Represents a subset of the constraints (including variable bounds and +// integrality) of a `ModelProto`. +message ModelSubsetProto { + message Bounds { + bool lower = 1; + bool upper = 2; + } + + // Keys are variable IDs, and must be in [0, max(int64)). Values indicate + // which of the lower and upper variable bounds are included in the subsystem. + map variable_bounds = 1; + + // Variable IDs. Values must be in [0, max(int64)) and strictly increasing. + repeated int64 variable_integrality = 2; + + // Keys are linear constraint IDs, and must be in [0, max(int64)). Values + // indicate which of the lower and upper bounds on the linear constraint are + // included in the subsystem. + map linear_constraints = 3; + + // Keys are quadratic constraint IDs, and must be in [0, max(int64)). Values + // indicate which of the lower and upper bounds on the quadratic constraint + // are included in the subsystem. + map quadratic_constraints = 4; + + // Second-order cone constraint IDs. Values must be in [0, max(int64)) and + // strictly increasing. + repeated int64 second_order_cone_constraints = 5; + + // SOS1 constraint IDs. Values must be in [0, max(int64)) and strictly + // increasing. + repeated int64 sos1_constraints = 6; + + // SOS2 constraint IDs. Values must be in [0, max(int64)) and strictly + // increasing. + repeated int64 sos2_constraints = 7; + + // Indicator constraint IDs. Values must be in [0, max(int64)) and strictly + // increasing. + repeated int64 indicator_constraints = 8; +} + +message InfeasibleSubsystemResultProto { + // The primal feasibility status of the model, as determined by the solver. + FeasibilityStatusProto feasibility = 1; + + // An infeasible subsystem of the input model. Set if `feasibility` is + // INFEASIBLE and empty otherwise. The IDs correspond to those constraints + // included in the infeasible subsystem. Submessages with `Bounds` values + // indicate which side of a potentially ranged constraint are included in the + // subsystem: lower bound, upper bound, or both. + ModelSubsetProto infeasible_subsystem = 2; + + // True if the solver has certified that the returned subsystem is minimal + // (the instance is feasible if any additional constraint is removed). + bool is_minimal = 3; +} diff --git a/ortools/math_opt/io/BUILD.bazel b/ortools/math_opt/io/BUILD.bazel index d5140e8ddd..a09f308880 100644 --- a/ortools/math_opt/io/BUILD.bazel +++ b/ortools/math_opt/io/BUILD.bazel @@ -20,11 +20,10 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//ortools/base:status_macros", - # Only needed for linear_solver/model_validator.h, we should break that - # target up. "//ortools/linear_solver", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/core:math_opt_proto_utils", "//ortools/math_opt/core:sparse_vector_view", @@ -48,7 +47,6 @@ cc_library( "//ortools/linear_solver:model_exporter", "//ortools/lp_data:mps_reader", "//ortools/math_opt:model_cc_proto", - "//ortools/util:file_util", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], @@ -63,3 +61,17 @@ cc_library( "//ortools/math_opt:model_update_cc_proto", ], ) + +cc_library( + name = "lp_converter", + srcs = ["lp_converter.cc"], + hdrs = ["lp_converter.h"], + deps = [ + ":proto_converter", + "//ortools/base:status_macros", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_exporter", + "//ortools/math_opt:model_cc_proto", + "@com_google_absl//absl/status:statusor", + ], +) diff --git a/ortools/math_opt/io/lp_converter.cc b/ortools/math_opt/io/lp_converter.cc new file mode 100644 index 0000000000..88c349fe3d --- /dev/null +++ b/ortools/math_opt/io/lp_converter.cc @@ -0,0 +1,33 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/io/lp_converter.h" + +#include + +#include "absl/status/statusor.h" +#include "ortools/base/status_macros.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/model_exporter.h" +#include "ortools/math_opt/io/proto_converter.h" +#include "ortools/math_opt/model.pb.h" + +namespace operations_research::math_opt { + +absl::StatusOr ModelProtoToLp(const ModelProto& model) { + ASSIGN_OR_RETURN(const MPModelProto mp_model_proto, + MathOptModelToMPModelProto(model)); + return ExportModelAsLpFormat(mp_model_proto, {.show_unused_variables = true}); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/io/lp_converter.h b/ortools/math_opt/io/lp_converter.h new file mode 100644 index 0000000000..0958d985e3 --- /dev/null +++ b/ortools/math_opt/io/lp_converter.h @@ -0,0 +1,38 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_IO_LP_CONVERTER_H_ +#define OR_TOOLS_MATH_OPT_IO_LP_CONVERTER_H_ + +#include + +#include "absl/status/statusor.h" +#include "ortools/math_opt/model.pb.h" + +namespace operations_research::math_opt { + +// Returns the model in "CPLEX LP" format. +// +// The RemoveNames() function can be used on the model to remove names if they +// should not be exported. +// +// For more information about the different LP file formats: +// http://lpsolve.sourceforge.net/5.5/lp-format.htm +// http://lpsolve.sourceforge.net/5.5/CPLEX-format.htm +// https://www.ibm.com/docs/en/icos/12.8.0.0?topic=cplex-lp-file-format-algebraic-representation +// http://www.gurobi.com/documentation/5.1/reference-manual/node871 +absl::StatusOr ModelProtoToLp(const ModelProto& model); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_IO_LP_CONVERTER_H_ diff --git a/ortools/math_opt/io/mps_converter.cc b/ortools/math_opt/io/mps_converter.cc index a841503a72..854ce8adf7 100644 --- a/ortools/math_opt/io/mps_converter.cc +++ b/ortools/math_opt/io/mps_converter.cc @@ -22,7 +22,6 @@ #include "ortools/lp_data/mps_reader.h" #include "ortools/math_opt/io/proto_converter.h" #include "ortools/math_opt/model.pb.h" -#include "ortools/util/file_util.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/io/mps_converter.h b/ortools/math_opt/io/mps_converter.h index e0da62362b..2ff8007f68 100644 --- a/ortools/math_opt/io/mps_converter.h +++ b/ortools/math_opt/io/mps_converter.h @@ -31,7 +31,6 @@ absl::StatusOr ModelProtoToMps(const ModelProto& model); // Reads an MPS file and converts it to a ModelProto (like MpsToModelProto // above, but takes a file name instead of the file contents and reads the file. // -// // The file can be stored as plain text or gzipped (with the .gz extension). // absl::StatusOr ReadMpsFile(absl::string_view filename); diff --git a/ortools/math_opt/io/proto_converter.cc b/ortools/math_opt/io/proto_converter.cc index 33518c2e0a..e2c0c1d871 100644 --- a/ortools/math_opt/io/proto_converter.cc +++ b/ortools/math_opt/io/proto_converter.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -40,11 +41,9 @@ namespace operations_research { namespace math_opt { namespace { -constexpr double kInf = std::numeric_limits::infinity(); - absl::Status IsSupported(const MPModelProto& model) { std::string validity_string = FindErrorInMPModelProto(model); - if (validity_string.length() > 0) { + if (!validity_string.empty()) { return absl::InvalidArgumentError(validity_string); } for (const MPGeneralConstraintProto& general_constraint : @@ -55,9 +54,6 @@ absl::Status IsSupported(const MPModelProto& model) { return absl::InvalidArgumentError("Unsupported general constraint"); } } - if (model.solution_hint().var_index_size() > 0) { - return absl::InvalidArgumentError("Solution Hint not supported"); - } return absl::OkStatus(); } @@ -368,9 +364,34 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) { return output; } +absl::StatusOr> +MPModelProtoSolutionHintToMathOptHint(const MPModelProto& model) { + std::string validity_string = FindErrorInMPModelProto(model); + if (!validity_string.empty()) { + return absl::InvalidArgumentError(validity_string); + } + + if (model.solution_hint().var_index_size() == 0) { + return std::nullopt; + } + + SolutionHintProto hint; + auto& variable_values = *hint.mutable_variable_values(); + LinearTermsFromMPModelToMathOpt( + model.solution_hint().var_index(), model.solution_hint().var_value(), + *variable_values.mutable_ids(), *variable_values.mutable_values()); + + return hint; +} + absl::StatusOr<::operations_research::MPModelProto> MathOptModelToMPModelProto( const ::operations_research::math_opt::ModelProto& model) { RETURN_IF_ERROR(ValidateModel(model).status()); + if (!model.second_order_cone_constraints().empty()) { + return absl::InvalidArgumentError( + "translating models with second-order cone constraints is not " + "supported"); + } const bool vars_have_name = model.variables().names_size() > 0; const bool constraints_have_name = diff --git a/ortools/math_opt/io/proto_converter.h b/ortools/math_opt/io/proto_converter.h index 9a1d946a57..e83fafb5dc 100644 --- a/ortools/math_opt/io/proto_converter.h +++ b/ortools/math_opt/io/proto_converter.h @@ -14,22 +14,38 @@ #ifndef OR_TOOLS_MATH_OPT_IO_PROTO_CONVERTER_H_ #define OR_TOOLS_MATH_OPT_IO_PROTO_CONVERTER_H_ +#include + #include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" namespace operations_research::math_opt { // Returns a ModelProto equivalent to the input linear_solver Model. The input -// MPModelProto must be valid, as checked by `FindErrorInMPModelProto`. +// MPModelProto must be valid, as checked by `FindErrorInMPModelProto()`. // // The linear_solver Model stores all general constraints (e.g., quadratic, SOS) // in a single repeated field, while ModelProto stores then in separate maps. // The output constraint maps will each be populated with consecutive indices // starting from 0 (hence the indices may change). +// +// MPModelProto can contain an optional `solution_hint` which is ignored by this +// function. In MathOpt the hints are parameters passed to the solve functions +// instead of being in the model. The `MPModelProtoSolutionHintToMathOptHint()` +// function can be used to extract it. absl::StatusOr MPModelProtoToMathOptModel( const MPModelProto& model); +// Returns the optional `model.solution_hint` as a MathOpt hint. Returns nullopt +// if no hint is set on the input model or if the hint is empty. +// +// The input MPModelProto must be valid, as checked by +// `FindErrorInMPModelProto()`. +absl::StatusOr> +MPModelProtoSolutionHintToMathOptHint(const MPModelProto& model); + // Returns a linear_solver MPModelProto equivalent to the input math_opt Model. // The input Model must be in a valid state, as checked by `ValidateModel`. // diff --git a/ortools/math_opt/labs/BUILD.bazel b/ortools/math_opt/labs/BUILD.bazel new file mode 100644 index 0000000000..48e8b83c8e --- /dev/null +++ b/ortools/math_opt/labs/BUILD.bazel @@ -0,0 +1,42 @@ +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//ortools/math_opt:__subpackages__"]) + +cc_library( + name = "solution_feasibility_checker", + srcs = ["solution_feasibility_checker.cc"], + hdrs = ["solution_feasibility_checker.h"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/base:mathutil", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/util:fp_roundtrip_conv", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) + +cc_library( + name = "linear_expr_util", + srcs = ["linear_expr_util.cc"], + hdrs = ["linear_expr_util.h"], + deps = [ + "//ortools/math_opt/cpp:math_opt", + "@com_google_absl//absl/algorithm:container", + ], +) diff --git a/ortools/math_opt/labs/linear_expr_util.cc b/ortools/math_opt/labs/linear_expr_util.cc new file mode 100644 index 0000000000..6c1554bce4 --- /dev/null +++ b/ortools/math_opt/labs/linear_expr_util.cc @@ -0,0 +1,60 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/labs/linear_expr_util.h" + +#include +#include + +#include "absl/algorithm/container.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { +namespace { + +double ComputeBound(const LinearExpression& linear_expression, + bool is_upper_bound) { + // The algorithm used is as follows: + // (1) Make a list of the terms to add up, e.g. + // [offset, x1.lb()*c1, x3.ub()*c3] + // (2) Sort the list by {abs(x), x} lexicographically + // (3) Sum up the values from the smallest absolute value to largest. + // The result will give deterministic output with reasonable precision. + std::vector terms_to_add; + terms_to_add.reserve(linear_expression.terms().size() + 1); + terms_to_add.push_back(linear_expression.offset()); + for (const auto [var, coef] : linear_expression.terms()) { + const bool use_ub = + (is_upper_bound && coef > 0) || (!is_upper_bound && coef < 0); + const double val = + use_ub ? var.upper_bound() * coef : var.lower_bound() * coef; + terms_to_add.push_back(val); + } + // all values in terms_to_add are finite. + absl::c_sort(terms_to_add, [](const double left, const double right) { + return std::pair(std::abs(left), left) < std::pair(std::abs(right), right); + }); + return absl::c_accumulate(terms_to_add, 0.0); +} + +} // namespace + +double LowerBound(const LinearExpression& linear_expression) { + return ComputeBound(linear_expression, /*is_upper_bound=*/false); +} + +double UpperBound(const LinearExpression& linear_expression) { + return ComputeBound(linear_expression, /*is_upper_bound=*/true); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/labs/linear_expr_util.h b/ortools/math_opt/labs/linear_expr_util.h new file mode 100644 index 0000000000..c12a931dbb --- /dev/null +++ b/ortools/math_opt/labs/linear_expr_util.h @@ -0,0 +1,61 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Methods for manipulating LinearExpressions. +// +// Why in labs? Lots of users seem to need this (e.g. for big-M calculations), +// but there are several possible algorithms, and it is not clear what, if +// anything, would be used widely. The function also makes many assumptions on +// the input that are not easy to verify and can lead to confusing errors, +// it is worth seeing if the API can be hardened a bit. +#ifndef OR_TOOLS_MATH_OPT_LABS_LINEAR_EXPR_UTIL_H_ +#define OR_TOOLS_MATH_OPT_LABS_LINEAR_EXPR_UTIL_H_ + +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +// Computes a lower bound on the value a linear expression can take based on +// the variable bounds. +// +// The user must ensure: +// * Variable lower bounds are in [-inf, +inf) (required at solve time as well) +// * Variable upper bounds are in (-inf, +inf] (required at solve time as well) +// * Variables bounds are not NaN +// * The expression has no NaNs and all finite coefficients +// * The output computation does not overflow when summing finite terms (rarely +// an issue, as then your problem is very poorly scaled). +// Under these assumptions, the returned value will be in [-inf, +inf). If an +// assumption is broken, it is possible to return NaN or +inf. +// +// This function is deterministic, but runs in O(n log n) and will allocate. +// +// Alternatives: +// * If more precision is needed, see AccurateSum +// * For a faster method that does not allocate, is less precise, and not +// deterministic, simply add each term to the result in the hash map's +// iteration order. +double LowerBound(const LinearExpression& linear_expression); + +// Computes an upper bound on the value a linear expression can take based on +// the variable bounds. +// +// The returned value will be in (-inf, +inf] on valid input (see LowerBound() +// above, the requirements are the same). +// +// See LowerBound() above for more details. +double UpperBound(const LinearExpression& linear_expression); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_LABS_LINEAR_EXPR_UTIL_H_ diff --git a/ortools/math_opt/labs/solution_feasibility_checker.cc b/ortools/math_opt/labs/solution_feasibility_checker.cc new file mode 100644 index 0000000000..9f22d83b4f --- /dev/null +++ b/ortools/math_opt/labs/solution_feasibility_checker.cc @@ -0,0 +1,380 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/labs/solution_feasibility_checker.h" + +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "ortools/base/mathutil.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/fp_roundtrip_conv.h" + +namespace operations_research::math_opt { +namespace { + +absl::Status ValidateOptions(const FeasibilityCheckerOptions& options) { + const auto tolerance_is_valid = [](const double tolerance) { + return tolerance >= 0 && !std::isnan(tolerance); + }; + if (!tolerance_is_valid(options.absolute_constraint_tolerance)) { + return util::InvalidArgumentErrorBuilder() + << "invalid absolute_constraint_tolerance value: " + << options.absolute_constraint_tolerance; + } + if (!tolerance_is_valid(options.integrality_tolerance)) { + return util::InvalidArgumentErrorBuilder() + << "invalid integrality_tolerance value: " + << options.integrality_tolerance; + } + if (!tolerance_is_valid(options.nonzero_tolerance)) { + return util::InvalidArgumentErrorBuilder() + << "invalid nonzero_tolerance value: " << options.nonzero_tolerance; + } + return absl::OkStatus(); +} + +bool IsNearlyLessThan(const double lhs, const double rhs, + const double absolute_tolerance) { + return lhs <= rhs + absolute_tolerance; +} + +bool IsNearlyEqualTo(const double actual, const double target, + const double absolute_tolerance) { + return std::fabs(actual - target) <= absolute_tolerance; +} + +absl::Status ValidateVariables(const Model& model, + const VariableMap& variable_values) { + for (const Variable variable : model.Variables()) { + if (!variable_values.contains(variable)) { + return util::InvalidArgumentErrorBuilder() + << "Variable present in `model` but not `variable_values`: " + << variable; + } + } + for (const auto [variable, unused] : variable_values) { + if (variable.storage() != model.storage()) { + return util::InvalidArgumentErrorBuilder() + << "Variable present in `variable_values` but not `model`: " + << variable; + } + } + return absl::OkStatus(); +} + +ModelSubset::Bounds CheckBoundedConstraint( + const double expr_value, const double lower_bound, const double upper_bound, + const FeasibilityCheckerOptions& options) { + return {.lower = !IsNearlyLessThan(lower_bound, expr_value, + options.absolute_constraint_tolerance), + .upper = !IsNearlyLessThan(expr_value, upper_bound, + options.absolute_constraint_tolerance)}; +} + +// CHECK-fails if `variable` and `variable_values` come from different models. +ModelSubset::Bounds CheckVariableBounds( + const Variable variable, const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + return CheckBoundedConstraint(variable_values.at(variable), + variable.lower_bound(), variable.upper_bound(), + options); +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +ModelSubset::Bounds CheckLinearConstraint( + const LinearConstraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + const BoundedLinearExpression bounded_expr = + constraint.AsBoundedLinearExpression(); + return CheckBoundedConstraint( + bounded_expr.expression.Evaluate(variable_values), + bounded_expr.lower_bound, bounded_expr.upper_bound, options); +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +ModelSubset::Bounds CheckQuadraticConstraint( + const QuadraticConstraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + const BoundedQuadraticExpression bounded_expr = + constraint.AsBoundedQuadraticExpression(); + return CheckBoundedConstraint( + bounded_expr.expression.Evaluate(variable_values), + bounded_expr.lower_bound, bounded_expr.upper_bound, options); +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +bool CheckSecondOrderConeConstraint(const SecondOrderConeConstraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + double args_to_norm_value = 0.0; + for (const LinearExpression& expr : constraint.ArgumentsToNorm()) { + // This is liable to overflow, but if it does so it will return inf, which + // will ultimately cause this function to return false. + args_to_norm_value += MathUtil::IPow(expr.Evaluate(variable_values), 2); + } + return IsNearlyLessThan(std::sqrt(args_to_norm_value), + constraint.UpperBound().Evaluate(variable_values), + options.absolute_constraint_tolerance); +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +bool CheckSos1Constraint(const Sos1Constraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + // For expression i: was any expression with index in [0, i) nonzero? + bool previous_nonzero = false; + for (int i = 0; i < constraint.num_expressions(); ++i) { + const bool expr_is_nonzero = + !IsNearlyEqualTo(constraint.Expression(i).Evaluate(variable_values), + 0.0, options.nonzero_tolerance); + if (expr_is_nonzero && previous_nonzero) { + // We've seen two nonzero expressions, the SOS1 constraint is violated. + return false; + } + previous_nonzero |= expr_is_nonzero; + } + return true; +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +bool CheckSos2Constraint(const Sos2Constraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + // For expression i: was expression i-1 nonzero? + bool preceding_was_nonzero = false; + // For expression i: was any expression with index in [0, i-1) nonzero? + bool any_other_before_was_nonzero = false; + for (int i = 0; i < constraint.num_expressions(); ++i) { + const bool expr_is_nonzero = + !IsNearlyEqualTo(constraint.Expression(i).Evaluate(variable_values), + 0.0, options.nonzero_tolerance); + if (expr_is_nonzero && any_other_before_was_nonzero) { + // Expressions i and j are nonzero, for some j in [0, i-1), so the SOS2 + // constraint is violated. + return false; + } + // Update values for expression i+1. + any_other_before_was_nonzero |= preceding_was_nonzero; + preceding_was_nonzero = expr_is_nonzero; + } + return true; +} + +// CHECK-fails if `constraint` and `variable_values` come from different models. +// Only check the implication, not that the indicator variable is binary. +bool CheckIndicatorConstraint(const IndicatorConstraint constraint, + const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + if (!constraint.indicator_variable().has_value()) { + // Null indicator variables mean the constraint is vacuously satisfied. + return true; + } + if (!IsNearlyEqualTo(variable_values.at(*constraint.indicator_variable()), + constraint.activate_on_zero() ? 0.0 : 1.0, + options.nonzero_tolerance)) { + // If the indicator variable is not (nearly) at its indication value, the + // constraint holds (there is no implication). + return true; + } + const BoundedLinearExpression bounded_expr = constraint.ImpliedConstraint(); + // At this point in the function, we know that the implication should hold. + // So, the indicator constraint is satisfied iff both sides of the implied + // constraints are satisfied. + return CheckBoundedConstraint( + bounded_expr.expression.Evaluate(variable_values), + bounded_expr.lower_bound, bounded_expr.upper_bound, options) + .empty(); +} + +} // namespace + +absl::StatusOr CheckPrimalSolutionFeasibility( + const Model& model, const VariableMap& variable_values, + const FeasibilityCheckerOptions& options) { + RETURN_IF_ERROR(ValidateOptions(options)); + RETURN_IF_ERROR(ValidateVariables(model, variable_values)); + + ModelSubset violated_constraints; + for (const Variable variable : model.Variables()) { + const ModelSubset::Bounds violations = + CheckVariableBounds(variable, variable_values, options); + if (!violations.empty()) { + violated_constraints.variable_bounds[variable] = violations; + } + if (variable.is_integer()) { + const double variable_value = variable_values.at(variable); + const double rounded_variable_value = std::round(variable_value); + if (std::fabs(rounded_variable_value - variable_value) > + options.integrality_tolerance) { + violated_constraints.variable_integrality.insert(variable); + } + } + } + + for (const LinearConstraint linear_constraint : model.LinearConstraints()) { + const ModelSubset::Bounds violations = + CheckLinearConstraint(linear_constraint, variable_values, options); + if (!violations.empty()) { + violated_constraints.linear_constraints[linear_constraint] = violations; + } + } + + for (const QuadraticConstraint quadratic_constraint : + model.QuadraticConstraints()) { + const ModelSubset::Bounds violations = CheckQuadraticConstraint( + quadratic_constraint, variable_values, options); + if (!violations.empty()) { + violated_constraints.quadratic_constraints[quadratic_constraint] = + violations; + } + } + + for (const SecondOrderConeConstraint soc_constraint : + model.SecondOrderConeConstraints()) { + if (!CheckSecondOrderConeConstraint(soc_constraint, variable_values, + options)) { + violated_constraints.second_order_cone_constraints.insert(soc_constraint); + } + } + + for (const Sos1Constraint sos1_constraint : model.Sos1Constraints()) { + if (!CheckSos1Constraint(sos1_constraint, variable_values, options)) { + violated_constraints.sos1_constraints.insert(sos1_constraint); + } + } + + for (const Sos2Constraint sos2_constraint : model.Sos2Constraints()) { + if (!CheckSos2Constraint(sos2_constraint, variable_values, options)) { + violated_constraints.sos2_constraints.insert(sos2_constraint); + } + } + + for (const IndicatorConstraint indicator_constraint : + model.IndicatorConstraints()) { + if (!CheckIndicatorConstraint(indicator_constraint, variable_values, + options)) { + violated_constraints.indicator_constraints.insert(indicator_constraint); + } + } + return violated_constraints; +} + +namespace { + +// `variables` and `variable_values` must share a common Model. +std::string VariableValuesAsString(std::vector variables, + const VariableMap& variable_values) { + absl::c_sort(variables, [](const Variable lhs, const Variable rhs) { + return lhs.typed_id() < rhs.typed_id(); + }); + return absl::StrCat( + "{", + absl::StrJoin( + variables, ", ", + [&](std::string* const out, const Variable variable) { + absl::StrAppendFormat( + out, "{%s, %s}", absl::FormatStreamed(variable), + RoundTripDoubleFormat::ToString(variable_values.at(variable))); + }), + "}"); +} + +// Requires T::ToString() and T::NonzeroVariables() for duck-typing. +template +std::string ViolatedConstraintAsString( + const T violated_constraint, const VariableMap& variable_values, + const absl::string_view constraint_type) { + return absl::StrFormat( + "violated %s %s: %s, with variable values %s", constraint_type, + absl::FormatStreamed(violated_constraint), violated_constraint.ToString(), + VariableValuesAsString(violated_constraint.NonzeroVariables(), + variable_values)); +} + +// Requires T::ToString() and T::NonzeroVariables() for duck-typing. +template +void AppendViolatedConstraintsAsStrings( + const std::vector& violated_constraints, + const VariableMap& variable_values, + const absl::string_view constraint_type, std::vector& output) { + for (const T violated_constraint : violated_constraints) { + output.push_back(ViolatedConstraintAsString( + violated_constraint, variable_values, constraint_type)); + } +} + +} // namespace + +absl::StatusOr> ViolatedConstraintsAsStrings( + const Model& model, const ModelSubset& violated_constraints, + const VariableMap& variable_values) { + RETURN_IF_ERROR(violated_constraints.CheckModelStorage(model.storage())) + << "violated_constraints and model are inconsistent"; + RETURN_IF_ERROR(ValidateVariables(model, variable_values)); + + std::vector result; + for (const Variable variable : + SortedKeys(violated_constraints.variable_bounds)) { + result.push_back(absl::StrFormat( + "violated variable bound: %s ≤ %s ≤ %s, with variable value %s", + RoundTripDoubleFormat::ToString(variable.lower_bound()), + absl::FormatStreamed(variable), + RoundTripDoubleFormat::ToString(variable.upper_bound()), + RoundTripDoubleFormat::ToString(variable_values.at(variable)))); + } + for (const Variable variable : + SortedElements(violated_constraints.variable_integrality)) { + result.push_back(absl::StrFormat( + "violated variable integrality: %s, with variable value %s", + absl::FormatStreamed(variable), + RoundTripDoubleFormat::ToString(variable_values.at(variable)))); + } + for (const LinearConstraint linear_constraint : + SortedKeys(violated_constraints.linear_constraints)) { + result.push_back(absl::StrFormat( + "violated linear constraint %s: %s, with variable values %s", + absl::FormatStreamed(linear_constraint), linear_constraint.ToString(), + VariableValuesAsString(model.RowNonzeros(linear_constraint), + variable_values))); + } + AppendViolatedConstraintsAsStrings( + SortedKeys(violated_constraints.quadratic_constraints), variable_values, + "quadratic constraint", result); + AppendViolatedConstraintsAsStrings( + SortedElements(violated_constraints.second_order_cone_constraints), + variable_values, "second-order cone constraint", result); + AppendViolatedConstraintsAsStrings( + SortedElements(violated_constraints.sos1_constraints), variable_values, + "SOS1 constraint", result); + AppendViolatedConstraintsAsStrings( + SortedElements(violated_constraints.sos2_constraints), variable_values, + "SOS2 constraint", result); + AppendViolatedConstraintsAsStrings( + SortedElements(violated_constraints.indicator_constraints), + variable_values, "indicator constraint", result); + return result; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/labs/solution_feasibility_checker.h b/ortools/math_opt/labs/solution_feasibility_checker.h new file mode 100644 index 0000000000..8b26b620e6 --- /dev/null +++ b/ortools/math_opt/labs/solution_feasibility_checker.h @@ -0,0 +1,84 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_LABS_SOLUTION_FEASIBILITY_CHECKER_H_ +#define OR_TOOLS_MATH_OPT_LABS_SOLUTION_FEASIBILITY_CHECKER_H_ + +#include +#include + +#include "absl/status/statusor.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +struct FeasibilityCheckerOptions { + // Used for evaluating the feasibility of primal solution values with respect + // to linear constraints and variable bounds. + // + // For example, variable values x are considered feasible with respect to a + // constraint ≤ b iff ≤ b + absolute_constraint_tolerance. + // + // Cannot be negative or NaN. + double absolute_constraint_tolerance = 1.0e-6; + + // An absolute tolerance used for evaluating the feasibility of a variable's + // value with respect to integrality constraints on that variable, if present. + // + // For example, a value x for an integer variable is considered feasible with + // respect to its integrality constraints iff + // |x - round(x)| ≤ integrality_tolerance. + // + // Cannot be negative or NaN. + double integrality_tolerance = 1.0e-5; + + // Absolute tolerance for evaluating if an expression is sufficiently close to + // a particular value (usually zero, hence the name). + // + // This is used for evaluating if SOS1 and SOS2 constraints are satisfied, as + // well as for evaluating indicator constraint feasibility (i.e., is the + // indicator variable at its "activation value"). + // + // For example, variable values x are considered feasible with respect to an + // SOS1 constraint {expr_1(x), ..., expr_d(x)}-is-SOS1 iff there is at most + // one j such that |expr_j(x)| > nonzero_tolerance. + // + // Cannot be negative or NaN. + double nonzero_tolerance = 1.0e-5; +}; + +// Returns a subset of `model`s constraints that are violated at the point in +// `variable_values`. A point feasible with respect to all constraints will +// return an empty subset, which can be checked via ModelSubset::empty(). +// +// Feasibility is checked within tolerances that can be configured in `options`. +// +// Returns an InvalidArgument error if `variable_values` does not contain an +// entry for each variable in `model` (and no extras). +absl::StatusOr CheckPrimalSolutionFeasibility( + const Model& model, const VariableMap& variable_values, + const FeasibilityCheckerOptions& options = {}); + +// Returns a collection of strings that provide a human-readable representation +// of the `violated_constraints` (one string for each violated constraint). +// Useful for logging. +// +// Returns an InvalidArgument error if `variable_values` does not contain an +// entry for each variable in `model` (and no extras). +absl::StatusOr> ViolatedConstraintsAsStrings( + const Model& model, const ModelSubset& violated_constraints, + const VariableMap& variable_values); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_LABS_SOLUTION_FEASIBILITY_CHECKER_H_ diff --git a/ortools/math_opt/model.proto b/ortools/math_opt/model.proto index e096bdede4..9851b14ab6 100644 --- a/ortools/math_opt/model.proto +++ b/ortools/math_opt/model.proto @@ -145,6 +145,25 @@ message QuadraticConstraintProto { string name = 5; } +// A single second-order cone constraint of the form: +// +// ||`arguments_to_norm`||_2 <= `upper_bound`, +// +// where `upper_bound` and each element of `arguments_to_norm` are linear +// expressions. +// +// If a variable involved in this constraint is deleted, it is treated as if it +// were set to zero. +message SecondOrderConeConstraintProto { + LinearExpressionProto upper_bound = 1; + repeated LinearExpressionProto arguments_to_norm = 2; + + // Parent messages may have uniqueness requirements on this field; e.g., see + // `ModelProto.second_order_cone_constraints` and + // `SecondOrderConeConstraintUpdatesProto.new_constraints`. + string name = 3; +} + // Data for representing a single SOS1 or SOS2 constraint. // // If a variable involved in this constraint is deleted, it is treated as if it @@ -167,11 +186,14 @@ message SosConstraintProto { } // Data for representing a single indicator constraint of the form: -// Variable(indicator_id) = (activate_on_zero ? 0 : 1) --> expression sense rhs +// Variable(indicator_id) = (activate_on_zero ? 0 : 1) ⇒ +// lower_bound <= expression <= upper_bound. // -// If a variable involved in this constraint is deleted, it is treated as if it -// were set to zero. In particular, deleting the indicator variable means that -// the indicator constraint is vacuous. +// If a variable involved in this constraint (either the indicator, or appearing +// in `expression`) is deleted, it is treated as if it were set to zero. In +// particular, deleting the indicator variable means that the indicator +// constraint is vacuous if `activate_on_zero` is false, and that it is +// equivalent to a linear constraint if `activate_on_zero` is true. message IndicatorConstraintProto { // An ID corresponding to a binary variable, or unset. If unset, the indicator // constraint is ignored. If set, we require that: @@ -214,6 +236,7 @@ message IndicatorConstraintProto { // - A number of constraints types, including: // * Linear constraints // * Quadratic constraints +// * Second-order cone constraints // * Logical constraints // > SOS1 and SOS2 constraints // > Indicator constraints @@ -259,6 +282,9 @@ message ModelProto { // Quadratic constraints in the model. map quadratic_constraints = 6; + // Second-order cone constraints in the model. + map second_order_cone_constraints = 11; + // SOS1 constraints in the model, which constrain that at most one // `expression` can be nonzero. The optional `weights` entries are an // implementation detail used by the solver to (hopefully) converge more diff --git a/ortools/math_opt/model_update.proto b/ortools/math_opt/model_update.proto index a01ff320cf..45c3368e70 100644 --- a/ortools/math_opt/model_update.proto +++ b/ortools/math_opt/model_update.proto @@ -151,6 +151,23 @@ message QuadraticConstraintUpdatesProto { map new_constraints = 2; } +// Updates to second-order cone constraints; only addition and deletion, no +// support for in-place constraint updates. +message SecondOrderConeConstraintUpdatesProto { + // Removes second-order cone constraints from the model. + // + // Each value must be in [0, max(int64)). Values must be in strictly + // increasing order. Applies only to existing second-order cone constraint ids + // that have not yet been deleted. + repeated int64 deleted_constraint_ids = 1; + + // Add new second-order cone constraints to the model. All keys must be in + // [0, max(int64)), and must be greater than any ids used in the initial model + // and previous updates. All nonempty names should be distinct from existing + // names and each other. + map new_constraints = 2; +} + // Data for updates to SOS1 and SOS2 constraints; only addition and deletion, no // support for in-place constraint updates. message SosConstraintUpdatesProto { @@ -246,6 +263,10 @@ message ModelUpdateProto { // Updates the quadratic constraints (addition and deletion only). QuadraticConstraintUpdatesProto quadratic_constraint_updates = 9; + // Updates the second-order cone constraints (addition and deletion only). + SecondOrderConeConstraintUpdatesProto second_order_cone_constraint_updates = + 14; + // Updates the general constraints (addition and deletion only). SosConstraintUpdatesProto sos1_constraint_updates = 10; SosConstraintUpdatesProto sos2_constraint_updates = 11; diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index 1341487789..66a60a3810 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -17,6 +17,7 @@ syntax = "proto3"; package operations_research.math_opt; import "google/protobuf/duration.proto"; +import "ortools/math_opt/solvers/glpk.proto"; import "ortools/math_opt/solvers/gurobi.proto"; option java_package = "com.google.ortools.mathopt"; @@ -26,35 +27,40 @@ import "ortools/glop/parameters.proto"; import "ortools/gscip/gscip.proto"; import "ortools/sat/sat_parameters.proto"; +// The solvers supported by MathOpt. enum SolverTypeProto { SOLVER_TYPE_UNSPECIFIED = 0; - // Solving Constraint Integer Programs (SCIP) solver. + // Solving Constraint Integer Programs (SCIP) solver (third party). // - // It supports both MIPs and LPs. No dual data for LPs is returned though. To - // solve LPs, SOLVER_TYPE_GLOP should be preferred. + // Supports LP, MIP, and nonconvex integer quadratic problems. No dual data + // for LPs is returned though. Prefer GLOP for LPs. SOLVER_TYPE_GSCIP = 1; - // Gurobi solver. + // Gurobi solver (third party). // - // It supports both MIPs and LPs. + // Supports LP, MIP, and nonconvex integer quadratic problems. Generally the + // fastest option, but has special licensing, see go/gurobi-google for + // details. SOLVER_TYPE_GUROBI = 2; - // Google's Glop linear solver. + // Google's Glop solver. // - // It only solves LPs. + // Supports LP with primal and dual simplex methods. SOLVER_TYPE_GLOP = 3; // Google's CP-SAT solver. // - // It supports solving IPs and can scale MIPs to solve them as IPs. + // Supports problems where all variables are integer and bounded (or implied + // to be after presolve). Experimental support to rescale and discretize + // problems with continuous variables. SOLVER_TYPE_CP_SAT = 4; reserved 5; - // GNU Linear Programming Kit (GLPK). + // GNU Linear Programming Kit (GLPK) (third party). // - // It supports both MIPs and LPs. + // Supports MIP and LP. // // Thread-safety: GLPK use thread-local storage for memory allocations. As a // consequence Solver instances must be destroyed on the same thread as they @@ -69,6 +75,21 @@ enum SolverTypeProto { SOLVER_TYPE_GLPK = 6; reserved 7; + + // The Embedded Conic Solver (ECOS) (third party). + // + // Supports LP and SOCP problems. Uses interior point methods (barrier). + SOLVER_TYPE_ECOS = 8; + + // The Splitting Conic Solver (SCS) (third party). + // + // Supports LP and SOCP problems. Uses a first-order method. + SOLVER_TYPE_SCS = 9; + + // The HiGHS Solver (third party). + // + // Supports LP and MIP problems (convex QPs are unimplemented). + SOLVER_TYPE_HIGHS = 10; } // Selects an algorithm for solving linear programs. @@ -88,6 +109,13 @@ enum LPAlgorithmProto { // also produce rays on unbounded/infeasible problems. A basis is not given // unless the underlying solver does "crossover" and finishes with simplex. LP_ALGORITHM_BARRIER = 3; + + // An algorithm based around a first-order method. These will typically + // produce both primal and dual solutions, and potentially also certificates + // of primal and/or dual infeasibility. First-order methods typically will + // provide solutions with lower accuracy, so users should take care to set + // solution quality parameters (e.g., tolerances) and to validate solutions. + LP_ALGORITHM_FIRST_ORDER = 4; } // Effort level applied to an optional task while solving (see @@ -304,8 +332,8 @@ message SolveParametersProto { glop.GlopParameters glop = 14; sat.SatParameters cp_sat = 15; reserved 16; - reserved 19; + GlpkParametersProto glpk = 26; reserved 11; // Deleted } diff --git a/ortools/math_opt/samples/BUILD.bazel b/ortools/math_opt/samples/BUILD.bazel index c46c5e7a8e..9cef4dc98c 100644 --- a/ortools/math_opt/samples/BUILD.bazel +++ b/ortools/math_opt/samples/BUILD.bazel @@ -15,7 +15,7 @@ package(default_visibility = ["//visibility:public"]) cc_binary( name = "basic_example", - srcs = ["basic_example_mo.cc"], + srcs = ["basic_example.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -28,7 +28,7 @@ cc_binary( cc_binary( name = "cocktail_hour", - srcs = ["cocktail_hour_mo.cc"], + srcs = ["cocktail_hour.cc"], deps = [ "//ortools/base", "//ortools/base:map_util", @@ -45,7 +45,7 @@ cc_binary( cc_binary( name = "linear_programming", - srcs = ["linear_programming_mo.cc"], + srcs = ["linear_programming.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -59,7 +59,7 @@ cc_binary( cc_binary( name = "integer_programming", - srcs = ["integer_programming_mo.cc"], + srcs = ["integer_programming.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -72,7 +72,7 @@ cc_binary( cc_binary( name = "cutting_stock", - srcs = ["cutting_stock_mo.cc"], + srcs = ["cutting_stock.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -86,7 +86,7 @@ cc_binary( cc_binary( name = "facility_lp_benders", - srcs = ["facility_lp_benders_mo.cc"], + srcs = ["facility_lp_benders.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -110,7 +110,7 @@ cc_binary( cc_binary( name = "lagrangian_relaxation", - srcs = ["lagrangian_relaxation_mo.cc"], + srcs = ["lagrangian_relaxation.cc"], deps = [ "//ortools/base", "//ortools/base:container_logging", @@ -130,7 +130,7 @@ cc_binary( cc_binary( name = "tsp", - srcs = ["tsp_mo.cc"], + srcs = ["tsp.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -143,3 +143,61 @@ cc_binary( "@com_google_absl//absl/strings", ], ) + +cc_binary( + name = "linear_regression", + srcs = ["linear_regression.cc"], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:gurobi_solver", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status", + ], +) + +cc_binary( + name = "advanced_linear_programming", + srcs = ["advanced_linear_programming.cc"], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:glop_solver", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + ], +) + +cc_binary( + name = "time_indexed_scheduling", + srcs = ["time_indexed_scheduling.cc"], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:cp_sat_solver", + "//ortools/math_opt/solvers:gscip_solver", + "//ortools/math_opt/solvers:gurobi_solver", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + ], +) + +cc_binary( + name = "graph_coloring", + srcs = ["graph_coloring.cc"], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:cp_sat_solver", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + ], +) diff --git a/ortools/math_opt/samples/advanced_linear_programming.cc b/ortools/math_opt/samples/advanced_linear_programming.cc new file mode 100644 index 0000000000..d28706bc9c --- /dev/null +++ b/ortools/math_opt/samples/advanced_linear_programming.cc @@ -0,0 +1,125 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Advanced linear programming example + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +constexpr double kInf = std::numeric_limits::infinity(); + +// Model and solve the problem: +// max 10 * x0 + 6 * x1 + 4 * x2 +// s.t. 10 * x0 + 4 * x1 + 5 * x2 <= 600 +// 2 * x0 + 2 * x1 + 6 * x2 <= 300 +// x0 + x1 + x2 <= 100 +// x0 in [0, infinity) +// x1 in [0, infinity) +// x2 in [0, infinity) +// +absl::Status Main() { + math_opt::Model model("Advanced linear programming example"); + + // Variables + std::vector x; + for (int j = 0; j < 3; j++) { + x.push_back(model.AddContinuousVariable(0.0, kInf, absl::StrCat("x", j))); + } + + // Constraints + std::vector constraints; + constraints.push_back( + model.AddLinearConstraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, "c1")); + constraints.push_back( + model.AddLinearConstraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, "c2")); + // sum(x[i]) <= 100 + constraints.push_back(model.AddLinearConstraint(Sum(x) <= 100, "c3")); + + // Objective + model.Maximize(10 * x[0] + 6 * x[1] + 4 * x[2]); + + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGlop)); + if (result.termination.reason != math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "model failed to solve to optimality" << result.termination; + } + + std::cout << "Problem solved in " << result.solve_time() << std::endl; + std::cout << "Objective value: " << result.objective_value() << std::endl; + + std::cout << "Variable values: [" + << absl::StrJoin(Values(result.variable_values(), x), ", ") << "]" + << std::endl; + + if (!result.has_dual_feasible_solution()) { + // MathOpt does not require solvers to return a dual solution on optimal, + // but most LP solvers always will, see go/mathopt-solver-contracts for + // details. + return util::InternalErrorBuilder() + << "no dual solution was returned on optimal"; + } + + std::cout << "Constraint duals: [" + << absl::StrJoin(Values(result.dual_values(), constraints), ", ") + << "]" << std::endl; + std::cout << "Reduced costs: [" + << absl::StrJoin(Values(result.reduced_costs(), x), ", ") << "]" + << std::endl; + + if (!result.has_basis()) { + // MathOpt does not require solvers to return a basis on optimal, but most + // Simplex LP solvers (like Glop) always will, see + // go/mathopt-solver-contracts for details. + return util::InternalErrorBuilder() << "no basis was returned on optimal"; + } + + std::cout << "Constraint basis status: [" + << absl::StrJoin(Values(result.constraint_status(), constraints), + ", ", absl::StreamFormatter()) + << "]" << std::endl; + + std::cout << "Variable basis status: [" + << absl::StrJoin(Values(result.variable_status(), x), ", ", + absl::StreamFormatter()) + << "]" << std::endl; + + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/basic_example.cc b/ortools/math_opt/samples/basic_example.cc new file mode 100644 index 0000000000..c88de3dd06 --- /dev/null +++ b/ortools/math_opt/samples/basic_example.cc @@ -0,0 +1,72 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Testing correctness of the code snippets in the comments of math_opt.h. + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +// Model the problem: +// max 2.0 * x + y +// s.t. x + y <= 1.5 +// x in {0.0, 1.0} +// y in [0.0, 2.5] +// +absl::Status Main() { + math_opt::Model model("my_model"); + const math_opt::Variable x = model.AddBinaryVariable("x"); + const math_opt::Variable y = model.AddContinuousVariable(0.0, 2.5, "y"); + // We can directly use linear combinations of variables ... + model.AddLinearConstraint(x + y <= 1.5, "c"); + // ... or build them incrementally. + math_opt::LinearExpression objective_expression; + objective_expression += 2 * x; + objective_expression += y; + model.Maximize(objective_expression); + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGscip)); + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + std::cout << "Objective value: " << result.objective_value() << std::endl + << "Value for variable x: " << result.variable_values().at(x) + << std::endl; + return absl::OkStatus(); + default: + return util::InternalErrorBuilder() + << "model failed to solve: " << result.termination; + } +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/cocktail_hour.cc b/ortools/math_opt/samples/cocktail_hour.cc new file mode 100644 index 0000000000..de07eee34d --- /dev/null +++ b/ortools/math_opt/samples/cocktail_hour.cc @@ -0,0 +1,380 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Pick ingredients to buy to make the maximum number of cocktails. +// +// Given a list of cocktails, each of which is made from a list of ingredients, +// and a budget of how many ingredients you can buy, solve a MIP to pick a +// subset of the ingredients so that you can make the largest number of +// cocktails. +// +// This program can be run in three modes: +// text: Outputs the optimal set of ingredients and cocktails that can be +// produced as plain text to standard out. +// latex: Outputs a menu of the cocktails that can be made as LaTeX code to +// standard out. +// analysis: Computes the number of cocktails that can be made as a function +// of the number of ingredients for all values. +// +// In latex mode, the output can be piped directly to pdflatex, e.g. +// blaze run -c opt \ +// ortools/math_opt/examples/cpp/cocktail_hour \ +// -- --num_ingredients 10 --mode latex | pdflatex -output-directory /tmp +// will create a PDF in /tmp. +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/string_view.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/map_util.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/status_macros.h" + +ABSL_FLAG(std::string, mode, "text", + "One of \"text\", \"latex\", or \"analysis\"."); +ABSL_FLAG(int, num_ingredients, 10, + "How many ingredients to buy (ignored in analysis mode)."); +ABSL_FLAG(std::vector, existing_ingredients, {}, + "Ingredients you already have (ignored in analysis mode)."); +ABSL_FLAG(std::vector, unavailable_ingredients, {}, + "Ingredients you cannot get (ignored in analysis mode)."); +ABSL_FLAG(std::vector, required_cocktails, {}, + "Cocktails you must be able to make (ignored in analysis mode)."); +ABSL_FLAG(std::vector, blocked_cocktails, {}, + "Cocktails to exclude from the menu (ignored in analysis mode)."); + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +constexpr absl::string_view kIngredients[] = {"Amaro Nonino", + "All Spice Dram", + "Aperol", + "Bitters", + "Bourbon", + "Brandy", + "Campari", + "Cinnamon", + "Chambord", + "Cherry", + "Cloves", + "Cointreau", + "Coke", + "Cranberry", + "Creme de Cacao", + "Creme de Violette", + "Cucumber", + "Egg", + "Gin", + "Green Chartreuse", + "Heavy Cream", + "Lemon", + "Lillet Blanc", + "Lime", + "Luxardo", + "Mint", + "Orange", + "Orange Flower Water Extract", + "Orgeat", + "Pickle", + "Pineapple Juice", + "Pisco", + "Prosecco", + "Raspberry Vodka", + "Ruby Port", + "Rum", + "Seltzer", + "Simple Syrup", + "Sugar", + "Sweet Vermouth", + "Tequila", + "Tonic Water", + "Vodka"}; + +constexpr std::size_t kIngredientsSize = + sizeof(kIngredients) / sizeof(kIngredients[0]); + +struct Cocktail { + std::string name; + std::vector ingredients; +}; + +std::vector AllCocktails() { + return { + // Aperitifs + {.name = "Prosecco glass", .ingredients = {"Prosecco"}}, + {.name = "Aperol Spritz", .ingredients = {"Prosecco", "Aperol"}}, + {.name = "Chambord Spritz", .ingredients = {"Prosecco", "Chambord"}}, + {.name = "Improved French 75", + .ingredients = {"Prosecco", "Vodka", "Lemon", "Simple Syrup"}}, + // Quick and Simple + {.name = "Gin and Tonic", .ingredients = {"Gin", "Tonic Water", "Lime"}}, + {.name = "Rum and Coke", .ingredients = {"Rum", "Coke"}}, + {.name = "Improved Manhattan", + .ingredients = {"Bourbon", "Sweet Vermouth", "Bitters"}}, + // Vodka + + // Serve with a sugared rim + {.name = "Lemon Drop", + .ingredients = {"Vodka", "Cointreau", "Lemon", "Simple Syrup"}}, + // Shake, then float 2oz Prosecco after pouring + {.name = "Big Crush", + .ingredients = {"Raspberry Vodka", "Cointreau", "Lemon", "Chambord", + "Prosecco"}}, + {.name = "Cosmopolitan", + .ingredients = {"Vodka", "Cranberry", "Cointreau", "Lime"}}, + // A shot, chase with 1/3 of pickle spear + {.name = "Vodka/Pickle", .ingredients = {"Vodka", "Pickle"}}, + + // Gin + {.name = "Last Word", + .ingredients = {"Gin", "Green Chartreuse", "Luxardo", "Lime"}}, + {.name = "Corpse Reviver #2 (Lite)", + .ingredients = {"Gin", "Cointreau", "Lillet Blanc", "Lemon"}}, + {.name = "Negroni", .ingredients = {"Gin", "Sweet Vermouth", "Campari"}}, + // "Float" Creme de Violette (it will sink) + {.name = "Aviation", + .ingredients = {"Gin", "Luxardo", "Lemon", "Creme de Violette"}}, + + // Bourbon + {.name = "Paper Plane", + .ingredients = {"Bourbon", "Aperol", "Amaro Nonino", "Lemon"}}, + {.name = "Derby", + .ingredients = {"Bourbon", "Sweet Vermouth", "Lime", "Cointreau"}}, + // Muddle sugar, water, bitters, and orange peel. Garnish with a Luxardo + // cherry (do not cheap out), spill cherry syrup generously in drink + {.name = "Old Fashioned", + .ingredients = {"Bourbon", "Sugar", "Bitters", "Orange", "Cherry"}}, + {.name = "Boulevardier", + .ingredients = {"Bourbon", "Sweet Vermouth", "Campari"}}, + + // Tequila + {.name = "Margarita", .ingredients = {"Tequila", "Cointreau", "Lime"}}, + // Shake with chopped cucumber and strain. Garnish with cucumber. + {.name = "Midnight Cruiser", + .ingredients = {"Tequila", "Aperol", "Lime", "Pineapple Juice", + "Cucumber", "Simple Syrup"}}, + + {.name = "Tequila shot", .ingredients = {"Tequila"}}, + // Rum + + // Shake with light rum, float a dark rum on top. + {.name = "Pineapple Mai Tai", + .ingredients = {"Rum", "Lime", "Orgeat", "Cointreau", + "Pineapple Juice"}}, + {.name = "Daiquiri", .ingredients = {"Rum", "Lime", "Simple Syrup"}}, + {.name = "Mojito", + .ingredients = {"Rum", "Lime", "Simple Syrup", "Mint", "Seltzer"}}, + // Add bitters generously. Invert half lime to form a cup, fill with + // Green Chartreuse and cloves. Float lime cup on drink and ignite. + {.name = "Kennedy", + .ingredients = {"Rum", "All Spice Dram", "Bitters", "Lime", + "Simple Syrup", "Cloves", "Green Chartreuse"}}, + + // Egg + + {.name = "Pisco Sour", + .ingredients = {"Pisco", "Lime", "Simple Syrup", "Egg", "Bitters"}}, + {.name = "Viana", + .ingredients = {"Ruby Port", "Brandy", "Creme de Cacao", "Sugar", "Egg", + "Cinnamon"}}, + // Add cream last before shaking (and seltzer after shaking). Shake for 10 + // minutes, no less. + {.name = "Ramos gin fizz", + .ingredients = {"Gin", "Seltzer", "Heavy Cream", + "Orange Flower Water Extract", "Egg", "Lemon", "Lime", + "Simple Syrup"}}}; +} + +struct Menu { + std::vector ingredients; + std::vector cocktails; +}; + +absl::StatusOr SolveForMenu( + const int max_new_ingredients, const bool enable_solver_output, + const absl::flat_hash_set& existing_ingredients, + const absl::flat_hash_set& unavailable_ingredients, + const absl::flat_hash_set& required_cocktails, + const absl::flat_hash_set& blocked_cocktails) { + const std::vector all_cocktails = AllCocktails(); + math_opt::Model model("Cocktail hour"); + absl::flat_hash_map ingredient_vars; + for (const absl::string_view ingredient : kIngredients) { + const double lb = existing_ingredients.contains(ingredient) ? 1.0 : 0.0; + const double ub = unavailable_ingredients.contains(ingredient) ? 0.0 : 1.0; + const math_opt::Variable v = model.AddIntegerVariable(lb, ub, ingredient); + gtl::InsertOrDie(&ingredient_vars, std::string(ingredient), v); + } + math_opt::LinearExpression ingredients_used; + for (const auto& [name, ingredient_var] : ingredient_vars) { + ingredients_used += ingredient_var; + } + model.AddLinearConstraint(ingredients_used <= + max_new_ingredients + existing_ingredients.size()); + + absl::flat_hash_map cocktail_vars; + for (const Cocktail& cocktail : all_cocktails) { + const double lb = required_cocktails.contains(cocktail.name) ? 1.0 : 0.0; + const double ub = blocked_cocktails.contains(cocktail.name) ? 0.0 : 1.0; + const math_opt::Variable v = + model.AddIntegerVariable(lb, ub, cocktail.name); + for (const std::string& ingredient : cocktail.ingredients) { + model.AddLinearConstraint(v <= + gtl::FindOrDie(ingredient_vars, ingredient)); + } + gtl::InsertOrDie(&cocktail_vars, cocktail.name, v); + } + math_opt::LinearExpression cocktails_made; + for (const auto& [name, cocktail_var] : cocktail_vars) { + cocktails_made += cocktail_var; + } + model.Maximize(cocktails_made); + const math_opt::SolveArguments args = { + .parameters = {.enable_output = enable_solver_output}}; + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, math_opt::SolverType::kGscip, args)); + + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + break; + default: + return util::InternalErrorBuilder() + << "failed to find a solution: " << result.termination; + } + Menu menu; + for (const absl::string_view ingredient : kIngredients) { + if (result.variable_values().at(ingredient_vars.at(ingredient)) > 0.5) { + menu.ingredients.push_back(std::string(ingredient)); + } + } + for (const Cocktail& cocktail : all_cocktails) { + if (result.variable_values().at(cocktail_vars.at(cocktail.name)) > 0.5) { + menu.cocktails.push_back(cocktail); + } + } + return menu; +} + +absl::flat_hash_set SetFromVec( + const std::vector& vec) { + return {vec.begin(), vec.end()}; +} + +absl::Status AnalysisMode() { + std::cout << "Considering " << AllCocktails().size() << " cocktails and " + << kIngredientsSize << " ingredients." << std::endl; + std::cout << "Solving for number of cocktails that can be made as a function " + "of number of ingredients" + << std::endl; + + std::cout << "ingredients | cocktails" << std::endl; + for (int i = 1; i <= kIngredientsSize; ++i) { + const absl::StatusOr menu = SolveForMenu( + i, false, /*existing_ingredients=*/{}, /*unavailable_ingredients=*/{}, + /*required_cocktails=*/{}, /*blocked_cocktails=*/{}); + RETURN_IF_ERROR(menu.status()) + << "Failure when solving for " << i << " ingredients"; + std::cout << i << " | " << menu->cocktails.size() << std::endl; + } + return absl::OkStatus(); +} + +std::string ExportToLaTeX(const std::vector& cocktails, + absl::string_view title = "Cocktail Hour") { + std::vector lines; + lines.push_back("\\documentclass{article}"); + lines.push_back("\\usepackage{fullpage}"); + lines.push_back("\\linespread{2}"); + lines.push_back("\\begin{document}"); + lines.push_back("\\begin{center}"); + lines.push_back(absl::StrCat("\\begin{Huge}", title, "\\end{Huge}")); + lines.push_back(""); + for (const Cocktail& cocktail : cocktails) { + lines.push_back(absl::StrCat(cocktail.name, "---{\\em ", + absl::StrJoin(cocktail.ingredients, ", "), + "}")); + lines.push_back(""); + } + lines.push_back("\\end{center}"); + lines.push_back("\\end{document}"); + + return absl::StrReplaceAll(absl::StrJoin(lines, "\n"), {{"#", "\\#"}}); +} + +absl::Status Main() { + const std::string mode = absl::GetFlag(FLAGS_mode); + CHECK(absl::flat_hash_set({"text", "latex", "analysis"}) + .contains(mode)) + << "Unexpected mode: " << mode; + + // We are in analysis mode. + if (mode == "analysis") { + return AnalysisMode(); + } + + OR_ASSIGN_OR_RETURN3( + Menu menu, + SolveForMenu(absl::GetFlag(FLAGS_num_ingredients), mode == "text", + SetFromVec(absl::GetFlag(FLAGS_existing_ingredients)), + SetFromVec(absl::GetFlag(FLAGS_unavailable_ingredients)), + SetFromVec(absl::GetFlag(FLAGS_required_cocktails)), + SetFromVec(absl::GetFlag(FLAGS_blocked_cocktails))), + _ << "error when solving for optimal set of ingredients"); + + // We are in latex mode. + if (mode == "latex") { + std::cout << ExportToLaTeX(menu.cocktails) << std::endl; + return absl::OkStatus(); + } + + // We are in text mode + std::cout << "Considered " << AllCocktails().size() << " cocktails and " + << kIngredientsSize << " ingredients." << std::endl; + std::cout << "Solution has " << menu.ingredients.size() + << " ingredients to make " << menu.cocktails.size() << " cocktails." + << std::endl + << std::endl; + + std::cout << "Ingredients:" << std::endl; + for (const std::string& ingredient : menu.ingredients) { + std::cout << " " << ingredient << std::endl; + } + std::cout << "Cocktails:" << std::endl; + for (const Cocktail& cocktail : menu.cocktails) { + std::cout << " " << cocktail.name << std::endl; + } + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/cutting_stock.cc b/ortools/math_opt/samples/cutting_stock.cc new file mode 100644 index 0000000000..f947e8104b --- /dev/null +++ b/ortools/math_opt/samples/cutting_stock.cc @@ -0,0 +1,277 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The Cutting Stock problem is as follows. You begin with unlimited boards, all +// of the same length. You are also given a list of smaller pieces to cut out, +// each with a length and a demanded quantity. You want to cut out all these +// pieces using as few of your starting boards as possible. +// +// E.g. you begin with boards that are 20 feet long, and you must cut out 3 +// pieces that are 6 feet long and 5 pieces that are 8 feet long. An optimal +// solution is: +// [(6,), (8, 8) (8, 8), (6, 6, 8)] +// (We cut a 6 foot piece from the first board, two 8 foot pieces from +// the second board, and so on.) +// +// This example approximately solves the problem with a column generation +// heuristic. The leader problem is a set cover problem, and the worker is a +// knapsack problem. We alternate between solving the LP relaxation of the +// leader incrementally, and solving the worker to generate new a configuration +// (a column) for the leader. When the worker can no longer find a column +// improving the LP cost, we convert the leader problem to a MIP and solve +// again. We now give precise statements of the leader and worker. +// +// Problem data: +// * l_i: the length of each piece we need to cut out. +// * d_i: how many copies each piece we need. +// * L: the length of our initial boards. +// * q_ci: for configuration c, the quantity of piece i produced. +// +// Leader problem variables: +// * x_c: how many copies of configuration c to produce. +// +// Leader problem formulation: +// min sum_c x_c +// s.t. sum_c q_ci * x_c = d_i for all i +// x_c >= 0, integer for all c. +// +// The worker problem is to generate new configurations for the leader problem +// based on the dual variables of the demand constraints in the LP relaxation. +// Worker problem data: +// * p_i: The "price" of piece i (dual value from leader's demand constraint) +// +// Worker decision variables: +// * y_i: How many copies of piece i should be in the configuration. +// +// Worker formulation +// max sum_i p_i * y_i +// s.t. sum_i l_i * y_i <= L +// y_i >= 0, integer for all i +// +// An optimal solution y* defines a new configuration c with q_ci = y_i* for all +// i. If the solution has objective value <= 1, no further improvement on the LP +// is possible. For additional background and proofs see: +// https://people.orie.cornell.edu/shmoys/or630/notes-06/lec16.pdf +// or any other reference on the "Cutting Stock Problem". +// +// Note: this problem is equivalent to symmetric bin packing: +// https://en.wikipedia.org/wiki/Bin_packing_problem#Formal_statement +// but typically in bin packing it is not assumed that you should exploit having +// multiple items of the same size. +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +namespace math_opt = operations_research::math_opt; +constexpr double kInf = std::numeric_limits::infinity(); + +// piece_sizes and piece_demands must have equal length. +// every piece must have 0 < size <= board_length. +// every piece must have demand > 0. +struct CuttingStockInstance { + std::vector piece_sizes; + std::vector piece_demands; + int board_length; +}; + +// pieces and quantity must have equal size. +// Defined for a related CuttingStockInstance, the total length all pieces +// weighted by their quantity must not exceed board_length. +struct Configuration { + std::vector pieces; + std::vector quantity; +}; + +// configurations and quantity must have equal size. +// objective_value is the sum of the vales in quantity (how many total boards +// are used). +// To be feasible, the demand for each piece type must be met by the produced +// configurations. +struct CuttingStockSolution { + std::vector configurations; + std::vector quantity; + int objective_value = 0; +}; + +// Solves the worker problem. +// +// Solves the problem on finding the configuration (with its objective value) to +// add the to model that will give the greatest improvement in the LP +// relaxation. This is equivalent to a knapsack problem. +absl::StatusOr> BestConfiguration( + const std::vector& piece_prices, + const std::vector& piece_sizes, const int board_size) { + int num_pieces = piece_prices.size(); + CHECK_EQ(piece_sizes.size(), num_pieces); + math_opt::Model model("knapsack"); + std::vector pieces; + for (int i = 0; i < num_pieces; ++i) { + pieces.push_back( + model.AddIntegerVariable(0, kInf, absl::StrCat("item_", i))); + } + model.Maximize(math_opt::InnerProduct(pieces, piece_prices)); + model.AddLinearConstraint(math_opt::InnerProduct(pieces, piece_sizes) <= + board_size); + ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result, + math_opt::Solve(model, math_opt::SolverType::kCpSat)); + if (solve_result.termination.reason != + math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "Failed to solve knapsack pricing problem to optimality: " + << solve_result.termination; + } + Configuration config; + for (int i = 0; i < num_pieces; ++i) { + const int use = static_cast( + std::round(solve_result.variable_values().at(pieces[i]))); + if (use > 0) { + config.pieces.push_back(i); + config.quantity.push_back(use); + } + } + return std::make_pair(config, solve_result.objective_value()); +} + +// Solves the full cutting stock problem by decomposition. +absl::StatusOr SolveCuttingStock( + const CuttingStockInstance& instance) { + math_opt::Model model("cutting_stock"); + model.set_minimize(); + const int n = instance.piece_sizes.size(); + std::vector demand_met; + for (int i = 0; i < n; ++i) { + const int d = instance.piece_demands[i]; + demand_met.push_back(model.AddLinearConstraint(d, d)); + } + std::vector> configs; + auto add_config = [&](const Configuration& config) { + const math_opt::Variable v = model.AddContinuousVariable(0.0, kInf); + model.set_objective_coefficient(v, 1); + for (int i = 0; i < config.pieces.size(); ++i) { + const int item = config.pieces[i]; + const int use = config.quantity[i]; + if (use >= 1) { + model.set_coefficient(demand_met[item], v, use); + } + } + configs.push_back({config, v}); + }; + + // To ensure the leader problem is always feasible, begin a configuration for + // every item that has a single copy of the item. + for (int i = 0; i < n; ++i) { + add_config(Configuration{.pieces = {i}, .quantity = {1}}); + } + + ASSIGN_OR_RETURN(auto solver, math_opt::IncrementalSolver::New( + &model, math_opt::SolverType::kGlop)); + int pricing_round = 0; + while (true) { + ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve()); + if (solve_result.termination.reason != + math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "Failed to solve leader LP problem to optimality at " + "iteration " + << pricing_round << " termination: " << solve_result.termination; + } + if (!solve_result.has_dual_feasible_solution()) { + // MathOpt does not require solvers to return a dual solution on optimal, + // but most LP solvers always will, see go/mathopt-solver-contracts for + // details. + return util::InternalErrorBuilder() + << "no dual solution was returned with optimal solution at " + "iteration " + << pricing_round; + } + std::vector prices; + for (const math_opt::LinearConstraint d : demand_met) { + prices.push_back(solve_result.dual_values().at(d)); + } + ASSIGN_OR_RETURN( + (const auto [config, value]), + BestConfiguration(prices, instance.piece_sizes, instance.board_length)); + if (value <= 1 + 1e-3) { + // The LP relaxation is solved, we can stop adding columns. + break; + } + add_config(config); + LOG(INFO) << "round: " << pricing_round + << " lp objective: " << solve_result.objective_value(); + pricing_round++; + } + LOG(INFO) << "Done adding columns, switching to MIP"; + for (const auto& [config, var] : configs) { + model.set_integer(var); + } + ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result, + math_opt::Solve(model, math_opt::SolverType::kCpSat)); + switch (solve_result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + break; + default: + return util::InternalErrorBuilder() + << "Failed to solve final cutting stock MIP, termination: " + << solve_result.termination; + } + CuttingStockSolution solution; + for (const auto& [config, var] : configs) { + int use = + static_cast(std::round(solve_result.variable_values().at(var))); + if (use > 0) { + solution.configurations.push_back(config); + solution.quantity.push_back(use); + solution.objective_value += use; + } + } + return solution; +} + +absl::Status RealMain() { + // Data from https://en.wikipedia.org/wiki/Cutting_stock_problem + CuttingStockInstance instance; + instance.board_length = 5600; + instance.piece_sizes = {1380, 1520, 1560, 1710, 1820, 1880, 1930, + 2000, 2050, 2100, 2140, 2150, 2200}; + instance.piece_demands = {22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20}; + ASSIGN_OR_RETURN(CuttingStockSolution solution, SolveCuttingStock(instance)); + std::cout << "Best known solution uses 73 rolls." << std::endl; + std::cout << "Total rolls used in actual solution found: " + << solution.objective_value << std::endl; + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = RealMain(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/facility_lp_benders.cc b/ortools/math_opt/samples/facility_lp_benders.cc new file mode 100644 index 0000000000..36f609a0cf --- /dev/null +++ b/ortools/math_opt/samples/facility_lp_benders.cc @@ -0,0 +1,683 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An advanced benders decomposition example +// +// We consider a network design problem where each location has a demand that +// must be met by its neighboring facilities, and each facility can control +// its total capacity. In this version we also require that locations cannot +// use more that a specified fraction of a facilities capacity. +// +// Problem data: +// * F: set of facilities. +// * L: set of locations. +// * E: subset of {(f,l) : f in F, l in L} that describes the network between +// facilities and locations. +// * d: demand at location (all demands are equal for simplicity). +// * c: cost per unit of capacity at a facility (all facilities are have the +// same cost for simplicity). +// * h: cost per unit transported through an edge. +// * a: fraction of a facility's capacity that can be used by each location. +// +// Decision variables: +// * z_f: capacity at facility f in F. +// * x_(f,l): flow from facility f to location l for all (f,l) in E. +// +// Formulation: +// +// min c * sum(z_f : f in F) + sum(h_e * x_e : e in E) +// s.t. +// x_(f,l) <= a * z_f for all (f,l) in E +// sum(x_(f,l) : l such that (f,l) in E) <= z_f for all f in F +// sum(x_(f,l) : f such that (f,l) in E) >= d for all l in L +// x_e >= 0 for all e in E +// z_f >= 0 for all f in F +// +// Below we solve this problem directly and using a benders decompostion +// approach. + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/flags/flag.h" +#include "absl/random/random.h" +#include "absl/random/seed_sequences.h" +#include "absl/random/uniform_int_distribution.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/status_macros.h" + +ABSL_FLAG(int, num_facilities, 3000, "Number of facilities."); +ABSL_FLAG(int, num_locations, 50, "Number of locations."); +ABSL_FLAG(double, edge_probability, 0.99, "Edge probability."); +ABSL_FLAG(double, benders_precission, 1e-9, "Benders target precission."); +ABSL_FLAG(double, location_demand, 1, "Client demands."); +ABSL_FLAG(double, facility_cost, 100, "Facility capacity cost."); +ABSL_FLAG( + double, location_fraction, 0.001, + "Fraction of a facility's capacity that can be used by each location."); +ABSL_FLAG(operations_research::math_opt::SolverType, solver_type, + operations_research::math_opt::SolverType::kGlop, + "the LP solver to use, possible values: glop, gurobi, glpk, pdlp"); + +namespace { + +namespace math_opt = operations_research::math_opt; +constexpr double kInf = std::numeric_limits::infinity(); +constexpr double kZeroTol = 1.0e-3; + +//////////////////////////////////////////////////////////////////////////////// +// Facility location instance representation and generation +//////////////////////////////////////////////////////////////////////////////// + +// First element is a facility and second is a location. +using Edge = std::pair; + +// A simple randomly-generated facility-location network. +class Network { + public: + Network(int num_facilities, int num_locations, double edge_probability); + + int num_facilities() const { return num_facilities_; } + int num_locations() const { return num_locations_; } + + const std::vector& edges() const { return edges_; } + + const std::vector& edges_incident_to_facility( + const int facility) const { + return facility_edge_incidence_[facility]; + } + + const std::vector& edges_incident_to_location( + const int location) const { + return location_edge_incidence_[location]; + } + + double edge_cost(const Edge& edge) const { return edge_costs_.at(edge); } + + private: + int num_facilities_; + int num_locations_; + // No order is assumed for the following lists of edges. + std::vector edges_; + absl::flat_hash_map edge_costs_; + std::vector> facility_edge_incidence_; + std::vector> location_edge_incidence_; +}; + +Network::Network(const int num_facilities, const int num_locations, + const double edge_probability) { + absl::SeedSeq seq({1, 2, 3}); + absl::BitGen bitgen(seq); + + num_facilities_ = num_facilities; + num_locations_ = num_locations; + facility_edge_incidence_ = std::vector>(num_facilities); + location_edge_incidence_ = + std::vector>(num_locations, std::vector()); + for (int facility = 0; facility < num_facilities_; ++facility) { + for (int location = 0; location < num_locations_; ++location) { + if (absl::Bernoulli(bitgen, edge_probability)) { + const Edge edge({facility, location}); + facility_edge_incidence_[facility].push_back(edge); + location_edge_incidence_[location].push_back(edge); + edges_.push_back(edge); + edge_costs_.insert({edge, absl::Uniform(bitgen, 0, 1.0)}); + } + } + } + // Ensure every facility is connected to at least one location and every + // location is connected to at least one facility. + for (int facility = 0; facility < num_facilities; ++facility) { + auto& locations = facility_edge_incidence_[facility]; + if (locations.empty()) { + const int location = + absl::uniform_int_distribution(0, num_locations - 1)(bitgen); + const std::pair edge({facility, location}); + locations.push_back(edge); + location_edge_incidence_[location].push_back(edge); + edges_.push_back(edge); + edge_costs_.insert({edge, absl::Uniform(bitgen, 0, 1.0)}); + } + } + for (int location = 0; location < num_locations; ++location) { + auto& facilities = location_edge_incidence_[location]; + if (facilities.empty()) { + const int facility = + absl::uniform_int_distribution(0, num_facilities - 1)(bitgen); + const std::pair edge({facility, location}); + facilities.push_back(edge); + facility_edge_incidence_[facility].push_back(edge); + edges_.push_back(edge); + edge_costs_.insert({edge, absl::Uniform(bitgen, 0, 1.0)}); + } + } +} + +struct FacilityLocationInstance { + Network network; + double location_demand; + double facility_cost; + double location_fraction; +}; + +//////////////////////////////////////////////////////////////////////////////// +// Direct solve +//////////////////////////////////////////////////////////////////////////////// + +// See file level comment for problem description and formulation. +absl::Status FullProblem(const FacilityLocationInstance& instance, + const math_opt::SolverType solver_type) { + const int num_facilities = instance.network.num_facilities(); + const int num_locations = instance.network.num_locations(); + + math_opt::Model model("Full network design problem"); + + // Capacity variables + std::vector z; + for (int j = 0; j < num_facilities; j++) { + z.push_back(model.AddContinuousVariable(0.0, kInf)); + } + + // Flow variables + absl::flat_hash_map x; + for (const auto& edge : instance.network.edges()) { + const math_opt::Variable x_edge = model.AddContinuousVariable(0.0, kInf); + x.insert({edge, x_edge}); + } + + // Objective Function + math_opt::LinearExpression objective_for_edges; + for (const auto& [edge, x_edge] : x) { + objective_for_edges += instance.network.edge_cost(edge) * x_edge; + } + model.Minimize(objective_for_edges + + instance.facility_cost * math_opt::Sum(z)); + + // Demand constraints + for (int location = 0; location < num_locations; ++location) { + math_opt::LinearExpression incomming_supply; + for (const auto& edge : + instance.network.edges_incident_to_location(location)) { + incomming_supply += x.at(edge); + } + model.AddLinearConstraint(incomming_supply >= instance.location_demand); + } + + // Supply constraints + for (int facility = 0; facility < num_facilities; ++facility) { + math_opt::LinearExpression outgoing_supply; + for (const auto& edge : + instance.network.edges_incident_to_facility(facility)) { + outgoing_supply += x.at(edge); + } + model.AddLinearConstraint(outgoing_supply <= z[facility]); + } + + // Arc constraints + for (int facility = 0; facility < num_facilities; ++facility) { + for (const auto& edge : + instance.network.edges_incident_to_facility(facility)) { + model.AddLinearConstraint(x.at(edge) <= + instance.location_fraction * z[facility]); + } + } + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, solver_type)); + if (result.termination.reason != math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "failed to find an optimal solution: " << result.termination; + } + + std::cout << "Full problem optimal objective: " + << absl::StrFormat("%.9f", result.objective_value()) << std::endl; + return absl::OkStatus(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Benders solver +//////////////////////////////////////////////////////////////////////////////// + +// Setup first stage model: +// +// min c * sum(z_f : f in F) + w +// s.t. +// z_f >= 0 for all f in F +// sum(fcut_f^i z_f) + fcut_const^i <= 0 for i = 1,... +// sum(ocut_f^j z_f) + ocut_const^j <= w for j = 1,... +struct FirstStageProblem { + math_opt::Model model; + std::vector z; + math_opt::Variable w; + + FirstStageProblem(const Network& network, const double facility_cost); +}; + +FirstStageProblem::FirstStageProblem(const Network& network, + const double facility_cost) + : model("First stage problem"), w(model.AddContinuousVariable(0.0, kInf)) { + const int num_facilities = network.num_facilities(); + + // Capacity variables + for (int j = 0; j < num_facilities; j++) { + z.push_back(model.AddContinuousVariable(0.0, kInf)); + } + + // First stage objective + model.Minimize(w + facility_cost * Sum(z)); +} + +// Represents a cut if the form: +// +// z_coefficients^T z + constant <= w_coefficient * w +// +// This will be a feasibility cut if w_coefficient = 0.0 and an optimality +// cut if w_coefficient = 1. +struct Cut { + std::vector z_coefficients; + double constant; + double w_coefficient; +}; + +// Solves the second stage model: +// +// min sum(h_e * x_e : e in E) +// s.t. +// x_(f,l) <= a * zz_f for all (f,l) in E +// sum(x_(f,l) : l such that (f,l) in E) <= zz_f for all f in F +// sum(x_(f,l) : f such that (f,l) in E) >= d for all l in L +// x_e >= 0 for all e in E +// +// where zz_f are fixed values for z_f from the first stage model, and generates +// an infeasibility or optimality cut as needed. +class SecondStageSolver { + public: + static absl::StatusOr> New( + FacilityLocationInstance instance, math_opt::SolverType solver_type); + + absl::StatusOr> Solve( + const std::vector& z_values, double w_value, + double fist_stage_objective); + + private: + SecondStageSolver(FacilityLocationInstance instance, + math_opt::SolveParameters second_stage_params); + + absl::StatusOr OptimalityCut( + const math_opt::SolveResult& second_stage_result); + absl::StatusOr FeasibilityCut( + const math_opt::SolveResult& second_stage_result); + + math_opt::Model second_stage_model_; + const Network network_; + const double location_fraction_; + math_opt::SolveParameters second_stage_params_; + + absl::flat_hash_map x_; + std::vector supply_constraints_; + std::vector demand_constraints_; + std::unique_ptr solver_; +}; + +absl::StatusOr EnsureDualRaySolveParameters( + const math_opt::SolverType solver_type) { + math_opt::SolveParameters parameters; + switch (solver_type) { + case math_opt::SolverType::kGurobi: + parameters.gurobi.param_values["InfUnbdInfo"] = "1"; + break; + case math_opt::SolverType::kGlop: + parameters.presolve = math_opt::Emphasis::kOff; + parameters.scaling = math_opt::Emphasis::kOff; + parameters.lp_algorithm = math_opt::LPAlgorithm::kDualSimplex; + break; + case math_opt::SolverType::kGlpk: + parameters.presolve = math_opt::Emphasis::kOff; + parameters.lp_algorithm = math_opt::LPAlgorithm::kDualSimplex; + parameters.glpk.compute_unbound_rays_if_possible = true; + break; + default: + return util::InternalErrorBuilder() + << "unsupported solver: " << solver_type; + } + return parameters; +} + +absl::StatusOr> SecondStageSolver::New( + FacilityLocationInstance instance, const math_opt::SolverType solver_type) { + // Set solver arguments to ensure a dual ray is returned. + ASSIGN_OR_RETURN(math_opt::SolveParameters parameters, + EnsureDualRaySolveParameters(solver_type)); + + std::unique_ptr second_stage_solver = + absl::WrapUnique( + new SecondStageSolver(std::move(instance), parameters)); + ASSIGN_OR_RETURN(std::unique_ptr solver, + math_opt::IncrementalSolver::New( + &second_stage_solver->second_stage_model_, solver_type)); + second_stage_solver->solver_ = std::move(solver); + return std::move(second_stage_solver); +} + +SecondStageSolver::SecondStageSolver( + FacilityLocationInstance instance, + math_opt::SolveParameters second_stage_params) + : second_stage_model_("Second stage model"), + network_(std::move(instance.network)), + location_fraction_(instance.location_fraction), + second_stage_params_(second_stage_params) { + const int num_facilities = network_.num_facilities(); + const int num_locations = network_.num_locations(); + + // Flow variables + for (const auto& edge : network_.edges()) { + const math_opt::Variable x_edge = + second_stage_model_.AddContinuousVariable(0.0, kInf); + x_.insert({edge, x_edge}); + } + + // Objective Function + math_opt::LinearExpression objective_for_edges; + for (const auto& [edge, x_edge] : x_) { + objective_for_edges += network_.edge_cost(edge) * x_edge; + } + second_stage_model_.Minimize(objective_for_edges); + + // Demand constraints + for (int location = 0; location < num_locations; ++location) { + math_opt::LinearExpression incomming_supply; + for (const auto& edge : network_.edges_incident_to_location(location)) { + incomming_supply += x_.at(edge); + } + demand_constraints_.push_back(second_stage_model_.AddLinearConstraint( + incomming_supply >= instance.location_demand)); + } + + // Supply constraints + for (int facility = 0; facility < num_facilities; ++facility) { + math_opt::LinearExpression outgoing_supply; + for (const auto& edge : network_.edges_incident_to_facility(facility)) { + outgoing_supply += x_.at(edge); + } + // Set supply constraint with trivial upper bound to be updated with first + // stage information. + supply_constraints_.push_back( + second_stage_model_.AddLinearConstraint(outgoing_supply <= kInf)); + } +} + +absl::StatusOr> SecondStageSolver::Solve( + const std::vector& z_values, const double w_value, + const double fist_stage_objective) { + const int num_facilities = network_.num_facilities(); + + // Update second stage with first stage solution. + for (int facility = 0; facility < num_facilities; ++facility) { + if (z_values[facility] < -kZeroTol) { + return util::InternalErrorBuilder() + << "negative z_value in first stage: " << z_values[facility] + << " for facility " << facility; + } + // Make sure variable bounds are valid (lb <= ub). + const double capacity_value = std::max(z_values[facility], 0.0); + for (const auto& edge : network_.edges_incident_to_facility(facility)) { + second_stage_model_.set_upper_bound(x_.at(edge), + location_fraction_ * capacity_value); + } + second_stage_model_.set_upper_bound(supply_constraints_[facility], + capacity_value); + } + // Solve and process second stage. + ASSIGN_OR_RETURN(const math_opt::SolveResult second_stage_result, + solver_->Solve(math_opt::SolveArguments{ + .parameters = second_stage_params_})); + switch (second_stage_result.termination.reason) { + case math_opt::TerminationReason::kInfeasible: + // If the second stage problem is infeasible we can construct a + // feasibility cut from a returned dual ray. + { + OR_ASSIGN_OR_RETURN3(const Cut feasibility_cut, + FeasibilityCut(second_stage_result), + _ << "on infeasible for second stage solver"); + return std::make_pair(kInf, feasibility_cut); + } + case math_opt::TerminationReason::kOptimal: + // If the second stage problem is optimal we can construct an optimality + // cut from a returned dual optimal solution. We can also update the upper + // bound. + { + // Upper bound is obtained by switching predicted second stage objective + // value w with the true second stage objective value. + const double upper_bound = fist_stage_objective - w_value + + second_stage_result.objective_value(); + OR_ASSIGN_OR_RETURN3(const Cut optimality_cut, + OptimalityCut(second_stage_result), + _ << "on optimal for second stage solver"); + return std::make_pair(upper_bound, optimality_cut); + } + default: + return util::InternalErrorBuilder() + << "second stage was not solved to optimality or infeasibility: " + << second_stage_result.termination; + } +} + +// If the second stage problem is infeasible we get a dual ray (r, y) such that +// +// sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) +// + sum(y_f*zz_f : f in F, y_f < 0) +// + sum(y_l*d : l in L, y_l > 0) > 0. +// +// Then we get the feasibility cut (go/math_opt-advanced-dual-use#benders) +// +// sum(fcut_f*z_f) + fcut_const <= 0, +// +// where +// +// fcut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) +// + min{y_f, 0} +// fcut_const = sum*(y_l*d : l in L, y_l > 0) +absl::StatusOr SecondStageSolver::FeasibilityCut( + const math_opt::SolveResult& second_stage_result) { + const int num_facilities = network_.num_facilities(); + Cut result; + + if (!second_stage_result.has_dual_ray()) { + // MathOpt does not require solvers to return a dual ray on infeasible, + // but most LP solvers always will, see go/mathopt-solver-contracts for + // details. + return util::InternalErrorBuilder() + << "no dual ray available for feasibility cut"; + } + + for (int facility = 0; facility < num_facilities; ++facility) { + double coefficient = 0.0; + for (const auto& edge : network_.edges_incident_to_facility(facility)) { + const double reduced_cost = + second_stage_result.ray_reduced_costs().at(x_.at(edge)); + coefficient += location_fraction_ * std::min(reduced_cost, 0.0); + } + const double dual_value = + second_stage_result.ray_dual_values().at(supply_constraints_[facility]); + coefficient += std::min(dual_value, 0.0); + result.z_coefficients.push_back(coefficient); + } + result.constant = 0.0; + for (const auto& constraint : demand_constraints_) { + const double dual_value = + second_stage_result.ray_dual_values().at(constraint); + result.constant += std::max(dual_value, 0.0); + } + result.w_coefficient = 0.0; + return result; +} + +// If the second stage problem is optimal we get a dual solution (r, y) such +// that the optimal objective value is equal to +// +// sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) +// + sum(y_f*zz_f : f in F, y_f < 0) +// + sum*(y_l*d : l in L, y_l > 0) > 0. +// +// Then we get the optimality cut (go/math_opt-advanced-dual-use#benders) +// +// sum(ocut_f*z_f) + ocut_const <= w, +// +// where +// +// ocut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) +// + min{y_f, 0} +// ocut_const = sum*(y_l*d : l in L, y_l > 0) +absl::StatusOr SecondStageSolver::OptimalityCut( + const math_opt::SolveResult& second_stage_result) { + const int num_facilities = network_.num_facilities(); + Cut result; + + if (!second_stage_result.has_dual_feasible_solution()) { + // MathOpt does not require solvers to return a dual solution on optimal, + // but most LP solvers always will, see go/mathopt-solver-contracts for + // details. + return util::InternalErrorBuilder() + << "no dual solution available for optimality cut"; + } + + for (int facility = 0; facility < num_facilities; ++facility) { + double coefficient = 0.0; + for (const auto& edge : network_.edges_incident_to_facility(facility)) { + const double reduced_cost = + second_stage_result.reduced_costs().at(x_.at(edge)); + coefficient += location_fraction_ * std::min(reduced_cost, 0.0); + } + double dual_value = + second_stage_result.dual_values().at(supply_constraints_[facility]); + coefficient += std::min(dual_value, 0.0); + result.z_coefficients.push_back(coefficient); + } + result.constant = 0.0; + for (const auto& constraint : demand_constraints_) { + double dual_value = second_stage_result.dual_values().at(constraint); + result.constant += std::max(dual_value, 0.0); + } + result.w_coefficient = 1.0; + return result; +} + +absl::Status Benders(const FacilityLocationInstance& instance, + const double target_precission, + const math_opt::SolverType solver_type, + const int maximum_iterations = 30000) { + const int num_facilities = instance.network.num_facilities(); + + // Setup first stage model and solver. + FirstStageProblem first_stage(instance.network, instance.facility_cost); + ASSIGN_OR_RETURN( + const std::unique_ptr first_stage_solver, + math_opt::IncrementalSolver::New(&first_stage.model, solver_type)); + // Setup second stage solver. + ASSIGN_OR_RETURN(std::unique_ptr second_stage_solver, + SecondStageSolver::New(instance, solver_type)); + // Start Benders + int iteration = 0; + double best_upper_bound = kInf; + std::vector z_values; + z_values.resize(num_facilities); + while (true) { + LOG(INFO) << "Iteration: " << iteration; + + // Solve and process first stage. + ASSIGN_OR_RETURN(const math_opt::SolveResult first_stage_result, + first_stage_solver->Solve()); + if (first_stage_result.termination.reason != + math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "could not solve first stage problem to optimality:" + << first_stage_result.termination; + } + for (int j = 0; j < num_facilities; j++) { + z_values[j] = first_stage_result.variable_values().at(first_stage.z[j]); + } + const double lower_bound = first_stage_result.objective_value(); + LOG(INFO) << "LB = " << lower_bound; + // Solve and process second stage. + ASSIGN_OR_RETURN( + (auto [upper_bound, cut]), + second_stage_solver->Solve( + z_values, first_stage_result.variable_values().at(first_stage.w), + first_stage_result.objective_value())); + math_opt::LinearExpression cut_expression; + for (int j = 0; j < num_facilities; j++) { + cut_expression += cut.z_coefficients[j] * first_stage.z[j]; + } + cut_expression += cut.constant; + first_stage.model.AddLinearConstraint(cut_expression <= + cut.w_coefficient * first_stage.w); + best_upper_bound = std::min(upper_bound, best_upper_bound); + LOG(INFO) << "UB = " << best_upper_bound; + ++iteration; + if (best_upper_bound - lower_bound < target_precission) { + std::cout << "Total iterations = " << iteration << std::endl; + std::cout << "Final LB = " << absl::StrFormat("%.9f", lower_bound) + << std::endl; + std::cout << "Final UB = " << absl::StrFormat("%.9f", best_upper_bound) + << std::endl; + break; + } + if (iteration > maximum_iterations) { + break; + } + } + return absl::OkStatus(); +} +absl::Status Main() { + const FacilityLocationInstance instance{ + .network = Network(absl::GetFlag(FLAGS_num_facilities), + absl::GetFlag(FLAGS_num_locations), + absl::GetFlag(FLAGS_edge_probability)), + .location_demand = absl::GetFlag(FLAGS_location_demand), + .facility_cost = absl::GetFlag(FLAGS_facility_cost), + .location_fraction = absl::GetFlag(FLAGS_location_fraction)}; + absl::Time start = absl::Now(); + RETURN_IF_ERROR(FullProblem(instance, absl::GetFlag(FLAGS_solver_type))) + << "full solve failed"; + std::cout << "Full solve time: " << absl::Now() - start << std::endl; + start = absl::Now(); + RETURN_IF_ERROR(Benders(instance, absl::GetFlag(FLAGS_benders_precission), + absl::GetFlag(FLAGS_solver_type))) + << "Benders solve failed"; + std::cout << "Benders solve time: " << absl::Now() - start << std::endl; + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/graph_coloring.cc b/ortools/math_opt/samples/graph_coloring.cc new file mode 100644 index 0000000000..bfd782c1c7 --- /dev/null +++ b/ortools/math_opt/samples/graph_coloring.cc @@ -0,0 +1,201 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// In the context of the graph coloring problem, we say that a proper coloring +// is an assignment of colors (labels) to the vertices such that adjacent +// vertices have different colors. Usually one is interested in finding the +// chromatic number of a graph, that is, the minimum number of colors that a +// proper coloring should have. However, in this example, we are only interested +// in the feasibility problem: given the graph G = (V, E) and a number k, is +// there a proper coloring which uses at most k colors? In this model, for each +// vertex i and color c, we have a binary variable x_i,c which indicates if +// vertex i is colored with color c. Then, enforcing the constraint +// x_i,c + x_j,c <= 1, +// for each edge (i, j) and color c, is equivalent to saying that +// adjacent vertices should have different colors. Hence, the model is as +// follows: +// min 0 * x +// s.t. x_i,c + x_j,c <= 1, for all edges (i, j) and color c +// sum(x_(i,c) : color c) == 1, for all vertex i +// x_i,c binary, for all vertex i and color c +// This example uses a graph based on the bordering adjacencies of south +// american countries. +#include +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(int, num_colors, 4, "Maximum number of colors."); +ABSL_FLAG(bool, solver_output, false, "Enable solver output."); + +namespace { + +namespace math_opt = operations_research::math_opt; + +// a graph coloring solution is simply an assignment of colors +// to the vertices +struct GraphColoringSolution { + std::vector vertex_color; +}; + +// An instance of the graph coloring problem, to color the nodes of a graph +// using at most max_num_colors such that no neighboring nodes use the same +// color. +struct GraphColoringInstance { + // The number of nodes in the graph + int num_nodes = 0; + + // The number of colors allowed + int num_colors = 0; + + // The undirected edges of the graph + std::vector> edges; +}; + +// Solves the graph coloring problem. +absl::StatusOr SolveGraphColoring( + const GraphColoringInstance& instance) { + // Create the model. Since we are just checking feasibility, + // the objective function is empty, i.e., zero + math_opt::Model model("graph_coloring"); + + // declare variables x_{i, c} for each vertex i and color c + std::vector> x(instance.num_nodes); + for (int i = 0; i < instance.num_nodes; ++i) { + x[i].reserve(instance.num_colors); + for (int c = 0; c < instance.num_colors; ++c) { + x[i].push_back(model.AddBinaryVariable(absl::StrCat("x_", i, "_", c))); + } + } + + // add color conflict constraints + for (const auto [i, j] : instance.edges) { + for (int c = 0; c < instance.num_colors; ++c) { + model.AddLinearConstraint(x[i][c] + x[j][c] <= 1.0, + absl::StrCat("edge_", i, ",", j, "_color_", c)); + } + } + + // add requirement that each vertex should have a color + for (int i = 0; i < instance.num_nodes; ++i) { + model.AddLinearConstraint(math_opt::Sum(x[i]) == 1.0, + absl::StrCat("vertex_", i)); + } + + // Set parameters, e.g. turn on logging. + math_opt::SolveArguments args; + args.parameters.enable_output = absl::GetFlag(FLAGS_solver_output); + + // solve the model and check the result + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kCpSat)); + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + break; + default: + return util::InternalErrorBuilder() + << "model failed to solve: " << result.termination; + } + + // build solution from solver output + GraphColoringSolution solution; + solution.vertex_color.resize(instance.num_nodes); + for (int i = 0; i < instance.num_nodes; ++i) { + for (int c = 0; c < instance.num_colors; ++c) { + if (std::round(result.variable_values().at(x[i][c])) == 1.0) { + solution.vertex_color[i] = c; + } + } + } + + return solution; +} + +absl::Status RealMain() { + // ids for south america countries + constexpr int kColombia = 0; + constexpr int kEcuador = 1; + constexpr int kVenezuela = 2; + constexpr int kGuyana = 3; + constexpr int kSuriname = 4; + constexpr int kFrenchGuyana = 5; + constexpr int kBrazil = 6; + constexpr int kPeru = 7; + constexpr int kBolivia = 8; + constexpr int kChile = 9; + constexpr int kArgentina = 10; + constexpr int kUruguay = 11; + constexpr int kParaguay = 12; + + GraphColoringInstance instance; + instance.num_nodes = 13; + instance.num_colors = absl::GetFlag(FLAGS_num_colors); + instance.edges = {{kBrazil, kFrenchGuyana}, {kBrazil, kSuriname}, + {kBrazil, kGuyana}, {kBrazil, kVenezuela}, + {kBrazil, kColombia}, {kBrazil, kPeru}, + {kBrazil, kBolivia}, {kBrazil, kParaguay}, + {kBrazil, kUruguay}, {kBrazil, kArgentina}, + {kArgentina, kUruguay}, {kArgentina, kParaguay}, + {kArgentina, kBolivia}, {kArgentina, kChile}, + {kPeru, kEcuador}, {kPeru, kColombia}, + {kPeru, kBolivia}, {kPeru, kChile}, + {kBolivia, kChile}, {kBolivia, kParaguay}, + {kColombia, kEcuador}, {kColombia, kVenezuela}, + {kGuyana, kSuriname}, {kGuyana, kVenezuela}, + {kSuriname, kFrenchGuyana}}; + + // The chromatic number of this graph is 4. The graph is planar + // and it has a 4-clique (Brazil, Bolivia, Paraguay, Argentina) + // https://en.wikipedia.org/wiki/Four_color_theorem + ASSIGN_OR_RETURN(GraphColoringSolution solution, + SolveGraphColoring(instance)); + + const std::vector vertex_names = { + "Colombia", "Ecuador", "Venezuela", "Guyana", "Suriname", + "French Guyana", "Brazil", "Peru", "Bolivia", "Chile", + "Argentina", "Uruguay", "Paraguay"}; + const std::vector color_names = {"Red", "Green", "Blue", + "Yellow"}; + + std::cout << "Graph can be colored with " << absl::GetFlag(FLAGS_num_colors) + << " colors as follows:" << std::endl; + for (int i = 0; i < instance.num_nodes; ++i) { + std::cout << "country: " << vertex_names[i] + << " color: " << color_names[solution.vertex_color[i]] + << std::endl; + } + + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = RealMain(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/integer_programming.cc b/ortools/math_opt/samples/integer_programming.cc new file mode 100644 index 0000000000..3c2cf6969e --- /dev/null +++ b/ortools/math_opt/samples/integer_programming.cc @@ -0,0 +1,85 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Simple integer programming example + +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +constexpr double kInf = std::numeric_limits::infinity(); + +// Model and solve the problem: +// max x + 10 * y +// s.t. x + 7 * y <= 17.5 +// x <= 3.5 +// x in {0.0, 1.0, 2.0, ..., +// y in {0.0, 1.0, 2.0, ..., +// +absl::Status Main() { + math_opt::Model model("Integer programming example"); + + // Variables + const math_opt::Variable x = model.AddIntegerVariable(0.0, kInf, "x"); + const math_opt::Variable y = model.AddIntegerVariable(0.0, kInf, "y"); + + // Constraints + model.AddLinearConstraint(x + 7 * y <= 17.5, "c1"); + model.AddLinearConstraint(x <= 3.5, "c2"); + + // Objective + model.Maximize(x + 10 * y); + + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGscip)); + + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + // A feasible solution is always available on termination reason kOptimal, + // and kFeasible, but in the later case the solution may be sub-optimal. + std::cout << "Problem solved in " << result.solve_time() << std::endl; + std::cout << "Objective value: " << result.objective_value() << std::endl; + std::cout << "Variable values: [x=" + << std::round(result.variable_values().at(x)) + << ", y=" << std::round(result.variable_values().at(y)) << "]" + << std::endl; + return absl::OkStatus(); + default: + return util::InternalErrorBuilder() + << "model failed to solve: " << result.termination; + } +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/lagrangian_relaxation.cc b/ortools/math_opt/samples/lagrangian_relaxation.cc new file mode 100644 index 0000000000..0638e42f3c --- /dev/null +++ b/ortools/math_opt/samples/lagrangian_relaxation.cc @@ -0,0 +1,493 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Solves a constrained shortest path problem via Lagrangian Relaxation. The +// Lagrangian dual is solved with subgradient ascent. +// +// Problem data: +// * N: set of nodes. +// * A: set of arcs. +// * R: set of resources. +// * c_(i,j): cost of traversing arc (i,j) in A. +// * r_(i,j,k): resource k spent by traversing arc (i,j) in A, for all k in R. +// * b_i: flow balance at node i in N (+1 at the source, -1 at the sink, and 0 +// otherwise). +// * r_max_k: availability of resource k for a path, for all k in R. +// +// Decision variables: +// * x_(i,j): flow through arc (i,j) in A. +// +// Formulation: +// Z = min sum(c_(i,j) * x_(i,j): (i,j) in A) +// s.t. +// sum(x_(i,j): (i,j) in A) - sum(x_(j,i): (j,i) in A) = b_i for all i in N, +// sum(r_(i,j,k) * x_(i,j): (i,j) in A) <= r_max_k for all k in R, +// x_(i,j) in {0,1} for all (i,j) in A. +// +// Upon dualizing a subset of the constraints (here we chose to relax some or +// all of the knapsack constraints), we obtaing a subproblem parameterized by +// dual variables mu (one per dualized constraint). We refer to this as the +// Lagrangian subproblem. Let R+ be the set of knapsack constraints that we +// keep, and R- the set of knapsack constraints that get dualized. The +// Lagrangian subproblem follows: +// +// z(mu) = min sum( +// (c_(i,j) - sum(mu_k * r_(i,j,k): k in R)) * x_(i,j): (i,j) in A) +// + sum(mu_k * r_max_k: k in R-) +// s.t. +// sum(x_(i,j): (i,j) in A) - sum(x_(j,i): (j,i) in A) = b_i for all i in N, +// sum(r_(i,j,k) * x_(i,j): (i,j) in A) <= r_max_k for all k in R+, +// x_(i,j) in {0,1} for all (i,j) in A. +// +// We seek to solve the Lagrangian dual, which is of the form: +// Z_D = max{ z(mu) : mu <=0 }. Concavity of z(mu) allows us to solve the +// Lagrangian dual with the iterates: +// mu_(t+1) = mu_t + step_size_t * grad_mu_t, where +// grad_mu_t = r_max - sum(t_(i,j) * x_(i,j)^t: (i,j) in A) is a subgradient of +// z(mu_t) and x^t is an optimal solution to the problem induced by z(mu_t). +// +// In general we have that Z_D <= Z. For convex problems, Z_D = Z. For MIPs, +// Z_LP <= Z_D <= Z, where Z_LP is the linear relaxation of the original +// problem. +// +// In this particular example, we use two resource constraints. Either +// constraint or both can be dualized via the flags `dualize_resource_1` and +// `dualize_resource_2`. If both constraints are dualized, we have that Z_LP = +// Z_D because the resulting Lagrangian subproblem can be solved as a linear +// program (i.e., the problem becomes a pure shortest path problem upon +// dualizing all the side constraints). When only one of the side constraints is +// dualized, we can have Z_LP <= Z_D because the resulting Lagrangian subproblem +// needs to be solved as an MIP. For the particular data used in this example, +// dualizing only the first resource constraint leads to Z_LP < Z_D, while +// dualizing only the second resource constraint leads to Z_LP = Z_D. In either +// case, solving the Lagrandual dual also provides an upper bound to Z. +// +// Usage: blaze build -c opt +// ortools/math_opt/examples:lagrangian_relaxation +// blaze-bin/ortools/math_opt/examples/lagrangian_relaxation + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "ortools/base/container_logging.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/mathutil.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(double, step_size, 0.95, + "Stepsize for gradient ascent, determined as step_size^t."); +ABSL_FLAG(int, max_iterations, 1000, + "Max number of iterations for gradient ascent."); +ABSL_FLAG(bool, dualize_resource_1, true, + "If true, the side constraint associated to resource 1 is dualized."); +ABSL_FLAG(bool, dualize_resource_2, false, + "If true, the side constraint associated to resource 2 is dualized."); + +ABSL_FLAG(bool, lagrangian_output, false, + "If true, shows the iteration log of the subgradient ascent " + "procedure use to solve the Lagrangian problem"); + +constexpr double kZeroTol = 1.0e-8; + +namespace { +using ::operations_research::MathUtil; + +namespace math_opt = ::operations_research::math_opt; + +struct Arc { + int i; + int j; + double cost; + double resource_1; + double resource_2; +}; + +struct Graph { + int num_nodes; + std::vector arcs; + int source; + int sink; +}; + +struct FlowModel { + FlowModel() : model(std::make_unique("LagrangianProblem")) {} + std::unique_ptr model; + math_opt::LinearExpression cost; + math_opt::LinearExpression resource_1; + math_opt::LinearExpression resource_2; + std::vector flow_vars; +}; + +// Populates `model` with variables and constraints of a shortest path problem. +FlowModel CreateShortestPathModel(const Graph graph) { + FlowModel flow_model; + math_opt::Model& model = *flow_model.model; + for (const Arc& arc : graph.arcs) { + math_opt::Variable var = model.AddContinuousVariable( + /*lower_bound=*/0, /*upper_bound=*/1, + /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j)); + flow_model.cost += arc.cost * var; + flow_model.resource_1 += arc.resource_1 * var; + flow_model.resource_2 += arc.resource_2 * var; + flow_model.flow_vars.push_back(var); + } + + // Flow balance constraints + std::vector out_flow(graph.num_nodes); + std::vector in_flow(graph.num_nodes); + for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) { + out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index]; + in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index]; + } + for (int node_index = 0; node_index < graph.num_nodes; ++node_index) { + int rhs = node_index == graph.source ? 1 + : node_index == graph.sink ? -1 + : 0; + model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] == + rhs); + } + + return flow_model; +} + +Graph CreateSampleNetwork() { + std::vector arcs; + arcs.push_back( + {.i = 0, .j = 1, .cost = 12, .resource_1 = 1, .resource_2 = 1}); + arcs.push_back( + {.i = 0, .j = 2, .cost = 3, .resource_1 = 2.5, .resource_2 = 1}); + arcs.push_back( + {.i = 1, .j = 3, .cost = 5, .resource_1 = 1, .resource_2 = 1.5}); + arcs.push_back( + {.i = 1, .j = 4, .cost = 5, .resource_1 = 2.5, .resource_2 = 1}); + arcs.push_back( + {.i = 2, .j = 1, .cost = 7, .resource_1 = 2.5, .resource_2 = 1}); + arcs.push_back( + {.i = 2, .j = 3, .cost = 5, .resource_1 = 7, .resource_2 = 2.5}); + arcs.push_back( + {.i = 2, .j = 4, .cost = 1, .resource_1 = 6.5, .resource_2 = 1}); + arcs.push_back( + {.i = 3, .j = 5, .cost = 6, .resource_1 = 1, .resource_2 = 2.0}); + arcs.push_back( + {.i = 4, .j = 3, .cost = 3, .resource_1 = 1, .resource_2 = 0.5}); + arcs.push_back( + {.i = 4, .j = 5, .cost = 5, .resource_1 = 2.5, .resource_2 = 1}); + const Graph graph = {.num_nodes = 6, .arcs = arcs, .source = 0, .sink = 5}; + + return graph; +} + +// Solves the constrained shortest path as an MIP. +absl::StatusOr SolveMip(const Graph graph, + const double max_resource_1, + const double max_resource_2) { + FlowModel flow_model; + math_opt::Model& model = *flow_model.model; + for (const Arc& arc : graph.arcs) { + math_opt::Variable var = model.AddBinaryVariable( + /*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j)); + flow_model.cost += arc.cost * var; + flow_model.resource_1 += +arc.resource_1 * var; + flow_model.resource_2 += arc.resource_2 * var; + flow_model.flow_vars.push_back(var); + } + + // Flow balance constraints + std::vector out_flow(graph.num_nodes); + std::vector in_flow(graph.num_nodes); + for (int arc_index = 0; arc_index < graph.arcs.size(); ++arc_index) { + out_flow[graph.arcs[arc_index].i] += flow_model.flow_vars[arc_index]; + in_flow[graph.arcs[arc_index].j] += flow_model.flow_vars[arc_index]; + } + for (int node_index = 0; node_index < graph.num_nodes; ++node_index) { + int rhs = node_index == graph.source ? 1 + : node_index == graph.sink ? -1 + : 0; + model.AddLinearConstraint(out_flow[node_index] - in_flow[node_index] == + rhs); + } + + model.AddLinearConstraint(flow_model.resource_1 <= max_resource_1, + "resource_ctr_1"); + model.AddLinearConstraint(flow_model.resource_2 <= max_resource_2, + "resource_ctr_2"); + model.Minimize(flow_model.cost); + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGscip)); + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + std::cout << "MIP Solution with 2 side constraints" << std::endl; + std::cout << absl::StrFormat("MIP objective value: %6.3f", + result.objective_value()) + << std::endl; + std::cout << "Resource 1: " + << flow_model.resource_1.Evaluate(result.variable_values()) + << std::endl; + std::cout << "Resource 2: " + << flow_model.resource_2.Evaluate(result.variable_values()) + << std::endl; + std::cout << "========================================" << std::endl; + return flow_model; + default: + return util::InternalErrorBuilder() + << "model failed to solve: " << result.termination; + } +} + +// Solves the linear relaxation of a constrained shortest path problem +// formulated as an MIP. +absl::Status SolveLinearRelaxation(FlowModel& flow_model, const Graph& graph, + const double max_resource_1, + const double max_resource_2) { + math_opt::Model& model = *flow_model.model; + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGscip)); + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + std::cout << "LP relaxation with 2 side constraints" << std::endl; + std::cout << absl::StrFormat("LP objective value: %6.3f", + result.objective_value()) + << std::endl; + std::cout << "Resource 1: " + << flow_model.resource_1.Evaluate(result.variable_values()) + << std::endl; + std::cout << "Resource 2: " + << flow_model.resource_2.Evaluate(result.variable_values()) + << std::endl; + std::cout << "========================================" << std::endl; + return absl::OkStatus(); + default: + return util::InternalErrorBuilder() + << "model failed to solve: " << result.termination; + } +} + +absl::Status SolveLagrangianRelaxation(const Graph graph, + const double max_resource_1, + const double max_resource_2) { + // Model, variables, and linear expressions. + FlowModel flow_model = CreateShortestPathModel(graph); + math_opt::Model& model = *flow_model.model; + math_opt::LinearExpression& cost = flow_model.cost; + math_opt::LinearExpression& resource_1 = flow_model.resource_1; + math_opt::LinearExpression& resource_2 = flow_model.resource_2; + + // Dualized constraints and dual variable iterates. + std::vector mu; + std::vector grad_mu; + const bool dualized_resource_1 = absl::GetFlag(FLAGS_dualize_resource_1); + const bool dualized_resource_2 = absl::GetFlag(FLAGS_dualize_resource_2); + const bool lagrangian_output = absl::GetFlag(FLAGS_lagrangian_output); + CHECK(dualized_resource_1 || dualized_resource_2) + << "At least one of the side constraints should be dualized."; + + // Modify the lagrangian problem according to the constraints that are + // dualized. We use a initial dual value different from zero to prioritize + // finding a feasible solution. + const double initial_dual_value = -10; + if (dualized_resource_1 && !dualized_resource_2) { + mu.push_back(initial_dual_value); + grad_mu.push_back(max_resource_1 - resource_1); + model.AddLinearConstraint(resource_2 <= max_resource_2); + for (math_opt::Variable& var : flow_model.flow_vars) { + model.set_integer(var); + } + } else if (!dualized_resource_1 && dualized_resource_2) { + mu.push_back(initial_dual_value); + grad_mu.push_back(max_resource_2 - resource_2); + model.AddLinearConstraint(resource_1 <= max_resource_1); + for (math_opt::Variable& var : flow_model.flow_vars) { + model.set_integer(var); + } + } else { + mu.push_back(initial_dual_value); + mu.push_back(initial_dual_value); + grad_mu.push_back(max_resource_1 - resource_1); + grad_mu.push_back(max_resource_2 - resource_2); + } + + // Gradient ascent setup + bool termination = false; + int iterations = 1; + const double step_size = absl::GetFlag(FLAGS_step_size); + CHECK_GT(step_size, 0) << "step_size must be strictly positive"; + CHECK_LT(step_size, 1) << "step_size must be strictly less than 1"; + const int max_iterations = absl::GetFlag(FLAGS_max_iterations); + CHECK_GT(max_iterations, 0) + << "Number of iterations must be strictly positive."; + + // Upper and lower bounds on the full problem. + double upper_bound = std::numeric_limits::infinity(); + double lower_bound = -std::numeric_limits::infinity(); + double best_solution_resource_1 = 0; + double best_solution_resource_2 = 0; + + if (lagrangian_output) { + std::cout << "Starting gradient ascent..." << std::endl; + std::cout << absl::StrFormat("%4s %6s %6s %9s %10s %10s", "Iter", "LB", + "UB", "Step size", "mu_t", "grad_mu_t") + << std::endl; + } + + while (!termination) { + math_opt::LinearExpression lagrangian_function; + lagrangian_function += cost; + for (int k = 0; k < mu.size(); ++k) { + lagrangian_function += mu[k] * grad_mu[k]; + } + model.Minimize(lagrangian_function); + ASSIGN_OR_RETURN(math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGscip)); + switch (result.termination.reason) { + case math_opt::TerminationReason::kOptimal: + case math_opt::TerminationReason::kFeasible: + break; + default: + return util::InternalErrorBuilder() + << "failed to minimize lagrangian function: " + << result.termination; + } + + const math_opt::VariableMap& vars_val = result.variable_values(); + bool feasible = true; + + // Iterate update. Takes a step in the direction of the gradient (since the + // Lagrangian dual is a max problem), and projects onto {mu: mu <=0} to + // satisfy the sign of the dual variable. In general, convergence to an + // optimal solution requires diminishing step sizes satisfying: + // * sum(step_size_t: t=1...) = infinity and, + // * sum((step_size_t)^2: t=1...) < infinity + // See details in Prop 3.2.6 Bertsekas 2015, Convex Optimization Algorithms. + // Here we use step_size_t = step_size^t which does NOT satisfy the + // first condition, but is good enough for the purpose of this example. + std::vector grad_mu_vals; + const double step_size_t = MathUtil::IPow(step_size, iterations); + for (int k = 0; k < mu.size(); ++k) { + // Evaluate resource k and evaluate the gradient of z(mu). + const double grad_mu_val = grad_mu[k].Evaluate(vars_val); + grad_mu_vals.push_back(grad_mu_val); + mu[k] = std::min(0.0, mu[k] + step_size_t * grad_mu_val); + if (grad_mu_val < 0) { + feasible = false; + } + } + // Bounds update + const double path_cost = cost.Evaluate(vars_val); + if (feasible && path_cost < upper_bound) { + best_solution_resource_1 = resource_1.Evaluate(vars_val); + best_solution_resource_2 = resource_2.Evaluate(vars_val); + if (lagrangian_output) { + std::cout << "Feasible solution with" + << absl::StrFormat( + "cost=%4.2f, resource_1=%4.2f, and resource_2=%4.2f. ", + path_cost, best_solution_resource_1, + best_solution_resource_2) + << std::endl; + } + upper_bound = path_cost; + } + if (lower_bound < result.objective_value()) { + lower_bound = result.objective_value(); + } + + if (lagrangian_output) { + std::cout << absl::StrFormat("%4d %6.3f %6.3f %9.3f", iterations, + lower_bound, upper_bound, step_size_t) + << " " << gtl::LogContainer(mu) << " " + << gtl::LogContainer(grad_mu_vals) << std::endl; + } + + // Termination criteria + double norm = 0; + for (double value : grad_mu_vals) { + norm += (value * value); + } + norm = sqrt(norm); + if (iterations == max_iterations || lower_bound == upper_bound || + step_size_t * norm < kZeroTol) { + termination = true; + } + iterations++; + } + + std::cout << "Lagrangian relaxation with 2 side constraints" << std::endl; + std::cout << "Constraint for resource 1 dualized: " + << (dualized_resource_1 ? "true" : "false") << std::endl; + std::cout << "Constraint for resource 2 dualized: " + << (dualized_resource_2 ? "true" : "false") << std::endl; + std::cout << absl::StrFormat("Lower bound: %6.3f", lower_bound) << std::endl; + std::cout << absl::StrFormat("Upper bound: %6.3f (Integer solution)", + upper_bound) + << std::endl; + std::cout << "========================================" << std::endl; + return absl::OkStatus(); +} + +void RelaxModel(FlowModel& flow_model) { + for (math_opt::Variable& var : flow_model.flow_vars) { + flow_model.model->set_continuous(var); + flow_model.model->set_lower_bound(var, 0.0); + flow_model.model->set_upper_bound(var, 1.0); + } +} + +absl::Status SolveFullModel(const Graph& graph, double max_resource_1, + double max_resource_2) { + ASSIGN_OR_RETURN(FlowModel flow_model, + SolveMip(graph, max_resource_1, max_resource_2)); + RelaxModel(flow_model); + return SolveLinearRelaxation(flow_model, graph, max_resource_1, + max_resource_2); +} + +absl::Status Main() { + // Problem data + const Graph graph = CreateSampleNetwork(); + const double max_resource_1 = 10; + const double max_resource_2 = 4; + + RETURN_IF_ERROR(SolveFullModel(graph, max_resource_1, max_resource_2)) + << "full solve failed"; + RETURN_IF_ERROR( + SolveLagrangianRelaxation(graph, max_resource_1, max_resource_2)) + << "lagrangian solve failed"; + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/linear_programming.cc b/ortools/math_opt/samples/linear_programming.cc new file mode 100644 index 0000000000..aa427ff6fe --- /dev/null +++ b/ortools/math_opt/samples/linear_programming.cc @@ -0,0 +1,93 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Simple linear programming example + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +constexpr double kInf = std::numeric_limits::infinity(); + +// Model and solve the problem: +// max 10 * x0 + 6 * x1 + 4 * x2 +// s.t. 10 * x0 + 4 * x1 + 5 * x2 <= 600 +// 2 * x0 + 2 * x1 + 6 * x2 <= 300 +// x0 + x1 + x2 <= 100 +// x0 in [0, infinity) +// x1 in [0, infinity) +// x2 in [0, infinity) +// +absl::Status Main() { + math_opt::Model model("Linear programming example"); + + // Variables + std::vector x; + for (int j = 0; j < 3; j++) { + x.push_back(model.AddContinuousVariable(0.0, kInf, absl::StrCat("x", j))); + } + + // Constraints + std::vector constraints; + constraints.push_back( + model.AddLinearConstraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, "c1")); + constraints.push_back( + model.AddLinearConstraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, "c2")); + // sum(x[i]) <= 100 + constraints.push_back(model.AddLinearConstraint(Sum(x) <= 100, "c3")); + + // Objective + model.Maximize(10 * x[0] + 6 * x[1] + 4 * x[2]); + + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, math_opt::SolverType::kGlop)); + if (result.termination.reason != math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "model failed to solve to optimality" << result.termination; + } + + std::cout << "Problem solved in " << result.solve_time() << std::endl; + std::cout << "Objective value: " << result.objective_value() << std::endl; + + std::cout << "Variable values: [" + << absl::StrJoin(Values(result.variable_values(), x), ", ") << "]" + << std::endl; + + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/linear_regression.cc b/ortools/math_opt/samples/linear_regression.cc new file mode 100644 index 0000000000..991137fc4e --- /dev/null +++ b/ortools/math_opt/samples/linear_regression.cc @@ -0,0 +1,208 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Solve a linear regression problem on random data. +// +// Problem data: +// There are num_features features, indexed by j. +// There are num_examples training examples indexed by i. +// x_ij: The feature value for example i and feature j in the training data. +// y_i: The label for example i in the training data. +// +// Decision variables: +// beta_j: the coefficient to learn for each feature j. +// z_i: the prediction error for example i. +// +// Optimization problem: +// min sum_i z_i^2 +// s.t. y_i - sum_j beta_j * x_ij = z_i +// +// This is the unregularized linear regression problem. +// +// This example solves the problem on randomly generated (x, y) data. The data +// is generated by assuming some true values for beta (generated at random, +// i.i.d. N(0, 1)), then drawing each x_ij as N(0, 1) and then computing +// y_i = beta * x_i + N(0, noise) +// where noise is a command line flag. +// +// After solving the optimization problem above to recover values for beta, the +// in sample and out of sample loss (average squared prediction error) for the +// learned model are printed. +// +// For an advanced version, see: +// ortools/math_opt/codelabs/regression/ +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/random/random.h" +#include "absl/status/status.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(operations_research::math_opt::SolverType, solver_type, + operations_research::math_opt::SolverType::kGurobi, + "The solver needs to support quadratic objectives, e.g. pdlp, " + "gurobi, or osqp."); + +ABSL_FLAG(int, num_features, 10, + "The number of features in the linear regression model."); +ABSL_FLAG(int, num_examples, 100, + "The number of examples to use in the train and test sets."); +ABSL_FLAG(double, noise, 3.0, + "The standard deviation of the noise on the labels."); + +namespace { + +namespace math_opt = operations_research::math_opt; + +// Data for a learned function f(x) = sum_j beta_j * x_j. +struct LinearModel { + // Has size equal to the number of features. + std::vector betas; +}; + +// Creates a random linear function where each beta is i.i.d. Normal(0,1). +LinearModel RandomLinearModel(const int num_features) { + LinearModel result; + absl::BitGen bit_gen; + for (int j = 0; j < num_features; ++j) { + result.betas.push_back(absl::Gaussian(bit_gen)); + } + return result; +} + +// A training example associating the label `y` to features `xs`. +struct LabeledExample { + // Has size equal to the number of features. + std::vector xs; + double y = 0.0; +}; + +// Creates num_examples random examples where y approx ground_truth(xs). +// +// Specifically, y = ground_truth(xs) + N(0, noise_stddev), where the xs are +// generated randomly as N(0,1) and are jointly independent with the noise. +std::vector RandomData(const LinearModel& ground_truth, + const int num_examples, + const double noise_stddev) { + const int num_features = static_cast(ground_truth.betas.size()); + absl::BitGen bit_gen; + std::vector examples; + for (int i = 0; i < num_examples; ++i) { + LabeledExample example; + for (int j = 0; j < num_features; ++j) { + example.xs.push_back(absl::Gaussian(bit_gen)); + } + example.y = + absl::c_inner_product(example.xs, ground_truth.betas, + absl::Gaussian(bit_gen, 0.0, noise_stddev)); + examples.push_back(std::move(example)); + } + return examples; +} + +// Computes the average squared error between model(example.xs) and example.y. +double L2Loss(const LinearModel& model, + const std::vector& examples) { + double result = 0.0; + for (const LabeledExample& example : examples) { + CHECK_EQ(example.xs.size(), model.betas.size()); + const double error = + example.y - absl::c_inner_product(example.xs, model.betas, 0.0); + result += error * error; + } + return examples.empty() ? 0.0 : result / examples.size(); +} + +// Computes and returns the linear function minimizing L2Loss on train_data by +// solving a quadratic optimization problem, or returns a Status error if the +// solver fails to find an optimal solution. +absl::StatusOr Train( + const std::vector& train_data) { + const int num_features = static_cast(train_data[0].xs.size()); + const int num_train = static_cast(train_data.size()); + math_opt::Model model("linear_regression"); + // Create the decision variables: beta, and z. + std::vector betas; + for (int j = 0; j < num_features; ++j) { + betas.push_back(model.AddVariable(absl::StrCat("beta_", j))); + } + std::vector zs; + for (int i = 0; i < num_train; ++i) { + zs.push_back(model.AddVariable(absl::StrCat("z_", i))); + } + // Set the objective function: + // minimize sum_i z_i^2 + model.Minimize(math_opt::QuadraticExpression::InnerProduct(zs, zs)); + // Add the constraints: + // z_i = y_i - x_i * beta + for (int i = 0; i < num_train; ++i) { + const LabeledExample& example = train_data[i]; + model.AddLinearConstraint( + zs[i] == example.y - math_opt::InnerProduct(example.xs, betas)); + } + + // Done building the model, now solve. + math_opt::SolveArguments args; + args.parameters.enable_output = true; + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + Solve(model, absl::GetFlag(FLAGS_solver_type), args)); + if (result.termination.reason != math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "Expected termination reason optimal, but termination was: " + << result.termination; + } + std::cout << "Training time: " << result.solve_time() << std::endl; + return LinearModel{.betas = Values(result.variable_values(), betas)}; +} + +absl::Status Main() { + const int num_features = absl::GetFlag(FLAGS_num_features); + const int num_train = absl::GetFlag(FLAGS_num_examples); + const double noise_stddev = absl::GetFlag(FLAGS_noise); + const int num_test = num_train; + + // Generate random training and test data. + const LinearModel ground_truth = RandomLinearModel(num_features); + const std::vector train_data = + RandomData(ground_truth, num_train, noise_stddev); + const std::vector test_data = + RandomData(ground_truth, num_test, noise_stddev); + + // Solve the regression problem. + ASSIGN_OR_RETURN(const LinearModel learned_model, Train(train_data)); + + // Evaluate the solution. + std::cout << "In sample loss: " << L2Loss(learned_model, train_data) + << std::endl; + std::cout << "Out of sample loss: " << L2Loss(learned_model, test_data) + << std::endl; + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/time_indexed_scheduling.cc b/ortools/math_opt/samples/time_indexed_scheduling.cc new file mode 100644 index 0000000000..8fa55ec8fb --- /dev/null +++ b/ortools/math_opt/samples/time_indexed_scheduling.cc @@ -0,0 +1,219 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Problem statement. +// +// Data: +// * n jobs +// * processing time p_i for i = 1,...,n +// * release time r_i for i = 1,...,n +// * Implied: T = max_i r_i + sum_i p_i, the time horizon, all jobs must start +// in [0, T]. +// +// Problem: schedule the jobs sequentially (on a single machine) to minimize the +// sum of the completion times, where each job cannot start until the release +// time. In the scheduling literature, this problem is 1|r_i|sum_i C_i. This +// problem is known to be NP-Hard (e.g. see "Elements of Scheduling" by Lenstra +// and Shmoys 2020, Chapter 4). +// +// Variables: +// * x_it for job i = 1,...,n and time t = 1,...,T, if job i starts at time t. +// +// Model: +// min sum_i sum_t (t + p_i) * x_it +// s.t. sum_t x_it = 1 for all i = 1,...,n (1) +// sum_i sum_{s=t-p_i+1}^t x_is <= 1 for all t = 0,...,T (2) +// x_it = 0 for all i, for t < r_i (3) +// x_it in {0, 1} for all i and t +// +// In the objective, t + p_i is the time the job is completed if it starts at t. +// Constraint (1) ensures that each job is scheduled once, constraint (2) +// ensures that no two jobs overlap in when they are running, and constraint (3) +// enforces the release dates. + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/random/random.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_builder.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(operations_research::math_opt::SolverType, solver_type, + operations_research::math_opt::SolverType::kGscip, + "The solver needs to support binary IP."); + +ABSL_FLAG(int, num_jobs, 30, "How many jobs to schedule"); +ABSL_FLAG(bool, use_test_data, false, + "Solve a small hard coded instance instead of a large random one."); + +namespace { + +namespace math_opt = operations_research::math_opt; + +struct Job { + int processing_time = 0; + int release_time = 0; +}; + +std::vector RandomJobs(int num_jobs) { + // Processing times are uniform [1, kProcessingTimeUb]. + constexpr int kProcessingTimeUb = 20; + + // Release times are uniform in [0, kReleaseTimeUb]. + const int kReleaseTimeUb = num_jobs * kProcessingTimeUb / 2; + + absl::BitGen bit_gen; + std::vector result; + result.reserve(num_jobs); + for (int i = 0; i < num_jobs; ++i) { + result.push_back( + {.processing_time = absl::Uniform(bit_gen, 1, kProcessingTimeUb), + .release_time = absl::Uniform(bit_gen, 0, kReleaseTimeUb)}); + } + return result; +} + +// A small instance for testing. The optimal solution is to run: +// Job 1 at time 1 +// Job 2 at time 2 +// Job 0 at time 7 +// This gives a sum of completion times of 2 + 7 + 17 = 26 +// +// Note that the above schedule idles at time 0. If instead, we did +// Job 2 at time 0 +// Job 1 at time 5 +// Job 0 at time 6 +// This gives a sum of completion times of 5 + 6 + 16 = 27 +std::vector TestInstance() { + return {{.processing_time = 10, .release_time = 0}, + {.processing_time = 1, .release_time = 1}, + {.processing_time = 5, .release_time = 0}}; +} + +int TimeHorizon(const std::vector& jobs) { + int max_release = 0; + int sum_processing = 0; + for (const Job& job : jobs) { + max_release = std::max(job.release_time, max_release); + sum_processing += job.processing_time; + } + return max_release + sum_processing; +} + +struct Schedule { + std::vector start_times; + int sum_of_completion_times = 0; +}; + +absl::StatusOr Solve(const std::vector& jobs, + const math_opt::SolverType solver_type) { + const int kTimeHorizon = TimeHorizon(jobs); + math_opt::Model model; + // x[i][t] indicates that we start job i at time t. + std::vector> x(jobs.size()); + math_opt::LinearExpression sum_completion_times; + for (int i = 0; i < jobs.size(); ++i) { + for (int t = 0; t < kTimeHorizon; ++t) { + math_opt::Variable v = model.AddBinaryVariable(); + const int completion_time = t + jobs[i].processing_time; + sum_completion_times += completion_time * v; + if (t < jobs[i].release_time) { + model.set_upper_bound(v, 0.0); + } + x[i].push_back(v); + } + // Pick one time to run job i. + model.AddLinearConstraint(math_opt::Sum(x[i]) == 1.0); + } + model.Minimize(sum_completion_times); + // Run at most one job at a time + for (int t = 0; t < kTimeHorizon; ++t) { + math_opt::LinearExpression conflicts; + for (int i = 0; i < jobs.size(); ++i) { + for (int s = std::max(0, t - jobs[i].processing_time + 1); s <= t; s++) { + conflicts += x[i][s]; + } + } + model.AddLinearConstraint(conflicts <= 1.0); + } + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, solver_type, + {.parameters = {.enable_output = true}})); + if (!result.has_primal_feasible_solution()) { + return util::InvalidArgumentErrorBuilder() + << "no primal feasible solution, termination: " + << result.termination; + } + Schedule schedule; + schedule.sum_of_completion_times = + sum_completion_times.Evaluate(result.variable_values()); + for (int i = 0; i < jobs.size(); ++i) { + for (int t = 0; t < kTimeHorizon; ++t) { + const double var_value = result.variable_values().at(x[i][t]); + if (var_value > 0.5) { + schedule.start_times.push_back(t); + break; + } + } + } + return schedule; +} + +void PrintSchedule(const std::vector& jobs, const Schedule& schedule) { + std::cout << "sum of completion times: " << schedule.sum_of_completion_times + << std::endl; + std::vector> jobs_by_start_time; + for (int i = 0; i < jobs.size(); ++i) { + jobs_by_start_time.push_back({schedule.start_times[i], jobs[i]}); + } + absl::c_sort(jobs_by_start_time, [](const auto& left, const auto& right) { + return left.first < right.first; + }); + std::cout << "start time, processing time, release time" << std::endl; + for (const auto [start_time, job] : jobs_by_start_time) { + std::cout << start_time << ", " << job.processing_time << ", " + << job.release_time << std::endl; + } +} + +absl::Status Main() { + std::vector jobs; + if (absl::GetFlag(FLAGS_use_test_data)) { + jobs = TestInstance(); + } else { + const int num_jobs = absl::GetFlag(FLAGS_num_jobs); + jobs = RandomJobs(num_jobs); + } + ASSIGN_OR_RETURN(const Schedule schedule, + Solve(jobs, absl::GetFlag(FLAGS_solver_type))); + PrintSchedule(jobs, schedule); + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/tsp.cc b/ortools/math_opt/samples/tsp.cc new file mode 100644 index 0000000000..3c6ac26a33 --- /dev/null +++ b/ortools/math_opt/samples/tsp.cc @@ -0,0 +1,359 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A minimal TSP solver using MathOpt. +// +// In the Euclidean Traveling Salesperson Problem (TSP), you are given a list of +// n cities, each with an (x, y) coordinate, and you must find an order to visit +// the cities in to minimize the (Euclidean) travel distance. +// +// The MIP "cutset" formulation for the problem is as follows: +// * Data: +// n: An integer, the number of cities +// (x_i, y_i): a pair of floats for each i in 1..n, the location of each +// city +// d_ij for all (i, j) pairs of cities, the distance between city i and j. +// * Decision variables: +// x_ij: A binary variable, indicates if the edge connecting i and j is +// used. Note that x_ij == x_ji, because the problem is symmetric. We +// only create variables for i < j, and have x_ji as an alias for +// x_ij. +// * MIP model: +// minimize sum_{i=1}^n sum_{j=1, j < i}^n d_ij * x_ij +// s.t. sum_{j=1, j != i}^n x_ij = 2 for all i = 1..n +// sum_{i in S} sum_{j not in S} x_ij >= 2 for all S subset {1,...,n} +// |S| >= 3, |S| <= n - 3 +// x_ij in {0, 1} +// The first set of constraints are called the degree constraints, and the +// second set of constraints are called the cutset constraints. There are +// exponentially many cutset, so we cannot add them all at the start of the +// solve. Instead, we will use a solver callback to view each integer solution +// and add any violated cutset constraints that exist. +// +// Note that, while there are exponentially many cutset constraints, we can +// quickly identify violated ones by exploiting that the solution is integer +// and the degree constraints are all already in the model and satisfied. As a +// result, the graph n nodes and edges when x_ij = 1 will be a degree two graph, +// so it will be a collection of cycles. If it is a single large cycle, then the +// solution is feasible, and if there multiple cycles, then taking the nodes of +// any cycle as S produces a violated cutset constraint. +// +// Note that this is a minimal TSP solution, more sophisticated MIP methods are +// possible. + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/random/random.h" +#include "absl/random/uniform_real_distribution.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "ortools/base/helpers.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/port/proto_utils.h" + +ABSL_FLAG(int, num_cities, 50, "Number of cities in random TSP."); +ABSL_FLAG(std::string, output, "", + "Write a svg of the solution here, or to standard out if empty."); +ABSL_FLAG(bool, test_instance, false, + "Solve the test TSP instead of a random instance."); +ABSL_FLAG(int, threads, 0, + "How many threads to solve with, or solver default if <= 0."); +ABSL_FLAG(bool, solve_logs, false, + "Have the solver print logs to standard out."); + +namespace { + +namespace math_opt = operations_research::math_opt; +using Cycle = std::vector; + +// Creates variables modeling the undirected edges for the TSP. For every (i, j) +// pair in [0,n) * [0, n), a variable is created only for j < i, but querying +// for the variable x_ij with j > i returns x_ji. Querying for x_ii (which does +// not exist) gives a CHECK failure. +// +// The Model object passed in to create EdgeVariables must outlive this. +class EdgeVariables { + public: + EdgeVariables(math_opt::Model& model, const int n) { + variables_.resize(n); + for (int i = 0; i < n; ++i) { + variables_[i].reserve(i); + for (int j = 0; j < i; ++j) { + variables_[i].push_back( + model.AddBinaryVariable(absl::StrCat("e_", i, "_", j))); + } + } + } + + math_opt::Variable get(const int i, const int j) const { + CHECK_NE(i, j); + return i > j ? variables_[i][j] : variables_[j][i]; + } + + int num_cities() const { return variables_.size(); } + + private: + std::vector> variables_; +}; + +// Produces a random TSP problem where cities have random locations that are +// I.I.D Uniform [0, 1]. +std::vector> RandomCities(int num_cities) { + absl::BitGen rand; + std::vector> cities; + for (int i = 0; i < num_cities; ++i) { + cities.push_back({absl::Uniform(rand, 0.0, 1.0), + absl::Uniform(rand, 0.0, 1.0)}); + } + return cities; +} + +std::vector> TestCities() { + return {{0, 0}, {0, 0.1}, {0.1, 0}, {0.1, 0.1}, + {1, 0}, {1, 0.1}, {0.9, 0}, {0.9, 0.1}}; +} + +// Given an n city TSP instance, computes the n by n distance matrix using the +// Euclidean distance. +std::vector> DistanceMatrix( + const std::vector>& cities) { + const int num_cities = cities.size(); + std::vector> distance_matrix( + num_cities, std::vector(num_cities, 0.0)); + for (int i = 0; i < num_cities; ++i) { + for (int j = 0; j < num_cities; ++j) { + if (i != j) { + const double dx = cities[i].first - cities[j].first; + const double dy = cities[i].second - cities[j].second; + distance_matrix[i][j] = std::sqrt(dx * dx + dy * dy); + } + } + } + return distance_matrix; +} + +// Given the EdgeVariables and a var_values containing the value of each edge in +// a solution, returns an n by n boolean matrix of which edges are used (with +// false diagonal elements). It is assumed that var_values are approximately 0-1 +// integer. +std::vector> EdgeValues( + const EdgeVariables& edge_vars, + const math_opt::VariableMap& var_values) { + const int n = edge_vars.num_cities(); + std::vector> edge_values(n, std::vector(n, false)); + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + if (i != j) { + edge_values[i][j] = var_values.at(edge_vars.get(i, j)) > 0.5; + } + } + } + return edge_values; +} + +// Given an n by n boolean matrix of edge values, returns a cycle decomposition. +// it is assumed that edge values respects the degree constraints (each row has +// only two true entries). Each cycle is represented as a list of cities with +// no repeats. +std::vector FindCycles( + const std::vector>& edge_values) { + // Algorithm: maintain a "visited" bit for each city indicating if we have + // formed a cycle containing this city. Consider the cities in order. When you + // find an unvisited city, start a new cycle beginning at this city. Then, + // build the cycle by finding an unvisited neighbor until no such neighbor + // exists (every city will have two neighbors, but eventually both will be + // visited). To find the "unvisited neighbor", we simply do a linear scan + // over the cities, checking both the adjacency matrix and the visited bit. + // + // Note that for this algorithm, in each cycle, the city with lowest index + // will be first, and the cycles will be sorted by their city of lowest index. + // This is an implementation detail and should not be relied upon. + const int n = edge_values.size(); + std::vector result; + std::vector visited(n, false); + for (int i = 0; i < n; ++i) { + if (visited[i]) { + continue; + } + std::vector cycle; + std::optional next = i; + while (next.has_value()) { + cycle.push_back(*next); + visited[*next] = true; + int current = *next; + next = std::nullopt; + // Scan for an unvisited neighbor. We can start at i+1 since we know that + // everything from i back is visited. + for (int j = i + 1; j < n; ++j) { + if (!visited[j] && edge_values[current][j]) { + next = j; + break; + } + } + } + result.push_back(cycle); + } + return result; +} + +// Given a cycle and an EdgeVariables, returns the cutset constraint for the set +// of nodes in cycle. +math_opt::BoundedLinearExpression CutsetConstraint( + const Cycle& cycle, const EdgeVariables& edge_vars) { + const int n = edge_vars.num_cities(); + const absl::flat_hash_set cycle_as_set(cycle.begin(), cycle.end()); + std::vector not_in_cycle; + for (int i = 0; i < n; ++i) { + if (!cycle_as_set.contains(i)) { + not_in_cycle.push_back(i); + } + } + math_opt::LinearExpression cutset_edges; + for (const int in_cycle : cycle) { + for (const int out_of_cycle : not_in_cycle) { + cutset_edges += edge_vars.get(in_cycle, out_of_cycle); + } + } + return cutset_edges >= 2; +} + +// Solves the TSP by returning the ordering of the cities that minimizes travel +// distance. +absl::StatusOr SolveTsp( + const std::vector>& cities) { + const int n = cities.size(); + const std::vector> distance_matrix = + DistanceMatrix(cities); + CHECK_GE(n, 3); + math_opt::Model model("tsp"); + const EdgeVariables edge_vars(model, n); + math_opt::LinearExpression edge_cost; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + edge_cost += edge_vars.get(i, j) * distance_matrix[i][j]; + } + } + model.Minimize(edge_cost); + + // Add the degree constraints + for (int i = 0; i < n; ++i) { + math_opt::LinearExpression neighbors; + for (int j = 0; j < n; ++j) { + if (i != j) { + neighbors += edge_vars.get(i, j); + } + } + model.AddLinearConstraint(neighbors == 2, absl::StrCat("n_", i)); + } + math_opt::SolveArguments args; + args.parameters.enable_output = absl::GetFlag(FLAGS_solve_logs); + const int threads = absl::GetFlag(FLAGS_threads); + if (threads > 0) { + args.parameters.threads = threads; + } + args.callback_registration.events.insert( + math_opt::CallbackEvent::kMipSolution); + args.callback_registration.add_lazy_constraints = true; + args.callback = [&edge_vars](const math_opt::CallbackData& cb_data) { + // At event CallbackEvent::kMipSolution, a solution is always present. + CHECK(cb_data.solution.has_value()); + const std::vector cycles = + FindCycles(EdgeValues(edge_vars, *cb_data.solution)); + math_opt::CallbackResult result; + if (cycles.size() > 1) { + for (const Cycle& cycle : cycles) { + result.AddLazyConstraint(CutsetConstraint(cycle, edge_vars)); + } + } + return result; + }; + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, math_opt::SolverType::kGurobi, args)); + if (result.termination.reason != math_opt::TerminationReason::kOptimal) { + return util::InternalErrorBuilder() + << "Expected TSP solve terminate with reason optimal, found: " + << result.termination; + } + std::cout << "Route length: " << result.objective_value() << std::endl; + const std::vector cycles = + FindCycles(EdgeValues(edge_vars, result.variable_values())); + CHECK_EQ(cycles.size(), 1); + CHECK_EQ(cycles[0].size(), n); + return cycles[0]; +} + +// Produces an SVG to draw a route for a TSP. +std::string RouteSvg(const std::vector>& cities, + const Cycle& cycle) { + constexpr int image_px = 1000; + constexpr int r = 5; + constexpr int image_plus_border = image_px + 2 * r; + std::vector svg_lines; + svg_lines.push_back(absl::StrCat("")); + std::vector polygon_coords; + for (const int city : cycle) { + const int x = + static_cast(std::round(cities[city].first * image_px)) + r; + const int y = + static_cast(std::round(cities[city].second * image_px)) + r; + svg_lines.push_back(absl::StrCat("")); + polygon_coords.push_back(absl::StrCat(x, ",", y)); + } + std::string polygon_coords_string = absl::StrJoin(polygon_coords, " "); + svg_lines.push_back( + absl::StrCat("")); + svg_lines.push_back(""); + return absl::StrJoin(svg_lines, "\n"); +} + +void RealMain() { + std::vector> cities; + if (absl::GetFlag(FLAGS_test_instance)) { + cities = TestCities(); + } else { + cities = RandomCities(absl::GetFlag(FLAGS_num_cities)); + } + absl::StatusOr solution = SolveTsp(cities); + if (!solution.ok()) { + LOG(QFATAL) << solution.status(); + } + const std::string svg = RouteSvg(cities, *solution); + if (absl::GetFlag(FLAGS_output).empty()) { + std::cout << svg << std::endl; + } else { + QCHECK_OK( + file::SetContents(absl::GetFlag(FLAGS_output), svg, file::Defaults())); + } +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + RealMain(); + return 0; +} diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 872093da29..725fcbecb3 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -24,8 +24,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":gscip_solver_callback", - ":gscip_solver_message_callback_handler", - "//ortools/base", + ":message_callback_data", "//ortools/base:cleanup", "//ortools/base:map_util", "//ortools/base:protoutil", @@ -36,6 +35,7 @@ cc_library( "//ortools/gscip:gscip_parameters", "//ortools/linear_solver:scip_with_glop", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -54,13 +54,15 @@ cc_library( "//ortools/port:proto_utils", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/log:die_if_null", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", "@com_google_absl//absl/types:span", - "@com_google_protobuf//:protobuf", ], alwayslink = 1, ) @@ -108,13 +110,13 @@ cc_library( ":gurobi_callback", ":gurobi_cc_proto", ":message_callback_data", - "//ortools/base", "//ortools/base:linked_hash_map", "//ortools/base:map_util", "//ortools/base:protoutil", "//ortools/base:status_macros", "//ortools/gurobi:environment", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -134,7 +136,10 @@ cc_library( "//ortools/port:proto_utils", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/memory", + "@com_google_absl//absl/meta:type_traits", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -152,9 +157,7 @@ cc_library( ], visibility = ["//visibility:public"], deps = [ - "//ortools/base", "//ortools/base:cleanup", - "//ortools/base:int_type", "//ortools/base:map_util", "//ortools/base:protoutil", "//ortools/base:status_macros", @@ -165,6 +168,7 @@ cc_library( "//ortools/lp_data", "//ortools/lp_data:base", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -179,8 +183,12 @@ cc_library( "//ortools/math_opt/core:sparse_vector_view", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", + "//ortools/util:logging", + "//ortools/util:strong_integers", "//ortools/util:time_limit", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", @@ -199,12 +207,12 @@ cc_library( ], visibility = ["//visibility:public"], deps = [ - "//ortools/base", "//ortools/base:protoutil", "//ortools/base:status_macros", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver/proto_solver", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -222,6 +230,8 @@ cc_library( "//ortools/port:proto_utils", "//ortools/sat:sat_parameters_cc_proto", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", @@ -236,6 +246,11 @@ cc_library( name = "message_callback_data", srcs = ["message_callback_data.cc"], hdrs = ["message_callback_data.h"], + deps = [ + "//ortools/math_opt/core:solver_interface", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + ], ) cc_library( @@ -259,21 +274,6 @@ cc_library( ], ) -cc_library( - name = "gscip_solver_message_callback_handler", - srcs = ["gscip_solver_message_callback_handler.cc"], - hdrs = ["gscip_solver_message_callback_handler.h"], - deps = [ - ":message_callback_data", - "//ortools/gscip", - "//ortools/gscip:gscip_message_handler", - "//ortools/math_opt/core:solver_interface", - "@com_google_absl//absl/base:core_headers", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/synchronization", - ], -) - cc_library( name = "glpk_solver", srcs = [ @@ -281,8 +281,8 @@ cc_library( "glpk_solver.h", ], deps = [ + ":glpk_cc_proto", ":message_callback_data", - "//ortools/base", "//ortools/base:cleanup", "//ortools/base:protoutil", "//ortools/base:status_macros", @@ -290,6 +290,7 @@ cc_library( "//ortools/glpk:glpk_env_deleter", "//ortools/glpk:glpk_formatters", "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -297,6 +298,7 @@ cc_library( "//ortools/math_opt:result_cc_proto", "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:empty_bounds", "//ortools/math_opt/core:inverted_bounds", "//ortools/math_opt/core:math_opt_proto_utils", "//ortools/math_opt/core:solve_interrupter", @@ -307,13 +309,13 @@ cc_library( "//ortools/math_opt/solvers/glpk:rays", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", - "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", "@glpk", ], @@ -329,3 +331,13 @@ cc_proto_library( name = "gurobi_cc_proto", deps = [":gurobi_proto"], ) + +cc_proto_library( + name = "glpk_cc_proto", + deps = [":glpk_proto"], +) + +proto_library( + name = "glpk_proto", + srcs = ["glpk.proto"], +) diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index 2689d3b963..32b8e728fb 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -13,7 +13,6 @@ #include "ortools/math_opt/solvers/cp_sat_solver.h" -#include #include #include #include @@ -25,6 +24,7 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -32,10 +32,10 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "absl/types/span.h" -#include "absl/log/check.h" #include "ortools/base/logging.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" @@ -47,6 +47,7 @@ #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/io/proto_converter.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" @@ -113,6 +114,10 @@ std::vector SetSolveParameters( request.set_solver_time_limit_seconds(absl::ToDoubleSeconds( util_time::DecodeGoogleApiProto(parameters.time_limit()).value())); } + if (parameters.has_iteration_limit()) { + warnings.push_back( + "The iteration_limit parameter is not supported for CP-SAT."); + } if (parameters.has_node_limit()) { warnings.push_back("The node_limit parameter is not supported for CP-SAT."); } @@ -167,7 +172,7 @@ std::vector SetSolveParameters( } if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { warnings.push_back( - absl::StrCat("Setting the LP Algorithm (was set to ", + absl::StrCat("Setting lp_algorithm (was set to ", ProtoEnumToString(parameters.lp_algorithm()), ") is not supported for CP_SAT solver")); } @@ -421,7 +426,7 @@ absl::StatusOr CpSatSolver::Solve( std::function logging_callback; if (message_cb != nullptr) { - logging_callback = [&](const std::string& message) { + logging_callback = [&](absl::string_view message) { message_cb(absl::StrSplit(message, '\n')); }; } @@ -548,6 +553,13 @@ InvertedBounds CpSatSolver::ListInvertedBounds() const { return inverted_bounds; } +absl::StatusOr CpSatSolver::InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* const interrupter) { + return absl::UnimplementedError( + "CPSAT does not provide a method to compute an infeasible subsystem"); +} + MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_CP_SAT, CpSatSolver::New); } // namespace math_opt diff --git a/ortools/math_opt/solvers/cp_sat_solver.h b/ortools/math_opt/solvers/cp_sat_solver.h index ed46448a84..9e4ff67808 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.h +++ b/ortools/math_opt/solvers/cp_sat_solver.h @@ -18,7 +18,6 @@ #include #include -#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/types/span.h" #include "ortools/linear_solver/linear_solver.pb.h" @@ -26,6 +25,7 @@ #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -48,6 +48,9 @@ class CpSatSolver : public SolverInterface { const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* interrupter) override; absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) override; private: CpSatSolver(MPModelProto cp_sat_model, std::vector variable_ids, diff --git a/ortools/math_opt/solvers/glop_solver.cc b/ortools/math_opt/solvers/glop_solver.cc index e8f8056fc8..dab090a9d6 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -24,6 +24,7 @@ #include #include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -35,8 +36,6 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/base/cleanup.h" -#include "ortools/base/int_type.h" -#include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/protoutil.h" @@ -48,10 +47,12 @@ #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" #include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -61,6 +62,8 @@ #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/validators/callback_validator.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/logging.h" +#include "ortools/util/strong_integers.h" #include "ortools/util/time_limit.h" namespace operations_research { @@ -281,7 +284,8 @@ void GlopSolver::UpdateLinearConstraintBounds( absl::StatusOr GlopSolver::MergeSolveParameters( const SolveParametersProto& solve_parameters, - const bool setting_initial_basis, const bool has_message_callback) { + const bool setting_initial_basis, const bool has_message_callback, + const bool is_maximization) { // Validate first the user specific Glop parameters. RETURN_IF_ERROR(ValidateGlopParameters(solve_parameters.glop())) << "invalid SolveParametersProto.glop value"; @@ -394,11 +398,32 @@ absl::StatusOr GlopSolver::MergeSolveParameters( if (solve_parameters.has_cutoff_limit()) { warnings.push_back("GLOP does not support 'cutoff_limit' parameter"); } + // Solver stops once optimal objective is proven strictly greater than limit. + // limit. + const auto set_upper_limit_if_missing = [&result](const double limit) { + if (!result.has_objective_upper_limit()) { + result.set_objective_upper_limit(limit); + } + }; + // Solver stops once optimal objective is proven strictly less than limit. + const auto set_lower_limit_if_missing = [&result](const double limit) { + if (!result.has_objective_lower_limit()) { + result.set_objective_lower_limit(limit); + } + }; if (solve_parameters.has_objective_limit()) { - warnings.push_back("GLOP does not support 'objective_limit' parameter"); + if (is_maximization) { + set_upper_limit_if_missing(solve_parameters.objective_limit()); + } else { + set_lower_limit_if_missing(solve_parameters.objective_limit()); + } } if (solve_parameters.has_best_bound_limit()) { - warnings.push_back("GLOP does not support 'best_bound_limit' parameter"); + if (is_maximization) { + set_lower_limit_if_missing(solve_parameters.best_bound_limit()); + } else { + set_upper_limit_if_missing(solve_parameters.best_bound_limit()); + } } if (solve_parameters.has_solution_limit()) { warnings.push_back("GLOP does not support 'solution_limit' parameter"); @@ -807,7 +832,8 @@ absl::StatusOr GlopSolver::Solve( MergeSolveParameters( parameters, /*setting_initial_basis=*/model_parameters.has_initial_basis(), - /*has_message_callback=*/message_cb != nullptr)); + /*has_message_callback=*/message_cb != nullptr, + linear_program_.IsMaximizationProblem())); lp_solver_.SetParameters(glop_parameters); if (model_parameters.has_initial_basis()) { @@ -834,7 +860,7 @@ absl::StatusOr GlopSolver::Solve( // all in the cleanup below. CHECK_EQ(lp_solver_.GetSolverLogger().NumInfoLoggingCallbacks(), 0); lp_solver_.GetSolverLogger().AddInfoLoggingCallback( - [&](const std::string& message) { + [&](absl::string_view message) { message_cb(absl::StrSplit(message, '\n')); }); } @@ -912,6 +938,13 @@ absl::StatusOr GlopSolver::Update(const ModelUpdateProto& model_update) { return true; } +absl::StatusOr GlopSolver::InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) { + return absl::UnimplementedError( + "GLOP does not implement a method to compute an infeasible subsystem"); +} + MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GLOP, GlopSolver::New) } // namespace math_opt diff --git a/ortools/math_opt/solvers/glop_solver.h b/ortools/math_opt/solvers/glop_solver.h index 7746f66ee4..eb42ef1c4e 100644 --- a/ortools/math_opt/solvers/glop_solver.h +++ b/ortools/math_opt/solvers/glop_solver.h @@ -16,15 +16,12 @@ #include -#include #include -#include -#include -#include #include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/glop/lp_solver.h" #include "ortools/glop/parameters.pb.h" @@ -34,6 +31,7 @@ #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -57,12 +55,15 @@ class GlopSolver : public SolverInterface { const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* interrupter) override; absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) override; // Returns the merged parameters and a list of warnings from any parameter // settings that are invalid for this solver. static absl::StatusOr MergeSolveParameters( const SolveParametersProto& solver_parameters, bool setting_initial_basis, - bool has_message_callback); + bool has_message_callback, bool is_maximization); private: GlopSolver(); diff --git a/ortools/math_opt/solvers/glpk.proto b/ortools/math_opt/solvers/glpk.proto new file mode 100644 index 0000000000..aa9492d1ac --- /dev/null +++ b/ortools/math_opt/solvers/glpk.proto @@ -0,0 +1,42 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Proto messages specific to GLPK. +syntax = "proto3"; + +package operations_research.math_opt; + +// GLPK specific parameters for solving. +// +// Fields are optional to enable to capture user intention; if they set +// explicitly a value to then no generic solve parameters will overwrite this +// parameter. User specified solver specific parameters have priority on generic +// parameters. +message GlpkParametersProto { + // Compute the primal or dual unbound ray when the variable (structural or + // auxiliary) causing the unboundness is identified (see glp_get_unbnd_ray()). + // + // The unset value is equivalent to false. + // + // Rays are only available when solving linear programs, they are not + // available for MIPs. On top of that they are only available when using a + // simplex algorithm with the presolve disabled. + // + // A primal ray can only be built if the chosen LP algorithm is + // LP_ALGORITHM_PRIMAL_SIMPLEX. Same for a dual ray and + // LP_ALGORITHM_DUAL_SIMPLEX. + // + // The computation involves the basis factorization to be available which may + // lead to extra computations/errors. + optional bool compute_unbound_rays_if_possible = 1; +} diff --git a/ortools/math_opt/solvers/glpk_solver.cc b/ortools/math_opt/solvers/glpk_solver.cc index 0683f42c94..de98721baf 100644 --- a/ortools/math_opt/solvers/glpk_solver.cc +++ b/ortools/math_opt/solvers/glpk_solver.cc @@ -23,21 +23,18 @@ #include #include #include -#include -#include -#include +#include // IWYU pragma: keep #include #include -#include "absl/base/thread_annotations.h" #include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" -#include "absl/synchronization/mutex.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "ortools/base/cleanup.h" @@ -47,18 +44,21 @@ #include "ortools/glpk/glpk_env_deleter.h" #include "ortools/glpk/glpk_formatters.h" #include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/empty_bounds.h" #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_submatrix.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/result.pb.h" #include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/solvers/glpk.pb.h" #include "ortools/math_opt/solvers/glpk/glpk_sparse_vector.h" #include "ortools/math_opt/solvers/glpk/rays.h" #include "ortools/math_opt/solvers/message_callback_data.h" @@ -388,19 +388,31 @@ absl::Status SetSharedParameters(const SolveParametersProto& parameters, glpk_parameters.msg_lev = GLP_MSG_OFF; } if (parameters.has_node_limit()) { - warnings.push_back("Parameter node_limit not supported by GLPK"); + warnings.push_back("parameter node_limit not supported by GLPK"); } if (parameters.has_objective_limit()) { - warnings.push_back("Parameter objective_limit not supported by GLPK"); + warnings.push_back("parameter objective_limit not supported by GLPK"); } if (parameters.has_best_bound_limit()) { - warnings.push_back("Parameter best_bound_limit not supported by GLPK"); + warnings.push_back("parameter best_bound_limit not supported by GLPK"); } if (parameters.has_cutoff_limit()) { - warnings.push_back("Parameter cutoff_limit not supported by GLPK"); + warnings.push_back("parameter cutoff_limit not supported by GLPK"); } if (parameters.has_solution_limit()) { - warnings.push_back("Parameter solution_limit not supported by GLPK"); + warnings.push_back("parameter solution_limit not supported by GLPK"); + } + if (parameters.has_random_seed()) { + warnings.push_back("parameter random_seed not supported by GLPK"); + } + if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter cuts not supported by GLPK"); + } + if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter heuristics not supported by GLPK"); + } + if (parameters.scaling() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter scaling not supported by GLPK"); } if (!warnings.empty()) { return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); @@ -428,6 +440,11 @@ void SetTimeLimitParameter(const SolveParametersProto& parameters, absl::Status SetLPParameters(const SolveParametersProto& parameters, glp_smcp& glpk_parameters) { std::vector warnings; + if (parameters.has_iteration_limit()) { + int limit = static_cast(std::min( + std::numeric_limits::max(), parameters.iteration_limit())); + glpk_parameters.it_lim = limit; + } switch (parameters.presolve()) { case EMPHASIS_UNSPECIFIED: // Keep the default. @@ -534,14 +551,21 @@ void MipCallback(glp_tree* const tree, void* const info) { } // Returns the MathOpt ids of the rows/columns with lower_bound > upper_bound. +// +// For variables we use the unrounded bounds as we don't want to return a +// failing status when rounded bounds of integer variables cross due to the +// rounding. See EmptyIntegerBoundsResult() for dealing with this case. InvertedBounds ListInvertedBounds( glp_prob* const problem, const std::vector& variable_ids, + const std::vector& unrounded_variable_lower_bounds, + const std::vector& unrounded_variable_upper_bounds, const std::vector& linear_constraint_ids) { InvertedBounds inverted_bounds; const int num_cols = glp_get_num_cols(problem); for (int c = 1; c <= num_cols; ++c) { - if (glp_get_col_lb(problem, c) > glp_get_col_ub(problem, c)) { + if (unrounded_variable_lower_bounds[c - 1] > + unrounded_variable_upper_bounds[c - 1]) { inverted_bounds.variables.push_back(variable_ids[c - 1]); } } @@ -708,7 +732,7 @@ absl::StatusOr SimplexTerminationOnSuccess( // The parameters `(variable|linear_constraint)_ids` are the // `GlpkSolver::(LinearConstraints|Variables)::ids`. absl::StatusOr BuildTermination( - glp_prob* const problem, const std::string_view fn_name, const int rc, + glp_prob* const problem, const absl::string_view fn_name, const int rc, const std::function(glp_prob*)> termination_on_success, MipCallbackData* const mip_cb_data, const bool has_feasible_solution, @@ -729,12 +753,10 @@ absl::StatusOr BuildTermination( // GLP_EBOUND is returned when a variable or a constraint has the GLP_DB // bounds type and lower_bound >= upper_bound. The code in this file makes // sure we don't use GLP_DB but GLP_FX when lower_bound == upper_bound - // thus we expect GLP_EBOUND only when lower_bound > upper_bound. - RETURN_IF_ERROR( - ListInvertedBounds(problem, - /*variable_ids=*/variable_ids, - /*linear_constraint_ids=*/linear_constraint_ids) - .ToStatus()); + // thus we expect GLP_EBOUND only when lower_bound > upper_bound. This + // should never happen as we call ListInvertedBounds() and + // EmptyIntegerBoundsResult() before we call GLPK. Thus we don't expect + // GLP_EBOUND to happen. return util::InternalErrorBuilder() << fn_name << "() returned `" << ReturnCodeString(rc) << "` but the model does not contain variables with inverted " @@ -748,10 +770,7 @@ absl::StatusOr BuildTermination( case GLP_EMIPGAP: return TerminateForReason( TERMINATION_REASON_OPTIMAL, - // absl::StrCat() does not compile with std::string_view on WASM. - // - absl::StrCat(std::string(fn_name), "() returned ", - ReturnCodeString(rc))); + absl::StrCat(fn_name, "() returned ", ReturnCodeString(rc))); case GLP_ESTOP: return TerminateForLimit(LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution); @@ -771,10 +790,7 @@ absl::StatusOr BuildTermination( // Numeric stability solving Newtonian system (for glp_interior). return TerminateForReason( TERMINATION_REASON_NUMERICAL_ERROR, - // absl::StrCat() does not compile with std::string_view on WASM. - // - absl::StrCat(std::string(fn_name), "() returned ", - ReturnCodeString(rc), + absl::StrCat(fn_name, "() returned ", ReturnCodeString(rc), " which means that there is a numeric stability issue " "solving Newtonian system")); default: @@ -784,44 +800,11 @@ absl::StatusOr BuildTermination( } } -class TermHookData { - public: - explicit TermHookData(SolverInterface::MessageCallback callback) - : callback_(std::move(callback)) {} - - void Parse(const std::string_view message) { - // Here we keep the lock while calling the callback. This should not be an - // issue since we don't expect code in a message callback to trigger a new - // message. On top of that, for proper interleaving it may be better to use - // the lock anyway. - const absl::MutexLock lock(&mutex_); - std::vector new_lines = buffer_.Parse(message); - if (!new_lines.empty()) { - callback_(new_lines); - } - } - - // Flushes the buffer and calls the callback if the result is not empty. - void Flush() { - // See comment in Parse() about holding the lock while calling the callback. - const absl::MutexLock lock(&mutex_); - std::vector new_lines = buffer_.Flush(); - if (!new_lines.empty()) { - callback_(new_lines); - } - } - - private: - absl::Mutex mutex_; - MessageCallbackData buffer_ ABSL_GUARDED_BY(mutex_); - const SolverInterface::MessageCallback callback_; -}; - // Callback for glp_term_hook(). // // It expects `info` to be a pointer on a TermHookData. int TermHook(void* const info, const char* const message) { - static_cast(info)->Parse(message); + static_cast(info)->OnMessage(message); // Returns non-zero to remove any terminal output. return 1; @@ -1019,25 +1002,52 @@ absl::StatusOr GlpkSolver::Solve( const absl::Time start = absl::Now(); + const auto set_solve_time = + [&start](SolveResultProto& result) -> absl::Status { + RETURN_IF_ERROR(util_time::EncodeGoogleApiProto( + absl::Now() - start, + result.mutable_solve_stats()->mutable_solve_time())) + << "failed to set SolveResultProto.solve_stats.solve_time"; + return absl::OkStatus(); + }; + + RETURN_IF_ERROR( + ListInvertedBounds( + problem_, + /*variable_ids=*/variables_.ids, + /*unrounded_variable_lower_bounds=*/variables_.unrounded_lower_bounds, + /*unrounded_variable_upper_bounds=*/variables_.unrounded_upper_bounds, + /*linear_constraint_ids=*/linear_constraints_.ids) + .ToStatus()); + + // Deal with empty integer bounds that result in inverted bounds due to bounds + // rounding. + { // Limit scope of `result`. + std::optional result = EmptyIntegerBoundsResult(); + if (result.has_value()) { + RETURN_IF_ERROR(set_solve_time(result.value())); + return std::move(result).value(); + } + } + RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, /*supported_events=*/{})); - std::unique_ptr term_hook_data; - if (message_cb != nullptr) { - term_hook_data = std::make_unique(std::move(message_cb)); - + BufferedMessageCallback term_hook_data(std::move(message_cb)); + if (term_hook_data.has_user_message_callback()) { // Note that glp_term_hook() uses get_env_ptr() that relies on thread local // storage to have a different environment per thread. Thus using // glp_term_hook() is thread-safe. // - glp_term_hook(TermHook, term_hook_data.get()); + glp_term_hook(TermHook, &term_hook_data); } // We must reset the term hook when before exiting or before flushing the last // unfinished line. auto message_cb_cleanup = absl::MakeCleanup([&]() { - if (term_hook_data != nullptr) { + if (term_hook_data.has_user_message_callback()) { glp_term_hook(/*func=*/nullptr, /*info=*/nullptr); + term_hook_data.Flush(); } }); @@ -1076,14 +1086,34 @@ absl::StatusOr GlpkSolver::Solve( glp_iocp glpk_parameters; glp_init_iocp(&glpk_parameters); RETURN_IF_ERROR(SetSharedParameters( - parameters, - /*has_message_callback=*/term_hook_data != nullptr, glpk_parameters)); + parameters, term_hook_data.has_user_message_callback(), + glpk_parameters)); SetTimeLimitParameter(parameters, glpk_parameters); // TODO(b/187027049): glp_intopt with presolve off requires an optional // solution of the relaxed problem. Here we simply always enable pre-solve // but we should support disabling the presolve and call glp_simplex() in // that case. glpk_parameters.presolve = GLP_ON; + if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { + return util::InvalidArgumentErrorBuilder() + << "parameter presolve not supported by GLPK for MIP"; + } + if (parameters.has_relative_gap_tolerance()) { + glpk_parameters.mip_gap = parameters.relative_gap_tolerance(); + } + if (parameters.has_absolute_gap_tolerance()) { + return util::InvalidArgumentErrorBuilder() + << "parameter absolute_gap_tolerance not supported by GLPK " + "(relative_gap_tolerance is supported)"; + } + if (parameters.has_iteration_limit()) { + return util::InvalidArgumentErrorBuilder() + << "parameter iteration_limit not supported by GLPK for MIP"; + } + if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + return util::InvalidArgumentErrorBuilder() + << "parameter lp_algorithm not supported by GLPK for MIP"; + } MipCallbackData mip_cb_data(interrupter); glpk_parameters.cb_func = MipCallback; glpk_parameters.cb_info = &mip_cb_data; @@ -1118,12 +1148,12 @@ absl::StatusOr GlpkSolver::Solve( glp_init_iptcp(&glpk_parameters); if (parameters.has_time_limit()) { return absl::InvalidArgumentError( - "Parameter time_limit not supported by GLPK for interior point " - "algorithm."); + "parameter time_limit not supported by GLPK for interior point " + "algorithm"); } RETURN_IF_ERROR(SetSharedParameters( - parameters, - /*has_message_callback=*/term_hook_data != nullptr, glpk_parameters)); + parameters, term_hook_data.has_user_message_callback(), + glpk_parameters)); // glp_interior() does not support being called with an empty model and // returns GLP_EFAIL. Thus we use placeholders in that case. @@ -1175,8 +1205,8 @@ absl::StatusOr GlpkSolver::Solve( glp_smcp glpk_parameters; glp_init_smcp(&glpk_parameters); RETURN_IF_ERROR(SetSharedParameters( - parameters, - /*has_message_callback=*/term_hook_data != nullptr, glpk_parameters)); + parameters, term_hook_data.has_user_message_callback(), + glpk_parameters)); SetTimeLimitParameter(parameters, glpk_parameters); RETURN_IF_ERROR(SetLPParameters(parameters, glpk_parameters)); @@ -1206,13 +1236,8 @@ absl::StatusOr GlpkSolver::Solve( } } - // Flushes the potential last unfinished line. - if (term_hook_data != nullptr) { - // Make sure no calls happen to the message callback before we flush. - std::move(message_cb_cleanup).Invoke(); - term_hook_data->Flush(); - term_hook_data.reset(); - } + // Unregister the callback and flush the potential last unfinished line. + std::move(message_cb_cleanup).Invoke(); double best_primal_bound = maximize ? -kInf : kInf; switch (get_prim_stat(problem_)) { @@ -1237,13 +1262,11 @@ absl::StatusOr GlpkSolver::Solve( solution.has_basis()) { *result.add_solutions() = std::move(solution); } - // TODO(b/200695800): add a parameter to enable the computation of the - // rays. This involves matrices inversion so this is not free to compute and - // should thus be only done when the user wants it. - RETURN_IF_ERROR(AddPrimalOrDualRay(model_parameters, result)); + if (parameters.glpk().compute_unbound_rays_if_possible()) { + RETURN_IF_ERROR(AddPrimalOrDualRay(model_parameters, result)); + } - CHECK_OK(util_time::EncodeGoogleApiProto( - absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time())); + RETURN_IF_ERROR(set_solve_time(result)); return result; } @@ -1517,7 +1540,7 @@ absl::Status GlpkSolver::AddPrimalOrDualRay( const int num_cstrs = linear_constraints_.ids.size(); switch (opt_unbound_ray->type) { case GlpkRayType::kPrimal: { - const int num_cstrs = linear_constraints_.ids.size(); + const int num_cstrs = static_cast(linear_constraints_.ids.size()); // Note that GlpkComputeUnboundRay() returned ray considers the variables // of the computational form. Thus it contains both structural and // auxiliary variables. In the MathOpt's primal ray we only consider @@ -1667,6 +1690,42 @@ absl::Status GlpkSolver::CheckCurrentThread() { return absl::OkStatus(); } +std::optional GlpkSolver::EmptyIntegerBoundsResult() { + const int num_cols = glp_get_num_cols(problem_); + for (int c = 1; c <= num_cols; ++c) { + if (!variables_.IsInteger(problem_, c)) { + continue; + } + const double lb = variables_.unrounded_lower_bounds[c - 1]; + const double ub = variables_.unrounded_upper_bounds[c - 1]; + if (lb > ub) { + // Unrounded bounds are inverted; this case is covered by + // ListInvertedBounds(). We don't want to depend on the order of calls of + // the two functions here so we exclude this case. + continue; + } + if (std::ceil(lb) <= std::floor(ub)) { + continue; + } + + // We found a variable with empty integer bounds (that is lb <= ub but + // ceil(lb) > floor(ub)). + return ResultForIntegerInfeasible( + /*is_maximize=*/glp_get_obj_dir(problem_) == GLP_MAX, + /*bad_variable_id=*/variables_.ids[c - 1], + /*lb=*/lb, /*ub=*/ub); + } + + return std::nullopt; +} + +absl::StatusOr GlpkSolver::InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* const interrupter) { + return absl::UnimplementedError( + "GLPK does not provide a method to compute an infeasible subsystem"); +} + MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GLPK, GlpkSolver::New) } // namespace math_opt diff --git a/ortools/math_opt/solvers/glpk_solver.h b/ortools/math_opt/solvers/glpk_solver.h index ec2ae9631e..d011b460e7 100644 --- a/ortools/math_opt/solvers/glpk_solver.h +++ b/ortools/math_opt/solvers/glpk_solver.h @@ -27,11 +27,14 @@ #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" extern "C" { #include @@ -54,6 +57,9 @@ class GlpkSolver : public SolverInterface { const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* interrupter) override; absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) override; private: // The columns of the GPLK problem. @@ -193,6 +199,14 @@ class GlpkSolver : public SolverInterface { // Returns an error if the current thread is no thread_id_. absl::Status CheckCurrentThread(); + // Returns an "infeasible" result if the model has integer variables with + // empty bounds. + // + // Integer variables' bounds have to be rounded when passed to GLPK. Thus when + // the bounds don't contain an integer point (e.g. lb:3.5 ub:3.6) we end up + // with inverted bounds (e.g. lb:4 ub:3). + std::optional EmptyIntegerBoundsResult(); + // Id of the thread where GlpkSolver was called. const std::thread::id thread_id_; diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index f798288376..f785fe66fa 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -26,6 +26,8 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" +#include "absl/log/die_if_null.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -35,14 +37,10 @@ #include "absl/time/clock.h" #include "absl/time/time.h" #include "absl/types/span.h" -#include "google/protobuf/map.h" -#include "absl/log/check.h" #include "ortools/base/cleanup.h" -#include "absl/log/die_if_null.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/protoutil.h" -#include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" #include "ortools/gscip/gscip.h" #include "ortools/gscip/gscip.pb.h" @@ -56,6 +54,7 @@ #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_submatrix.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -63,7 +62,7 @@ #include "ortools/math_opt/result.pb.h" #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/solvers/gscip_solver_callback.h" -#include "ortools/math_opt/solvers/gscip_solver_message_callback_handler.h" +#include "ortools/math_opt/solvers/message_callback_data.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/validators/callback_validator.h" #include "ortools/port/proto_utils.h" @@ -681,41 +680,47 @@ absl::StatusOr GScipSolver::MergeParameters( util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value(), &result); } - - if (solve_parameters.has_threads()) { - GScipSetMaxNumThreads(solve_parameters.threads(), &result); - } - - if (solve_parameters.has_relative_gap_tolerance()) { - (*result.mutable_real_params())["limits/gap"] = - solve_parameters.relative_gap_tolerance(); - } - - if (solve_parameters.has_absolute_gap_tolerance()) { - (*result.mutable_real_params())["limits/absgap"] = - solve_parameters.absolute_gap_tolerance(); + if (solve_parameters.has_iteration_limit()) { + warnings.push_back("parameter iteration_limit not supported for gSCIP."); } if (solve_parameters.has_node_limit()) { (*result.mutable_long_params())["limits/totalnodes"] = solve_parameters.node_limit(); } - + if (solve_parameters.has_cutoff_limit()) { + result.set_objective_limit(solve_parameters.cutoff_limit()); + } if (solve_parameters.has_objective_limit()) { warnings.push_back("parameter objective_limit not supported for gSCIP."); } if (solve_parameters.has_best_bound_limit()) { warnings.push_back("parameter best_bound_limit not supported for gSCIP."); } - - if (solve_parameters.has_cutoff_limit()) { - result.set_objective_limit(solve_parameters.cutoff_limit()); - } - if (solve_parameters.has_solution_limit()) { (*result.mutable_int_params())["limits/solutions"] = solve_parameters.solution_limit(); } - + // GScip has also GScipSetOutputEnabled() but this changes the log + // level. Setting `silence_output` sets the `quiet` field on the default + // message handler of SCIP which removes the output. Here it is important to + // use this rather than changing the log level so that if the user provides + // a MessageCallback function they do get some messages even when + // `enable_output` is false. + result.set_silence_output(!solve_parameters.enable_output()); + if (solve_parameters.has_threads()) { + GScipSetMaxNumThreads(solve_parameters.threads(), &result); + } + if (solve_parameters.has_random_seed()) { + GScipSetRandomSeed(&result, solve_parameters.random_seed()); + } + if (solve_parameters.has_absolute_gap_tolerance()) { + (*result.mutable_real_params())["limits/absgap"] = + solve_parameters.absolute_gap_tolerance(); + } + if (solve_parameters.has_relative_gap_tolerance()) { + (*result.mutable_real_params())["limits/gap"] = + solve_parameters.relative_gap_tolerance(); + } if (solve_parameters.has_solution_pool_size()) { result.set_num_solutions(solve_parameters.solution_pool_size()); // We must set limits/maxsol (the internal solution pool) and @@ -729,20 +734,8 @@ absl::StatusOr GScipSolver::MergeParameters( solve_parameters.solution_pool_size(); } - // GScip has also GScipSetOutputEnabled() but this changes the log - // level. Setting `silence_output` sets the `quiet` field on the default - // message handler of SCIP which removes the output. Here it is important to - // use this rather than changing the log level so that if the user registers - // for CALLBACK_EVENT_MESSAGE they do get some messages even when - // `enable_output` is false. - result.set_silence_output(!solve_parameters.enable_output()); - - if (solve_parameters.has_random_seed()) { - GScipSetRandomSeed(&result, solve_parameters.random_seed()); - } - if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { - char alg; + char alg = 's'; switch (solve_parameters.lp_algorithm()) { case LP_ALGORITHM_PRIMAL_SIMPLEX: alg = 'p'; @@ -751,8 +744,17 @@ absl::StatusOr GScipSolver::MergeParameters( alg = 'd'; break; case LP_ALGORITHM_BARRIER: + // As SCIP is configured in ortools, this is an error, since we are not + // connected to any LP solver that runs barrier. + warnings.push_back( + "parameter lp_algorithm with value BARRIER is not supported for " + "gSCIP in ortools."); alg = 'c'; break; + case LP_ALGORITHM_FIRST_ORDER: + warnings.push_back( + "parameter lp_algorithm with value FIRST_ORDER is not supported."); + break; default: LOG(FATAL) << "LPAlgorithm: " << ProtoEnumToString(solve_parameters.lp_algorithm()) @@ -760,7 +762,9 @@ absl::StatusOr GScipSolver::MergeParameters( } (*result.mutable_char_params())["lp/initalgorithm"] = alg; } - + if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) { + result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve())); + } if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) { result.set_separating(ConvertMathOptEmphasis(solve_parameters.cuts())); } @@ -768,9 +772,6 @@ absl::StatusOr GScipSolver::MergeParameters( result.set_heuristics( ConvertMathOptEmphasis(solve_parameters.heuristics())); } - if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) { - result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve())); - } if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) { int scaling_value; switch (solve_parameters.scaling()) { @@ -803,13 +804,13 @@ absl::StatusOr GScipSolver::MergeParameters( namespace { -std::string JoinDetails(const std::string& gscip_detail, - const std::string& math_opt_detail) { +std::string JoinDetails(absl::string_view gscip_detail, + absl::string_view math_opt_detail) { if (gscip_detail.empty()) { - return math_opt_detail; + return std::string(math_opt_detail); } if (math_opt_detail.empty()) { - return gscip_detail; + return std::string(gscip_detail); } return absl::StrCat(gscip_detail, "; ", math_opt_detail); } @@ -1083,11 +1084,9 @@ absl::StatusOr GScipSolver::Solve( GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb, start, gscip_->scip()); - std::unique_ptr message_cb_handler; - if (message_cb != nullptr) { - message_cb_handler = - std::make_unique(message_cb); - } + BufferedMessageCallback buffered_message_callback(std::move(message_cb)); + auto message_cb_cleanup = absl::MakeCleanup( + [&buffered_message_callback]() { buffered_message_callback.Flush(); }); ASSIGN_OR_RETURN(auto gscip_parameters, MergeParameters(parameters)); @@ -1115,15 +1114,21 @@ absl::StatusOr GScipSolver::Solve( RETURN_IF_ERROR(ListInvertedBounds().ToStatus()); RETURN_IF_ERROR(ListInvalidIndicators().ToStatus()); - ASSIGN_OR_RETURN(GScipResult gscip_result, - gscip_->Solve(gscip_parameters, - /*legacy_params=*/"", - message_cb_handler != nullptr - ? message_cb_handler->MessageHandler() - : nullptr)); + GScipMessageHandler gscip_msg_cb = nullptr; + if (buffered_message_callback.has_user_message_callback()) { + gscip_msg_cb = [&buffered_message_callback]( + const auto, const absl::string_view message) { + buffered_message_callback.OnMessage(message); + }; + } - // Flushes the last unfinished message as early as possible. - message_cb_handler.reset(); + ASSIGN_OR_RETURN( + GScipResult gscip_result, + gscip_->Solve(gscip_parameters, + /*legacy_params=*/"", std::move(gscip_msg_cb))); + + // Flush the potential last unfinished line. + std::move(message_cb_cleanup).Invoke(); if (callback_handler) { RETURN_IF_ERROR(callback_handler->Flush()); @@ -1423,6 +1428,13 @@ SCIP_RETCODE GScipSolver::InterruptEventHandler::TryCallInterruptIfNeeded( } } +absl::StatusOr GScipSolver::InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* const interrupter) { + return absl::UnimplementedError( + "SCIP does not provide a method to compute an infeasible subsystem"); +} + MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GSCIP, GScipSolver::New) } // namespace math_opt diff --git a/ortools/math_opt/solvers/gscip_solver.h b/ortools/math_opt/solvers/gscip_solver.h index e9b4d29a7c..9cc9f86684 100644 --- a/ortools/math_opt/solvers/gscip_solver.h +++ b/ortools/math_opt/solvers/gscip_solver.h @@ -25,8 +25,6 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/types/span.h" -#include "google/protobuf/map.h" -#include "ortools/base/status_macros.h" #include "ortools/gscip/gscip.h" #include "ortools/gscip/gscip.pb.h" #include "ortools/gscip/gscip_event_handler.h" @@ -35,6 +33,7 @@ #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -59,6 +58,9 @@ class GScipSolver : public SolverInterface { const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* interrupter) override; absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) override; // Returns the merged parameters and a list of warnings for unsupported // parameters. diff --git a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc deleted file mode 100644 index 3806628a19..0000000000 --- a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/solvers/gscip_solver_message_callback_handler.h" - -#include -#include -#include -#include - -#include "absl/strings/string_view.h" -#include "absl/synchronization/mutex.h" -#include "ortools/gscip/gscip_message_handler.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "ortools/math_opt/solvers/message_callback_data.h" - -namespace operations_research { -namespace math_opt { - -GScipSolverMessageCallbackHandler::GScipSolverMessageCallbackHandler( - SolverInterface::MessageCallback message_callback) - : message_callback_(std::move(message_callback)) {} - -GScipSolverMessageCallbackHandler::~GScipSolverMessageCallbackHandler() { - const absl::MutexLock lock(&message_mutex_); - const std::vector lines = message_callback_data_.Flush(); - if (!lines.empty()) { - message_callback_(lines); - } -} - -GScipMessageHandler GScipSolverMessageCallbackHandler::MessageHandler() { - return std::bind(&GScipSolverMessageCallbackHandler::MessageCallback, this, - std::placeholders::_1, std::placeholders::_2); -} - -void GScipSolverMessageCallbackHandler::MessageCallback( - GScipMessageType, const absl::string_view message) { - const absl::MutexLock lock(&message_mutex_); - const std::vector lines = message_callback_data_.Parse(message); - if (!lines.empty()) { - message_callback_(lines); - } -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h deleted file mode 100644 index a6119523aa..0000000000 --- a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_ -#define OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_ - -#include "absl/base/thread_annotations.h" -#include "absl/strings/string_view.h" -#include "absl/synchronization/mutex.h" -#include "ortools/gscip/gscip.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "ortools/math_opt/solvers/message_callback_data.h" - -namespace operations_research { -namespace math_opt { - -// Handler for message callbacks. -// -// The message callback is called on calls to MessageHandler() and when this -// object is destroyed (i.e. when we flush the message callback data). Doing so -// in the destructor ensures that even in case of solver failure we do call the -// message callback with the last pending messages before returning the error. -// -// Usage: -// -// std:unique_ptr message_callback_handler; -// if (message_callback != nullptr) { -// message_callback_handler = -// std::make_unique(message_callback); -// } -// -// GScip* gscip = ...; -// RETURN_IF_ERROR( -// gscip->Solve(..., -// message_callback_handler != nullptr -// ? message_callback_handler.MessageHandler() -// : nullptr); -// -// // Flush the last unset message as soon as the solve is done. GScip won't -// // call the MessageHandler() after the end of the solve so there is no need -// // to wait here. -// message_callback_handler.reset(); -// -// ... -class GScipSolverMessageCallbackHandler { - public: - // The input callback must not be null. - explicit GScipSolverMessageCallbackHandler( - SolverInterface::MessageCallback message_callback); - - // Calls the message callback with the last unfinished line if it exists. - ~GScipSolverMessageCallbackHandler(); - - GScipSolverMessageCallbackHandler(const GScipSolverMessageCallbackHandler&) = - delete; - GScipSolverMessageCallbackHandler& operator=( - const GScipSolverMessageCallbackHandler&) = delete; - - // Returns the handler to pass to GScip::Solve(). - GScipMessageHandler MessageHandler(); - - private: - // Updates message_callback_data_ and makes the call to the message callback - // if necessary. This method has the expected signature for a - // GScipMessageHandler. - void MessageCallback(GScipMessageType, absl::string_view message); - - // Mutex serializing access to message_callback_data_ and the serialization of - // calls to the message callback. - absl::Mutex message_mutex_; - - // The message callback; never nullptr. The message_mutex_ should be held - // while calling it to ensure proper ordering of the messages. - const SolverInterface::MessageCallback message_callback_ - ABSL_GUARDED_BY(message_mutex_); - - // The buffer used to generate the message events. - MessageCallbackData message_callback_data_ ABSL_GUARDED_BY(message_mutex_); -}; - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_ diff --git a/ortools/math_opt/solvers/gurobi/BUILD.bazel b/ortools/math_opt/solvers/gurobi/BUILD.bazel index f8277095f7..311999a8ec 100644 --- a/ortools/math_opt/solvers/gurobi/BUILD.bazel +++ b/ortools/math_opt/solvers/gurobi/BUILD.bazel @@ -28,6 +28,7 @@ cc_library( "//ortools/base:status_macros", "//ortools/gurobi:environment", "//ortools/math_opt/solvers:gurobi_cc_proto", + "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings:str_format", diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index fe4039b15c..74a4860632 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -20,6 +20,7 @@ #include #include +#include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" @@ -61,6 +62,77 @@ int GurobiCallback(GRBmodel* const model, void* const cbdata, const int where, return kGrbOk; } +// A class for handling callback management (setting/unsetting) and their +// associated errors. Users create this handler to register their callback, do +// something, then call `Flush()` to flush errors returned from the callback, +// and then finally call `Release()` to clear the registered callback. This +// class uses RAII to attempt to automatically clear the callback if your code +// returns prior to calling `Release()` manually, but note that this does not +// propagate any errors if it fails. + +// A typical use case would be: +// +// ASSIGN_OR_RETURN(const auto scope, ScopedCallback::New(this, std::move(cb))); +// const int error = GRBxxx(gurobi_model_); +// RETURN_IF_ERROR(scope->Flush()); +// RETURN_IF_ERROR(ToStatus(error)); +// return scope->Release(); +class ScopedCallback { + public: + ScopedCallback(const ScopedCallback&) = delete; + ScopedCallback& operator=(const ScopedCallback&) = delete; + ScopedCallback(ScopedCallback&&) = delete; + ScopedCallback& operator=(ScopedCallback&&) = delete; + + // Returned object retains a pointer to `gurobi`, which must not be null. + static absl::StatusOr> New( + Gurobi* const gurobi, Gurobi::Callback cb) { + CHECK(gurobi != nullptr); + auto scope = absl::WrapUnique(new ScopedCallback(gurobi)); + if (cb != nullptr) { + scope->user_cb_data_.user_cb = std::move(cb); + scope->user_cb_data_.gurobi = gurobi; + RETURN_IF_ERROR(gurobi->ToStatus(GRBsetcallbackfunc( + gurobi->model(), GurobiCallback, &scope->user_cb_data_))); + scope->needs_cleanup_ = true; + } + return scope; + } + + // Propagates any errors returned from the callback. + absl::Status Flush() { + const absl::Status status = std::move(user_cb_data_.status); + user_cb_data_.status = absl::OkStatus(); + return status; + } + + // Clears the registered callback. + absl::Status Release() { + if (needs_cleanup_) { + needs_cleanup_ = false; + return gurobi_->ToStatus( + GRBsetcallbackfunc(gurobi_->model(), nullptr, nullptr)); + } + return absl::OkStatus(); + } + + ~ScopedCallback() { + if (const absl::Status s = Flush(); !s.ok()) { + LOG(ERROR) << "Error returned from callback: " << s; + } + if (const absl::Status s = Release(); !s.ok()) { + LOG(ERROR) << "Error cleaning up callback: " << s; + } + } + + private: + explicit ScopedCallback(Gurobi* const gurobi) : gurobi_(gurobi) {} + + bool needs_cleanup_ = false; + Gurobi* gurobi_; + UserCallbackData user_cb_data_; +}; + } // namespace void GurobiFreeEnv::operator()(GRBenv* const env) const { @@ -454,35 +526,26 @@ absl::Status Gurobi::UpdateModel() { } absl::Status Gurobi::Optimize(Callback cb) { - bool needs_cb_cleanup = false; - UserCallbackData user_cb_data; - if (cb != nullptr) { - user_cb_data.user_cb = std::move(cb); - user_cb_data.gurobi = this; - RETURN_IF_ERROR(ToStatus( - GRBsetcallbackfunc(gurobi_model_, GurobiCallback, &user_cb_data))); - needs_cb_cleanup = true; - } + ASSIGN_OR_RETURN(const auto scope, ScopedCallback::New(this, std::move(cb))); + const int error = GRBoptimize(gurobi_model_); + RETURN_IF_ERROR(scope->Flush()); + RETURN_IF_ERROR(ToStatus(error)); + return scope->Release(); +} - // Failsafe to try and clear the callback if there is another error. We cannot - // raise an error in a destructor, we can only log it. - auto callback_cleanup = absl::MakeCleanup([&]() { - if (needs_cb_cleanup) { - int error = GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr); - if (error != kGrbOk) { - LOG(ERROR) << "Error cleaning up callback"; - } - } - }); - absl::Status solve_status = ToStatus(GRBoptimize(gurobi_model_)); - RETURN_IF_ERROR(user_cb_data.status) << "Error in Optimize callback."; - RETURN_IF_ERROR(solve_status); - if (needs_cb_cleanup) { - needs_cb_cleanup = false; - RETURN_IF_ERROR( - ToStatus(GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr))); +absl::StatusOr Gurobi::ComputeIIS(Callback cb) { + ASSIGN_OR_RETURN(const auto scope, ScopedCallback::New(this, std::move(cb))); + const int error = GRBcomputeIIS(gurobi_model_); + RETURN_IF_ERROR(scope->Flush()); + if (error == GRB_ERROR_IIS_NOT_INFEASIBLE) { + RETURN_IF_ERROR(scope->Release()); + return false; + } else if (error == kGrbOk) { + RETURN_IF_ERROR(scope->Release()); + return true; } - return absl::OkStatus(); + RETURN_IF_ERROR(ToStatus(error)); + return scope->Release(); } bool Gurobi::IsAttrAvailable(const char* name) const { @@ -623,6 +686,20 @@ absl::Status Gurobi::SetCharAttrList(const char* const name, const_cast(new_values.data()))); } +absl::StatusOr Gurobi::GetIntAttrElement(const char* const name, + const int element) const { + int value; + RETURN_IF_ERROR( + ToStatus(GRBgetintattrelement(gurobi_model_, name, element, &value))); + return value; +} + +absl::Status Gurobi::SetIntAttrElement(const char* const name, + const int element, const int new_value) { + return ToStatus( + GRBsetintattrelement(gurobi_model_, name, element, new_value)); +} + absl::StatusOr Gurobi::GetDoubleAttrElement(const char* const name, const int element) const { double value; diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.h b/ortools/math_opt/solvers/gurobi/g_gurobi.h index fe082b1230..033704dc5d 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.h +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.h @@ -472,6 +472,17 @@ class Gurobi { // Calls GRBterminate(). void Terminate(); + // Calls GRBcomputeIIS(). + // + // Returns: + // * a status if Gurobi errors, + // * false if Gurobi determines that the model is feasible, or + // * true otherwise (e.g., infeasibility is proven or a limit is reached). + // setup/teardown, and true otherwise. + // + // The callback, if specified, is set before solving and cleared after. + absl::StatusOr ComputeIIS(Callback cb = nullptr); + ////////////////////////////////////////////////////////////////////////////// // Attributes ////////////////////////////////////////////////////////////////////////////// @@ -514,6 +525,9 @@ class Gurobi { absl::Status SetCharAttrList(const char* name, absl::Span ind, absl::Span new_values); + absl::StatusOr GetIntAttrElement(const char* name, int element) const; + absl::Status SetIntAttrElement(const char* name, int element, int new_value); + absl::StatusOr GetDoubleAttrElement(const char* name, int element) const; absl::Status SetDoubleAttrElement(const char* name, int element, @@ -556,6 +570,10 @@ class Gurobi { // Typically not needed. GRBmodel* model() const { return gurobi_model_; } + absl::Status ToStatus( + int grb_err, absl::StatusCode code = absl::StatusCode::kInvalidArgument, + absl::SourceLocation loc = absl::SourceLocation::current()) const; + private: // optional_owned_primary_env can be null, model and model_env cannot. Gurobi(GRBenvUniquePtr optional_owned_primary_env, GRBmodel* model, @@ -564,10 +582,6 @@ class Gurobi { static absl::StatusOr> New( GRBenvUniquePtr optional_owned_primary_env, GRBenv* primary_env); - absl::Status ToStatus( - int grb_err, absl::StatusCode code = absl::StatusCode::kInvalidArgument, - absl::SourceLocation loc = absl::SourceLocation::current()) const; - const GRBenvUniquePtr owned_primary_env_; // Invariant: Not null. GRBmodel* const gurobi_model_; diff --git a/ortools/math_opt/solvers/gurobi_init_arguments.cc b/ortools/math_opt/solvers/gurobi_init_arguments.cc index b6b8b0fbab..e9166cab49 100644 --- a/ortools/math_opt/solvers/gurobi_init_arguments.cc +++ b/ortools/math_opt/solvers/gurobi_init_arguments.cc @@ -15,7 +15,6 @@ #include #include -#include #include "absl/status/statusor.h" #include "ortools/math_opt/solvers/gurobi.pb.h" diff --git a/ortools/math_opt/solvers/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index 68daaaff32..7cea9a5d8e 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -27,7 +28,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/memory/memory.h" +#include "absl/meta/type_traits.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/escaping.h" @@ -37,7 +40,6 @@ #include "absl/time/clock.h" #include "absl/time/time.h" #include "absl/types/span.h" -#include "absl/log/check.h" #include "ortools/base/linked_hash_map.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" @@ -51,6 +53,7 @@ #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -71,8 +74,10 @@ namespace { constexpr SupportedProblemStructures kGurobiSupportedStructures = { .integer_variables = SupportType::kSupported, + .multi_objectives = SupportType::kSupported, .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kSupported, + .second_order_cone_constraints = SupportType::kSupported, .sos1_constraints = SupportType::kSupported, .sos2_constraints = SupportType::kSupported, .indicator_constraints = SupportType::kSupported}; @@ -133,8 +138,10 @@ inline int GrbVariableStatus(const BasisStatusProto status) { } } -GurobiParametersProto MergeParameters( - const SolveParametersProto& solve_parameters) { +// is_mip indicates if the problem has integer variables or constraints that +// would cause Gurobi to treat the problem as a MIP, e.g. SOS, indicator. +absl::StatusOr MergeParameters( + const SolveParametersProto& solve_parameters, const bool is_mip) { GurobiParametersProto merged_parameters; { @@ -194,6 +201,10 @@ GurobiParametersProto MergeParameters( } if (solve_parameters.has_objective_limit()) { + if (!is_mip) { + return absl::InvalidArgumentError( + "objective_limit is only supported for Gurobi on MIP models"); + } GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_DBL_PAR_BESTOBJSTOP); @@ -201,6 +212,10 @@ GurobiParametersProto MergeParameters( } if (solve_parameters.has_best_bound_limit()) { + if (!is_mip) { + return absl::InvalidArgumentError( + "best_bound_limit is only supported for Gurobi on MIP models"); + } GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_DBL_PAR_BESTBDSTOP); @@ -245,6 +260,9 @@ GurobiParametersProto MergeParameters( case LP_ALGORITHM_BARRIER: parameter->set_value(absl::StrCat(GRB_METHOD_BARRIER)); break; + case LP_ALGORITHM_FIRST_ORDER: + return absl::InvalidArgumentError( + "lp_algorithm FIRST_ORDER is not supported for gurobi"); default: LOG(FATAL) << "LPAlgorithm: " << ProtoEnumToString(solve_parameters.lp_algorithm()) @@ -800,22 +818,34 @@ absl::StatusOr GurobiSolver::GetProblemStatus( absl::StatusOr GurobiSolver::ExtractSolveResultProto( const absl::Time start, const ModelSolveParametersProto& model_parameters) { SolveResultProto result; + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + SolutionClaims solution_claims; + if (grb_termination == GRB_CUTOFF) { + // Cutoff will not be triggered by bounds e.g. for LP dual feasible + // solutions. In particular, if the problem is both primal and dual + // infeasible, we will not get a bound and should not be returning CUTOFF. + // + // TODO(b/272268188): test that this has no bad interactions with primal + + // dual infeasible problems. + solution_claims = {.primal_feasible_solution_exists = false, + .dual_feasible_solution_exists = true}; + } else { + ASSIGN_OR_RETURN(SolutionsAndClaims solution_and_claims, + GetSolutions(model_parameters)); + solution_claims = solution_and_claims.solution_claims; - ASSIGN_OR_RETURN((auto [solutions, solution_claims]), - GetSolutions(model_parameters)); + // TODO(b/195295177): Add tests for rays in unbounded MIPs + RETURN_IF_ERROR(FillRays(model_parameters, solution_claims, result)); - // TODO(b/195295177): Add tests for rays in unbounded MIPs - RETURN_IF_ERROR(FillRays(model_parameters, solution_claims, result)); - - for (auto& solution : solutions) { - *result.add_solutions() = std::move(solution); + for (SolutionProto& solution : solution_and_claims.solutions) { + *result.add_solutions() = std::move(solution); + } } ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start, solution_claims)); - ASSIGN_OR_RETURN(const int grb_termination, - gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); ASSIGN_OR_RETURN(*result.mutable_termination(), ConvertTerminationReason(grb_termination, solution_claims)); return std::move(result); @@ -823,6 +853,7 @@ absl::StatusOr GurobiSolver::ExtractSolveResultProto( absl::StatusOr GurobiSolver::GetSolutions( const ModelSolveParametersProto& model_parameters) { + // Note that all multi-objective models will have `IsMip()` return true. ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); ASSIGN_OR_RETURN(const bool is_qp, IsQP()); ASSIGN_OR_RETURN(const bool is_qcp, IsQCP()); @@ -897,6 +928,19 @@ absl::StatusOr GurobiSolver::GetMipSolutions( ASSIGN_OR_RETURN(const double sol_val, gurobi_->GetDoubleAttr(GRB_DBL_ATTR_POOLOBJVAL)); primal_solution.set_objective_value(sol_val); + if (is_multi_objective_mode()) { + for (const auto [id, grb_index] : multi_objectives_map_) { + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_OBJNUMBER, grb_index)); + ASSIGN_OR_RETURN(const double obj_val, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJNVAL)); + // If unset, this is the primary objective. We have already queried its + // value via PoolObjVal above. + if (id.has_value()) { + (*primal_solution.mutable_auxiliary_objective_values())[*id] = + obj_val; + } + } + } primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); ASSIGN_OR_RETURN( const std::vector grb_var_values, @@ -908,6 +952,8 @@ absl::StatusOr GurobiSolver::GetMipSolutions( std::move(primal_solution); } + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); // Set solution claims ASSIGN_OR_RETURN(const double best_dual_bound, GetBestDualBound()); // Note: here the existence of a dual solution refers to a dual solution to @@ -917,18 +963,24 @@ absl::StatusOr GurobiSolver::GetMipSolutions( // that best_dual_bound being finite implies the existence of the trivial // convex relaxation given by (assuming a minimization problem with objective // function c^T x): min{c^T x : c^T x >= best_dual_bound}. + // + // If this is a multi-objective model, Gurobi v10 does not expose ObjBound. + // Instead, we fake its existence for optimal solves only. const SolutionClaims solution_claims = { .primal_feasible_solution_exists = num_solutions > 0, - .dual_feasible_solution_exists = std::isfinite(best_dual_bound)}; + .dual_feasible_solution_exists = + std::isfinite(best_dual_bound) || + (is_multi_objective_mode() && grb_termination == GRB_OPTIMAL)}; // Check consistency of solutions, bounds and statuses. - ASSIGN_OR_RETURN(const int grb_termination, - gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); if (grb_termination == GRB_OPTIMAL && num_solutions == 0) { return absl::InternalError( "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but solution pool is empty."); } - if (grb_termination == GRB_OPTIMAL && !std::isfinite(best_dual_bound)) { + // As set above, in multi-objective mode the dual bound is not informative and + // it will not pass this validation. + if (!is_multi_objective_mode() && grb_termination == GRB_OPTIMAL && + !std::isfinite(best_dual_bound)) { return absl::InternalError( "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but GRB_DBL_ATTR_OBJBOUND is " "unavailable or infinite."); @@ -1054,7 +1106,11 @@ absl::StatusOr GurobiSolver::GetBestPrimalBound( } absl::StatusOr GurobiSolver::GetBestDualBound() { - if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJBOUND)) { + // As of v9.0.2, on multi objective models Gurobi incorrectly reports that + // ObjBound is available. We work around this by adding a check if we are in + // multi objective mode. + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJBOUND) && + !is_multi_objective_mode()) { ASSIGN_OR_RETURN(const double obj_bound, gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJBOUND)); // Note: Unbounded models return GRB_DBL_ATTR_OBJBOUND = GRB_INFINITY so @@ -1295,7 +1351,9 @@ absl::StatusOr GurobiSolver::GetQcpSolution( absl::Status GurobiSolver::SetParameters( const SolveParametersProto& parameters) { - const GurobiParametersProto gurobi_parameters = MergeParameters(parameters); + ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); + ASSIGN_OR_RETURN(const GurobiParametersProto gurobi_parameters, + MergeParameters(parameters, is_mip)); std::vector parameter_errors; for (const GurobiParametersProto::Parameter& parameter : gurobi_parameters.parameters()) { @@ -1334,6 +1392,71 @@ absl::Status GurobiSolver::AddNewVariables( return absl::OkStatus(); } +absl::Status GurobiSolver::AddSingleObjective(const ObjectiveProto& objective) { + const int model_sense = objective.maximize() ? GRB_MAXIMIZE : GRB_MINIMIZE; + RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense)); + RETURN_IF_ERROR( + gurobi_->SetDoubleAttr(GRB_DBL_ATTR_OBJCON, objective.offset())); + RETURN_IF_ERROR(UpdateDoubleListAttribute(objective.linear_coefficients(), + GRB_DBL_ATTR_OBJ, variables_map_)); + RETURN_IF_ERROR( + ResetQuadraticObjectiveTerms(objective.quadratic_coefficients())); + return absl::OkStatus(); +} + +absl::Status GurobiSolver::AddMultiObjectives( + const ObjectiveProto& primary_objective, + const google::protobuf::Map& + auxiliary_objectives) { + absl::flat_hash_set priorities = {primary_objective.priority()}; + for (const auto& [id, objective] : auxiliary_objectives) { + const int64_t priority = objective.priority(); + if (!priorities.insert(priority).second) { + return util::InvalidArgumentErrorBuilder() + << "repeated objective priority: " << priority; + } + } + const bool is_maximize = primary_objective.maximize(); + RETURN_IF_ERROR(gurobi_->SetIntAttr( + GRB_INT_ATTR_MODELSENSE, is_maximize ? GRB_MAXIMIZE : GRB_MINIMIZE)); + RETURN_IF_ERROR(AddNewMultiObjective( + primary_objective, /*objective_id=*/std::nullopt, is_maximize)); + for (const auto& [id, objective] : auxiliary_objectives) { + RETURN_IF_ERROR(AddNewMultiObjective(objective, id, is_maximize)); + } + return absl::OkStatus(); +} + +absl::Status GurobiSolver::AddNewMultiObjective( + const ObjectiveProto& objective, + const std::optional objective_id, + const bool is_maximize) { + std::vector var_indices; + var_indices.reserve(objective.linear_coefficients().ids_size()); + for (const int64_t var_id : objective.linear_coefficients().ids()) { + var_indices.push_back(variables_map_.at(var_id)); + } + const GurobiMultiObjectiveIndex grb_index = + static_cast(multi_objectives_map_.size()); + // * MathOpt and Gurobi have different priority orderings (lower and higher + // are more important, respectively). Therefore, we negate priorities from + // MathOpt (which is OK as they are restricted to be nonnegative in MathOpt, + // but not in Gurobi). + // * Tolerances are set to default values, as of Gurobi v9.5. + // * Gurobi exposes only a single objective sense for the entire model. We use + // the objective weight to handle mixing senses across objectives (weight of + // 1 if objective sense agrees with model sense, -1 otherwise). + RETURN_IF_ERROR(gurobi_->SetNthObjective( + /*index=*/grb_index, /*priority=*/static_cast(-objective.priority()), + /*weight=*/objective.maximize() == is_maximize ? +1.0 : -1.0, + /*abs_tol=*/1.0e-6, + /*rel_tol=*/0.0, /*name=*/objective.name(), + /*constant=*/objective.offset(), /*lind=*/var_indices, + /*lval=*/objective.linear_coefficients().values())); + multi_objectives_map_.insert({objective_id, grb_index}); + return absl::OkStatus(); +} + // Given a vector of pairs add a // slack variable for each of the constraints in the underlying `gurobi_` using // the referenced bounds. @@ -1520,6 +1643,84 @@ absl::Status GurobiSolver::AddNewQuadraticConstraints( return absl::OkStatus(); } +absl::StatusOr +GurobiSolver::ExtractVariableEqualToExpression( + const LinearExpressionProto& expression, const bool allow_reuse) { + if (allow_reuse && expression.offset() == 0 && expression.ids_size() == 1 && + expression.coefficients(0) == 1) { + const VariableId var_id = expression.ids(0); + // In this case, the expression is equivalent to just a single variable, and + // hence we just return it. + return VariableEqualToExpression{.variable_index = + variables_map_.at(var_id)}; + } + // This `expression` is nontrivial, so we introduce a new `slack_variable` and + // the linear constraint: `expression` == `slack_variable`. + const GurobiVariableIndex slack_variable = num_gurobi_variables_; + std::vector slack_col_indices = {slack_variable}; + std::vector slack_coeffs = {-1.0}; + for (int j = 0; j < expression.ids_size(); ++j) { + slack_col_indices.push_back(variables_map_.at(expression.ids(j))); + slack_coeffs.push_back(expression.coefficients(j)); + } + RETURN_IF_ERROR(gurobi_->AddVar(0, -kInf, kInf, GRB_CONTINUOUS, "")); + RETURN_IF_ERROR(gurobi_->AddConstr(slack_col_indices, slack_coeffs, GRB_EQUAL, + -expression.offset(), "")); + return VariableEqualToExpression{.variable_index = num_gurobi_variables_++, + .constraint_index = num_gurobi_lin_cons_++}; +} + +absl::Status GurobiSolver::AddNewSecondOrderConeConstraints( + const google::protobuf::Map& constraints) { + for (const auto& [id, constraint] : constraints) { + // The MathOpt proto representation for a second-order cone constraint is: + // ||`constraint`.arguments_to_norm||_2 <= `constraint`.upper_bound. + // Gurobi requires second-order cone constraints to be passed via the + // quadratic constraint API as: + // arg_var[0]^2 + ... + arg_var[d]^2 <= ub_var^2 + // ub_var >= 0, + // for variables arg_var[0], ..., arg_var[d], ub_var. To get to this form, + // we add slack variables: + // ub_var = `constraint`.upper_bound + // arg_var[i] = `constraint`.arguments_to_norm[i] for each i + // Note that we elide adding a slack variable/constraint if the expression + // we are setting it equal to is just a variable already in the model. + SecondOrderConeConstraintData& constraint_data = + gtl::InsertKeyOrDie(&soc_constraints_map_, id); + constraint_data.constraint_index = num_gurobi_quad_cons_; + // We force a new variable to be added so that we can add a lower bound on + // it. Otherwise, we must update the model to flush bounds, or risk either + // a Gurobi error, or stomping on a potentially stronger bound. + ASSIGN_OR_RETURN((const auto [ub_var, ub_cons]), + ExtractVariableEqualToExpression(constraint.upper_bound(), + /*allow_reuse=*/false)); + RETURN_IF_ERROR( + gurobi_->SetDoubleAttrElement(GRB_DBL_ATTR_LB, ub_var, 0.0)); + CHECK(ub_cons.has_value()); + constraint_data.slack_variables.push_back(ub_var); + constraint_data.slack_constraints.push_back(*ub_cons); + std::vector quad_var_indices = {ub_var}; + std::vector quad_coeffs = {-1.0}; + for (const LinearExpressionProto& expression : + constraint.arguments_to_norm()) { + ASSIGN_OR_RETURN((const auto [arg_var, maybe_arg_cons]), + ExtractVariableEqualToExpression(expression)); + quad_var_indices.push_back(arg_var); + quad_coeffs.push_back(1.0); + if (maybe_arg_cons.has_value()) { + constraint_data.slack_variables.push_back(arg_var); + constraint_data.slack_constraints.push_back(*maybe_arg_cons); + } + } + RETURN_IF_ERROR(gurobi_->AddQConstr({}, {}, quad_var_indices, + quad_var_indices, quad_coeffs, + GRB_LESS_EQUAL, 0.0, "")); + ++num_gurobi_quad_cons_; + } + return absl::OkStatus(); +} + absl::Status GurobiSolver::AddNewSosConstraints( const google::protobuf::Map& constraints, @@ -1532,41 +1733,20 @@ absl::Status GurobiSolver::AddNewSosConstraints( std::vector sos_var_indices; std::vector weights; for (int i = 0; i < constraint.expressions_size(); ++i) { - const LinearExpressionProto& expression = constraint.expressions(i); weights.push_back(constraint.weights().empty() ? i + 1 : constraint.weights(i)); - if (expression.offset() == 0 && expression.ids_size() == 1 && - expression.coefficients(0) == 1) { - const VariableId var_id = expression.ids(0); - // In this case, the expression is equivalent to just a single variable. - // Therefore, we can safely pass this variable to the SOS constraint, - // and avoid adding a slack variable. - sos_var_indices.push_back(variables_map_.at(var_id)); + ASSIGN_OR_RETURN( + (const auto [var_index, maybe_cons_index]), + ExtractVariableEqualToExpression(constraint.expressions(i))); + sos_var_indices.push_back(var_index); + if (maybe_cons_index.has_value()) { + constraint_data.slack_variables.push_back(var_index); + constraint_data.slack_constraints.push_back(*maybe_cons_index); + } else if (sos_type == 2) { // If this variable is deleted, Gurobi will drop the corresponding term // from the SOS constraint, potentially changing the meaning of an SOS2. - if (sos_type == 2) { - undeletable_variables_.insert(var_id); - } - continue; + undeletable_variables_.insert(var_index); } - // This term in the SOS constraint is a nontrivial expression `expr`, but - // Gurobi only accepts a single variable. Therefore we introduce a new - // `slack` variable and add the linear constraint: `expr` == `slack`. - sos_var_indices.push_back(num_gurobi_variables_); - constraint_data.slack_variables.push_back(num_gurobi_variables_); - constraint_data.slack_constraints.push_back(num_gurobi_lin_cons_); - std::vector slack_col_indices = { - num_gurobi_variables_}; - std::vector slack_coeffs = {-1.0}; - for (int j = 0; j < expression.ids_size(); ++j) { - slack_col_indices.push_back(variables_map_.at(expression.ids(j))); - slack_coeffs.push_back(expression.coefficients(j)); - } - RETURN_IF_ERROR(gurobi_->AddVar(0, -kInf, kInf, GRB_CONTINUOUS, "")); - ++num_gurobi_variables_; - RETURN_IF_ERROR(gurobi_->AddConstr(slack_col_indices, slack_coeffs, - GRB_EQUAL, -expression.offset(), "")); - ++num_gurobi_lin_cons_; } RETURN_IF_ERROR(gurobi_->AddSos({sos_type}, {0}, sos_var_indices, weights)); ++num_gurobi_sos_cons_; @@ -1579,6 +1759,13 @@ absl::Status GurobiSolver::AddNewIndicatorConstraints( IndicatorConstraintProto>& constraints) { for (const auto& [id, constraint] : constraints) { if (!constraint.has_indicator_id()) { + if (constraint.activate_on_zero()) { + return util::UnimplementedErrorBuilder() + << "MathOpt does not currently support Gurobi models with " + "indicator constraints that activate on zero with unset " + "indicator variables; encountered one at id: " + << id; + } gtl::InsertOrDie(&indicator_constraints_map_, id, std::nullopt); continue; } @@ -1612,10 +1799,11 @@ absl::Status GurobiSolver::AddNewIndicatorConstraints( sense = GRB_EQUAL; rhs = lb; } else { - // We do not currently support ranged indicator constraints, though it is - // possible to support this if there is a need. + // We do not currently support ranged indicator constraints, though it + // is possible to support this if there is a need. return absl::UnimplementedError( - "ranged indicator constraints are not currently supported in Gurobi " + "ranged indicator constraints are not currently supported in " + "Gurobi " "interface"); } RETURN_IF_ERROR(gurobi_->AddIndicator( @@ -1686,7 +1874,8 @@ absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(AddNewLinearConstraints(input_model.linear_constraints())); RETURN_IF_ERROR( AddNewQuadraticConstraints(input_model.quadratic_constraints())); - + RETURN_IF_ERROR(AddNewSecondOrderConeConstraints( + input_model.second_order_cone_constraints())); RETURN_IF_ERROR(AddNewSosConstraints(input_model.sos1_constraints(), GRB_SOS_TYPE1, sos1_constraints_map_)); RETURN_IF_ERROR(AddNewSosConstraints(input_model.sos2_constraints(), @@ -1696,17 +1885,12 @@ absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(ChangeCoefficients(input_model.linear_constraint_matrix())); - const int model_sense = - input_model.objective().maximize() ? GRB_MAXIMIZE : GRB_MINIMIZE; - RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense)); - RETURN_IF_ERROR(gurobi_->SetDoubleAttr(GRB_DBL_ATTR_OBJCON, - input_model.objective().offset())); - - RETURN_IF_ERROR( - UpdateDoubleListAttribute(input_model.objective().linear_coefficients(), - GRB_DBL_ATTR_OBJ, variables_map_)); - RETURN_IF_ERROR(ResetQuadraticObjectiveTerms( - input_model.objective().quadratic_coefficients())); + if (input_model.auxiliary_objectives().empty()) { + RETURN_IF_ERROR(AddSingleObjective(input_model.objective())); + } else { + RETURN_IF_ERROR(AddMultiObjectives(input_model.objective(), + input_model.auxiliary_objectives())); + } return absl::OkStatus(); } @@ -1749,8 +1933,8 @@ absl::Status GurobiSolver::UpdateQuadraticObjectiveTerms( const double new_coefficient = terms.coefficients(k); // Gurobi will maintain any existing quadratic coefficients unless we // call GRBdelq (which we don't). So, since stored entries in terms - // specify the target coefficients, we need to compute the difference from - // the existing coefficient with Gurobi, if any. + // specify the target coefficients, we need to compute the difference + // from the existing coefficient with Gurobi, if any. coefficient_updates[k] = new_coefficient - quadratic_objective_coefficients_[qp_term_key]; quadratic_objective_coefficients_[qp_term_key] = new_coefficient; @@ -1780,8 +1964,8 @@ absl::Status GurobiSolver::UpdateLinearConstraints( // We want to avoid changing the right-hand-side, sense, or slacks of each // constraint more than once. Since we can refer to the same constraint ID - // both in the `constraint_upper_bounds` and `constraint_lower_bounds` sparse - // vectors, we collect all changes into a single structure: + // both in the `constraint_upper_bounds` and `constraint_lower_bounds` + // sparse vectors, we collect all changes into a single structure: struct UpdateConstraintData { LinearConstraintId constraint_id; LinearConstraintData& source; @@ -1894,8 +2078,8 @@ absl::Status GurobiSolver::UpdateLinearConstraints( } // If the constraint had a slack, and now is marked for deletion, we reset // the stored slack_index in linear_constraints_map_[id], save the index - // in the list of variables to be deleted later on and remove the constraint - // from slack_map_. + // in the list of variables to be deleted later on and remove the + // constraint from slack_map_. if (delete_slack && update_data.source.slack_index != kUnspecifiedIndex) { deleted_variables_index.emplace_back(update_data.source.slack_index); update_data.source.slack_index = kUnspecifiedIndex; @@ -1923,10 +2107,10 @@ absl::Status GurobiSolver::UpdateLinearConstraints( } // This function re-assign indices for variables and constraints after -// deletion. The updated indices are computed from the previous indices, sorted -// in incremental form, but re-assigned so that all indices are contiguous -// between [0, num_variables-1], [0, num_linear_constraints-1], and [0, -// num_quad_constraints-1]. +// deletion. The updated indices are computed from the previous indices, +// sorted in incremental form, but re-assigned so that all indices are +// contiguous between [0, num_variables-1], [0, num_linear_constraints-1], and +// [0, num_quad_constraints-1], etc. void GurobiSolver::UpdateGurobiIndices(const DeletedIndices& deleted_indices) { // Recover the updated indices of variables. if (!deleted_indices.variables.empty()) { @@ -1942,6 +2126,12 @@ void GurobiSolver::UpdateGurobiIndices(const DeletedIndices& deleted_indices) { CHECK_NE(lin_con_data.slack_index, kDeletedIndex); } } + for (auto& [_, soc_con_data] : soc_constraints_map_) { + for (GurobiVariableIndex& index : soc_con_data.slack_variables) { + index = old_to_new[index]; + CHECK_NE(index, kDeletedIndex); + } + } for (auto& [_, sos1_con_data] : sos1_constraints_map_) { for (GurobiVariableIndex& index : sos1_con_data.slack_variables) { index = old_to_new[index]; @@ -1963,6 +2153,13 @@ void GurobiSolver::UpdateGurobiIndices(const DeletedIndices& deleted_indices) { lin_con_data.constraint_index = old_to_new[lin_con_data.constraint_index]; CHECK_NE(lin_con_data.constraint_index, kDeletedIndex); } + for (auto& [_, soc_con_data] : soc_constraints_map_) { + for (GurobiLinearConstraintIndex& index : + soc_con_data.slack_constraints) { + index = old_to_new[index]; + CHECK_NE(index, kDeletedIndex); + } + } for (auto& [_, sos1_con_data] : sos1_constraints_map_) { for (GurobiLinearConstraintIndex& index : sos1_con_data.slack_constraints) { @@ -1987,6 +2184,11 @@ void GurobiSolver::UpdateGurobiIndices(const DeletedIndices& deleted_indices) { grb_index = old_to_new[grb_index]; CHECK_NE(grb_index, kDeletedIndex); } + for (auto& [_, soc_con_data] : soc_constraints_map_) { + GurobiQuadraticConstraintIndex& grb_index = soc_con_data.constraint_index; + grb_index = old_to_new[soc_con_data.constraint_index]; + CHECK_NE(grb_index, kDeletedIndex); + } } // Recover the updated indices of SOS constraints. if (!deleted_indices.sos_constraints.empty()) { @@ -2031,6 +2233,19 @@ absl::StatusOr GurobiSolver::Update( if (!UpdateIsSupported(model_update, kGurobiSupportedStructures)) { return false; } + // As of 2022-12-06 we do not support incrementalism for multi-objective + // models: not adding/deleting/modifying the auxiliary objectives... + if (const AuxiliaryObjectivesUpdatesProto& objs_update = + model_update.auxiliary_objectives_updates(); + !objs_update.deleted_objective_ids().empty() || + !objs_update.new_objectives().empty() || + !objs_update.objective_updates().empty()) { + return false; + } + // ...or modifying the primary objective of a multi-objective model. + if (is_multi_objective_mode() && model_update.has_objective_updates()) { + return false; + } RETURN_IF_ERROR(AddNewVariables(model_update.new_variables())); @@ -2039,7 +2254,8 @@ absl::StatusOr GurobiSolver::Update( RETURN_IF_ERROR(AddNewQuadraticConstraints( model_update.quadratic_constraint_updates().new_constraints())); - + RETURN_IF_ERROR(AddNewSecondOrderConeConstraints( + model_update.second_order_cone_constraint_updates().new_constraints())); RETURN_IF_ERROR(AddNewSosConstraints( model_update.sos1_constraint_updates().new_constraints(), GRB_SOS_TYPE1, sos1_constraints_map_)); @@ -2113,8 +2329,8 @@ absl::StatusOr GurobiSolver::Update( ++it; } } - // We cache all Gurobi variables and constraint indices that must be deleted, - // and perform deletions at the end of the update call. + // We cache all Gurobi variables and constraint indices that must be + // deleted, and perform deletions at the end of the update call. DeletedIndices deleted_indices; RETURN_IF_ERROR(UpdateLinearConstraints( @@ -2145,6 +2361,22 @@ absl::StatusOr GurobiSolver::Update( quadratic_constraints_map_.erase(id); } + for (const SecondOrderConeConstraintId id : + model_update.second_order_cone_constraint_updates() + .deleted_constraint_ids()) { + deleted_indices.quadratic_constraints.push_back( + soc_constraints_map_.at(id).constraint_index); + for (const GurobiVariableIndex index : + soc_constraints_map_.at(id).slack_variables) { + deleted_indices.variables.push_back(index); + } + for (const GurobiLinearConstraintIndex index : + soc_constraints_map_.at(id).slack_constraints) { + deleted_indices.linear_constraints.push_back(index); + } + soc_constraints_map_.erase(id); + } + const auto sos_updater = [&](const SosConstraintData& sos_constraint) { deleted_indices.sos_constraints.push_back(sos_constraint.constraint_index); for (const GurobiVariableIndex index : sos_constraint.slack_variables) { @@ -2224,6 +2456,19 @@ absl::StatusOr> GurobiSolver::New( } RETURN_IF_ERROR( ModelIsSupported(input_model, kGurobiSupportedStructures, "Gurobi")); + if (!input_model.auxiliary_objectives().empty() && + !input_model.objective().quadratic_coefficients().row_ids().empty()) { + return util::InvalidArgumentErrorBuilder() + << "Gurobi does not support multiple objective models with " + "quadratic objectives"; + } + for (const auto& [id, obj] : input_model.auxiliary_objectives()) { + if (!obj.quadratic_coefficients().row_ids().empty()) { + return util::InvalidArgumentErrorBuilder() + << "Gurobi does not support multiple objective models with " + "quadratic objectives"; + } + } ASSIGN_OR_RETURN(std::unique_ptr gurobi, GurobiFromInitArgs(init_args)); auto gurobi_solver = absl::WrapUnique(new GurobiSolver(std::move(gurobi))); @@ -2244,7 +2489,7 @@ GurobiSolver::RegisterCallback(const CallbackRegistrationProto& registration, // https://www.gurobi.com/documentation/9.1/refman/ismip.html. // // Here we assume that we get MIP related events and use a MIP solving - // stragegy when IS_MIP is true. + // strategy when IS_MIP is true. ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP)); RETURN_IF_ERROR(CheckRegisteredCallbackEvents( @@ -2335,6 +2580,10 @@ absl::StatusOr GurobiSolver::ListInvalidIndicators() const { return invalid_indicators; } +bool GurobiSolver::is_multi_objective_mode() const { + return !multi_objectives_map_.empty(); +} + absl::StatusOr GurobiSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, @@ -2342,26 +2591,34 @@ absl::StatusOr GurobiSolver::Solve( const CallbackRegistrationProto& callback_registration, const Callback cb, SolveInterrupter* const interrupter) { const absl::Time start = absl::Now(); - // We must set the parameters before calling RegisterCallback since it changes - // some parameters depending on the callback registration. + + // Need to run GRBupdatemodel before: + // 1. setting parameters (to test if the problem is a MIP) + // 2. registering callbacks (to test if the problem is a MIP), + // 3. setting basis and getting the obj sense. + // We just run it first. + RETURN_IF_ERROR(gurobi_->UpdateModel()); + + // We must set the parameters before calling RegisterCallback since it + // changes some parameters depending on the callback registration. RETURN_IF_ERROR(SetParameters(parameters)); // We use a local interrupter that will triggers the calls to GRBterminate() - // when either the user interrupter is triggered or when a callback returns a - // true `terminate`. + // when either the user interrupter is triggered or when a callback returns + // a true `terminate`. std::unique_ptr local_interrupter; if (cb != nullptr || interrupter != nullptr) { local_interrupter = std::make_unique(); } const ScopedSolveInterrupterCallback scoped_terminate_callback( local_interrupter.get(), [&]() { - // Make an immediate call to GRBterminate() as soon as this interrupter - // is triggered (which may immediately happen in the code below when it - // is chained with the optional user interrupter). + // Make an immediate call to GRBterminate() as soon as this + // interrupter is triggered (which may immediately happen in the code + // below when it is chained with the optional user interrupter). // // This call may happen too early. This is not an issue since we will - // repeat this call at each call of the Gurobi callback. See the comment - // in GurobiCallbackImpl() for details. + // repeat this call at each call of the Gurobi callback. See the + // comment in GurobiCallbackImpl() for details. gurobi_->Terminate(); }); @@ -2369,15 +2626,11 @@ absl::StatusOr GurobiSolver::Solve( // interrupter is triggered, this triggers the local interrupter. This may // happen immediately if the user interrupter is already triggered. // - // The local interrupter can also be triggered by a callback returning a true - // `terminate`. + // The local interrupter can also be triggered by a callback returning a + // true `terminate`. const ScopedSolveInterrupterCallback scoped_chaining_callback( interrupter, [&]() { local_interrupter->Interrupt(); }); - // Need to run GRBupdatemodel before registering callbacks (to test if the - // problem is a MIP), setting basis and getting the obj sense. - RETURN_IF_ERROR(gurobi_->UpdateModel()); - if (model_parameters.has_initial_basis()) { RETURN_IF_ERROR(SetGurobiBasis(model_parameters.initial_basis())); } @@ -2417,9 +2670,9 @@ absl::StatusOr GurobiSolver::Solve( RETURN_IF_ERROR(inverted_bounds.ToStatus()); } - // Gurobi will silently impose that indicator variables are binary even if not - // so specified by the user in the model. We return an error here if this is - // the case to be consistent across solvers. + // Gurobi will silently impose that indicator variables are binary even if + // not so specified by the user in the model. We return an error here if + // this is the case to be consistent across solvers. { ASSIGN_OR_RETURN(const InvalidIndicators invalid_indicators, ListInvalidIndicators()); @@ -2445,6 +2698,14 @@ absl::StatusOr GurobiSolver::Solve( return solve_result; } +absl::StatusOr +GurobiSolver::InfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* const interrupter) { + return absl::UnimplementedError( + "MathOpt does not currently support Gurobi's IIS functionality"); +} + MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GUROBI, GurobiSolver::New) } // namespace math_opt diff --git a/ortools/math_opt/solvers/gurobi_solver.h b/ortools/math_opt/solvers/gurobi_solver.h index 09579e44e2..0307c4250f 100644 --- a/ortools/math_opt/solvers/gurobi_solver.h +++ b/ortools/math_opt/solvers/gurobi_solver.h @@ -34,6 +34,7 @@ #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/solve_interrupter.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -61,6 +62,9 @@ class GurobiSolver : public SolverInterface { const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* interrupter) override; absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr InfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + SolveInterrupter* interrupter) override; private: struct GurobiCallbackData { @@ -89,13 +93,16 @@ class GurobiSolver : public SolverInterface { // For easing reading the code, we declare these types: using VariableId = int64_t; + using AuxiliaryObjectiveId = int64_t; using LinearConstraintId = int64_t; using QuadraticConstraintId = int64_t; + using SecondOrderConeConstraintId = int64_t; using Sos1ConstraintId = int64_t; using Sos2ConstraintId = int64_t; using IndicatorConstraintId = int64_t; using AnyConstraintId = int64_t; using GurobiVariableIndex = int; + using GurobiMultiObjectiveIndex = int; using GurobiLinearConstraintIndex = int; using GurobiQuadraticConstraintIndex = int; using GurobiSosConstraintIndex = int; @@ -123,6 +130,12 @@ class GurobiSolver : public SolverInterface { double upper_bound = kInf; }; + struct SecondOrderConeConstraintData { + GurobiQuadraticConstraintIndex constraint_index = kUnspecifiedConstraint; + std::vector slack_variables; + std::vector slack_constraints; + }; + struct SosConstraintData { GurobiSosConstraintIndex constraint_index = kUnspecifiedConstraint; std::vector slack_variables; @@ -157,11 +170,11 @@ class GurobiSolver : public SolverInterface { using IdHashMap = gtl::linked_hash_map; absl::StatusOr GetProblemStatus( - const int grb_termination, const SolutionClaims solution_claims); + int grb_termination, SolutionClaims solution_claims); absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters); absl::Status FillRays(const ModelSolveParametersProto& model_parameters, - const SolutionClaims solution_claims, + SolutionClaims solution_claims, SolveResultProto& result); absl::StatusOr GetSolutions( const ModelSolveParametersProto& model_parameters); @@ -192,7 +205,8 @@ class GurobiSolver : public SolverInterface { absl::StatusOr GetQcpSolution( const ModelSolveParametersProto& model_parameters); // Returns solution information appropriate and available for a MIP - // (integrality on some/all decision variables). + // (integrality on some/all decision variables). Following Gurobi's API, this + // is also used for any multi-objective model. absl::StatusOr GetMipSolutions( const ModelSolveParametersProto& model_parameters); @@ -211,6 +225,9 @@ class GurobiSolver : public SolverInterface { absl::Status AddNewQuadraticConstraints( const google::protobuf::Map& constraints); + absl::Status AddNewSecondOrderConeConstraints( + const google::protobuf::Map& constraints); absl::Status AddNewSosConstraints( const google::protobuf::Map& constraints, @@ -220,6 +237,19 @@ class GurobiSolver : public SolverInterface { const google::protobuf::Map& constraints); absl::Status AddNewVariables(const VariablesProto& new_variables); + absl::Status AddSingleObjective(const ObjectiveProto& objective); + absl::Status AddMultiObjectives( + const ObjectiveProto& primary_objective, + const google::protobuf::Map& + auxiliary_objectives); + // * `objective_id` is the corresponding key into `multi_objectives_map_`; see + // that field for further comment. + // * `is_maximize` is true if the entire Gurobi model is the maximization + // sense (only one sense is permitted per model to be shared by all + // objectives). Note that this need not agree with `objective.maximize`. + absl::Status AddNewMultiObjective( + const ObjectiveProto& objective, + std::optional objective_id, bool is_maximize); absl::Status AddNewSlacks( const std::vector& new_slacks); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); @@ -280,16 +310,37 @@ class GurobiSolver : public SolverInterface { absl::StatusOr> RegisterCallback( const CallbackRegistrationProto& registration, Callback cb, - const MessageCallback message_cb, absl::Time start, + MessageCallback message_cb, absl::Time start, SolveInterrupter* interrupter); // Returns the ids of variables and linear constraints with inverted bounds. absl::StatusOr ListInvertedBounds() const; + // True if the model is in "multi-objective" mode: That is, at some point it + // has been modified via the multi-objective API. + bool is_multi_objective_mode() const; + // Returns the ids of indicator constraint/variables that are invalid because // the indicator is not a binary variable. absl::StatusOr ListInvalidIndicators() const; + struct VariableEqualToExpression { + GurobiVariableIndex variable_index; + std::optional constraint_index; + }; + + // Returns a Gurobi variable that is constrained to be equal to `expression` + // in `.variable_index`. If `expression` is equal to a single variable in the + // model, we return it. Otherwise, the variable is newly added to the model + // and `.constraint_index` is set and refers to the Gurobi linear constraint + // index for a slack constraint just added to the model. + // If `allow_reuse` is false, we require that a new slack variable/constraint + // are introduced. + // TODO(b/267310257): Use this for linear constraint slacks, and maybe move it + // up the stack to a bridge. + absl::StatusOr ExtractVariableEqualToExpression( + const LinearExpressionProto& expression, bool allow_reuse = true); + const std::unique_ptr gurobi_; // Note that we use linked_hash_map for the indices of the gurobi_model_ @@ -301,6 +352,11 @@ class GurobiSolver : public SolverInterface { // Internal correspondence from variable proto IDs to Gurobi-numbered // variables. gtl::linked_hash_map variables_map_; + // An unset key corresponds to the `ModelProto.objective` field; all other + // entries come from `ModelProto.auxiliary_objectives`. + absl::flat_hash_map, + GurobiMultiObjectiveIndex> + multi_objectives_map_; // Internal correspondence from linear constraint proto IDs to // Gurobi-numbered linear constraint and extra information. gtl::linked_hash_map @@ -309,6 +365,11 @@ class GurobiSolver : public SolverInterface { // Gurobi-numbered quadratic constraint. absl::flat_hash_map quadratic_constraints_map_; + // Internal correspondence from second-order cone constraint proto IDs to + // corresponding Gurobi information. + absl::flat_hash_map + soc_constraints_map_; // Internal correspondence from SOS1 constraint proto IDs to Gurobi-numbered // SOS constraint (Gurobi ids are shared between SOS1 and SOS2). absl::flat_hash_map diff --git a/ortools/math_opt/solvers/message_callback_data.cc b/ortools/math_opt/solvers/message_callback_data.cc index 0c0ce13af9..625deed62c 100644 --- a/ortools/math_opt/solvers/message_callback_data.cc +++ b/ortools/math_opt/solvers/message_callback_data.cc @@ -16,36 +16,38 @@ #include #include #include -#include #include #include +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + namespace operations_research { namespace math_opt { std::vector MessageCallbackData::Parse( - const std::string_view message) { + const absl::string_view message) { std::vector strings; // Iterate on all complete lines (lines ending with a '\n'). - std::string_view remainder = message; + absl::string_view remainder = message; for (std::size_t end = 0; end = remainder.find('\n'), end != remainder.npos; remainder = remainder.substr(end + 1)) { const auto line = remainder.substr(0, end); if (!unfinished_line_.empty()) { std::string new_message = std::move(unfinished_line_); unfinished_line_.clear(); - new_message += line; + absl::StrAppend(&new_message, line); strings.push_back(std::move(new_message)); } else { strings.emplace_back(line); } } - // At the end of the loop, the remainder may contains the last unfinished - // line. This could be the first line too if the entire message does not - // contain '\n'. - unfinished_line_ += remainder; + // At the end of the loop, the remainder may contain the last unfinished line. + // This could be the first line too if the entire message does not contain + // '\n'. + absl::StrAppend(&unfinished_line_, remainder); return strings; } @@ -60,5 +62,39 @@ std::vector MessageCallbackData::Flush() { return strings; } +BufferedMessageCallback::BufferedMessageCallback( + SolverInterface::MessageCallback user_message_callback) + : user_message_callback_(std::move(user_message_callback)) {} + +void BufferedMessageCallback::OnMessage(absl::string_view message) { + if (!has_user_message_callback()) { + return; + } + std::vector messages; + { + absl::MutexLock lock(&mutex_); + messages = message_callback_data_.Parse(message); + } + // Do not hold lock during callback to user code. + if (!messages.empty()) { + user_message_callback_(messages); + } +} + +void BufferedMessageCallback::Flush() { + if (!has_user_message_callback()) { + return; + } + std::vector messages; + { + absl::MutexLock lock(&mutex_); + messages = message_callback_data_.Flush(); + } + // Do not hold lock during callback to user code. + if (!messages.empty()) { + user_message_callback_(messages); + } +} + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/solvers/message_callback_data.h b/ortools/math_opt/solvers/message_callback_data.h index 82b9b7185f..7e41314d6c 100644 --- a/ortools/math_opt/solvers/message_callback_data.h +++ b/ortools/math_opt/solvers/message_callback_data.h @@ -16,11 +16,13 @@ #include #include -#include #include -namespace operations_research { -namespace math_opt { +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "ortools/math_opt/core/solver_interface.h" + +namespace operations_research::math_opt { // Buffer for solvers messages that enforces the contract of MessageCallback. // @@ -44,12 +46,12 @@ class MessageCallbackData { MessageCallbackData& operator=(const MessageCallbackData&) = delete; // Parses the input message, returning a vector with all finished - // lines. Returns an empty vector if the input message did not contained any + // lines. Returns an empty vector if the input message did not contain any // '\n'. // // It updates this object with the last unfinished line to use it to complete // the next message. - std::vector Parse(std::string_view message); + std::vector Parse(absl::string_view message); // Returns a vector with the last unfinished line if it exists, else an empty // vector. @@ -60,7 +62,51 @@ class MessageCallbackData { std::string unfinished_line_; }; -} // namespace math_opt -} // namespace operations_research +// Buffers callback data into lines using MessageCallbackData and invokes the +// callback as new lines are ready. +// +// In MathOpt, typically used for solvers that provide a callback with the +// solver logs where the logs contain `\n` characters and messages may be both +// less than a complete line or multiple lines. Recommended use: +// * Register a callback with the underlying solver to get its logs. In the +// callback, when given a log string, call OnMessage() on it. +// * Run the solver's Solve() function. +// * Unregister the callback with the underlying solver. +// * Call Flush() to ensure any final incomplete lines are sent. +// +// If initialized with a nullptr for the user callback, all functions on this +// class have no effect. +// +// This class is threadsafe if the input callback is also threadsafe. +class BufferedMessageCallback { + public: + explicit BufferedMessageCallback( + SolverInterface::MessageCallback user_message_callback); + + // If false, incoming messages are ignored and OnMessage() and Flush() have no + // effect. + bool has_user_message_callback() const { + return user_message_callback_ != nullptr; + } + + // Appends `message` to the buffer, then invokes the callback once on all + // newly complete lines and removes those lines from the buffer. In + // particular, the callback is not invoked if message does not contain any + // `\n`. + void OnMessage(absl::string_view message); + + // If the buffer has any pending message, sends it to the callback. This + // function has no effect if called when the buffer is empty. Calling this + // function when the buffer is non-empty before the stream of logs is complete + // will result in the user getting extra line breaks. + void Flush(); + + private: + SolverInterface::MessageCallback user_message_callback_; + absl::Mutex mutex_; + MessageCallbackData message_callback_data_ ABSL_GUARDED_BY(mutex_); +}; + +} // namespace operations_research::math_opt #endif // OR_TOOLS_MATH_OPT_SOLVERS_MESSAGE_CALLBACK_DATA_H_ diff --git a/ortools/math_opt/solvers/pdlp_bridge.cc b/ortools/math_opt/solvers/pdlp_bridge.cc deleted file mode 100644 index 089724e345..0000000000 --- a/ortools/math_opt/solvers/pdlp_bridge.cc +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/solvers/pdlp_bridge.h" - -#include -#include -#include -#include - -#include "Eigen/Core" -#include "Eigen/SparseCore" -#include "absl/container/flat_hash_map.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "ortools/base/status_macros.h" -#include "ortools/math_opt/core/inverted_bounds.h" -#include "ortools/math_opt/core/math_opt_proto_utils.h" -#include "ortools/math_opt/core/sparse_vector_view.h" -#include "ortools/math_opt/model.pb.h" -#include "ortools/math_opt/solution.pb.h" -#include "ortools/math_opt/sparse_containers.pb.h" -#include "ortools/pdlp/quadratic_program.h" - -namespace operations_research { -namespace math_opt { -namespace { - -constexpr SupportedProblemStructures kPdlpSupportedStructures = { - .quadratic_objectives = SupportType::kSupported}; - -absl::StatusOr ExtractSolution( - const Eigen::VectorXd& values, const std::vector& pdlp_index_to_id, - const SparseVectorFilterProto& filter, const double scale) { - if (values.size() != pdlp_index_to_id.size()) { - return absl::InternalError( - absl::StrCat("Expected solution vector with ", pdlp_index_to_id.size(), - " elements, found: ", values.size())); - } - SparseVectorFilterPredicate predicate(filter); - SparseDoubleVectorProto result; - for (int i = 0; i < pdlp_index_to_id.size(); ++i) { - const double value = scale * values[i]; - const int64_t id = pdlp_index_to_id[i]; - if (predicate.AcceptsAndUpdate(id, value)) { - result.add_ids(id); - result.add_values(value); - } - } - return result; -} - -// We are implicitly assuming that all missing IDs have correspoding value 0. -Eigen::VectorXd EncodeSolution( - const SparseDoubleVectorProto& values, - const absl::flat_hash_map& id_to_pdlp_index, - const double scale) { - Eigen::VectorXd pdlp_vector(Eigen::VectorXd::Zero(id_to_pdlp_index.size())); - const int num_values = values.values_size(); - for (int k = 0; k < num_values; ++k) { - const int64_t index = id_to_pdlp_index.at(values.ids(k)); - pdlp_vector[index] = values.values(k) / scale; - } - return pdlp_vector; -} - -} // namespace - -absl::StatusOr PdlpBridge::FromProto( - const ModelProto& model_proto) { - RETURN_IF_ERROR( - ModelIsSupported(model_proto, kPdlpSupportedStructures, "PDLP")); - PdlpBridge result; - pdlp::QuadraticProgram& pdlp_lp = result.pdlp_lp_; - const VariablesProto& variables = model_proto.variables(); - const LinearConstraintsProto& linear_constraints = - model_proto.linear_constraints(); - pdlp_lp.ResizeAndInitialize(variables.ids_size(), - linear_constraints.ids_size()); - if (!model_proto.name().empty()) { - pdlp_lp.problem_name = model_proto.name(); - } - if (variables.names_size() > 0) { - pdlp_lp.variable_names = {variables.names().begin(), - variables.names().end()}; - } - if (linear_constraints.names_size() > 0) { - pdlp_lp.constraint_names = {linear_constraints.names().begin(), - linear_constraints.names().end()}; - } - for (int i = 0; i < variables.ids_size(); ++i) { - result.var_id_to_pdlp_index_[variables.ids(i)] = i; - result.pdlp_index_to_var_id_.push_back(variables.ids(i)); - pdlp_lp.variable_lower_bounds[i] = variables.lower_bounds(i); - pdlp_lp.variable_upper_bounds[i] = variables.upper_bounds(i); - } - for (int i = 0; i < linear_constraints.ids_size(); ++i) { - result.lin_con_id_to_pdlp_index_[linear_constraints.ids(i)] = i; - result.pdlp_index_to_lin_con_id_.push_back(linear_constraints.ids(i)); - pdlp_lp.constraint_lower_bounds[i] = linear_constraints.lower_bounds(i); - pdlp_lp.constraint_upper_bounds[i] = linear_constraints.upper_bounds(i); - } - const bool is_maximize = model_proto.objective().maximize(); - const double obj_scale = is_maximize ? -1.0 : 1.0; - pdlp_lp.objective_offset = obj_scale * model_proto.objective().offset(); - for (const auto [var_id, coef] : - MakeView(model_proto.objective().linear_coefficients())) { - pdlp_lp.objective_vector[result.var_id_to_pdlp_index_.at(var_id)] = - obj_scale * coef; - } - const SparseDoubleMatrixProto& quadratic_objective = - model_proto.objective().quadratic_coefficients(); - const int obj_nnz = quadratic_objective.row_ids().size(); - if (obj_nnz > 0) { - pdlp_lp.objective_matrix.emplace(); - pdlp_lp.objective_matrix->setZero(variables.ids_size()); - } - for (int i = 0; i < obj_nnz; ++i) { - const int64_t row_index = - result.var_id_to_pdlp_index_.at(quadratic_objective.row_ids(i)); - const int64_t column_index = - result.var_id_to_pdlp_index_.at(quadratic_objective.column_ids(i)); - const double value = obj_scale * quadratic_objective.coefficients(i); - if (row_index != column_index) { - return absl::InvalidArgumentError( - "PDLP cannot solve problems with non-diagonal objective matrices"); - } - // MathOpt represents quadratic objectives in "terms" form, i.e. as a sum - // of double * Variable * Variable terms. They are stored in upper - // triangular form with row_index <= column_index. In contrast, PDLP - // represents quadratic objectives in "matrix" form as 1/2 x'Qx, where Q is - // diagonal. To get to the right format, we simply double each diagonal - // entry. - pdlp_lp.objective_matrix->diagonal()[row_index] = 2 * value; - } - pdlp_lp.objective_scaling_factor = obj_scale; - // Note: MathOpt stores the constraint data in row major order, but PDLP - // wants the data in column major order. There is probably a more efficient - // method to do this transformation. - std::vector> mat_triplets; - const int nnz = model_proto.linear_constraint_matrix().row_ids_size(); - mat_triplets.reserve(nnz); - const SparseDoubleMatrixProto& proto_mat = - model_proto.linear_constraint_matrix(); - for (int i = 0; i < nnz; ++i) { - const int64_t row_index = - result.lin_con_id_to_pdlp_index_.at(proto_mat.row_ids(i)); - const int64_t column_index = - result.var_id_to_pdlp_index_.at(proto_mat.column_ids(i)); - const double value = proto_mat.coefficients(i); - mat_triplets.emplace_back(row_index, column_index, value); - } - pdlp_lp.constraint_matrix.setFromTriplets(mat_triplets.begin(), - mat_triplets.end()); - return result; -} - -InvertedBounds PdlpBridge::ListInvertedBounds() const { - InvertedBounds inverted_bounds; - for (int64_t var_index = 0; var_index < pdlp_index_to_var_id_.size(); - ++var_index) { - if (pdlp_lp_.variable_lower_bounds[var_index] > - pdlp_lp_.variable_upper_bounds[var_index]) { - inverted_bounds.variables.push_back(pdlp_index_to_var_id_[var_index]); - } - } - for (int64_t lin_con_index = 0; - lin_con_index < pdlp_index_to_lin_con_id_.size(); ++lin_con_index) { - if (pdlp_lp_.constraint_lower_bounds[lin_con_index] > - pdlp_lp_.constraint_upper_bounds[lin_con_index]) { - inverted_bounds.linear_constraints.push_back( - pdlp_index_to_lin_con_id_[lin_con_index]); - } - } - return inverted_bounds; -} - -absl::StatusOr PdlpBridge::PrimalVariablesToProto( - const Eigen::VectorXd& primal_values, - const SparseVectorFilterProto& variable_filter) const { - return ExtractSolution(primal_values, pdlp_index_to_var_id_, variable_filter, - /*scale=*/1.0); -} -absl::StatusOr PdlpBridge::DualVariablesToProto( - const Eigen::VectorXd& dual_values, - const SparseVectorFilterProto& linear_constraint_filter) const { - return ExtractSolution(dual_values, pdlp_index_to_lin_con_id_, - linear_constraint_filter, - /*scale=*/pdlp_lp_.objective_scaling_factor); -} -absl::StatusOr PdlpBridge::ReducedCostsToProto( - const Eigen::VectorXd& reduced_costs, - const SparseVectorFilterProto& variable_filter) const { - return ExtractSolution(reduced_costs, pdlp_index_to_var_id_, variable_filter, - /*scale=*/pdlp_lp_.objective_scaling_factor); -} - -pdlp::PrimalAndDualSolution PdlpBridge::SolutionHintToWarmStart( - const SolutionHintProto& solution_hint) const { - // We are implicitly assuming that all missing IDs have correspoding value 0. - pdlp::PrimalAndDualSolution result; - result.primal_solution = EncodeSolution(solution_hint.variable_values(), - var_id_to_pdlp_index_, /*scale=*/1.0); - result.dual_solution = - EncodeSolution(solution_hint.dual_values(), lin_con_id_to_pdlp_index_, - /*scale=*/pdlp_lp_.objective_scaling_factor); - return result; -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/solvers/pdlp_bridge.h b/ortools/math_opt/solvers/pdlp_bridge.h deleted file mode 100644 index 22b299b577..0000000000 --- a/ortools/math_opt/solvers/pdlp_bridge.h +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_MATH_OPT_SOLVERS_PDLP_BRIDGE_H_ -#define OR_TOOLS_MATH_OPT_SOLVERS_PDLP_BRIDGE_H_ - -#include -#include -#include - -#include "Eigen/Core" -#include "absl/container/flat_hash_map.h" -#include "absl/status/statusor.h" -#include "ortools/math_opt/core/inverted_bounds.h" -#include "ortools/math_opt/model.pb.h" -#include "ortools/math_opt/model_parameters.pb.h" -#include "ortools/math_opt/sparse_containers.pb.h" -#include "ortools/pdlp/primal_dual_hybrid_gradient.h" -#include "ortools/pdlp/quadratic_program.h" - -namespace operations_research { -namespace math_opt { - -// Builds a PDLP model (QuadraticProgram) from ModelProto, and provides methods -// to translate solutions back and forth. -// -// The primary difference in the models are: -// 1. PDLP maps the variable/constraint ids to consecutive indices -// [0, 1, ..., n). -// 2. PDLP does not support maximization. If the ModelProto is a maximization -// problem, the objective is negated (coefficients and offset) before -// passing to PDLP. On the way back, the objective value, and all dual -// variables/reduced costs (also for rays) must be negated. -// -// Throughout, it is assumed that the MathOpt protos have been validated, but -// no assumption is made on the PDLP output. Any Status errors resulting from -// invalid PDLP output use the status code kInternal. -class PdlpBridge { - public: - PdlpBridge() = default; - static absl::StatusOr FromProto(const ModelProto& model_proto); - - const pdlp::QuadraticProgram& pdlp_lp() const { return pdlp_lp_; } - - // Returns the ids of variables and linear constraints with inverted bounds. - InvertedBounds ListInvertedBounds() const; - - // TODO(b/183616124): we need to support the inverse of these methods for - // warm start. - absl::StatusOr PrimalVariablesToProto( - const Eigen::VectorXd& primal_values, - const SparseVectorFilterProto& variable_filter) const; - absl::StatusOr DualVariablesToProto( - const Eigen::VectorXd& dual_values, - const SparseVectorFilterProto& linear_constraint_filter) const; - absl::StatusOr ReducedCostsToProto( - const Eigen::VectorXd& reduced_costs, - const SparseVectorFilterProto& variable_filter) const; - pdlp::PrimalAndDualSolution SolutionHintToWarmStart( - const SolutionHintProto& solution_hint) const; - - private: - pdlp::QuadraticProgram pdlp_lp_; - absl::flat_hash_map var_id_to_pdlp_index_; - // NOTE: this vector is strictly increasing - std::vector pdlp_index_to_var_id_; - absl::flat_hash_map lin_con_id_to_pdlp_index_; - // NOTE: this vector is strictly increasing - std::vector pdlp_index_to_lin_con_id_; -}; - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_SOLVERS_PDLP_BRIDGE_H_ diff --git a/ortools/math_opt/solvers/pdlp_solver.cc b/ortools/math_opt/solvers/pdlp_solver.cc deleted file mode 100644 index 34a742e93a..0000000000 --- a/ortools/math_opt/solvers/pdlp_solver.cc +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/solvers/pdlp_solver.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/memory/memory.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_join.h" -#include "absl/time/time.h" -#include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" -#include "ortools/base/protoutil.h" -#include "ortools/base/status_macros.h" -#include "ortools/math_opt/callback.pb.h" -#include "ortools/math_opt/core/inverted_bounds.h" -#include "ortools/math_opt/core/math_opt_proto_utils.h" -#include "ortools/math_opt/core/solve_interrupter.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "ortools/math_opt/model.pb.h" -#include "ortools/math_opt/model_parameters.pb.h" -#include "ortools/math_opt/model_update.pb.h" -#include "ortools/math_opt/parameters.pb.h" -#include "ortools/math_opt/result.pb.h" -#include "ortools/math_opt/solution.pb.h" -#include "ortools/math_opt/solvers/pdlp_bridge.h" -#include "ortools/math_opt/sparse_containers.pb.h" -#include "ortools/math_opt/validators/callback_validator.h" -#include "ortools/pdlp/iteration_stats.h" -#include "ortools/pdlp/primal_dual_hybrid_gradient.h" -#include "ortools/pdlp/quadratic_program.h" -#include "ortools/pdlp/solve_log.pb.h" -#include "ortools/pdlp/solvers.pb.h" -#include "ortools/port/proto_utils.h" - -namespace operations_research { -namespace math_opt { - -using pdlp::PrimalAndDualSolution; -using pdlp::PrimalDualHybridGradientParams; -using pdlp::SolverResult; - -absl::StatusOr> PdlpSolver::New( - const ModelProto& model, const InitArgs& init_args) { - auto result = absl::WrapUnique(new PdlpSolver); - ASSIGN_OR_RETURN(result->pdlp_bridge_, PdlpBridge::FromProto(model)); - return result; -} - -absl::StatusOr PdlpSolver::MergeParameters( - const SolveParametersProto& parameters) { - PrimalDualHybridGradientParams result; - std::vector warnings; - if (parameters.enable_output()) { - result.set_verbosity_level(3); - } - if (parameters.has_threads()) { - result.set_num_threads(parameters.threads()); - } - if (parameters.has_time_limit()) { - result.mutable_termination_criteria()->set_time_sec_limit( - absl::ToDoubleSeconds( - util_time::DecodeGoogleApiProto(parameters.time_limit()).value())); - } - if (parameters.has_node_limit()) { - warnings.push_back("parameter node_limit not supported for PDLP"); - } - if (parameters.has_cutoff_limit()) { - warnings.push_back("parameter cutoff_limit not supported for PDLP"); - } - if (parameters.has_objective_limit()) { - warnings.push_back("parameter best_objective_limit not supported for PDLP"); - } - if (parameters.has_best_bound_limit()) { - warnings.push_back("parameter best_bound_limit not supported for PDLP"); - } - if (parameters.has_solution_limit()) { - warnings.push_back("parameter solution_limit not supported for PDLP"); - } - if (parameters.has_random_seed()) { - warnings.push_back("parameter random_seed not supported for PDLP"); - } - if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { - warnings.push_back("parameter lp_algorithm not supported for PDLP"); - } - if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { - warnings.push_back("parameter presolve not supported for PDLP"); - } - if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { - warnings.push_back("parameter cuts not supported for PDLP"); - } - if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { - warnings.push_back("parameter heuristics not supported for PDLP"); - } - if (parameters.scaling() != EMPHASIS_UNSPECIFIED) { - warnings.push_back("parameter scaling not supported for PDLP"); - } - if (parameters.has_iteration_limit()) { - const int64_t limit = std::min(std::numeric_limits::max(), - parameters.iteration_limit()); - result.mutable_termination_criteria()->set_iteration_limit( - static_cast(limit)); - } - result.MergeFrom(parameters.pdlp()); - if (!warnings.empty()) { - return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); - } - return result; -} - -namespace { - -absl::StatusOr ConvertReason( - const pdlp::TerminationReason pdlp_reason, const std::string& pdlp_detail) { - switch (pdlp_reason) { - case pdlp::TERMINATION_REASON_UNSPECIFIED: - return TerminateForReason(TERMINATION_REASON_UNSPECIFIED, pdlp_detail); - case pdlp::TERMINATION_REASON_OPTIMAL: - return TerminateForReason(TERMINATION_REASON_OPTIMAL, pdlp_detail); - case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE, pdlp_detail); - case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, - pdlp_detail); - case pdlp::TERMINATION_REASON_TIME_LIMIT: - return NoSolutionFoundTermination(LIMIT_TIME, pdlp_detail); - case pdlp::TERMINATION_REASON_ITERATION_LIMIT: - return NoSolutionFoundTermination(LIMIT_ITERATION, pdlp_detail); - case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT: - return NoSolutionFoundTermination(LIMIT_OTHER, pdlp_detail); - case pdlp::TERMINATION_REASON_NUMERICAL_ERROR: - return TerminateForReason(TERMINATION_REASON_NUMERICAL_ERROR, - pdlp_detail); - case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER: - return NoSolutionFoundTermination(LIMIT_INTERRUPTED, pdlp_detail); - case pdlp::TERMINATION_REASON_INVALID_PROBLEM: - // Indicates that the solver detected invalid problem data, e.g., - // inconsistent bounds. - return absl::InternalError( - absl::StrCat("Invalid problem sent to PDLP solver " - "(TERMINATION_REASON_INVALID_PROBLEM): ", - pdlp_detail)); - case pdlp::TERMINATION_REASON_INVALID_INITIAL_SOLUTION: - return absl::InvalidArgumentError( - absl::StrCat("PDLP solution hint invalid " - "(TERMINATION_REASON_INVALID_INITIAL_SOLUTION): ", - pdlp_detail)); - case pdlp::TERMINATION_REASON_INVALID_PARAMETER: - // Indicates that an invalid value for the parameters was detected. - return absl::InvalidArgumentError(absl::StrCat( - "PDLP parameters invalid (TERMINATION_REASON_INVALID_PARAMETER): ", - pdlp_detail)); - case pdlp::TERMINATION_REASON_OTHER: - return TerminateForReason(TERMINATION_REASON_OTHER_ERROR, pdlp_detail); - default: - LOG(FATAL) << "PDLP status: " << ProtoEnumToString(pdlp_reason) - << " not implemented."; - } -} - -ProblemStatusProto GetProblemStatus(const pdlp::TerminationReason pdlp_reason, - const bool has_finite_dual_bound) { - ProblemStatusProto problem_status; - - switch (pdlp_reason) { - case pdlp::TERMINATION_REASON_OPTIMAL: - problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE); - problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); - break; - case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: - problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE); - problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); - break; - case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: - problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); - problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE); - break; - case pdlp::TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE: - problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); - problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); - problem_status.set_primal_or_dual_infeasible(true); - break; - default: - problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); - problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); - break; - } - if (has_finite_dual_bound) { - problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); - } - return problem_status; -} - -} // namespace - -absl::StatusOr PdlpSolver::MakeSolveResult( - const pdlp::SolverResult& pdlp_result, - const ModelSolveParametersProto& model_params) { - SolveResultProto result; - ASSIGN_OR_RETURN(*result.mutable_termination(), - ConvertReason(pdlp_result.solve_log.termination_reason(), - pdlp_result.solve_log.termination_string())); - ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_solve_time(), - util_time::EncodeGoogleApiProto( - absl::Seconds(pdlp_result.solve_log.solve_time_sec()))); - result.mutable_solve_stats()->set_first_order_iterations( - pdlp_result.solve_log.iteration_count()); - const std::optional convergence_information = - pdlp::GetConvergenceInformation(pdlp_result.solve_log.solution_stats(), - pdlp_result.solve_log.solution_type()); - - // TODO(b/195295177): update description after changes to bounds below. - // Set default infinite primal/dual bounds. PDLP's default is a minimization - // problem for which the default primal and dual bounds are infinity and - // -infinity respectively. PDLP provides a scaling factor to flip the signs - // for maximization problems. Note that PDLP does not consider solutions that - // are feasible up to the solver's tolerances to update these bounds. PDLP - // provides a correction function for dual solutions that yields a true dual - // bound, but does not provide this function for primal solutions. - const double objective_scaling_factor = - pdlp_bridge_.pdlp_lp().objective_scaling_factor; - result.mutable_solve_stats()->set_best_primal_bound( - objective_scaling_factor * std::numeric_limits::infinity()); - result.mutable_solve_stats()->set_best_dual_bound( - -objective_scaling_factor * std::numeric_limits::infinity()); - - switch (pdlp_result.solve_log.termination_reason()) { - case pdlp::TERMINATION_REASON_OPTIMAL: - case pdlp::TERMINATION_REASON_TIME_LIMIT: - case pdlp::TERMINATION_REASON_ITERATION_LIMIT: - case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT: - case pdlp::TERMINATION_REASON_NUMERICAL_ERROR: - case pdlp::TERMINATION_REASON_INTERRUPTED_BY_USER: { - SolutionProto* solution_proto = result.add_solutions(); - { - auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto( - pdlp_result.primal_solution, model_params.variable_values_filter()); - RETURN_IF_ERROR(maybe_primal.status()); - PrimalSolutionProto* primal_proto = - solution_proto->mutable_primal_solution(); - primal_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); - *primal_proto->mutable_variable_values() = *std::move(maybe_primal); - // Note: the solution could be primal feasible for termination reasons - // other than TERMINATION_REASON_OPTIMAL, but in theory, PDLP could - // also be modified to return the best feasible solution encounered in - // an early termination run (if any). - if (pdlp_result.solve_log.termination_reason() == - pdlp::TERMINATION_REASON_OPTIMAL) { - primal_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); - } - if (convergence_information.has_value()) { - primal_proto->set_objective_value( - convergence_information->primal_objective()); - // TODO(b/195295177): update to return bounds. - // PDLP does not have a primal objective correction function so we - // skip the primal bound update. - } - } - { - auto maybe_dual = pdlp_bridge_.DualVariablesToProto( - pdlp_result.dual_solution, model_params.dual_values_filter()); - RETURN_IF_ERROR(maybe_dual.status()); - auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto( - pdlp_result.reduced_costs, model_params.reduced_costs_filter()); - RETURN_IF_ERROR(maybe_reduced.status()); - DualSolutionProto* dual_proto = solution_proto->mutable_dual_solution(); - dual_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); - *dual_proto->mutable_dual_values() = *std::move(maybe_dual); - *dual_proto->mutable_reduced_costs() = *std::move(maybe_reduced); - // Note: same comment on primal solution status holds here. - if (pdlp_result.solve_log.termination_reason() == - pdlp::TERMINATION_REASON_OPTIMAL) { - dual_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); - } - if (convergence_information.has_value()) { - const double dual_obj = convergence_information->dual_objective(); - dual_proto->set_objective_value(dual_obj); - // TODO(b/195295177): update to use dual_obj or corrected_dual_bound. - // Using PDLP's corrected dual objective to update dual bounds. - const double corrected_dual_bound = - convergence_information->corrected_dual_objective(); - result.mutable_solve_stats()->set_best_dual_bound( - corrected_dual_bound); - } - } - break; - } - case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: { - // NOTE: for primal infeasible problems, PDLP stores the infeasibility - // certificate (dual ray) in the dual variables and reduced costs. - auto maybe_dual = pdlp_bridge_.DualVariablesToProto( - pdlp_result.dual_solution, model_params.dual_values_filter()); - RETURN_IF_ERROR(maybe_dual.status()); - auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto( - pdlp_result.reduced_costs, model_params.reduced_costs_filter()); - RETURN_IF_ERROR(maybe_reduced.status()); - DualRayProto* dual_ray_proto = result.add_dual_rays(); - *dual_ray_proto->mutable_dual_values() = *std::move(maybe_dual); - *dual_ray_proto->mutable_reduced_costs() = *std::move(maybe_reduced); - break; - } - case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: { - // NOTE: for dual infeasible problems, PDLP stores the infeasibility - // certificate (primal ray) in the primal variables. - auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto( - pdlp_result.primal_solution, model_params.variable_values_filter()); - RETURN_IF_ERROR(maybe_primal.status()); - PrimalRayProto* primal_ray_proto = result.add_primal_rays(); - *primal_ray_proto->mutable_variable_values() = *std::move(maybe_primal); - break; - } - default: - break; - } - *result.mutable_solve_stats()->mutable_problem_status() = - GetProblemStatus(pdlp_result.solve_log.termination_reason(), - std::isfinite(result.solve_stats().best_dual_bound())); - return result; -} - -absl::StatusOr PdlpSolver::Solve( - const SolveParametersProto& parameters, - const ModelSolveParametersProto& model_parameters, - const MessageCallback message_cb, - const CallbackRegistrationProto& callback_registration, const Callback cb, - SolveInterrupter* const interrupter) { - // TODO(b/183502493): Implement message callback when PDLP supports that. - if (message_cb != nullptr) { - return absl::InvalidArgumentError(internal::kMessageCallbackNotSupported); - } - - RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, - /*supported_events=*/{})); - - ASSIGN_OR_RETURN(auto pdlp_params, MergeParameters(parameters)); - - // PDLP returns `(TERMINATION_REASON_INVALID_PROBLEM): The input problem has - // inconsistent bounds.` but we want a more detailed error. - RETURN_IF_ERROR(pdlp_bridge_.ListInvertedBounds().ToStatus()); - - std::atomic interrupt = false; - const ScopedSolveInterrupterCallback set_interrupt( - interrupter, [&]() { interrupt = true; }); - - std::optional initial_solution; - if (!model_parameters.solution_hints().empty()) { - initial_solution = pdlp_bridge_.SolutionHintToWarmStart( - model_parameters.solution_hints(0)); - } - - const SolverResult pdlp_result = PrimalDualHybridGradient( - pdlp_bridge_.pdlp_lp(), pdlp_params, initial_solution, &interrupt); - return MakeSolveResult(pdlp_result, model_parameters); -} - -absl::StatusOr PdlpSolver::Update(const ModelUpdateProto& model_update) { - return false; -} - -MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_PDLP, PdlpSolver::New); - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/solvers/pdlp_solver.h b/ortools/math_opt/solvers/pdlp_solver.h deleted file mode 100644 index 53cdd015aa..0000000000 --- a/ortools/math_opt/solvers/pdlp_solver.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_MATH_OPT_SOLVERS_PDLP_SOLVER_H_ -#define OR_TOOLS_MATH_OPT_SOLVERS_PDLP_SOLVER_H_ - -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "ortools/math_opt/callback.pb.h" -#include "ortools/math_opt/core/solve_interrupter.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "ortools/math_opt/model.pb.h" -#include "ortools/math_opt/model_parameters.pb.h" -#include "ortools/math_opt/model_update.pb.h" -#include "ortools/math_opt/parameters.pb.h" -#include "ortools/math_opt/result.pb.h" -#include "ortools/math_opt/solvers/pdlp_bridge.h" -#include "ortools/pdlp/primal_dual_hybrid_gradient.h" -#include "ortools/pdlp/solvers.pb.h" - -namespace operations_research { -namespace math_opt { - -class PdlpSolver : public SolverInterface { - public: - static absl::StatusOr> New( - const ModelProto& model, const InitArgs& init_args); - - absl::StatusOr Solve( - const SolveParametersProto& parameters, - const ModelSolveParametersProto& model_parameters, - MessageCallback message_cb, - const CallbackRegistrationProto& callback_registration, Callback cb, - SolveInterrupter* interrupter) override; - absl::StatusOr Update(const ModelUpdateProto& model_update) override; - - // Returns the merged parameters and a list of warnings. - static absl::StatusOr MergeParameters( - const SolveParametersProto& parameters); - - private: - PdlpSolver() = default; - - absl::StatusOr MakeSolveResult( - const pdlp::SolverResult& pdlp_result, - const ModelSolveParametersProto& model_params); - - PdlpBridge pdlp_bridge_; -}; - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_SOLVERS_PDLP_SOLVER_H_ diff --git a/ortools/math_opt/storage/BUILD.bazel b/ortools/math_opt/storage/BUILD.bazel index c9d454cedd..dbee63fed7 100644 --- a/ortools/math_opt/storage/BUILD.bazel +++ b/ortools/math_opt/storage/BUILD.bazel @@ -69,12 +69,11 @@ cc_library( hdrs = ["update_trackers.h"], deps = [ ":model_storage_types", - "//ortools/base", - #"//ortools/base:logging", "//ortools/base:intops", "//ortools/base:stl_util", "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/synchronization", ], ) @@ -108,11 +107,13 @@ cc_library( ":sparse_coefficient_map", ":sparse_matrix", "//ortools/base:intops", + "//ortools/base:map_util", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_protobuf//:protobuf", ], ) @@ -125,7 +126,6 @@ cc_library( ":range", ":sorted", ":sparse_matrix", - "//ortools/base", "//ortools/base:intops", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -133,6 +133,7 @@ cc_library( "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/meta:type_traits", "@com_google_absl//absl/strings", ], @@ -145,12 +146,12 @@ cc_library( ":model_storage_types", ":range", ":sorted", - "//ortools/base", "//ortools/base:intops", "//ortools/base:map_util", "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:check", "@com_google_protobuf//:protobuf", ], ) @@ -168,8 +169,6 @@ cc_library( ":sparse_matrix", ":update_trackers", ":variable_storage", - "//ortools/base", - #"//ortools/base:logging", "//ortools/base:intops", "//ortools/base:map_util", "//ortools/base:status_macros", @@ -178,12 +177,14 @@ cc_library( "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/constraints/indicator:storage", "//ortools/math_opt/constraints/quadratic:storage", + "//ortools/math_opt/constraints/second_order_cone:storage", "//ortools/math_opt/constraints/sos:storage", "//ortools/math_opt/core:model_summary", "//ortools/math_opt/core:sparse_vector_view", "//ortools/math_opt/validators:model_validator", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/meta:type_traits", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", @@ -202,3 +203,15 @@ cc_library( hdrs = ["iterators.h"], deps = [":range"], ) + +cc_library( + name = "linear_expression_data", + hdrs = ["linear_expression_data.h"], + deps = [ + ":sorted", + ":sparse_coefficient_map", + "//ortools/base:intops", + "//ortools/math_opt:sparse_containers_cc_proto", + "@com_google_absl//absl/container:flat_hash_map", + ], +) diff --git a/ortools/math_opt/storage/atomic_constraint_storage.h b/ortools/math_opt/storage/atomic_constraint_storage.h index 68cf2aca3e..b4e1a904b1 100644 --- a/ortools/math_opt/storage/atomic_constraint_storage.h +++ b/ortools/math_opt/storage/atomic_constraint_storage.h @@ -21,8 +21,8 @@ #include "absl/algorithm/container.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" -#include "google/protobuf/map.h" #include "absl/log/check.h" +#include "google/protobuf/map.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/storage/model_storage_types.h" diff --git a/ortools/math_opt/storage/linear_constraint_storage.cc b/ortools/math_opt/storage/linear_constraint_storage.cc index 7e4eaf63a4..821256d71d 100644 --- a/ortools/math_opt/storage/linear_constraint_storage.cc +++ b/ortools/math_opt/storage/linear_constraint_storage.cc @@ -130,12 +130,12 @@ LinearConstraintStorage::UpdateResult LinearConstraintStorage::Update( } absl::c_sort(result.deleted); - for (const LinearConstraintId c : SortedElements(diff.lower_bounds)) { + for (const LinearConstraintId c : SortedSetElements(diff.lower_bounds)) { result.updates.mutable_lower_bounds()->add_ids(c.value()); result.updates.mutable_lower_bounds()->add_values(lower_bound(c)); } - for (const LinearConstraintId c : SortedElements(diff.upper_bounds)) { + for (const LinearConstraintId c : SortedSetElements(diff.upper_bounds)) { result.updates.mutable_upper_bounds()->add_ids(c.value()); result.updates.mutable_upper_bounds()->add_values(upper_bound(c)); } diff --git a/ortools/math_opt/storage/linear_constraint_storage.h b/ortools/math_opt/storage/linear_constraint_storage.h index f3727d82f4..ad4abcd4eb 100644 --- a/ortools/math_opt/storage/linear_constraint_storage.h +++ b/ortools/math_opt/storage/linear_constraint_storage.h @@ -23,9 +23,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/meta/type_traits.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" diff --git a/ortools/math_opt/storage/linear_expression_data.h b/ortools/math_opt/storage/linear_expression_data.h new file mode 100644 index 0000000000..c75a8472e4 --- /dev/null +++ b/ortools/math_opt/storage/linear_expression_data.h @@ -0,0 +1,71 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_STORAGE_LINEAR_EXPRESSION_DATA_H_ +#define OR_TOOLS_MATH_OPT_STORAGE_LINEAR_EXPRESSION_DATA_H_ + +#include + +#include "absl/container/flat_hash_map.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/sorted.h" +#include "ortools/math_opt/storage/sparse_coefficient_map.h" + +namespace operations_research::math_opt { + +// Represents a linear expression in "raw ID" form. +// +// The data storage is not interesting, this struct exists to provide helpers +// that go to/from the proto representation (via member functions) and the C++ +// model representations (via raw functions in `constraints/util/model_util.h`). +struct LinearExpressionData { + inline LinearExpressionProto Proto() const; + // This method assumes that `expr_proto` is in a valid state; see the inline + // comments for `LinearExpressionProto` for details. + inline static LinearExpressionData FromProto( + const LinearExpressionProto& expr_proto); + + SparseCoefficientMap coeffs; + double offset = 0.0; +}; + +// Inline implementations. +LinearExpressionProto LinearExpressionData::Proto() const { + LinearExpressionProto proto_expr; + proto_expr.set_offset(offset); + { + const int num_terms = static_cast(coeffs.terms().size()); + proto_expr.mutable_ids()->Reserve(num_terms); + proto_expr.mutable_coefficients()->Reserve(num_terms); + } + for (const VariableId id : SortedMapKeys(coeffs.terms())) { + proto_expr.add_ids(id.value()); + proto_expr.add_coefficients(coeffs.get(id)); + } + return proto_expr; +} + +LinearExpressionData LinearExpressionData::FromProto( + const LinearExpressionProto& expr_proto) { + LinearExpressionData expr_data{.offset = expr_proto.offset()}; + for (int i = 0; i < expr_proto.ids_size(); ++i) { + expr_data.coeffs.set(VariableId(expr_proto.ids(i)), + expr_proto.coefficients(i)); + } + return expr_data; +} + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_STORAGE_LINEAR_EXPRESSION_DATA_H_ diff --git a/ortools/math_opt/storage/model_storage.cc b/ortools/math_opt/storage/model_storage.cc index a95271bdd1..30dc2cfc75 100644 --- a/ortools/math_opt/storage/model_storage.cc +++ b/ortools/math_opt/storage/model_storage.cc @@ -22,11 +22,11 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "absl/log/check.h" #include "ortools/base/map_util.h" #include "ortools/base/status_macros.h" #include "ortools/base/strong_int.h" @@ -54,18 +54,24 @@ absl::StatusOr> ModelStorage::FromModelProto( // sure duplicated names don't fail. RETURN_IF_ERROR(ValidateModel(model_proto, /*check_names=*/false).status()); - auto storage = std::make_unique(model_proto.name()); + auto storage = std::make_unique(model_proto.name(), + model_proto.objective().name()); // Add variables. storage->AddVariables(model_proto.variables()); // Set the objective. - storage->set_is_maximize(model_proto.objective().maximize()); - storage->set_objective_offset(model_proto.objective().offset()); + storage->set_is_maximize(kPrimaryObjectiveId, + model_proto.objective().maximize()); + storage->set_objective_offset(kPrimaryObjectiveId, + model_proto.objective().offset()); storage->UpdateLinearObjectiveCoefficients( - model_proto.objective().linear_coefficients()); + kPrimaryObjectiveId, model_proto.objective().linear_coefficients()); storage->UpdateQuadraticObjectiveCoefficients( - model_proto.objective().quadratic_coefficients()); + kPrimaryObjectiveId, model_proto.objective().quadratic_coefficients()); + + // Set the auxiliary objectives. + storage->AddAuxiliaryObjectives(model_proto.auxiliary_objectives()); // Add linear constraints. storage->AddLinearConstraints(model_proto.linear_constraints()); @@ -78,6 +84,10 @@ absl::StatusOr> ModelStorage::FromModelProto( storage->quadratic_constraints_.AddConstraints( model_proto.quadratic_constraints()); + // Add SOC constraints. + storage->soc_constraints_.AddConstraints( + model_proto.second_order_cone_constraints()); + // Add SOS constraints. storage->sos1_constraints_.AddConstraints(model_proto.sos1_constraints()); storage->sos2_constraints_.AddConstraints(model_proto.sos2_constraints()); @@ -89,19 +99,34 @@ absl::StatusOr> ModelStorage::FromModelProto( return storage; } +void ModelStorage::UpdateObjective(const ObjectiveId id, + const ObjectiveUpdatesProto& update) { + if (update.has_direction_update()) { + set_is_maximize(id, update.direction_update()); + } + if (update.has_priority_update()) { + set_objective_priority(id, update.priority_update()); + } + if (update.has_offset_update()) { + set_objective_offset(id, update.offset_update()); + } + UpdateLinearObjectiveCoefficients(id, update.linear_coefficients()); + UpdateQuadraticObjectiveCoefficients(id, update.quadratic_coefficients()); +} + void ModelStorage::UpdateLinearObjectiveCoefficients( - const SparseDoubleVectorProto& coefficients) { + const ObjectiveId id, const SparseDoubleVectorProto& coefficients) { for (const auto [var_id, value] : MakeView(coefficients)) { - set_linear_objective_coefficient(VariableId(var_id), value); + set_linear_objective_coefficient(id, VariableId(var_id), value); } } void ModelStorage::UpdateQuadraticObjectiveCoefficients( - const SparseDoubleMatrixProto& coefficients) { + const ObjectiveId id, const SparseDoubleMatrixProto& coefficients) { for (int i = 0; i < coefficients.row_ids_size(); ++i) { // This call is valid since this is an upper triangular matrix; there is no // duplicated terms. - set_quadratic_objective_coefficient(VariableId(coefficients.row_ids(i)), + set_quadratic_objective_coefficient(id, VariableId(coefficients.row_ids(i)), VariableId(coefficients.column_ids(i)), coefficients.coefficients(i)); } @@ -132,10 +157,14 @@ std::unique_ptr ModelStorage::Clone( // Update the next ids so that the clone does not reused any deleted id from // the original. clone.value()->ensure_next_variable_id_at_least(next_variable_id()); + clone.value()->ensure_next_auxiliary_objective_id_at_least( + next_auxiliary_objective_id()); clone.value()->ensure_next_linear_constraint_id_at_least( next_linear_constraint_id()); clone.value()->ensure_next_constraint_id_at_least( next_constraint_id()); + clone.value()->ensure_next_constraint_id_at_least( + next_constraint_id()); clone.value()->ensure_next_constraint_id_at_least( next_constraint_id()); clone.value()->ensure_next_constraint_id_at_least( @@ -172,7 +201,7 @@ void ModelStorage::DeleteVariable(const VariableId id) { const auto& trackers = update_trackers_.GetUpdatedTrackers(); // Reuse output of GetUpdatedTrackers() only once to ensure a consistent view, // do not call UpdateAndGetLinearConstraintDiffs() etc. - objective_.DeleteVariable( + objectives_.DeleteVariable( id, MakeUpdateDataFieldRange<&UpdateTrackerData::dirty_objective>(trackers)); linear_constraints_.DeleteVariable( @@ -180,6 +209,7 @@ void ModelStorage::DeleteVariable(const VariableId id) { MakeUpdateDataFieldRange<&UpdateTrackerData::dirty_linear_constraints>( trackers)); quadratic_constraints_.DeleteVariable(id); + soc_constraints_.DeleteVariable(id); sos1_constraints_.DeleteVariable(id); sos2_constraints_.DeleteVariable(id); indicator_constraints_.DeleteVariable(id); @@ -232,17 +262,38 @@ std::vector ModelStorage::SortedLinearConstraints() const { return linear_constraints_.SortedLinearConstraints(); } +void ModelStorage::AddAuxiliaryObjectives( + const google::protobuf::Map& objectives) { + for (const int64_t raw_id : SortedMapKeys(objectives)) { + const AuxiliaryObjectiveId id = AuxiliaryObjectiveId(raw_id); + ensure_next_auxiliary_objective_id_at_least(id); + const ObjectiveProto& proto = objectives.at(raw_id); + AddAuxiliaryObjective(proto.priority(), proto.name()); + set_is_maximize(id, proto.maximize()); + set_objective_offset(id, proto.offset()); + for (const auto [raw_var_id, coeff] : + MakeView(proto.linear_coefficients())) { + set_linear_objective_coefficient(id, VariableId(raw_var_id), coeff); + } + } +} + ModelProto ModelStorage::ExportModel() const { ModelProto result; result.set_name(name_); *result.mutable_variables() = variables_.Proto(); - *result.mutable_objective() = objective_.Proto(); + { + auto [primary, auxiliary] = objectives_.Proto(); + *result.mutable_objective() = std::move(primary); + *result.mutable_auxiliary_objectives() = std::move(auxiliary); + } { auto [constraints, matrix] = linear_constraints_.Proto(); *result.mutable_linear_constraints() = std::move(constraints); *result.mutable_linear_constraint_matrix() = std::move(matrix); } *result.mutable_quadratic_constraints() = quadratic_constraints_.Proto(); + *result.mutable_second_order_cone_constraints() = soc_constraints_.Proto(); *result.mutable_sos1_constraints() = sos1_constraints_.Proto(); *result.mutable_sos2_constraints() = sos2_constraints_.Proto(); *result.mutable_indicator_constraints() = indicator_constraints_.Proto(); @@ -256,10 +307,11 @@ ModelStorage::UpdateTrackerData::ExportModelUpdate( // ExportModelUpdate(). if (storage.variables_.diff_is_empty(dirty_variables) && - storage.objective_.diff_is_empty(dirty_objective) && + storage.objectives_.diff_is_empty(dirty_objective) && storage.linear_constraints_.diff_is_empty(dirty_linear_constraints) && storage.quadratic_constraints_.diff_is_empty( dirty_quadratic_constraints) && + storage.soc_constraints_.diff_is_empty(dirty_soc_constraints) && storage.sos1_constraints_.diff_is_empty(dirty_sos1_constraints) && storage.sos2_constraints_.diff_is_empty(dirty_sos2_constraints) && storage.indicator_constraints_.diff_is_empty( @@ -299,6 +351,10 @@ ModelStorage::UpdateTrackerData::ExportModelUpdate( *result.mutable_quadratic_constraint_updates() = storage.quadratic_constraints_.Update(dirty_quadratic_constraints); + // Second-order cone constraint updates + *result.mutable_second_order_cone_constraint_updates() = + storage.soc_constraints_.Update(dirty_soc_constraints); + // SOS constraint updates *result.mutable_sos1_constraint_updates() = storage.sos1_constraints_.Update(dirty_sos1_constraints); @@ -310,8 +366,12 @@ ModelStorage::UpdateTrackerData::ExportModelUpdate( storage.indicator_constraints_.Update(dirty_indicator_constraints); // Update the objective - *result.mutable_objective_updates() = storage.objective_.Update( - dirty_objective, dirty_variables.deleted, new_variables); + { + auto [primary, auxiliary] = storage.objectives_.Update( + dirty_objective, dirty_variables.deleted, new_variables); + *result.mutable_objective_updates() = std::move(primary); + *result.mutable_auxiliary_objectives_updates() = std::move(auxiliary); + } // Note: Named returned value optimization (NRVO) does not apply here. return {std::move(result)}; } @@ -319,12 +379,13 @@ ModelStorage::UpdateTrackerData::ExportModelUpdate( void ModelStorage::UpdateTrackerData::AdvanceCheckpoint( const ModelStorage& storage) { storage.variables_.AdvanceCheckpointInDiff(dirty_variables); - storage.objective_.AdvanceCheckpointInDiff(dirty_variables.checkpoint, - dirty_objective); + storage.objectives_.AdvanceCheckpointInDiff(dirty_variables.checkpoint, + dirty_objective); storage.linear_constraints_.AdvanceCheckpointInDiff( dirty_variables.checkpoint, dirty_linear_constraints); storage.quadratic_constraints_.AdvanceCheckpointInDiff( dirty_quadratic_constraints); + storage.soc_constraints_.AdvanceCheckpointInDiff(dirty_soc_constraints); storage.sos1_constraints_.AdvanceCheckpointInDiff(dirty_sos1_constraints); storage.sos2_constraints_.AdvanceCheckpointInDiff(dirty_sos2_constraints); storage.indicator_constraints_.AdvanceCheckpointInDiff( @@ -333,8 +394,9 @@ void ModelStorage::UpdateTrackerData::AdvanceCheckpoint( UpdateTrackerId ModelStorage::NewUpdateTracker() { return update_trackers_.NewUpdateTracker( - variables_, linear_constraints_, quadratic_constraints_, - sos1_constraints_, sos2_constraints_, indicator_constraints_); + variables_, objectives_, linear_constraints_, quadratic_constraints_, + soc_constraints_, sos1_constraints_, sos2_constraints_, + indicator_constraints_); } void ModelStorage::DeleteUpdateTracker(const UpdateTrackerId update_tracker) { @@ -363,6 +425,13 @@ absl::Status ModelStorage::ApplyUpdateProto( } RETURN_IF_ERROR( summary.variables.SetNextFreeId(variables_.next_id().value())); + for (const AuxiliaryObjectiveId id : SortedAuxiliaryObjectives()) { + RETURN_IF_ERROR( + summary.auxiliary_objectives.Insert(id.value(), objective_name(id))) + << "invalid auxiliary objective id in model"; + } + RETURN_IF_ERROR(summary.auxiliary_objectives.SetNextFreeId( + objectives_.next_id().value())); for (const LinearConstraintId id : SortedLinearConstraints()) { RETURN_IF_ERROR(summary.linear_constraints.Insert( id.value(), linear_constraint_name(id))) @@ -377,6 +446,13 @@ absl::Status ModelStorage::ApplyUpdateProto( } RETURN_IF_ERROR(summary.quadratic_constraints.SetNextFreeId( quadratic_constraints_.next_id().value())); + for (const auto id : SortedConstraints()) { + RETURN_IF_ERROR(summary.second_order_cone_constraints.Insert( + id.value(), soc_constraints_.data(id).name)) + << "invalid second-order cone constraint id in model"; + } + RETURN_IF_ERROR(summary.second_order_cone_constraints.SetNextFreeId( + soc_constraints_.next_id().value())); for (const Sos1ConstraintId id : SortedConstraints()) { RETURN_IF_ERROR(summary.sos1_constraints.Insert( id.value(), constraint_data(id).name())) @@ -408,6 +484,10 @@ absl::Status ModelStorage::ApplyUpdateProto( for (const int64_t v_id : update_proto.deleted_variable_ids()) { DeleteVariable(VariableId(v_id)); } + for (const int64_t o_id : + update_proto.auxiliary_objectives_updates().deleted_objective_ids()) { + DeleteAuxiliaryObjective(AuxiliaryObjectiveId(o_id)); + } for (const int64_t c_id : update_proto.deleted_linear_constraint_ids()) { DeleteLinearConstraint(LinearConstraintId(c_id)); } @@ -415,6 +495,10 @@ absl::Status ModelStorage::ApplyUpdateProto( update_proto.quadratic_constraint_updates().deleted_constraint_ids()) { DeleteAtomicConstraint(QuadraticConstraintId(c_id)); } + for (const int64_t c_id : update_proto.second_order_cone_constraint_updates() + .deleted_constraint_ids()) { + DeleteAtomicConstraint(SecondOrderConeConstraintId(c_id)); + } for (const int64_t c_id : update_proto.sos1_constraint_updates().deleted_constraint_ids()) { DeleteAtomicConstraint(Sos1ConstraintId(c_id)); @@ -454,9 +538,13 @@ absl::Status ModelStorage::ApplyUpdateProto( // Add the new variables and constraints. AddVariables(update_proto.new_variables()); + AddAuxiliaryObjectives( + update_proto.auxiliary_objectives_updates().new_objectives()); AddLinearConstraints(update_proto.new_linear_constraints()); quadratic_constraints_.AddConstraints( update_proto.quadratic_constraint_updates().new_constraints()); + soc_constraints_.AddConstraints( + update_proto.second_order_cone_constraint_updates().new_constraints()); sos1_constraints_.AddConstraints( update_proto.sos1_constraint_updates().new_constraints()); sos2_constraints_.AddConstraints( @@ -464,17 +552,14 @@ absl::Status ModelStorage::ApplyUpdateProto( indicator_constraints_.AddConstraints( update_proto.indicator_constraint_updates().new_constraints()); - // Update the objective. - if (update_proto.objective_updates().has_direction_update()) { - set_is_maximize(update_proto.objective_updates().direction_update()); + // Update the primary objective. + UpdateObjective(kPrimaryObjectiveId, update_proto.objective_updates()); + + // Update the auxiliary objectives. + for (const auto& [raw_id, objective_update] : + update_proto.auxiliary_objectives_updates().objective_updates()) { + UpdateObjective(AuxiliaryObjectiveId(raw_id), objective_update); } - if (update_proto.objective_updates().has_offset_update()) { - set_objective_offset(update_proto.objective_updates().offset_update()); - } - UpdateLinearObjectiveCoefficients( - update_proto.objective_updates().linear_coefficients()); - UpdateQuadraticObjectiveCoefficients( - update_proto.objective_updates().quadratic_coefficients()); // Update the linear constraints' coefficients. UpdateLinearConstraintCoefficients( diff --git a/ortools/math_opt/storage/model_storage.h b/ortools/math_opt/storage/model_storage.h index 3eb4af4956..c8df9433c7 100644 --- a/ortools/math_opt/storage/model_storage.h +++ b/ortools/math_opt/storage/model_storage.h @@ -25,15 +25,16 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/meta/type_traits.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "absl/log/check.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/constraints/indicator/storage.h" // IWYU pragma: export #include "ortools/math_opt/constraints/quadratic/storage.h" // IWYU pragma: export +#include "ortools/math_opt/constraints/second_order_cone/storage.h" #include "ortools/math_opt/constraints/sos/storage.h" // IWYU pragma: export #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -101,9 +102,9 @@ namespace math_opt { // -std::numeric_limits::infinity, 1.5, "c"); // model.set_linear_constraint_coefficient(x, c, 1.0); // model.set_linear_constraint_coefficient(y, c, 1.0); -// model.set_linear_objective_coefficient(x, 2.0); -// model.set_linear_objective_coefficient(y, 1.0); -// model.set_maximize(); +// model.set_linear_objective_coefficient(kPrimaryObjectiveId, x, 2.0); +// model.set_linear_objective_coefficient(kPrimaryObjectiveId, y, 1.0); +// model.set_maximize(kPrimaryObjectiveId); // // Now, export to a proto describing the model: // @@ -146,7 +147,7 @@ namespace math_opt { // ModelStorage::AdvanceCheckpoint(). Note that, for newly initialized models, // before the first checkpoint, there is no additional memory overhead from // tracking changes. See -// docs/ortools/math_opt/docs/model_building_complexity.md +// g3doc/ortools/math_opt/g3doc/model_building_complexity.md // for details. // // On bad input: @@ -171,7 +172,8 @@ class ModelStorage { const ModelProto& model_proto); // Creates an empty minimization problem. - explicit ModelStorage(absl::string_view name = "") : name_(name) {} + inline explicit ModelStorage(absl::string_view model_name = "", + absl::string_view primary_objective_name = ""); ModelStorage(const ModelStorage&) = delete; ModelStorage& operator=(const ModelStorage&) = delete; @@ -340,35 +342,46 @@ class ModelStorage { VariableId variable) const; ////////////////////////////////////////////////////////////////////////////// - // Objective + // Objectives + // + // The primary ObjectiveId is `PrimaryObjectiveId`. All auxiliary objectives + // are referenced by their corresponding `AuxiliaryObjectiveId`. ////////////////////////////////////////////////////////////////////////////// - inline bool is_maximize() const; - inline double objective_offset() const; + inline bool is_maximize(ObjectiveId id) const; + inline int64_t objective_priority(ObjectiveId id) const; + inline double objective_offset(ObjectiveId id) const; // Returns 0.0 if this variable has no linear objective coefficient. - inline double linear_objective_coefficient(VariableId variable) const; + inline double linear_objective_coefficient(ObjectiveId id, + VariableId variable) const; // The ordering of the input variables does not matter. inline double quadratic_objective_coefficient( - VariableId first_variable, VariableId second_variable) const; + ObjectiveId id, VariableId first_variable, + VariableId second_variable) const; inline bool is_linear_objective_coefficient_nonzero( - VariableId variable) const; + ObjectiveId id, VariableId variable) const; // The ordering of the input variables does not matter. inline bool is_quadratic_objective_coefficient_nonzero( - VariableId first_variable, VariableId second_variable) const; + ObjectiveId id, VariableId first_variable, + VariableId second_variable) const; + inline const std::string& objective_name(ObjectiveId id) const; - inline void set_is_maximize(bool is_maximize); - inline void set_maximize(); - inline void set_minimize(); - inline void set_objective_offset(double value); + inline void set_is_maximize(ObjectiveId id, bool is_maximize); + inline void set_maximize(ObjectiveId id); + inline void set_minimize(ObjectiveId id); + inline void set_objective_priority(ObjectiveId id, int64_t value); + inline void set_objective_offset(ObjectiveId id, double value); // Setting a value to 0.0 will delete the variable from the underlying sparse // representation (and has no effect if the variable is not present). - inline void set_linear_objective_coefficient(VariableId variable, + inline void set_linear_objective_coefficient(ObjectiveId id, + VariableId variable, double value); // Setting a value to 0.0 will delete the variable pair from the underlying // sparse representation (and has no effect if the pair is not present). // The ordering of the input variables does not matter. - inline void set_quadratic_objective_coefficient(VariableId first_variable, + inline void set_quadratic_objective_coefficient(ObjectiveId id, + VariableId first_variable, VariableId second_variable, double value); @@ -376,20 +389,71 @@ class ModelStorage { // variable with nonzero objective coefficient. // // Runs in O(# nonzero linear/quadratic objective terms). - inline void clear_objective(); + inline void clear_objective(ObjectiveId id); // The variables with nonzero linear objective coefficients. - inline const absl::flat_hash_map& linear_objective() - const; + inline const absl::flat_hash_map& linear_objective( + ObjectiveId id) const; - inline int64_t num_quadratic_objective_terms() const; + inline int64_t num_quadratic_objective_terms(ObjectiveId id) const; // The variable pairs with nonzero quadratic objective coefficients. The keys // are ordered such that .first <= .second. All values are nonempty. // // TODO(b/233630053) do no allocate the result, expose an iterator API. inline std::vector> - quadratic_objective_terms() const; + quadratic_objective_terms(ObjectiveId id) const; + + ////////////////////////////////////////////////////////////////////////////// + // Auxiliary objectives + ////////////////////////////////////////////////////////////////////////////// + + // Adds an auxiliary objective to the model and returns its id. + // + // The returned ids begin at zero and increase by one with each call to + // AddAuxiliaryObjective. Deleted ids are NOT reused. If no auxiliary + // objectives are deleted, the ids in the model will be consecutive. + // + // Objectives are minimized by default; you can change the sense via, e.g., + // `set_is_maximize()`. + inline AuxiliaryObjectiveId AddAuxiliaryObjective( + int64_t priority, absl::string_view name = ""); + + // Removes an auxiliary objective from the model. + // + // It is an error to use a deleted auxiliary objective id as input to any + // subsequent function calls on the model. Runs in O(#variables in the + // auxiliary objective). + inline void DeleteAuxiliaryObjective(AuxiliaryObjectiveId id); + + // The number of auxiliary objectives in the model. + // + // Equal to the number of auxiliary objectives created minus the number of + // auxiliary objectives deleted. + inline int num_auxiliary_objectives() const; + + // The returned id of the next call to AddAuxiliaryObjective. + // + // Equal to the number of auxiliary objectives created. + inline AuxiliaryObjectiveId next_auxiliary_objective_id() const; + + // Sets the next auxiliary objective id to be the maximum of + // next_auxiliary_objective_id() and id. + inline void ensure_next_auxiliary_objective_id_at_least( + AuxiliaryObjectiveId id); + + // Returns true if this id has been created and not yet deleted. + inline bool has_auxiliary_objective(AuxiliaryObjectiveId id) const; + + // The AuxiliaryObjectiveIds in use (not deleted), order not defined. + inline std::vector AuxiliaryObjectives() const; + + // Returns a sorted vector of all existing (not deleted) auxiliary objectives + // in the model. + // + // Runs in O(n log(n)), where n is the number of auxiliary objectives + // returned. + inline std::vector SortedAuxiliaryObjectives() const; ////////////////////////////////////////////////////////////////////////////// // Atomic Constraints @@ -573,19 +637,22 @@ class ModelStorage { private: struct UpdateTrackerData { UpdateTrackerData( - const VariableStorage& variables, + const VariableStorage& variables, const ObjectiveStorage& objectives, const LinearConstraintStorage& linear_constraints, const AtomicConstraintStorage& quadratic_constraints, + const AtomicConstraintStorage + soc_constraints, const AtomicConstraintStorage& sos1_constraints, const AtomicConstraintStorage& sos2_constraints, const AtomicConstraintStorage& indicator_constraints) : dirty_variables(variables), - dirty_objective(variables.next_id()), + dirty_objective(objectives, variables.next_id()), dirty_linear_constraints(linear_constraints, dirty_variables.checkpoint), dirty_quadratic_constraints(quadratic_constraints), + dirty_soc_constraints(soc_constraints), dirty_sos1_constraints(sos1_constraints), dirty_sos2_constraints(sos2_constraints), dirty_indicator_constraints(indicator_constraints) {} @@ -619,6 +686,8 @@ class ModelStorage { LinearConstraintStorage::Diff dirty_linear_constraints; AtomicConstraintStorage::Diff dirty_quadratic_constraints; + AtomicConstraintStorage::Diff + dirty_soc_constraints; AtomicConstraintStorage::Diff dirty_sos1_constraints; AtomicConstraintStorage::Diff dirty_sos2_constraints; AtomicConstraintStorage::Diff @@ -641,21 +710,27 @@ class ModelStorage { update_trackers_.GetUpdatedTrackers()); } - // Ids must be greater or equal to next_variable_id_. + // Ids must be greater or equal to `next_variable_id()`. void AddVariables(const VariablesProto& variables); - // Ids must be greater or equal to next_linear_constraint_id_. + // Ids must be greater or equal to `next_auxiliary_objective_id()`. + void AddAuxiliaryObjectives( + const google::protobuf::Map& objectives); + + // Ids must be greater or equal to `next_linear_constraint_id()`. void AddLinearConstraints(const LinearConstraintsProto& linear_constraints); + void UpdateObjective(ObjectiveId id, const ObjectiveUpdatesProto& update); + // Updates the objective linear coefficients. The coefficients of variables // not in the input are kept as-is. void UpdateLinearObjectiveCoefficients( - const SparseDoubleVectorProto& coefficients); + ObjectiveId id, const SparseDoubleVectorProto& coefficients); // Updates the objective quadratic coefficients. The coefficients of the pairs // of variables not in the input are kept as-is. void UpdateQuadraticObjectiveCoefficients( - const SparseDoubleMatrixProto& coefficients); + ObjectiveId id, const SparseDoubleMatrixProto& coefficients); // Updates the linear constraints' coefficients. The coefficients of // (constraint, variable) pairs not in the input are kept as-is. @@ -675,10 +750,11 @@ class ModelStorage { std::string name_; VariableStorage variables_; - ObjectiveStorage objective_; + ObjectiveStorage objectives_; LinearConstraintStorage linear_constraints_; AtomicConstraintStorage quadratic_constraints_; + AtomicConstraintStorage soc_constraints_; AtomicConstraintStorage sos1_constraints_; AtomicConstraintStorage sos2_constraints_; AtomicConstraintStorage indicator_constraints_; @@ -692,6 +768,10 @@ class ModelStorage { //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// +ModelStorage::ModelStorage(const absl::string_view model_name, + const absl::string_view primary_objective_name) + : name_(model_name), objectives_(primary_objective_name) {} + //////////////////////////////////////////////////////////////////////////////// // Variables //////////////////////////////////////////////////////////////////////////////// @@ -844,74 +924,142 @@ std::vector ModelStorage::linear_constraints_with_variable( } //////////////////////////////////////////////////////////////////////////////// -// Objective +// Objectives //////////////////////////////////////////////////////////////////////////////// -bool ModelStorage::is_maximize() const { return objective_.maximize(); } +bool ModelStorage::is_maximize(const ObjectiveId id) const { + return objectives_.maximize(id); +} -double ModelStorage::objective_offset() const { return objective_.offset(); } +int64_t ModelStorage::objective_priority(const ObjectiveId id) const { + return objectives_.priority(id); +} + +double ModelStorage::objective_offset(const ObjectiveId id) const { + return objectives_.offset(id); +} double ModelStorage::linear_objective_coefficient( - const VariableId variable) const { - return objective_.linear_term(variable); + const ObjectiveId id, const VariableId variable) const { + return objectives_.linear_term(id, variable); } double ModelStorage::quadratic_objective_coefficient( - const VariableId first_variable, const VariableId second_variable) const { - return objective_.quadratic_term(first_variable, second_variable); + const ObjectiveId id, const VariableId first_variable, + const VariableId second_variable) const { + return objectives_.quadratic_term(id, first_variable, second_variable); } bool ModelStorage::is_linear_objective_coefficient_nonzero( - const VariableId variable) const { - return objective_.linear_terms().contains(variable); + const ObjectiveId id, const VariableId variable) const { + return objectives_.linear_terms(id).contains(variable); } bool ModelStorage::is_quadratic_objective_coefficient_nonzero( - const VariableId first_variable, const VariableId second_variable) const { - return objective_.quadratic_terms().get(first_variable, second_variable) != + const ObjectiveId id, const VariableId first_variable, + const VariableId second_variable) const { + return objectives_.quadratic_terms(id).get(first_variable, second_variable) != 0.0; } -void ModelStorage::set_is_maximize(const bool is_maximize) { - objective_.set_maximize(is_maximize, UpdateAndGetObjectiveDiffs()); +const std::string& ModelStorage::objective_name(const ObjectiveId id) const { + return objectives_.name(id); } -void ModelStorage::set_maximize() { set_is_maximize(true); } - -void ModelStorage::set_minimize() { set_is_maximize(false); } - -void ModelStorage::set_objective_offset(const double value) { - objective_.set_offset(value, UpdateAndGetObjectiveDiffs()); +void ModelStorage::set_is_maximize(const ObjectiveId id, + const bool is_maximize) { + objectives_.set_maximize(id, is_maximize, UpdateAndGetObjectiveDiffs()); } -void ModelStorage::set_linear_objective_coefficient(const VariableId variable, +void ModelStorage::set_maximize(const ObjectiveId id) { + set_is_maximize(id, true); +} + +void ModelStorage::set_minimize(const ObjectiveId id) { + set_is_maximize(id, false); +} + +void ModelStorage::set_objective_priority(const ObjectiveId id, + const int64_t value) { + objectives_.set_priority(id, value, UpdateAndGetObjectiveDiffs()); +} + +void ModelStorage::set_objective_offset(const ObjectiveId id, + const double value) { + objectives_.set_offset(id, value, UpdateAndGetObjectiveDiffs()); +} + +void ModelStorage::set_linear_objective_coefficient(const ObjectiveId id, + const VariableId variable, const double value) { - objective_.set_linear_term(variable, value, UpdateAndGetObjectiveDiffs()); + objectives_.set_linear_term(id, variable, value, + UpdateAndGetObjectiveDiffs()); } void ModelStorage::set_quadratic_objective_coefficient( - const VariableId first_variable, const VariableId second_variable, - const double value) { - objective_.set_quadratic_term(first_variable, second_variable, value, - UpdateAndGetObjectiveDiffs()); + const ObjectiveId id, const VariableId first_variable, + const VariableId second_variable, const double value) { + objectives_.set_quadratic_term(id, first_variable, second_variable, value, + UpdateAndGetObjectiveDiffs()); } -void ModelStorage::clear_objective() { - objective_.Clear(UpdateAndGetObjectiveDiffs()); +void ModelStorage::clear_objective(const ObjectiveId id) { + objectives_.Clear(id, UpdateAndGetObjectiveDiffs()); } -const absl::flat_hash_map& ModelStorage::linear_objective() - const { - return objective_.linear_terms(); +const absl::flat_hash_map& ModelStorage::linear_objective( + const ObjectiveId id) const { + return objectives_.linear_terms(id); } -int64_t ModelStorage::num_quadratic_objective_terms() const { - return objective_.quadratic_terms().nonzeros(); +int64_t ModelStorage::num_quadratic_objective_terms( + const ObjectiveId id) const { + return objectives_.quadratic_terms(id).nonzeros(); } std::vector> -ModelStorage::quadratic_objective_terms() const { - return objective_.quadratic_terms().Terms(); +ModelStorage::quadratic_objective_terms(const ObjectiveId id) const { + return objectives_.quadratic_terms(id).Terms(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Auxiliary objectives +//////////////////////////////////////////////////////////////////////////////// + +AuxiliaryObjectiveId ModelStorage::AddAuxiliaryObjective( + const int64_t priority, const absl::string_view name) { + return objectives_.AddAuxiliaryObjective(priority, name); +} + +void ModelStorage::DeleteAuxiliaryObjective(const AuxiliaryObjectiveId id) { + objectives_.Delete(id, UpdateAndGetObjectiveDiffs()); +} + +int ModelStorage::num_auxiliary_objectives() const { + return static_cast(objectives_.num_auxiliary_objectives()); +} + +AuxiliaryObjectiveId ModelStorage::next_auxiliary_objective_id() const { + return objectives_.next_id(); +} + +void ModelStorage::ensure_next_auxiliary_objective_id_at_least( + const AuxiliaryObjectiveId id) { + objectives_.ensure_next_id_at_least(id); +} + +bool ModelStorage::has_auxiliary_objective( + const AuxiliaryObjectiveId id) const { + return objectives_.contains(id); +} + +std::vector ModelStorage::AuxiliaryObjectives() const { + return objectives_.AuxiliaryObjectives(); +} + +std::vector ModelStorage::SortedAuxiliaryObjectives() + const { + return objectives_.SortedAuxiliaryObjectives(); } //////////////////////////////////////////////////////////////////////////////// @@ -1029,6 +1177,28 @@ constexpr typename AtomicConstraintStorage::Diff return &UpdateTrackerData::dirty_quadratic_constraints; } +// ----------------------- Second-order cone constraints ----------------------- + +template <> +inline AtomicConstraintStorage& +ModelStorage::constraint_storage() { + return soc_constraints_; +} + +template <> +inline const AtomicConstraintStorage& +ModelStorage::constraint_storage() const { + return soc_constraints_; +} + +template <> +constexpr typename AtomicConstraintStorage::Diff + ModelStorage::UpdateTrackerData::* + ModelStorage::UpdateTrackerData::AtomicConstraintDirtyFieldPtr< + SecondOrderConeConstraintData>() { + return &UpdateTrackerData::dirty_soc_constraints; +} + // ----------------------------- SOS1 constraints ------------------------------ template <> diff --git a/ortools/math_opt/storage/model_storage_types.h b/ortools/math_opt/storage/model_storage_types.h index f46856ce43..6b2459fcc8 100644 --- a/ortools/math_opt/storage/model_storage_types.h +++ b/ortools/math_opt/storage/model_storage_types.h @@ -18,6 +18,7 @@ #define OR_TOOLS_MATH_OPT_STORAGE_MODEL_STORAGE_TYPES_H_ #include +#include #include "absl/strings/string_view.h" #include "ortools/base/strong_int.h" @@ -25,8 +26,14 @@ namespace operations_research::math_opt { DEFINE_STRONG_INT_TYPE(VariableId, int64_t); +DEFINE_STRONG_INT_TYPE(AuxiliaryObjectiveId, int64_t); +// nullopt denotes the primary objective. +using ObjectiveId = std::optional; +constexpr ObjectiveId kPrimaryObjectiveId = + std::nullopt; // NOLINT: Used by dependencies DEFINE_STRONG_INT_TYPE(LinearConstraintId, int64_t); DEFINE_STRONG_INT_TYPE(QuadraticConstraintId, int64_t); +DEFINE_STRONG_INT_TYPE(SecondOrderConeConstraintId, int64_t); DEFINE_STRONG_INT_TYPE(Sos1ConstraintId, int64_t); DEFINE_STRONG_INT_TYPE(Sos2ConstraintId, int64_t); DEFINE_STRONG_INT_TYPE(IndicatorConstraintId, int64_t); diff --git a/ortools/math_opt/storage/objective_storage.cc b/ortools/math_opt/storage/objective_storage.cc index b744f508c0..9f847cf7c2 100644 --- a/ortools/math_opt/storage/objective_storage.cc +++ b/ortools/math_opt/storage/objective_storage.cc @@ -14,9 +14,14 @@ #include "ortools/math_opt/storage/objective_storage.h" #include +#include +#include +#include #include #include "absl/container/flat_hash_set.h" +#include "google/protobuf/map.h" +#include "ortools/base/map_util.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -27,50 +32,177 @@ namespace operations_research::math_opt { -ObjectiveProto ObjectiveStorage::Proto() const { - ObjectiveProto result; - result.set_maximize(maximize_); - result.set_offset(offset_); - *result.mutable_linear_coefficients() = linear_terms_.Proto(); - *result.mutable_quadratic_coefficients() = quadratic_terms_.Proto(); - return result; -} - -ObjectiveUpdatesProto ObjectiveStorage::Update( - const Diff& diff, const absl::flat_hash_set& deleted_variables, - const std::vector& new_variables) const { - ObjectiveUpdatesProto result; - if (diff.direction) { - result.set_direction_update(maximize_); +void ObjectiveStorage::Diff::SingleObjective::DeleteVariable( + const VariableId deleted_variable, const VariableId variable_checkpoint, + const SparseSymmetricMatrix& quadratic_terms) { + if (deleted_variable >= variable_checkpoint) { + return; } - if (diff.offset) { - result.set_offset_update(offset_); - } - - for (const VariableId v : SortedElements(diff.linear_coefficients)) { - result.mutable_linear_coefficients()->add_ids(v.value()); - result.mutable_linear_coefficients()->add_values(linear_term(v)); - } - for (const VariableId v : new_variables) { - const double val = linear_term(v); - if (val != 0.0) { - result.mutable_linear_coefficients()->add_ids(v.value()); - result.mutable_linear_coefficients()->add_values(val); + linear_coefficients.erase(deleted_variable); + for (const VariableId v2 : + quadratic_terms.RelatedVariables(deleted_variable)) { + if (v2 < variable_checkpoint) { + quadratic_coefficients.erase( + {std::min(deleted_variable, v2), std::max(deleted_variable, v2)}); } } - *result.mutable_quadratic_coefficients() = quadratic_terms_.Update( - deleted_variables, new_variables, diff.quadratic_coefficients); - return result; +} + +ObjectiveProto ObjectiveStorage::ObjectiveData::Proto() const { + ObjectiveProto proto; + proto.set_maximize(maximize); + proto.set_priority(priority); + proto.set_offset(offset); + *proto.mutable_linear_coefficients() = linear_terms.Proto(); + *proto.mutable_quadratic_coefficients() = quadratic_terms.Proto(); + proto.set_name(name); + return proto; +} + +AuxiliaryObjectiveId ObjectiveStorage::AddAuxiliaryObjective( + const int64_t priority, const absl::string_view name) { + const AuxiliaryObjectiveId id = next_id_++; + CHECK(!auxiliary_objectives_.contains(id)); + { + ObjectiveData& data = auxiliary_objectives_[id]; + data.priority = priority; + data.name = std::string(name); + } + return id; +} + +std::vector ObjectiveStorage::AuxiliaryObjectives() + const { + std::vector ids; + ids.reserve(num_auxiliary_objectives()); + for (const auto& [id, unused] : auxiliary_objectives_) { + ids.push_back(id); + } + return ids; +} + +std::vector ObjectiveStorage::SortedAuxiliaryObjectives() + const { + std::vector ids = AuxiliaryObjectives(); + absl::c_sort(ids); + return ids; +} + +std::pair> +ObjectiveStorage::Proto() const { + google::protobuf::Map auxiliary_objectives; + for (const auto& [id, objective] : auxiliary_objectives_) { + auxiliary_objectives[id.value()] = objective.Proto(); + } + return {primary_objective_.Proto(), std::move(auxiliary_objectives)}; +} + +namespace { + +void EnsureHasValue(std::optional& update) { + if (!update.has_value()) { + update.emplace(); + } +} + +} // namespace + +std::optional ObjectiveStorage::ObjectiveData::Update( + const Diff::SingleObjective& diff_data, + const absl::flat_hash_set& deleted_variables, + const std::vector& new_variables) const { + std::optional update_proto; + + if (diff_data.direction) { + EnsureHasValue(update_proto); + update_proto->set_direction_update(maximize); + } + if (diff_data.priority) { + EnsureHasValue(update_proto); + update_proto->set_priority_update(priority); + } + if (diff_data.offset) { + EnsureHasValue(update_proto); + update_proto->set_offset_update(offset); + } + for (const VariableId v : SortedSetElements(diff_data.linear_coefficients)) { + EnsureHasValue(update_proto); + update_proto->mutable_linear_coefficients()->add_ids(v.value()); + update_proto->mutable_linear_coefficients()->add_values( + linear_terms.get(v)); + } + for (const VariableId v : new_variables) { + const double val = linear_terms.get(v); + if (val != 0.0) { + EnsureHasValue(update_proto); + update_proto->mutable_linear_coefficients()->add_ids(v.value()); + update_proto->mutable_linear_coefficients()->add_values(val); + } + } + SparseDoubleMatrixProto quadratic_update = quadratic_terms.Update( + deleted_variables, new_variables, diff_data.quadratic_coefficients); + if (!quadratic_update.row_ids().empty()) { + // Do not set the field if there is no quadratic term changes. + EnsureHasValue(update_proto); + *update_proto->mutable_quadratic_coefficients() = + std::move(quadratic_update); + } + return update_proto; +} + +std::pair +ObjectiveStorage::Update( + const Diff& diff, const absl::flat_hash_set& deleted_variables, + const std::vector& new_variables) const { + AuxiliaryObjectivesUpdatesProto auxiliary_result; + + for (const AuxiliaryObjectiveId id : diff.deleted) { + auxiliary_result.add_deleted_objective_ids(id.value()); + } + absl::c_sort(*auxiliary_result.mutable_deleted_objective_ids()); + + for (const auto& [id, objective] : auxiliary_objectives_) { + // Note that any `Delete()`d objective will not be in the `objectives_` map. + // Hence, each entry is either new (if not extracted) or potentially an + // update on an existing objective. + if (!diff.objective_tracked(id)) { + // An un-extracted objective goes in the `new_objectives` map. It is fresh + // and so there is no need to update, so we continue. + (*auxiliary_result.mutable_new_objectives())[id.value()] = + auxiliary_objectives_.at(id).Proto(); + continue; + } + + // `Diff` provides no guarantees on which objectives will have entries in + // `objective_diffs`; a missing entry is equivalent to one with an empty + // key. + std::optional update_proto = + objective.Update(gtl::FindWithDefault(diff.objective_diffs, id), + deleted_variables, new_variables); + if (update_proto.has_value()) { + // If the update message is empty we do not export it. This is + // particularly important for auxiliary objectives as we do not want to + // add empty map entries. + (*auxiliary_result.mutable_objective_updates())[id.value()] = + *std::move(update_proto); + } + } + + return {primary_objective_ + .Update(gtl::FindWithDefault(diff.objective_diffs, + kPrimaryObjectiveId), + deleted_variables, new_variables) + .value_or(ObjectiveUpdatesProto{}), + std::move(auxiliary_result)}; } void ObjectiveStorage::AdvanceCheckpointInDiff( const VariableId variable_checkpoint, Diff& diff) const { + diff.objective_checkpoint = std::max(diff.objective_checkpoint, next_id_); diff.variable_checkpoint = std::max(diff.variable_checkpoint, variable_checkpoint); - diff.offset = false; - diff.direction = false; - diff.linear_coefficients.clear(); - diff.quadratic_coefficients.clear(); + diff.objective_diffs.clear(); + diff.deleted.clear(); } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/storage/objective_storage.h b/ortools/math_opt/storage/objective_storage.h index 276adfe6b7..eedb942fce 100644 --- a/ortools/math_opt/storage/objective_storage.h +++ b/ortools/math_opt/storage/objective_storage.h @@ -15,11 +15,15 @@ #define OR_TOOLS_MATH_OPT_STORAGE_OBJECTIVE_STORAGE_H_ #include +#include +#include +#include #include #include #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "google/protobuf/map.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -37,58 +41,138 @@ class ObjectiveStorage { // // An instance of this class is owned by each update tracker of ModelStorage. struct Diff { - explicit Diff(const VariableId variable_checkpoint) - : variable_checkpoint(variable_checkpoint) {} + struct SingleObjective { + inline bool empty() const; + // The quadratic coefficient diff is not indexed symmetrically, so we need + // additional information to determine which quadratic terms are dirty. + void DeleteVariable(VariableId deleted_variable, + VariableId variable_checkpoint, + const SparseSymmetricMatrix& quadratic_terms); + bool direction = false; + bool priority = false; + bool offset = false; + // Only for terms where the variable is before the variable_checkpoint + // and, if an auxiliary objective, the objective is before the + // objective_checkpoint. + absl::flat_hash_set linear_coefficients; + + // For each entry, first <= second (the matrix is symmetric). + // Only holds entries with both variables before the variable checkpoint + // and, if an auxiliary objective, the objective is before the + // objective_checkpoint. + absl::flat_hash_set> + quadratic_coefficients; + }; + + inline explicit Diff(const ObjectiveStorage& storage, + VariableId variable_checkpoint); + + // Returns true if objective `id` is already tracked by the diff. Otherwise, + // it should be considered a "new" objective. + inline bool objective_tracked(ObjectiveId id) const; + + AuxiliaryObjectiveId objective_checkpoint{0}; VariableId variable_checkpoint{0}; - bool direction = false; - bool offset = false; - // Only holds variables before the variable checkpoint. - absl::flat_hash_set linear_coefficients; - - // For each entry, first <= second (the matrix is symmetric). - // Only holds entries with both variables before the variable checkpoint. - absl::flat_hash_set> - quadratic_coefficients; + // No guarantees provided on which objectives have corresponding entries, + // or that values are not `empty()`. + // TODO(b/259109678): Consider storing primary objective separately (like in + // `ObjectiveStorage`) if hashing is a noticeable bottleneck. + absl::flat_hash_map objective_diffs; + absl::flat_hash_set deleted; }; - bool maximize() const { return maximize_; } - double offset() const { return offset_; } - inline double linear_term(VariableId v) const; + inline explicit ObjectiveStorage(absl::string_view name = {}); - const absl::flat_hash_map& linear_terms() const { - return linear_terms_.terms(); - } - double quadratic_term(const VariableId v1, const VariableId v2) const { - return quadratic_terms_.get(v1, v2); - } - const SparseSymmetricMatrix& quadratic_terms() const { - return quadratic_terms_; - } + // Adds an auxiliary objective to the model and returns its id. + // + // The returned ids begin at zero and strictly increase (in particular, if + // ensure_next_id_at_least() is not used, they will be consecutive). Deleted + // ids are NOT reused. + AuxiliaryObjectiveId AddAuxiliaryObjective(int64_t priority, + absl::string_view name); + + inline bool maximize(ObjectiveId id) const; + inline int64_t priority(ObjectiveId id) const; + inline double offset(ObjectiveId id) const; + inline double linear_term(ObjectiveId id, VariableId v) const; + inline double quadratic_term(ObjectiveId id, VariableId v1, + VariableId v2) const; + inline const std::string& name(ObjectiveId id) const; + inline const absl::flat_hash_map& linear_terms( + ObjectiveId id) const; + inline const SparseSymmetricMatrix& quadratic_terms(ObjectiveId id) const; template - void set_maximize(bool maximize, const iterator_range& diffs); + void set_maximize(ObjectiveId id, bool maximize, + const iterator_range& diffs); template - void set_offset(double offset, const iterator_range& diffs); + void set_priority(ObjectiveId id, int64_t priority, + const iterator_range& diffs); template - void set_linear_term(VariableId variable, double value, + void set_offset(ObjectiveId id, double offset, + const iterator_range& diffs); + + template + void set_linear_term(ObjectiveId id, VariableId variable, double value, const iterator_range& diffs); template - void set_quadratic_term(VariableId v1, VariableId v2, double val, - const iterator_range& diffs); + void set_quadratic_term(ObjectiveId id, VariableId v1, VariableId v2, + double val, const iterator_range& diffs); + // Removes an auxiliary objective from the model. + // + // It is an error to use a deleted auxiliary objective id as input to any + // subsequent function calls on the model. template - void Clear(const iterator_range& diffs); + void Delete(AuxiliaryObjectiveId id, const iterator_range& diffs); - // Removes all occurrences of var from the objective. + // The number of auxiliary objectives in the model. + // + // Equal to the number of auxiliary objectives created minus the number of + // auxiliary objectives deleted. + inline int64_t num_auxiliary_objectives() const; + + // The returned id of the next call to AddAuxiliaryObjective. + // + // Equal to the number of auxiliary objectives created. + inline AuxiliaryObjectiveId next_id() const; + + // Sets the next auxiliary objective id to be the maximum of next_id() and + // `minimum`. + inline void ensure_next_id_at_least(AuxiliaryObjectiveId minimum); + + // Returns true if this id has been created and not yet deleted. + inline bool contains(AuxiliaryObjectiveId id) const; + + // The AuxiliaryObjectivesIds in use (not deleted), order not defined. + std::vector AuxiliaryObjectives() const; + + // Returns a sorted vector of all existing (not deleted) auxiliary objectives + // in the model. + // + // Runs in O(n log(n)), where n is the number of auxiliary objectives + // returned. + std::vector SortedAuxiliaryObjectives() const; + + // Clears the objective function (coefficients and offset), but not the + // sense or priority. + template + void Clear(ObjectiveId id, const iterator_range& diffs); + + // Removes all occurrences of var from the objective. Runs in O(# objectives) + // time (though this can potentially be improved to O(1) if the need arises). template void DeleteVariable(VariableId variable, const iterator_range& diffs); - ObjectiveProto Proto() const; + // Returns a proto description for the primary objective (.first) and all + // auxiliary objectives (.second). + std::pair> + Proto() const; ////////////////////////////////////////////////////////////////////////////// // Functions for working with Diff @@ -100,12 +184,11 @@ class ObjectiveStorage { // NOTE: when there are new variables with nonzero objective coefficient, the // Diff object can be empty (and diff_is_empty will return true), but Update() // can return a non-empty ObjectiveUpdatesProto. This behavior MAY CHANGE in - // the future, so diff_is_empty is true iff Update() returns an empty - // ObjectiveUpdatesProto (a more intuitive API, harder to implement - // efficiently). + // the future (this new behavior would be more intuitive, though it is harder + // to implement efficiently). inline bool diff_is_empty(const Diff& diff) const; - ObjectiveUpdatesProto Update( + std::pair Update( const Diff& diff, const absl::flat_hash_set& deleted_variables, const std::vector& new_variables) const; @@ -115,52 +198,141 @@ class ObjectiveStorage { Diff& diff) const; private: - bool maximize_ = false; - double offset_ = 0.0; - SparseCoefficientMap linear_terms_; - SparseSymmetricMatrix quadratic_terms_; + struct ObjectiveData { + ObjectiveProto Proto() const; + // Returns a proto representing the objective changes with respect to the + // `diff_data`. If there is no change, returns nullopt. + std::optional Update( + const Diff::SingleObjective& diff_data, + const absl::flat_hash_set& deleted_variables, + const std::vector& new_variables) const; + + inline void DeleteVariable(VariableId variable); + + bool maximize = false; + int64_t priority = 0; + double offset = 0.0; + SparseCoefficientMap linear_terms; + SparseSymmetricMatrix quadratic_terms; + std::string name = ""; + }; + + inline const ObjectiveData& objective(ObjectiveId id) const; + inline ObjectiveData& objective(ObjectiveId id); + + AuxiliaryObjectiveId next_id_{0}; + ObjectiveData primary_objective_; + absl::flat_hash_map + auxiliary_objectives_; }; //////////////////////////////////////////////////////////////////////////////// // Inline function implementation //////////////////////////////////////////////////////////////////////////////// -double ObjectiveStorage::linear_term(const VariableId v) const { - return linear_terms_.get(v); +bool ObjectiveStorage::Diff::SingleObjective::empty() const { + return !direction && !priority && !offset && linear_coefficients.empty() && + quadratic_coefficients.empty(); +} + +ObjectiveStorage::Diff::Diff(const ObjectiveStorage& storage, + const VariableId variable_checkpoint) + : objective_checkpoint(storage.next_id()), + variable_checkpoint(variable_checkpoint) {} + +ObjectiveStorage::ObjectiveStorage(const absl::string_view name) { + primary_objective_.name = std::string(name); +} + +bool ObjectiveStorage::maximize(const ObjectiveId id) const { + return objective(id).maximize; +} + +int64_t ObjectiveStorage::priority(const ObjectiveId id) const { + return objective(id).priority; +} + +double ObjectiveStorage::offset(const ObjectiveId id) const { + return objective(id).offset; +} + +double ObjectiveStorage::linear_term(const ObjectiveId id, + const VariableId v) const { + return objective(id).linear_terms.get(v); +} + +double ObjectiveStorage::quadratic_term(const ObjectiveId id, + const VariableId v1, + const VariableId v2) const { + return objective(id).quadratic_terms.get(v1, v2); +} + +const std::string& ObjectiveStorage::name(const ObjectiveId id) const { + return objective(id).name; +} + +const absl::flat_hash_map& ObjectiveStorage::linear_terms( + const ObjectiveId id) const { + return objective(id).linear_terms.terms(); +} + +const SparseSymmetricMatrix& ObjectiveStorage::quadratic_terms( + const ObjectiveId id) const { + return objective(id).quadratic_terms; } template -void ObjectiveStorage::set_maximize(const bool maximize, +void ObjectiveStorage::set_maximize(const ObjectiveId id, const bool maximize, const iterator_range& diffs) { - if (maximize_ == maximize) { + if (objective(id).maximize == maximize) { return; } - maximize_ = maximize; - for (ObjectiveStorage::Diff& diff : diffs) { - diff.direction = true; + objective(id).maximize = maximize; + for (Diff& diff : diffs) { + if (diff.objective_tracked(id)) { + diff.objective_diffs[id].direction = true; + } } } template -void ObjectiveStorage::set_offset(const double offset, +void ObjectiveStorage::set_priority(const ObjectiveId id, + const int64_t priority, + const iterator_range& diffs) { + if (objective(id).priority == priority) { + return; + } + objective(id).priority = priority; + for (Diff& diff : diffs) { + if (diff.objective_tracked(id)) { + diff.objective_diffs[id].priority = true; + } + } +} + +template +void ObjectiveStorage::set_offset(const ObjectiveId id, const double offset, const iterator_range& diffs) { - if (offset_ == offset) { + if (objective(id).offset == offset) { return; } - offset_ = offset; - for (ObjectiveStorage::Diff& diff : diffs) { - diff.offset = true; + objective(id).offset = offset; + for (Diff& diff : diffs) { + if (diff.objective_tracked(id)) { + diff.objective_diffs[id].offset = true; + } } } template -void ObjectiveStorage::set_linear_term(const VariableId variable, +void ObjectiveStorage::set_linear_term(const ObjectiveId id, + const VariableId variable, const double value, const iterator_range& diffs) { - if (linear_terms_.set(variable, value)) { - for (ObjectiveStorage::Diff& diff : diffs) { - if (variable < diff.variable_checkpoint) { - diff.linear_coefficients.insert(variable); + if (objective(id).linear_terms.set(variable, value)) { + for (Diff& diff : diffs) { + if (diff.objective_tracked(id) && variable < diff.variable_checkpoint) { + diff.objective_diffs[id].linear_coefficients.insert(variable); } } } @@ -168,12 +340,13 @@ void ObjectiveStorage::set_linear_term(const VariableId variable, template void ObjectiveStorage::set_quadratic_term( - const VariableId v1, const VariableId v2, const double val, - const iterator_range& diffs) { - if (quadratic_terms_.set(v1, v2, val)) { - for (ObjectiveStorage::Diff& diff : diffs) { - if (v1 < diff.variable_checkpoint && v2 < diff.variable_checkpoint) { - diff.quadratic_coefficients.insert( + const ObjectiveId id, const VariableId v1, const VariableId v2, + const double val, const iterator_range& diffs) { + if (objective(id).quadratic_terms.set(v1, v2, val)) { + for (Diff& diff : diffs) { + if (diff.objective_tracked(id) && v1 < diff.variable_checkpoint && + v2 < diff.variable_checkpoint) { + diff.objective_diffs[id].quadratic_coefficients.insert( {std::min(v1, v2), std::max(v1, v2)}); } } @@ -181,46 +354,119 @@ void ObjectiveStorage::set_quadratic_term( } template -void ObjectiveStorage::Clear(const iterator_range& diffs) { - set_offset(0.0, diffs); +void ObjectiveStorage::Delete(const AuxiliaryObjectiveId id, + const iterator_range& diffs) { + CHECK(auxiliary_objectives_.contains(id)); + for (Diff& diff : diffs) { + if (diff.objective_tracked(id)) { + diff.deleted.insert(id); + diff.objective_diffs.erase(id); + } + } + auxiliary_objectives_.erase(id); +} + +int64_t ObjectiveStorage::num_auxiliary_objectives() const { + return auxiliary_objectives_.size(); +} + +inline AuxiliaryObjectiveId ObjectiveStorage::next_id() const { + return next_id_; +} + +void ObjectiveStorage::ensure_next_id_at_least( + const AuxiliaryObjectiveId minimum) { + next_id_ = std::max(minimum, next_id_); +} + +bool ObjectiveStorage::contains(const AuxiliaryObjectiveId id) const { + return auxiliary_objectives_.contains(id); +} + +template +void ObjectiveStorage::Clear(const ObjectiveId id, + const iterator_range& diffs) { + ObjectiveData& data = objective(id); + set_offset(id, 0.0, diffs); for (ObjectiveStorage::Diff& diff : diffs) { - for (const auto [var, _] : linear_terms_.terms()) { + if (!diff.objective_tracked(id)) { + continue; + } + for (const auto [var, _] : data.linear_terms.terms()) { if (var < diff.variable_checkpoint) { - diff.linear_coefficients.insert(var); + diff.objective_diffs[id].linear_coefficients.insert(var); } } - for (const auto [v1, v2, _] : quadratic_terms_.Terms()) { + for (const auto [v1, v2, _] : data.quadratic_terms.Terms()) { if (v2 < diff.variable_checkpoint) { // v1 <= v2 is implied - diff.quadratic_coefficients.insert({v1, v2}); + diff.objective_diffs[id].quadratic_coefficients.insert({v1, v2}); } } } - linear_terms_.clear(); - quadratic_terms_.Clear(); + data.linear_terms.clear(); + data.quadratic_terms.Clear(); } template void ObjectiveStorage::DeleteVariable(const VariableId variable, const iterator_range& diffs) { - for (ObjectiveStorage::Diff& diff : diffs) { - if (variable >= diff.variable_checkpoint) { - continue; - } - diff.linear_coefficients.erase(variable); - for (const VariableId v2 : quadratic_terms_.RelatedVariables(variable)) { - if (v2 < diff.variable_checkpoint) { - diff.quadratic_coefficients.erase( - {std::min(variable, v2), std::max(variable, v2)}); - } + for (Diff& diff : diffs) { + for (auto& [id, obj_diff_data] : diff.objective_diffs) { + obj_diff_data.DeleteVariable(variable, diff.variable_checkpoint, + quadratic_terms(id)); } } - linear_terms_.erase(variable); - quadratic_terms_.Delete(variable); + primary_objective_.DeleteVariable(variable); + for (auto& [unused, aux_obj] : auxiliary_objectives_) { + aux_obj.DeleteVariable(variable); + } } bool ObjectiveStorage::diff_is_empty(const Diff& diff) const { - return !diff.offset && !diff.direction && diff.linear_coefficients.empty() && - diff.quadratic_coefficients.empty(); + if (next_id_ > diff.objective_checkpoint) { + // There is a new auxiliary objective that needs extracting. + return false; + } + for (const auto& [unused, diff_data] : diff.objective_diffs) { + if (!diff_data.empty()) { + // We must apply an objective modification. + return false; + } + } + // If nonempty we need to delete some auxiliary objectives. + return diff.deleted.empty(); +} + +bool ObjectiveStorage::Diff::objective_tracked(const ObjectiveId id) const { + // The primary objective is always present, so updates are always exported. + if (id == kPrimaryObjectiveId) { + return true; + } + return id < objective_checkpoint; +} + +const ObjectiveStorage::ObjectiveData& ObjectiveStorage::objective( + ObjectiveId id) const { + if (id == kPrimaryObjectiveId) { + return primary_objective_; + } + const auto it = auxiliary_objectives_.find(*id); + CHECK(it != auxiliary_objectives_.end()); + return it->second; +} + +ObjectiveStorage::ObjectiveData& ObjectiveStorage::objective(ObjectiveId id) { + if (id == kPrimaryObjectiveId) { + return primary_objective_; + } + const auto it = auxiliary_objectives_.find(*id); + CHECK(it != auxiliary_objectives_.end()); + return it->second; +} + +void ObjectiveStorage::ObjectiveData::DeleteVariable(VariableId variable) { + linear_terms.erase(variable); + quadratic_terms.Delete(variable); } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/storage/sorted.h b/ortools/math_opt/storage/sorted.h index 150fc704c4..ba90147341 100644 --- a/ortools/math_opt/storage/sorted.h +++ b/ortools/math_opt/storage/sorted.h @@ -25,7 +25,7 @@ namespace operations_research::math_opt { template -std::vector SortedElements(const absl::flat_hash_set& elements) { +std::vector SortedSetElements(const absl::flat_hash_set& elements) { std::vector result(elements.begin(), elements.end()); absl::c_sort(result); return result; diff --git a/ortools/math_opt/storage/update_trackers.h b/ortools/math_opt/storage/update_trackers.h index 7fa10c4151..420b8be41f 100644 --- a/ortools/math_opt/storage/update_trackers.h +++ b/ortools/math_opt/storage/update_trackers.h @@ -23,8 +23,8 @@ #include "absl/base/thread_annotations.h" #include "absl/container/flat_hash_set.h" -#include "absl/synchronization/mutex.h" #include "absl/log/check.h" +#include "absl/synchronization/mutex.h" #include "ortools/base/stl_util.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/storage/model_storage_types.h" diff --git a/ortools/math_opt/storage/variable_storage.cc b/ortools/math_opt/storage/variable_storage.cc index 9777368256..9c8326b1a7 100644 --- a/ortools/math_opt/storage/variable_storage.cc +++ b/ortools/math_opt/storage/variable_storage.cc @@ -102,15 +102,15 @@ VariableStorage::UpdateResult VariableStorage::Update(const Diff& diff) const { result.deleted.Add(v.value()); } absl::c_sort(result.deleted); - for (const VariableId v : SortedElements(diff.lower_bounds)) { + for (const VariableId v : SortedSetElements(diff.lower_bounds)) { result.updates.mutable_lower_bounds()->add_ids(v.value()); result.updates.mutable_lower_bounds()->add_values(lower_bound(v)); } - for (const VariableId v : SortedElements(diff.upper_bounds)) { + for (const VariableId v : SortedSetElements(diff.upper_bounds)) { result.updates.mutable_upper_bounds()->add_ids(v.value()); result.updates.mutable_upper_bounds()->add_values(upper_bound(v)); } - for (const VariableId v : SortedElements(diff.integer)) { + for (const VariableId v : SortedSetElements(diff.integer)) { result.updates.mutable_integers()->add_ids(v.value()); result.updates.mutable_integers()->add_values(is_integer(v)); } diff --git a/ortools/math_opt/tools/BUILD.bazel b/ortools/math_opt/tools/BUILD.bazel index 833d290f9e..8175237a9b 100644 --- a/ortools/math_opt/tools/BUILD.bazel +++ b/ortools/math_opt/tools/BUILD.bazel @@ -17,6 +17,7 @@ cc_binary( name = "mathopt_solve", srcs = ["mathopt_solve_main.cc"], deps = [ + ":file_format_flags", "//ortools/base", "//ortools/base:status_macros", "//ortools/math_opt:parameters_cc_proto", @@ -26,6 +27,7 @@ cc_binary( "//ortools/math_opt/io:mps_converter", "//ortools/math_opt/io:names_removal", "//ortools/math_opt/io:proto_converter", + "//ortools/math_opt/labs:solution_feasibility_checker", "//ortools/math_opt/solvers:cp_sat_solver", "//ortools/math_opt/solvers:glop_solver", "//ortools/math_opt/solvers:glpk_solver", @@ -40,3 +42,44 @@ cc_binary( "@com_google_protobuf//:protobuf", ], ) + +cc_binary( + name = "mathopt_convert", + srcs = ["mathopt_convert_main.cc"], + deps = [ + ":file_format_flags", + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/io:names_removal", + "//ortools/util:status_macros", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + ], +) + +cc_library( + name = "file_format_flags", + srcs = ["file_format_flags.cc"], + hdrs = ["file_format_flags.h"], + deps = [ + "//ortools/base", + "//ortools/base:file", + "//ortools/base:status_macros", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_parameters_cc_proto", + "//ortools/math_opt/io:lp_converter", + "//ortools/math_opt/io:mps_converter", + "//ortools/math_opt/io:proto_converter", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) diff --git a/ortools/math_opt/tools/file_format_flags.cc b/ortools/math_opt/tools/file_format_flags.cc new file mode 100644 index 0000000000..badf95f7e3 --- /dev/null +++ b/ortools/math_opt/tools/file_format_flags.cc @@ -0,0 +1,269 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/tools/file_format_flags.h" + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "ortools/base/helpers.h" +#include "ortools/base/logging.h" +#include "ortools/base/options.h" +#include "ortools/base/status_macros.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/math_opt/io/lp_converter.h" +#include "ortools/math_opt/io/mps_converter.h" +#include "ortools/math_opt/io/proto_converter.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" + +namespace operations_research::math_opt { + +absl::Span AllFileFormats() { + static constexpr FileFormat kValues[] = { + FileFormat::kMathOptBinary, + FileFormat::kMathOptText, + FileFormat::kLinearSolverBinary, + FileFormat::kLinearSolverText, + FileFormat::kMPS, + FileFormat::kLP, + }; + return absl::MakeConstSpan(kValues); +} + +std::string AbslUnparseFlag(const FileFormat f) { + switch (f) { + case FileFormat::kMathOptBinary: + return "mathopt"; + case FileFormat::kMathOptText: + return "mathopt_txt"; + case FileFormat::kLinearSolverBinary: + return "linear_solver"; + case FileFormat::kLinearSolverText: + return "linear_solver_txt"; + case FileFormat::kMPS: + return "mps"; + case FileFormat::kLP: + return "lp"; + } +} + +std::ostream& operator<<(std::ostream& out, const FileFormat f) { + out << AbslUnparseFlag(f); + return out; +} + +bool AbslParseFlag(const absl::string_view text, FileFormat* const f, + std::string* const error) { + for (const FileFormat candidate : AllFileFormats()) { + if (text == AbslUnparseFlag(candidate)) { + *f = candidate; + return true; + } + } + + absl::StrAppend(error, "not a known value"); + return false; +} + +absl::flat_hash_map ExtensionToFileFormat() { + return { + {".pb", FileFormat::kMathOptBinary}, + {".proto", FileFormat::kMathOptBinary}, + {".pb.txt", FileFormat::kMathOptText}, + {".pbtxt", FileFormat::kMathOptText}, + {".textproto", FileFormat::kMathOptText}, + {".mps", FileFormat::kMPS}, + {".mps.gz", FileFormat::kMPS}, + {".lp", FileFormat::kLP}, + }; +} + +std::optional FormatFromFilePath(absl::string_view file_path) { + const absl::flat_hash_map extensions = + ExtensionToFileFormat(); + + // Sort extensions in reverse lexicographic order. The point here would be to + // consider ".pb.txt" before we text for ".txt". + using ExtensionPair = std::pair; + std::vector sorted_extensions(extensions.begin(), + extensions.end()); + absl::c_sort(sorted_extensions, + [](const ExtensionPair& lhs, const ExtensionPair& rhs) { + return lhs > rhs; + }); + + for (const auto& [extension, format] : sorted_extensions) { + if (absl::EndsWith(file_path, extension)) { + return format; + } + } + return std::nullopt; +} + +std::optional FormatFromFlagOrFilePath( + const std::optional format_flag_value, + const absl::string_view file_path) { + if (format_flag_value.has_value()) { + return *format_flag_value; + } + return FormatFromFilePath(file_path); +} + +namespace { + +constexpr absl::string_view kListLinePrefix = "* "; +constexpr absl::string_view kSubListLinePrefix = " - "; + +} // namespace + +std::string OptionalFormatFlagPossibleValuesList() { + // Get the lines for each format and the introduction doc. + std::string list = FormatFlagPossibleValuesList(); + + // Add the doc of what happen when format is not specified. + absl::StrAppend(&list, "\n", kListLinePrefix, "", + ": to guess the format from the file extension:"); + + // Builds a map from formats to their extensions. + absl::flat_hash_map> + format_extensions; + for (const auto& [extension, format] : ExtensionToFileFormat()) { + format_extensions[format].push_back(extension); + } + for (auto& [format, extensions] : format_extensions) { + absl::c_sort(extensions); + } + + // Here we iterate on all formats so that we list them in the same order as in + // the enum. + for (const FileFormat format : AllFileFormats()) { + // Here we use operator[] as it is not an issue to create new empty entries + // in the map. + const std::vector& extensions = + format_extensions[format]; + if (extensions.empty()) { + continue; + } + + absl::StrAppend(&list, "\n", kSubListLinePrefix, + absl::StrJoin(extensions, ", "), ": ", + AbslUnparseFlag(format)); + } + return list; +} + +std::string FormatFlagPossibleValuesList() { + const absl::flat_hash_map format_help = { + {FileFormat::kMathOptBinary, "for a MathOpt ModelProto in binary"}, + {FileFormat::kMathOptText, "when the proto is in text"}, + {FileFormat::kLinearSolverBinary, + "for a LinearSolver MPModelProto in binary"}, + {FileFormat::kLinearSolverText, "when the proto is in text"}, + {FileFormat::kMPS, "for MPS file (which can be GZiped)"}, + {FileFormat::kLP, " for LP file"}, + }; + std::string list; + for (const FileFormat format : AllFileFormats()) { + absl::StrAppend(&list, "\n", kListLinePrefix, AbslUnparseFlag(format), ": ", + format_help.at(format)); + } + return list; +} + +absl::StatusOr>> +ReadModel(const absl::string_view file_path, const FileFormat format) { + switch (format) { + case FileFormat::kMathOptBinary: { + ASSIGN_OR_RETURN(ModelProto model, file::GetBinaryProto( + file_path, file::Defaults())); + return std::make_pair(std::move(model), std::nullopt); + } + case FileFormat::kMathOptText: { + ASSIGN_OR_RETURN(ModelProto model, file::GetTextProto( + file_path, file::Defaults())); + return std::make_pair(std::move(model), std::nullopt); + } + case FileFormat::kLinearSolverBinary: + case FileFormat::kLinearSolverText: { + ASSIGN_OR_RETURN( + const MPModelProto linear_solver_model, + format == FileFormat::kLinearSolverBinary + ? file::GetBinaryProto(file_path, file::Defaults()) + : file::GetTextProto(file_path, file::Defaults())); + ASSIGN_OR_RETURN(ModelProto model, + MPModelProtoToMathOptModel(linear_solver_model)); + ASSIGN_OR_RETURN( + std::optional hint, + MPModelProtoSolutionHintToMathOptHint(linear_solver_model)); + return std::make_pair(std::move(model), std::move(hint)); + } + case FileFormat::kMPS: { + ASSIGN_OR_RETURN(ModelProto model, ReadMpsFile(file_path)); + return std::make_pair(std::move(model), std::nullopt); + } + case FileFormat::kLP: { + return absl::UnimplementedError( + "MathOpt does not yet support reading .lp files; only writing them"); + } + } +} + +absl::Status WriteModel(const absl::string_view file_path, + const ModelProto& model_proto, + const std::optional& hint_proto, + const FileFormat format) { + switch (format) { + case FileFormat::kMathOptBinary: + return file::SetBinaryProto(file_path, model_proto, file::Defaults()); + case FileFormat::kMathOptText: + return file::SetTextProto(file_path, model_proto, file::Defaults()); + case FileFormat::kLinearSolverBinary: + case FileFormat::kLinearSolverText: { + ASSIGN_OR_RETURN(const MPModelProto linear_solver_model, + MathOptModelToMPModelProto(model_proto)); + if (hint_proto.has_value()) { + LOG(WARNING) << "support for converting a MathOpt hint to MPModelProto " + "is not yet supported thus the hint has been lost"; + } + return format == FileFormat::kLinearSolverBinary + ? file::SetBinaryProto(file_path, linear_solver_model, + file::Defaults()) + : file::SetTextProto(file_path, linear_solver_model, + file::Defaults()); + } + case FileFormat::kMPS: { + ASSIGN_OR_RETURN(const std::string mps_data, + ModelProtoToMps(model_proto)); + return file::SetContents(file_path, mps_data, file::Defaults()); + } + case FileFormat::kLP: { + ASSIGN_OR_RETURN(const std::string lp_data, ModelProtoToLp(model_proto)); + return file::SetContents(file_path, lp_data, file::Defaults()); + } + } +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/tools/file_format_flags.h b/ortools/math_opt/tools/file_format_flags.h new file mode 100644 index 0000000000..ad084ceea7 --- /dev/null +++ b/ortools/math_opt/tools/file_format_flags.h @@ -0,0 +1,164 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Library to handles --input_file and --format flags of a binary that reads or +// writes MathOpt models in various binary or text formats. +// +// This library provides: +// +// - a FileFormat enum type that can be used with ABSL_FLAG, either directly +// or wrapped in a std::optional to support guessing the file +// format based on the file name's extension. +// +// - functions that helps builds the help string of the flag using: +// * FileFormat: FormatFlagPossibleValuesList() +// * std::optional: OptionalFormatFlagPossibleValuesList() +// +// - a FormatFromFlagOrFilePath() function to handle the std::nullopt case +// when using std::optional for a flag. +// +// - functions ReadModel() and WriteModel() that take the FileFormat and +// read/write ModelProto. +// +#ifndef OR_TOOLS_MATH_OPT_TOOLS_FILE_FORMAT_FLAGS_H_ +#define OR_TOOLS_MATH_OPT_TOOLS_FILE_FORMAT_FLAGS_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" + +namespace operations_research::math_opt { + +// The supported file formats. +// +// The --format flag could be std::optional to support automatic +// guessing of the format based on the input file extension. +enum class FileFormat { + kMathOptBinary, + kMathOptText, + kLinearSolverBinary, + kLinearSolverText, + kMPS, + kLP, +}; + +// Streams the enum value using AbslUnparseFlag(). +std::ostream& operator<<(std::ostream& out, FileFormat f); + +// Used to define flags with std::optional or FileFormat. +// +// See OptionalFormatFlagPossibleValuesList() or FormatFlagPossibleValuesList() +// to build the related help strings. +// +// See FormatFromFlagOrFilePath() to handle the std::nullopt case when using an +// optional format. +bool AbslParseFlag(absl::string_view text, FileFormat* f, std::string* error); +std::string AbslUnparseFlag(FileFormat f); + +// Returns a span of FileFormat enum values. +absl::Span AllFileFormats(); + +// Returns a multi-line list listing all possible formats that can be used with +// a --format flag of type std::optional. Each entry is prefixed by +// a '\n'. +// +// The returned string contains a multiline list of bullet. +// +// See FormatFlagPossibleValuesList() for the alternative to use when the format +// value is not optional. +// +// Usage example: +// +// ABSL_FLAG( +// std::optional, input_format, +// std::nullopt, +// absl::StrCat( +// "the format of the --input_file; possible values:", +// operations_research::math_opt::OptionalFormatFlagPossibleValuesList())); +// +std::string OptionalFormatFlagPossibleValuesList(); + +// Same as OptionalFormatFlagPossibleValuesList() but for a flag of type +// FileFormat (i.e. with a mandatory value). +// +// Usage example: +// +// ABSL_FLAG( +// operations_research::math_opt::FileFormat, output_format, +// operations_research::math_opt::FileFormat::kMathOptBinary, +// absl::StrCat( +// "the format of the --output_file; possible values:", +// operations_research::math_opt::FormatFlagPossibleValuesList())); +// +std::string FormatFlagPossibleValuesList(); + +// Returns a map from file extensions to their format. +absl::flat_hash_map ExtensionToFileFormat(); + +// Uses ExtensionToFileFormat() to returns the potential format from a given +// file path. +// +// Note that they may exists multiple formats for the same extension (like +// ".pb"). In that case an arbitrary choice is made (e.g. using MathOpt's +// ModelProto for ".pb"). +std::optional FormatFromFilePath(absl::string_view file_path); + +// Returns either format_flag_value if not nullopt, else returns the result of +// FormatFromFilePath() on the input path. +// +// Note that in C++23 we could use std::optional::or_else(). +// +// Usage example: +// +// const FileFormat input_format = [&](){ +// const std::optional format = +// FormatFromFlagOrFilePath(absl::GetFlag(FLAGS_input_format), +// input_file_path); +// if (format.has_value()) { +// return *format; +// } +// LOG(QFATAL) +// << "Can't guess the format from the file extension, please " +// "use --input_format to specify the file format explicitly."; +// }(); +// +std::optional FormatFromFlagOrFilePath( + std::optional format_flag_value, absl::string_view file_path); + +// Returns the ModelProto read from the given file. Optionally returns a +// SolutionHintProto for kLinearSolverXxx format as it may contain one. +absl::StatusOr>> +ReadModel(absl::string_view file_path, FileFormat format); + +// Writes the given model with the given format. +// +// The optional hint is used when the output format supports it +// (e.g. MPModelProto). **It is not yet implemented though**; if you need it, +// please contact us. +absl::Status WriteModel(absl::string_view file_path, + const ModelProto& model_proto, + const std::optional& hint_proto, + FileFormat format); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_TOOLS_FILE_FORMAT_FLAGS_H_ diff --git a/ortools/math_opt/tools/mathopt_convert_main.cc b/ortools/math_opt/tools/mathopt_convert_main.cc new file mode 100644 index 0000000000..bd7fbee9ef --- /dev/null +++ b/ortools/math_opt/tools/mathopt_convert_main.cc @@ -0,0 +1,146 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tool to run use MathOpt to convert MIP/LP models formats. +// +// Examples: +// * Convert a text MPModelProto to a MPS file: +// mathopt_convert \ +// --input_file model.textproto \ +// --input_format linear_solver_txt \ +// --output_file model.mps +// * Convert a binary ModelProto to a binary MPModelProto: +// mathopt_convert \ +// --input_file model.pb \ +// --output_file model_linear_solver.pb \ +// --output_format linear_solver +// * Convert a binary ModelProto to a LP file: +// mathopt_convert \ +// --input_file model.pb \ +// --output_file model.lp +// * Anonymize a binary ModelProto: +// mathopt_convert \ +// --input_file model.pb \ +// --nonames \ +// --output_file anonymous-model.pb +#include +#include + +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/io/names_removal.h" +#include "ortools/math_opt/tools/file_format_flags.h" +#include "ortools/util/status_macros.h" + +ABSL_FLAG( + std::string, input_file, "", + "the file containing the model to solve; use --input_format to specify" + " the file format"); +ABSL_FLAG( + std::optional, input_format, + std::nullopt, + absl::StrCat( + "the format of the --input_file; possible values:", + operations_research::math_opt::OptionalFormatFlagPossibleValuesList())); + +ABSL_FLAG(std::string, output_file, "", + "the file to write to; use --output_format to specify" + " the file format"); +ABSL_FLAG( + std::optional, output_format, + std::nullopt, + absl::StrCat( + "the format of the --output_file; possible values:", + operations_research::math_opt::OptionalFormatFlagPossibleValuesList())); + +ABSL_FLAG(bool, names, true, + "use the names in the input models; ignoring names is useful when " + "the input contains duplicates or if the model must be anonymized"); + +namespace operations_research::math_opt { +namespace { + +// Returns the format to use for the file, or LOG(QFATAL) an error. +// +// Either use the format flag value is available, or guess the format based on +// the file_path's extension. +FileFormat ParseOptionalFormatFlag( + const absl::string_view format_flag_name, + const std::optional format_flag_value, + const absl::string_view file_path_flag_name, + const absl::string_view file_path_flag_value) { + const std::optional format = + FormatFromFlagOrFilePath(format_flag_value, file_path_flag_value); + if (format.has_value()) { + return *format; + } + LOG(QFATAL) << "Can't guess the format from the --" << file_path_flag_name + << " extension, please use --" << format_flag_name + << " to specify the file format explicitly."; +} + +absl::Status Main() { + const std::string input_file_path = absl::GetFlag(FLAGS_input_file); + if (input_file_path.empty()) { + LOG(QFATAL) << "The flag --input_file is mandatory."; + } + const std::string output_file_path = absl::GetFlag(FLAGS_output_file); + if (output_file_path.empty()) { + LOG(QFATAL) << "The flag --output_file is mandatory."; + } + + const FileFormat input_format = ParseOptionalFormatFlag( + /*format_flag_name=*/"input_format", absl::GetFlag(FLAGS_input_format), + /*file_path_flag_name=*/"input_file", input_file_path); + const FileFormat output_format = ParseOptionalFormatFlag( + /*format_flag_name=*/"output_format", absl::GetFlag(FLAGS_output_format), + /*file_path_flag_name=*/"output_file", output_file_path); + + // Read the model. + OR_ASSIGN_OR_RETURN3((auto [model_proto, optional_hint]), + ReadModel(input_file_path, input_format), + _ << "failed to read " << input_file_path); + + if (!absl::GetFlag(FLAGS_names)) { + RemoveNames(model_proto); + } + + // Write the model. + RETURN_IF_ERROR( + WriteModel(output_file_path, model_proto, optional_hint, output_format)) + << "failed to write " << output_file_path; + + return absl::OkStatus(); +} + +} // namespace +} // namespace operations_research::math_opt + +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, /*remove_flags=*/true); + + const absl::Status status = operations_research::math_opt::Main(); + // We don't use QCHECK_OK() here since the logged message contains more than + // the failing status. + if (!status.ok()) { + LOG(QFATAL) << status; + } + + return 0; +} diff --git a/ortools/math_opt/tools/mathopt_solve_main.cc b/ortools/math_opt/tools/mathopt_solve_main.cc index e1f461a79c..1a1a68a5ef 100644 --- a/ortools/math_opt/tools/mathopt_solve_main.cc +++ b/ortools/math_opt/tools/mathopt_solve_main.cc @@ -24,6 +24,11 @@ // mathopt_solve --input_file model.pb --solve_parameters 'threads: 4' // * Specify the file format: // mathopt_solve --input_file model --format=mathopt +// MOE: begin_strip +// * Solve a MIPLIB problem: +// mathopt_solve --input_file +// /google/src/head/depot/operations_research_data/MIP_MIPLIB/miplib2017/10teams.mps.gz +// MOE: end_strip #include #include #include @@ -49,27 +54,13 @@ #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/cpp/statistics.h" -#include "ortools/math_opt/io/mps_converter.h" #include "ortools/math_opt/io/names_removal.h" #include "ortools/math_opt/io/proto_converter.h" +#include "ortools/math_opt/labs/solution_feasibility_checker.h" #include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/tools/file_format_flags.h" #include "ortools/util/status_macros.h" -inline constexpr absl::string_view kMathOptBinaryFormat = "mathopt"; -inline constexpr absl::string_view kMathOptTextFormat = "mathopt_txt"; -inline constexpr absl::string_view kLinearSolverBinaryFormat = "linear_solver"; -inline constexpr absl::string_view kLinearSolverTextFormat = - "linear_solver_txt"; -inline constexpr absl::string_view kMPSFormat = "mps"; -inline constexpr absl::string_view kAutoFormat = "auto"; - -inline constexpr absl::string_view kPbExt = ".pb"; -inline constexpr absl::string_view kProtoExt = ".proto"; -inline constexpr absl::string_view kPbTxtExt = ".pb.txt"; -inline constexpr absl::string_view kTextProtoExt = ".textproto"; -inline constexpr absl::string_view kMPSExt = ".mps"; -inline constexpr absl::string_view kMPSGzipExt = ".mps.gz"; - namespace { struct SolverTypeProtoFormatter { @@ -86,35 +77,21 @@ ABSL_FLAG(std::string, input_file, "", "the file containing the model to solve; use --format to specify the " "file format"); ABSL_FLAG( - std::string, format, "auto", + std::optional, format, + std::nullopt, absl::StrCat( - "the format of the --input_file; possible values:\n", - // - "* ", kMathOptBinaryFormat, ": for a MathOpt ModelProto in binary\n", - // - "* ", kMathOptTextFormat, ": when the proto is in text\n", - // - "* ", kLinearSolverBinaryFormat, - ": for a LinearSolver MPModelProto in binary\n", - // - "* ", kLinearSolverTextFormat, ": when the proto is in text\n", - // - "* ", kMPSFormat, ": for MPS file (which can be GZiped)\n", - // - "* ", kAutoFormat, ": to guess the format from the file extension:\n", - // - " - '", kPbExt, "', '", kProtoExt, "': ", kMathOptBinaryFormat, "\n", - // - " - '", kPbTxtExt, "', '", kTextProtoExt, "': ", kMathOptTextFormat, - "\n", - // - " - '", kMPSExt, "', '", kMPSGzipExt, "': ", kMPSFormat)); + "the format of the --input_file; possible values:", + operations_research::math_opt::OptionalFormatFlagPossibleValuesList())); ABSL_FLAG( std::vector, update_files, {}, absl::StrCat( "the file containing ModelUpdateProto to apply to the --input_file; " "when this flag is used, the --format must be either ", - kMathOptBinaryFormat, " or ", kMathOptTextFormat)); + AbslUnparseFlag( + operations_research::math_opt::FileFormat::kMathOptBinary), + " or ", + AbslUnparseFlag( + operations_research::math_opt::FileFormat::kMathOptText))); ABSL_FLAG(operations_research::math_opt::SolverType, solver_type, operations_research::math_opt::SolverType::kGscip, @@ -138,74 +115,51 @@ ABSL_FLAG(bool, names, true, ABSL_FLAG(bool, ranges, false, "prints statistics about the ranges of the model values"); ABSL_FLAG(bool, print_model, false, "prints the model to stdout"); +ABSL_FLAG(bool, lp_relaxation, false, + "relax all integer variables to continuous"); +ABSL_FLAG( + bool, check_solutions, false, + "check the solutions feasibility; use --absolute_constraint_tolerance, " + "--integrality_tolerance, and --nonzero_tolerance values for tolerances"); +ABSL_FLAG(double, absolute_constraint_tolerance, + operations_research::math_opt::FeasibilityCheckerOptions{} + .absolute_constraint_tolerance, + "feasibility tolerance for constraints and variables bounds"); +ABSL_FLAG(double, integrality_tolerance, + operations_research::math_opt::FeasibilityCheckerOptions{} + .integrality_tolerance, + "feasibility tolerance for variables' integrality"); +ABSL_FLAG( + double, nonzero_tolerance, + operations_research::math_opt::FeasibilityCheckerOptions{} + .nonzero_tolerance, + "tolerance for checking if a value is nonzero (e.g., in SOS constraints)"); -namespace operations_research { -namespace math_opt { +namespace operations_research::math_opt { namespace { -// Returned the guessed format (one of the kXxxFormat constant) from the file -// extension; or nullopt. -std::optional FormatFromFilePath( - const absl::string_view file_path) { - const std::vector> - extension_to_format = { - {kPbExt, kMathOptBinaryFormat}, {kProtoExt, kMathOptBinaryFormat}, - {kPbTxtExt, kMathOptTextFormat}, {kTextProtoExt, kMathOptTextFormat}, - {kMPSExt, kMPSFormat}, {kMPSGzipExt, kMPSFormat}, - }; - - for (const auto& [ext, format] : extension_to_format) { - if (absl::EndsWith(file_path, ext)) { - return format; - } - } - - return std::nullopt; -} - -// Returns the ModelProto read from the given file. The format must not be -// kAutoFormat; other invalid values will be reported as QFATAL log mentioning -// the --format flag. -absl::StatusOr ReadModel(const absl::string_view file_path, - const absl::string_view format) { - if (format == kMathOptBinaryFormat) { - return file::GetBinaryProto(file_path, file::Defaults()); - } - if (format == kMathOptTextFormat) { - return file::GetTextProto(file_path, file::Defaults()); - } - if (format == kLinearSolverBinaryFormat || - format == kLinearSolverTextFormat) { - ASSIGN_OR_RETURN( - MPModelProto linear_solver_model, - format == kLinearSolverBinaryFormat - ? file::GetBinaryProto(file_path, file::Defaults()) - : file::GetTextProto(file_path, file::Defaults())); - return MPModelProtoToMathOptModel(linear_solver_model); - } - if (format == kMPSFormat) { - return ReadMpsFile(file_path); - } - LOG(QFATAL) << "Unsupported value of --format: " << format; -} - // Returns the ModelUpdateProto read from the given file. The format must be -// kMathOptBinaryFormat or kMathOptTextFormat; other values will generate an -// error. +// kMathOptBinary or kMathOptText; other values will generate an error. absl::StatusOr ReadModelUpdate( - const absl::string_view file_path, const absl::string_view format) { - if (format == kMathOptBinaryFormat) { - return file::GetBinaryProto(file_path, file::Defaults()); + const absl::string_view file_path, const FileFormat format) { + switch (format) { + case FileFormat::kMathOptBinary: + return file::GetBinaryProto(file_path, + file::Defaults()); + case FileFormat::kMathOptText: + return file::GetTextProto(file_path, file::Defaults()); + default: + return util::InternalErrorBuilder() << "invalid format " << format; } - if (format == kMathOptTextFormat) { - return file::GetTextProto(file_path, file::Defaults()); - } - return absl::InternalError( - absl::StrCat("invalid format in ReadModelUpdate(): ", format)); } // Prints the summary of the solve result. -absl::Status PrintSummary(const SolveResult& result) { +// +// If feasibility_check_tolerances is not nullopt then a check of feasibility of +// solution is done with the provided tolerances. +absl::Status PrintSummary(const Model& model, const SolveResult& result, + const std::optional + feasibility_check_tolerances) { std::cout << "Solve finished:\n" << " termination: " << result.termination << "\n" << " solve time: " << result.solve_stats.solve_time @@ -220,6 +174,21 @@ absl::Status PrintSummary(const SolveResult& result) { std::cout << " solution #" << (i + 1) << " objective: "; if (solution.primal_solution.has_value()) { std::cout << solution.primal_solution->objective_value; + if (feasibility_check_tolerances.has_value()) { + OR_ASSIGN_OR_RETURN3( + const ModelSubset broken_constraints, + CheckPrimalSolutionFeasibility( + model, solution.primal_solution->variable_values, + *feasibility_check_tolerances), + _ << "failed to check the primal solution feasibility of solution #" + << (i + 1)); + if (!broken_constraints.empty()) { + std::cout << " (numerically infeasible: " << broken_constraints + << ')'; + } else { + std::cout << " (numerically feasible)"; + } + } } else { std::cout << "n/a"; } @@ -236,28 +205,27 @@ absl::Status RunSolver() { } // Parses --format. - std::string format = absl::GetFlag(FLAGS_format); - if (format == kAutoFormat) { - const std::optional guessed_format = - FormatFromFilePath(input_file_path); - if (!guessed_format) { - LOG(QFATAL) << "Can't guess the format from the file extension, please " - "use --format to specify the file format explicitly."; + const FileFormat format = [&]() { + const std::optional format = + FormatFromFlagOrFilePath(absl::GetFlag(FLAGS_format), input_file_path); + if (format.has_value()) { + return *format; } - format = *guessed_format; - } + LOG(QFATAL) << "Can't guess the format from the file extension, please " + "use --format to specify the file format explicitly."; + }(); // We deal with input validation in the ReadModel() function. // Read the model and the optional updates. const std::vector update_file_paths = absl::GetFlag(FLAGS_update_files); - if (!update_file_paths.empty() && format != kMathOptBinaryFormat && - format != kMathOptTextFormat) { + if (!update_file_paths.empty() && format != FileFormat::kMathOptBinary && + format != FileFormat::kMathOptText) { LOG(QFATAL) << "Can't use --update_files with a input of format " << format << "."; } - OR_ASSIGN_OR_RETURN3(ModelProto model_proto, + OR_ASSIGN_OR_RETURN3((auto [model_proto, optional_hint]), ReadModel(input_file_path, format), _ << "failed to read " << input_file_path); @@ -283,6 +251,11 @@ absl::Status RunSolver() { RETURN_IF_ERROR(model->ApplyUpdateProto(update)) << "failed to apply the update file: " << update_file_paths[u]; } + if (absl::GetFlag(FLAGS_lp_relaxation)) { + for (const Variable v : model->Variables()) { + model->set_continuous(v); + } + } if (absl::GetFlag(FLAGS_ranges)) { std::cout << "Ranges of finite non-zero values in the model:\n" @@ -299,6 +272,14 @@ absl::Status RunSolver() { SolveParameters solve_parameters = absl::GetFlag(FLAGS_solve_parameters); solve_parameters.time_limit = absl::GetFlag(FLAGS_time_limit); SolveArguments solve_args = {.parameters = solve_parameters}; + if (optional_hint.has_value()) { + OR_ASSIGN_OR_RETURN3(ModelSolveParameters::SolutionHint hint, + ModelSolveParameters::SolutionHint::FromProto( + *model, optional_hint.value()), + _ << "invalid solution hint"); + solve_args.model_parameters.solution_hints.push_back(std::move(hint)); + std::cout << "Using the solution hint from the MPModelProto." << std::endl; + } if (absl::GetFlag(FLAGS_solver_logs)) { solve_args.message_callback = PrinterMessageCallback(std::cout, "logs| "); } @@ -307,14 +288,23 @@ absl::Status RunSolver() { Solve(*model, absl::GetFlag(FLAGS_solver_type), solve_args), _ << "the solver failed"); - RETURN_IF_ERROR(PrintSummary(result)); + const FeasibilityCheckerOptions feasiblity_checker_options = { + .absolute_constraint_tolerance = + absl::GetFlag(FLAGS_absolute_constraint_tolerance), + .integrality_tolerance = absl::GetFlag(FLAGS_integrality_tolerance), + .nonzero_tolerance = absl::GetFlag(FLAGS_nonzero_tolerance), + }; + RETURN_IF_ERROR( + PrintSummary(*model, result, + absl::GetFlag(FLAGS_check_solutions) + ? std::make_optional(feasiblity_checker_options) + : std::nullopt)); return absl::OkStatus(); } } // namespace -} // namespace math_opt -} // namespace operations_research +} // namespace operations_research::math_opt int main(int argc, char* argv[]) { InitGoogle(argv[0], &argc, &argv, /*remove_flags=*/true); diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index d26fbf7c17..e45b42976e 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -81,6 +81,7 @@ cc_library( "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/constraints/indicator:validator", "//ortools/math_opt/constraints/quadratic:validator", + "//ortools/math_opt/constraints/second_order_cone:validator", "//ortools/math_opt/constraints/sos:validator", "//ortools/math_opt/core:model_summary", "//ortools/math_opt/core:sparse_vector_view", @@ -218,3 +219,18 @@ cc_library( "@com_google_absl//absl/status", ], ) + +cc_library( + name = "infeasible_subsystem_validator", + srcs = ["infeasible_subsystem_validator.cc"], + hdrs = ["infeasible_subsystem_validator.h"], + deps = [ + ":ids_validator", + ":solve_stats_validator", + "//ortools/base:status_macros", + "//ortools/math_opt:infeasible_subsystem_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt/core:model_summary", + "@com_google_absl//absl/status", + ], +) diff --git a/ortools/math_opt/validators/enum_sets.cc b/ortools/math_opt/validators/enum_sets.cc deleted file mode 100644 index 5e24916997..0000000000 --- a/ortools/math_opt/validators/enum_sets.cc +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/math_opt/validators/enum_sets.h" - -#include -#include -#include -#include - -#include "absl/strings/str_cat.h" -#include "gtest/gtest.h" -#include "ortools/math_opt/result.pb.h" -#include "ortools/math_opt/solution.pb.h" -#include "ortools/port/proto_utils.h" - -namespace operations_research { -namespace math_opt { - -// portable. -std::vector AllFeasibilityStatuses() { - std::vector values; - for (int f = FeasibilityStatusProto_MIN; f <= FeasibilityStatusProto_MAX; - ++f) { - if (FeasibilityStatusProto_IsValid(f) && - f != FEASIBILITY_STATUS_UNSPECIFIED) { - values.push_back(static_cast(f)); - } - } - return values; -} - -std::vector AllSolutionStatuses() { - std::vector values; - for (int f = SolutionStatusProto_MIN; f <= SolutionStatusProto_MAX; ++f) { - if (SolutionStatusProto_IsValid(f) && f != SOLUTION_STATUS_UNSPECIFIED) { - values.push_back(static_cast(f)); - } - } - return values; -} - -void PrintTo(const SolutionStatusProto& proto, std::ostream* os) { - *os << ProtoEnumToString(proto); -} - -void PrintTo(const FeasibilityStatusProto& proto, std::ostream* os) { - *os << ProtoEnumToString(proto); -} - -void PrintTo(const std::tuple& proto, - std::ostream* os) { - *os << ProtoEnumToString(std::get<0>(proto)) << "_" - << ProtoEnumToString(std::get<1>(proto)); -} - -void PrintTo( - const std::tuple& proto, - std::ostream* os) { - *os << ProtoEnumToString(std::get<0>(proto)) << "_" - << ProtoEnumToString(std::get<1>(proto)); -} - -void PrintTo( - const std::tuple& proto, - std::ostream* os) { - *os << ProtoEnumToString(std::get<0>(proto)) << "_" - << ProtoEnumToString(std::get<1>(proto)); -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/validators/enum_sets.h b/ortools/math_opt/validators/enum_sets.h deleted file mode 100644 index 3459de2e0e..0000000000 --- a/ortools/math_opt/validators/enum_sets.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2010-2022 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// This header defines sets of enum values that can be used for parametrized -// tests. -#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ -#define OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ - -#include -#include -#include -#include - -#include "gtest/gtest.h" -#include "ortools/math_opt/result.pb.h" -#include "ortools/math_opt/solution.pb.h" - -namespace operations_research { -namespace math_opt { - -// Returns all valid feasibility statuses (i.e does not include UNSPECIFIED). -std::vector AllFeasibilityStatuses(); - -// Returns all valid solution statuses (i.e does not include UNSPECIFIED). -std::vector AllSolutionStatuses(); - -// Printing utilities for parametrized tests. -void PrintTo(const SolutionStatusProto& proto, std::ostream* os); -void PrintTo(const FeasibilityStatusProto& proto, std::ostream* os); -void PrintTo(const std::tuple& proto, - std::ostream* os); -void PrintTo( - const std::tuple& proto, - std::ostream* os); -void PrintTo( - const std::tuple& proto, - std::ostream* os); - -} // namespace math_opt -} // namespace operations_research -#endif // OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ diff --git a/ortools/math_opt/validators/infeasible_subsystem_validator.cc b/ortools/math_opt/validators/infeasible_subsystem_validator.cc new file mode 100644 index 0000000000..056ef44075 --- /dev/null +++ b/ortools/math_opt/validators/infeasible_subsystem_validator.cc @@ -0,0 +1,114 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/validators/infeasible_subsystem_validator.h" + +#include +#include + +#include "absl/status/status.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/validators/ids_validator.h" +#include "ortools/math_opt/validators/solve_stats_validator.h" + +namespace operations_research::math_opt { + +namespace { + +absl::Status CheckMapKeys( + const google::protobuf::Map& bounds_map, + const IdNameBiMap& universe) { + for (const auto& [id, unused] : bounds_map) { + if (!universe.HasId(id)) { + return util::InvalidArgumentErrorBuilder() << "unrecognized id: " << id; + } + } + return absl::OkStatus(); +} + +absl::Status CheckRepeatedIds( + const google::protobuf::RepeatedField& ids, + const IdNameBiMap& universe) { + RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(ids)); + RETURN_IF_ERROR(CheckIdsSubset(ids, universe)); + return absl::OkStatus(); +} + +} // namespace + +absl::Status ValidateModelSubset(const ModelSubsetProto& model_subset, + const ModelSummary& model_summary) { + RETURN_IF_ERROR( + CheckMapKeys(model_subset.variable_bounds(), model_summary.variables)) + << "bad ModelSubsetProto.variable_bounds"; + RETURN_IF_ERROR(CheckRepeatedIds(model_subset.variable_integrality(), + model_summary.variables)) + << "bad ModelSubsetProto.variable_integrality"; + RETURN_IF_ERROR(CheckMapKeys(model_subset.linear_constraints(), + model_summary.linear_constraints)) + << "bad ModelSubsetProto.linear_constraints"; + RETURN_IF_ERROR(CheckMapKeys(model_subset.quadratic_constraints(), + model_summary.quadratic_constraints)) + << "bad ModelSubsetProto.quadratic_constraints"; + RETURN_IF_ERROR(CheckRepeatedIds(model_subset.second_order_cone_constraints(), + model_summary.second_order_cone_constraints)) + << "bad ModelSubsetProto.second_order_cone_constraints"; + RETURN_IF_ERROR(CheckRepeatedIds(model_subset.sos1_constraints(), + model_summary.sos1_constraints)) + << "bad ModelSubsetProto.sos1_constraints"; + RETURN_IF_ERROR(CheckRepeatedIds(model_subset.sos2_constraints(), + model_summary.sos2_constraints)) + << "bad ModelSubsetProto.sos2_constraints"; + RETURN_IF_ERROR(CheckRepeatedIds(model_subset.indicator_constraints(), + model_summary.indicator_constraints)) + << "bad ModelSubsetProto.indicator_constraints"; + return absl::OkStatus(); +} + +absl::Status ValidateInfeasibleSubsystemResult( + const InfeasibleSubsystemResultProto& result, + const ModelSummary& model_summary) { + RETURN_IF_ERROR(ValidateInfeasibleSubsystemResultNoModel(result)); + if (result.feasibility() == FEASIBILITY_STATUS_INFEASIBLE) { + RETURN_IF_ERROR( + ValidateModelSubset(result.infeasible_subsystem(), model_summary)); + } + return absl::OkStatus(); +} + +absl::Status ValidateInfeasibleSubsystemResultNoModel( + const InfeasibleSubsystemResultProto& result) { + RETURN_IF_ERROR(ValidateFeasibilityStatus(result.feasibility())) + << "bad InfeasibleSubsystemResultProto.feasibility"; + if (result.feasibility() != FEASIBILITY_STATUS_INFEASIBLE) { + // Check that the `infeasible_subsystem` is empty by validating against an + // empty ModelSummary. + if (!ValidateModelSubset(result.infeasible_subsystem(), ModelSummary()) + .ok()) { + return util::InvalidArgumentErrorBuilder() + << "nonempty infeasible_subsystem with feasibility status: " + << FeasibilityStatusProto_Name(result.feasibility()); + } + if (result.is_minimal()) { + return util::InvalidArgumentErrorBuilder() + << "is_minimal is true with feasibility status: " + << FeasibilityStatusProto_Name(result.feasibility()); + } + } + return absl::OkStatus(); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/validators/infeasible_subsystem_validator.h b/ortools/math_opt/validators/infeasible_subsystem_validator.h new file mode 100644 index 0000000000..adf759fb70 --- /dev/null +++ b/ortools/math_opt/validators/infeasible_subsystem_validator.h @@ -0,0 +1,34 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_INFEASIBLE_SUBSYSTEM_VALIDATOR_H_ +#define OR_TOOLS_MATH_OPT_VALIDATORS_INFEASIBLE_SUBSYSTEM_VALIDATOR_H_ + +#include "absl/status/status.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" + +namespace operations_research::math_opt { + +absl::Status ValidateModelSubset(const ModelSubsetProto& model_subset, + const ModelSummary& summary); + +absl::Status ValidateInfeasibleSubsystemResult( + const InfeasibleSubsystemResultProto& result, const ModelSummary& summary); +// Validates the internal consistency of the fields. +absl::Status ValidateInfeasibleSubsystemResultNoModel( + const InfeasibleSubsystemResultProto& result); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_VALIDATORS_INFEASIBLE_SUBSYSTEM_VALIDATOR_H_ diff --git a/ortools/math_opt/validators/model_validator.cc b/ortools/math_opt/validators/model_validator.cc index a3ea4a7b02..0742409550 100644 --- a/ortools/math_opt/validators/model_validator.cc +++ b/ortools/math_opt/validators/model_validator.cc @@ -23,6 +23,7 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/constraints/indicator/validator.h" #include "ortools/math_opt/constraints/quadratic/validator.h" +#include "ortools/math_opt/constraints/second_order_cone/validator.h" #include "ortools/math_opt/constraints/sos/validator.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/sparse_vector_view.h" @@ -249,6 +250,9 @@ absl::StatusOr ValidateModel(const ModelProto& model, RETURN_IF_ERROR(ValidateConstraintMap(model.quadratic_constraints(), model_summary.variables)) << "ModelProto.quadratic_constraints invalid"; + RETURN_IF_ERROR(ValidateConstraintMap(model.second_order_cone_constraints(), + model_summary.variables)) + << "ModelProto.second_order_cone_constraints invalid"; RETURN_IF_ERROR( ValidateConstraintMap(model.sos1_constraints(), model_summary.variables)) << "ModelProto.sos1_constraints invalid"; @@ -308,6 +312,11 @@ absl::Status ValidateModelUpdate(const ModelUpdateProto& model_update, model_summary.variables)) << "ModelUpdateProto.quadratic_constraint_updates.new_constraints " "invalid"; + RETURN_IF_ERROR(ValidateConstraintMap( + model_update.second_order_cone_constraint_updates().new_constraints(), + model_summary.variables)) + << "ModelUpdateProto.second_order_cone_constraint_updates.new_" + "constraints invalid"; RETURN_IF_ERROR(ValidateConstraintMap( model_update.sos1_constraint_updates().new_constraints(), model_summary.variables)) diff --git a/ortools/math_opt/validators/solve_stats_validator.cc b/ortools/math_opt/validators/solve_stats_validator.cc index c77ddfac1e..fa0f2593e3 100644 --- a/ortools/math_opt/validators/solve_stats_validator.cc +++ b/ortools/math_opt/validators/solve_stats_validator.cc @@ -14,7 +14,6 @@ #include "ortools/math_opt/validators/solve_stats_validator.h" #include -#include #include #include "absl/status/status.h" @@ -29,7 +28,6 @@ namespace operations_research { namespace math_opt { -namespace { absl::Status ValidateFeasibilityStatus(const FeasibilityStatusProto& status) { if (!FeasibilityStatusProto_IsValid(status)) { @@ -41,7 +39,6 @@ absl::Status ValidateFeasibilityStatus(const FeasibilityStatusProto& status) { } return absl::OkStatus(); } -} // namespace absl::Status ValidateProblemStatus(const ProblemStatusProto& status) { RETURN_IF_ERROR(ValidateFeasibilityStatus(status.primal_status())) diff --git a/ortools/math_opt/validators/solve_stats_validator.h b/ortools/math_opt/validators/solve_stats_validator.h index 790db11402..3579b08f27 100644 --- a/ortools/math_opt/validators/solve_stats_validator.h +++ b/ortools/math_opt/validators/solve_stats_validator.h @@ -20,6 +20,7 @@ namespace operations_research { namespace math_opt { +absl::Status ValidateFeasibilityStatus(const FeasibilityStatusProto& status); absl::Status ValidateProblemStatus(const ProblemStatusProto& status); absl::Status ValidateSolveStats(const SolveStatsProto& solve_stats); diff --git a/ortools/pdlp/primal_dual_hybrid_gradient.cc b/ortools/pdlp/primal_dual_hybrid_gradient.cc index ea09124d95..28b51b5519 100644 --- a/ortools/pdlp/primal_dual_hybrid_gradient.cc +++ b/ortools/pdlp/primal_dual_hybrid_gradient.cc @@ -361,7 +361,7 @@ class PreprocessSolver { const VectorXd& last_primal_start_point, const VectorXd& last_dual_start_point, const std::atomic* interrupt_solve, IterationType iteration_type, - const IterationStats& full_stats, IterationStats& stats) const; + const IterationStats& full_stats, IterationStats& stats); // Computes solution statistics for the primal and dual input pair, which // should be a scaled solution. If `convergence_information != nullptr`, @@ -491,6 +491,8 @@ class PreprocessSolver { VectorXd col_scaling_vec_; VectorXd row_scaling_vec_; + // A counter used to trigger writing iteration headers. + int log_counter_ = 0; IterationStatsCallback iteration_stats_callback_; }; @@ -1425,7 +1427,7 @@ PreprocessSolver::UpdateIterationStatsAndCheckTermination( const VectorXd& last_dual_start_point, const std::atomic* interrupt_solve, const IterationType iteration_type, const IterationStats& full_stats, - IterationStats& stats) const { + IterationStats& stats) { ComputeConvergenceAndInfeasibilityFromWorkingSolution( params, working_primal_current, working_dual_current, POINT_TYPE_CURRENT_ITERATE, stats.add_convergence_information(), @@ -1452,9 +1454,8 @@ PreprocessSolver::UpdateIterationStatsAndCheckTermination( last_dual_start_point, stats); } constexpr int kLogEvery = 15; - static std::atomic_int log_counter{0}; if (params.verbosity_level() >= 2) { - if (log_counter == 0) { + if (log_counter_ == 0) { LogIterationStatsHeader(params.verbosity_level(), params.use_feasibility_polishing()); } @@ -1475,8 +1476,8 @@ PreprocessSolver::UpdateIterationStatsAndCheckTermination( } } } - if (++log_counter >= kLogEvery) { - log_counter = 0; + if (++log_counter_ >= kLogEvery) { + log_counter_ = 0; } if (iteration_stats_callback_ != nullptr) { iteration_stats_callback_(