diff --git a/ortools/math_opt/samples/cpp/area_socp.cc b/ortools/math_opt/samples/cpp/area_socp.cc index 109a877f1c..4747d8f35f 100644 --- a/ortools/math_opt/samples/cpp/area_socp.cc +++ b/ortools/math_opt/samples/cpp/area_socp.cc @@ -18,10 +18,10 @@ #include #include +#include "absl/flags/flag.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" diff --git a/ortools/math_opt/samples/cpp/basic_example.cc b/ortools/math_opt/samples/cpp/basic_example.cc index 39e4f34f2b..2f4e195911 100644 --- a/ortools/math_opt/samples/cpp/basic_example.cc +++ b/ortools/math_opt/samples/cpp/basic_example.cc @@ -18,10 +18,8 @@ #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" diff --git a/ortools/math_opt/samples/cpp/cocktail_hour.cc b/ortools/math_opt/samples/cpp/cocktail_hour.cc index ab5e40ff6d..9b17baac57 100644 --- a/ortools/math_opt/samples/cpp/cocktail_hour.cc +++ b/ortools/math_opt/samples/cpp/cocktail_hour.cc @@ -37,12 +37,17 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/flags/flag.h" +#include "absl/log/check.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/str_replace.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" @@ -270,8 +275,7 @@ absl::StatusOr SolveForMenu( return menu; } -absl::flat_hash_set SetFromVec( - const std::vector& vec) { +absl::flat_hash_set SetFromVec(absl::Span vec) { return {vec.begin(), vec.end()}; } @@ -294,7 +298,7 @@ absl::Status AnalysisMode() { return absl::OkStatus(); } -std::string ExportToLaTeX(const std::vector& cocktails, +std::string ExportToLaTeX(absl::Span cocktails, absl::string_view title = "Cocktail Hour") { std::vector lines; lines.push_back("\\documentclass{article}"); diff --git a/ortools/math_opt/samples/cpp/cutting_stock.cc b/ortools/math_opt/samples/cpp/cutting_stock.cc index 01e58b4d92..41bf833c24 100644 --- a/ortools/math_opt/samples/cpp/cutting_stock.cc +++ b/ortools/math_opt/samples/cpp/cutting_stock.cc @@ -81,8 +81,10 @@ #include #include +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" #include "ortools/base/status_builder.h" diff --git a/ortools/math_opt/samples/cpp/facility_lp_benders.cc b/ortools/math_opt/samples/cpp/facility_lp_benders.cc index 206a41d21a..04316619b0 100644 --- a/ortools/math_opt/samples/cpp/facility_lp_benders.cc +++ b/ortools/math_opt/samples/cpp/facility_lp_benders.cc @@ -56,6 +56,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/flags/flag.h" +#include "absl/memory/memory.h" #include "absl/random/random.h" #include "absl/random/seed_sequences.h" #include "absl/random/uniform_int_distribution.h" @@ -65,6 +66,7 @@ #include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" #include "ortools/base/status_macros.h" @@ -317,7 +319,7 @@ class SecondStageSolver { FacilityLocationInstance instance, math_opt::SolverType solver_type); absl::StatusOr> Solve( - const std::vector& z_values, double w_value, + absl::Span z_values, double w_value, double fist_stage_objective); private: @@ -428,7 +430,7 @@ SecondStageSolver::SecondStageSolver( } absl::StatusOr> SecondStageSolver::Solve( - const std::vector& z_values, const double w_value, + absl::Span z_values, const double w_value, const double fist_stage_objective) { const int num_facilities = network_.num_facilities(); diff --git a/ortools/math_opt/samples/cpp/graph_coloring.cc b/ortools/math_opt/samples/cpp/graph_coloring.cc index 7cf8769cba..75239ce742 100644 --- a/ortools/math_opt/samples/cpp/graph_coloring.cc +++ b/ortools/math_opt/samples/cpp/graph_coloring.cc @@ -38,11 +38,12 @@ #include #include +#include "absl/flags/flag.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/str_cat.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" diff --git a/ortools/math_opt/samples/cpp/integer_programming.cc b/ortools/math_opt/samples/cpp/integer_programming.cc index 86d1e04f03..279fd3a14f 100644 --- a/ortools/math_opt/samples/cpp/integer_programming.cc +++ b/ortools/math_opt/samples/cpp/integer_programming.cc @@ -18,11 +18,10 @@ #include #include -#include "absl/status/statusor.h" +#include "absl/status/status.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" diff --git a/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc b/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc index 65a1a3d990..677e03f09d 100644 --- a/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc +++ b/ortools/math_opt/samples/cpp/lagrangian_relaxation.cc @@ -87,7 +87,7 @@ #include #include "absl/flags/flag.h" -#include "absl/memory/memory.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" diff --git a/ortools/math_opt/samples/cpp/linear_programming.cc b/ortools/math_opt/samples/cpp/linear_programming.cc index 1b56ba57c4..3981db5d2b 100644 --- a/ortools/math_opt/samples/cpp/linear_programming.cc +++ b/ortools/math_opt/samples/cpp/linear_programming.cc @@ -19,13 +19,12 @@ #include #include -#include "absl/status/statusor.h" +#include "absl/status/status.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" diff --git a/ortools/math_opt/samples/cpp/linear_regression.cc b/ortools/math_opt/samples/cpp/linear_regression.cc index 274febf104..7e1603be97 100644 --- a/ortools/math_opt/samples/cpp/linear_regression.cc +++ b/ortools/math_opt/samples/cpp/linear_regression.cc @@ -47,11 +47,15 @@ #include #include "absl/algorithm/container.h" +#include "absl/flags/flag.h" +#include "absl/log/check.h" #include "absl/random/random.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.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" @@ -133,8 +137,7 @@ double L2Loss(const LinearModel& model, // 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) { +absl::StatusOr Train(absl::Span 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"); diff --git a/ortools/math_opt/samples/cpp/time_indexed_scheduling.cc b/ortools/math_opt/samples/cpp/time_indexed_scheduling.cc index f0f5c01575..c33a22556a 100644 --- a/ortools/math_opt/samples/cpp/time_indexed_scheduling.cc +++ b/ortools/math_opt/samples/cpp/time_indexed_scheduling.cc @@ -48,11 +48,15 @@ #include #include "absl/algorithm/container.h" +#include "absl/flags/flag.h" #include "absl/random/random.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/types/span.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, @@ -107,7 +111,7 @@ std::vector TestInstance() { {.processing_time = 5, .release_time = 0}}; } -int TimeHorizon(const std::vector& jobs) { +int TimeHorizon(absl::Span jobs) { int max_release = 0; int sum_processing = 0; for (const Job& job : jobs) { @@ -122,7 +126,7 @@ struct Schedule { int sum_of_completion_times = 0; }; -absl::StatusOr Solve(const std::vector& jobs, +absl::StatusOr Solve(absl::Span jobs, const math_opt::SolverType solver_type) { const int kTimeHorizon = TimeHorizon(jobs); math_opt::Model model; @@ -176,7 +180,7 @@ absl::StatusOr Solve(const std::vector& jobs, return schedule; } -void PrintSchedule(const std::vector& jobs, const Schedule& schedule) { +void PrintSchedule(absl::Span jobs, const Schedule& schedule) { std::cout << "sum of completion times: " << schedule.sum_of_completion_times << std::endl; std::vector> jobs_by_start_time; diff --git a/ortools/math_opt/samples/cpp/tsp.cc b/ortools/math_opt/samples/cpp/tsp.cc index 19b53715ae..c83f9485c7 100644 --- a/ortools/math_opt/samples/cpp/tsp.cc +++ b/ortools/math_opt/samples/cpp/tsp.cc @@ -61,18 +61,21 @@ #include #include +#include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" +#include "absl/log/check.h" #include "absl/random/random.h" -#include "absl/random/uniform_real_distribution.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/types/span.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/options.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, "", @@ -83,6 +86,9 @@ 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."); +ABSL_FLAG(operations_research::math_opt::SolverType, solver, + operations_research::math_opt::SolverType::kGscip, + "What underlying MIP solver to use (must support callbacks)."); namespace { @@ -113,7 +119,7 @@ class EdgeVariables { return i > j ? variables_[i][j] : variables_[j][i]; } - int num_cities() const { return variables_.size(); } + int num_cities() const { return static_cast(variables_.size()); } private: std::vector> variables_; @@ -139,8 +145,8 @@ std::vector> TestCities() { // 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(); + absl::Span> cities) { + const int num_cities = static_cast(cities.size()); std::vector> distance_matrix( num_cities, std::vector(num_cities, 0.0)); for (int i = 0; i < num_cities; ++i) { @@ -178,8 +184,7 @@ std::vector> EdgeValues( // 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) { +std::vector FindCycles(absl::Span> 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, @@ -191,7 +196,7 @@ std::vector FindCycles( // 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(); + const int n = static_cast(edge_values.size()); std::vector result; std::vector visited(n, false); for (int i = 0; i < n; ++i) { @@ -221,7 +226,7 @@ std::vector FindCycles( // Returns the cutset constraint for the given set of nodes. math_opt::BoundedLinearExpression CutsetConstraint( - const std::vector& nodes, const EdgeVariables& edge_vars) { + absl::Span nodes, const EdgeVariables& edge_vars) { const int n = edge_vars.num_cities(); const absl::flat_hash_set node_set(nodes.begin(), nodes.end()); std::vector not_in_set; @@ -242,8 +247,9 @@ math_opt::BoundedLinearExpression CutsetConstraint( // 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>& cities, + const math_opt::SolverType solver) { + const int n = static_cast(cities.size()); const std::vector> distance_matrix = DistanceMatrix(cities); CHECK_GE(n, 3); @@ -290,7 +296,7 @@ absl::StatusOr SolveTsp( return result; }; ASSIGN_OR_RETURN(const math_opt::SolveResult result, - math_opt::Solve(model, math_opt::SolverType::kGurobi, args)); + math_opt::Solve(model, solver, args)); RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); std::cout << "Route length: " << result.objective_value() << std::endl; const std::vector cycles = @@ -301,7 +307,7 @@ absl::StatusOr SolveTsp( } // Produces an SVG to draw a route for a TSP. -std::string RouteSvg(const std::vector>& cities, +std::string RouteSvg(absl::Span> cities, const Cycle& cycle) { constexpr int image_px = 1000; constexpr int r = 5; @@ -327,30 +333,32 @@ std::string RouteSvg(const std::vector>& cities, return absl::StrJoin(svg_lines, "\n"); } -void RealMain() { +absl::Status 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); + ASSIGN_OR_RETURN(const Cycle solution, + SolveTsp(cities, absl::GetFlag(FLAGS_solver))); + const std::string svg = RouteSvg(cities, solution); if (absl::GetFlag(FLAGS_output).empty()) { std::cout << svg << std::endl; } else { - QCHECK_OK( + RETURN_IF_ERROR( file::SetContents(absl::GetFlag(FLAGS_output), svg, file::Defaults())); } + return absl::OkStatus(); } } // namespace int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); - RealMain(); + const absl::Status status = RealMain(); + if (!status.ok()) { + LOG(QFATAL) << status; + } return 0; } diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index a2ec734b8e..153eb07f25 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -24,7 +24,6 @@ cc_library( ], visibility = ["//visibility:public"], deps = [ - ":gscip_solver_callback", ":message_callback_data", "//ortools/base:linked_hash_map", "//ortools/base:map_util", @@ -50,6 +49,7 @@ cc_library( "//ortools/math_opt/core:solver_interface", "//ortools/math_opt/core:sparse_submatrix", "//ortools/math_opt/core:sparse_vector_view", + "//ortools/math_opt/solvers/gscip:gscip_solver_constraint_handler", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", "@com_google_absl//absl/cleanup", @@ -117,6 +117,7 @@ cc_library( "//ortools/base:protoutil", "//ortools/base:status_macros", "//ortools/gurobi:environment", + "//ortools/gurobi/isv/public:gurobi_isv", "//ortools/math_opt:callback_cc_proto", "//ortools/math_opt:infeasible_subsystem_cc_proto", "//ortools/math_opt:model_cc_proto", @@ -259,27 +260,6 @@ cc_library( ], ) -cc_library( - name = "gscip_solver_callback", - srcs = ["gscip_solver_callback.cc"], - hdrs = ["gscip_solver_callback.h"], - deps = [ - "//ortools/base", - "//ortools/linear_solver:scip_helper_macros", - "//ortools/linear_solver:scip_with_glop", - "//ortools/math_opt:callback_cc_proto", - "//ortools/math_opt/core:math_opt_proto_utils", - "//ortools/math_opt/core:solver_interface", - "@com_google_absl//absl/base:core_headers", - "@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", - "@com_google_absl//absl/time", - ], -) - cc_library( name = "pdlp_bridge", srcs = ["pdlp_bridge.cc"], diff --git a/ortools/math_opt/solvers/glop_solver.cc b/ortools/math_opt/solvers/glop_solver.cc index 8da13fad09..cc33d174f5 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -459,7 +459,7 @@ absl::StatusOr GlopSolver::MergeSolveParameters( template SparseDoubleVectorProto FillSparseDoubleVector( - const std::vector& ids_in_order, + absl::Span ids_in_order, const absl::flat_hash_map& id_map, const glop::StrictITIVector& values, const SparseVectorFilterProto& filter) { @@ -495,7 +495,7 @@ BasisStatusProto FromGlopBasisStatus(const ValueType glop_basis_status) { template SparseBasisStatusVector FillSparseBasisStatusVector( - const std::vector& ids_in_order, + absl::Span ids_in_order, const absl::flat_hash_map& id_map, const glop::StrictITIVector& values) { SparseBasisStatusVector result; diff --git a/ortools/math_opt/solvers/glpk_solver.cc b/ortools/math_opt/solvers/glpk_solver.cc index 7fc7168335..cdc5d45c8f 100644 --- a/ortools/math_opt/solvers/glpk_solver.cc +++ b/ortools/math_opt/solvers/glpk_solver.cc @@ -38,6 +38,7 @@ #include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" @@ -209,7 +210,7 @@ void UpdateBounds(glp_prob* const problem, const Dimension& dimension, // convention). template void DeleteRowOrColData(std::vector& data, - const std::vector& sorted_deleted_rows_or_cols) { + absl::Span sorted_deleted_rows_or_cols) { if (sorted_deleted_rows_or_cols.empty()) { // Avoid looping when not necessary. return; @@ -355,8 +356,8 @@ SparseDoubleVectorProto FilteredVector(glp_prob* const problem, // Returns the ray data the corresponds to element id having the given value and // all other elements of ids having 0. SparseDoubleVectorProto FilteredRay(const SparseVectorFilterProto& filter, - const std::vector& ids, - const std::vector& values) { + absl::Span ids, + absl::Span values) { CHECK_EQ(ids.size(), values.size()); SparseDoubleVectorProto vec; SparseVectorFilterPredicate predicate(filter); @@ -558,10 +559,10 @@ void MipCallback(glp_tree* const tree, void* const info) { // 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) { + glp_prob* const problem, absl::Span variable_ids, + absl::Span unrounded_variable_lower_bounds, + absl::Span unrounded_variable_upper_bounds, + absl::Span linear_constraint_ids) { InvertedBounds inverted_bounds; const int num_cols = glp_get_num_cols(problem); diff --git a/ortools/math_opt/solvers/gscip/BUILD.bazel b/ortools/math_opt/solvers/gscip/BUILD.bazel new file mode 100644 index 0000000000..26ad355bf2 --- /dev/null +++ b/ortools/math_opt/solvers/gscip/BUILD.bazel @@ -0,0 +1,36 @@ +# Copyright 2010-2024 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/solvers:__subpackages__"]) + +cc_library( + name = "gscip_solver_constraint_handler", + srcs = ["gscip_solver_constraint_handler.cc"], + hdrs = ["gscip_solver_constraint_handler.h"], + deps = [ + "//ortools/base:linked_hash_map", + "//ortools/base:protoutil", + "//ortools/gscip", + "//ortools/gscip:gscip_callback_result", + "//ortools/gscip:gscip_constraint_handler", + "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:math_opt_proto_utils", + "//ortools/math_opt/core:solver_interface", + "//ortools/port:proto_utils", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/time", + "@scip//:libscip", + ], +) diff --git a/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.cc b/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.cc new file mode 100644 index 0000000000..6fd6656229 --- /dev/null +++ b/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.cc @@ -0,0 +1,300 @@ +// Copyright 2010-2024 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/gscip_solver_constraint_handler.h" + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/protoutil.h" +#include "ortools/base/status_macros.h" +#include "ortools/gscip/gscip.h" +#include "ortools/gscip/gscip_callback_result.h" +#include "ortools/gscip/gscip_constraint_handler.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/port/proto_utils.h" +#include "scip/type_var.h" + +namespace operations_research::math_opt { +namespace { + +// We set all priorities to -9'999'998, rather than the default of -1, so that +// our callback only checks constraints after all the constraints that are part +// of the model (e.g. linear constraints have enforcement priority -1'000'000). +// We still want to run before the count solutions constraint handler, which is +// -9'999'999. All the constraints appears to separate with priority >= 0, but +// if we want to run last, we can still pick -9'999'998. E.g. see: +// https://stackoverflow.com/questions/72921074/can-i-set-the-scip-constraint-handler-to-work-only-after-a-feasible-solution-is +// +// Note that these priorities are different from the GScip defaults in +// gscip_constraint_handler.h. Because we are forcing SCIPs API to look more +// like Gurobi's in MathOpt, the GScip defaults make less sense. +GScipConstraintHandlerProperties MakeHandlerProperties() { + return { + .name = "GScipSolverConstraintHandler", + .description = "A single handler for all mathopt callbacks", + .enforcement_priority = -9'999'998, + .feasibility_check_priority = -9'999'998, + .separation_priority = -9'999'998, + }; +} +} // namespace + +absl::Status GScipSolverConstraintData::Validate() const { + if (user_callback == nullptr) { + return absl::OkStatus(); + } + if (variables == nullptr) { + return absl::InternalError( + "GScipSolverConstraintData::variables must be set when " + "GScipSolverConstraintData::user_callback is not null"); + } + if (variable_node_filter == nullptr) { + return absl::InternalError( + "GScipSolverConstraintData::variable_node_filter must be set when " + "GScipSolverConstraintData::variable_node_filter is not null"); + } + if (variable_solution_filter == nullptr) { + return absl::InternalError( + "GScipSolverConstraintData::variable_solution_filter must be set when " + "GScipSolverConstraintData::user_callback is not null"); + } + if (interrupter == nullptr) { + return absl::InternalError( + "GScipSolverConstraintData::interrupter must be set when " + "GScipSolverConstraintData::user_callback is not null"); + } + return absl::OkStatus(); +} + +void GScipSolverConstraintData::SetWhenRunAndAdds( + const CallbackRegistrationProto& registration) { + for (const int event_int : registration.request_registration()) { + switch (static_cast(event_int)) { + case CALLBACK_EVENT_MIP_NODE: + run_at_nodes = true; + break; + case CALLBACK_EVENT_MIP_SOLUTION: + run_at_solutions = true; + break; + default: + break; + } + } + adds_cuts = registration.add_cuts(); + adds_lazy_constraints = registration.add_lazy_constraints(); +} + +GScipSolverConstraintHandler::GScipSolverConstraintHandler() + : GScipConstraintHandler(MakeHandlerProperties()) {} + +absl::StatusOr GScipSolverConstraintHandler::EnforceLp( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data, + bool solution_infeasible) { + RETURN_IF_ERROR(constraint_data.Validate()); + if (!constraint_data.run_at_solutions || + constraint_data.user_callback == nullptr) { + return GScipCallbackResult::kFeasible; + } + ASSIGN_OR_RETURN( + const CallbackDataProto cb_data, + MakeCbData(context, constraint_data, CALLBACK_EVENT_MIP_SOLUTION)); + + ASSIGN_OR_RETURN(const CallbackResultProto result, + constraint_data.user_callback(cb_data)); + return ApplyCallback(result, context, constraint_data, + ConstraintHandlerCallbackType::kEnfoLp); +} + +absl::StatusOr GScipSolverConstraintHandler::CheckIsFeasible( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data, + const bool check_integrality, const bool check_lp_rows, + const bool print_reason, const bool check_completely) { + if (check_completely) { + return absl::InternalError( + "check_completely inside of CONSCHECK not supported. This is called " + "only if you have set some SCIP parameters manually, e.g. " + "display/allviols=TRUE"); + } + RETURN_IF_ERROR(constraint_data.Validate()); + if (!constraint_data.run_at_solutions || + constraint_data.user_callback == nullptr) { + return true; + } + ASSIGN_OR_RETURN( + const CallbackDataProto cb_data, + MakeCbData(context, constraint_data, CALLBACK_EVENT_MIP_SOLUTION)); + ASSIGN_OR_RETURN(const CallbackResultProto result, + constraint_data.user_callback(cb_data)); + ASSIGN_OR_RETURN(const GScipCallbackResult cb_result, + ApplyCallback(result, context, constraint_data, + ConstraintHandlerCallbackType::kConsCheck)); + return cb_result == GScipCallbackResult::kFeasible; +} + +absl::StatusOr GScipSolverConstraintHandler::SeparateLp( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data) { + RETURN_IF_ERROR(constraint_data.Validate()); + if (!constraint_data.run_at_nodes || + constraint_data.user_callback == nullptr) { + return GScipCallbackResult::kDidNotFind; + } + ASSIGN_OR_RETURN( + const CallbackDataProto cb_data, + MakeCbData(context, constraint_data, CALLBACK_EVENT_MIP_NODE)); + ASSIGN_OR_RETURN(const CallbackResultProto result, + constraint_data.user_callback(cb_data)); + ASSIGN_OR_RETURN(const GScipCallbackResult cb_result, + ApplyCallback(result, context, constraint_data, + ConstraintHandlerCallbackType::kSepaLp)); + if (cb_result == GScipCallbackResult::kFeasible) { + return GScipCallbackResult::kDidNotFind; + } + return cb_result; +} + +absl::StatusOr +GScipSolverConstraintHandler::SeparateSolution( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data) { + RETURN_IF_ERROR(constraint_data.Validate()); + if (!constraint_data.run_at_solutions || + constraint_data.user_callback == nullptr) { + return GScipCallbackResult::kDidNotRun; + } + ASSIGN_OR_RETURN( + const CallbackDataProto cb_data, + MakeCbData(context, constraint_data, CALLBACK_EVENT_MIP_SOLUTION)); + ASSIGN_OR_RETURN(const CallbackResultProto result, + constraint_data.user_callback(cb_data)); + ASSIGN_OR_RETURN(const GScipCallbackResult cb_result, + ApplyCallback(result, context, constraint_data, + ConstraintHandlerCallbackType::kSepaSol)); + if (cb_result == GScipCallbackResult::kFeasible) { + return GScipCallbackResult::kDidNotFind; + } + return cb_result; +} + +absl::StatusOr GScipSolverConstraintHandler::MakeCbData( + GScipConstraintHandlerContext& context, + const GScipSolverConstraintData& constraint_data, + const CallbackEventProto event) { + if (event != CALLBACK_EVENT_MIP_NODE && + event != CALLBACK_EVENT_MIP_SOLUTION) { + return util::InternalErrorBuilder() + << "Only events MIP_NODE and MIP_SOLUTION are supported, but was " + "invoked on event: " + << ProtoEnumToString(event); + } + CallbackDataProto cb_data; + cb_data.set_event(event); + const SparseVectorFilterProto* filter = + event == CALLBACK_EVENT_MIP_NODE + ? constraint_data.variable_node_filter + : constraint_data.variable_solution_filter; + auto& var_values = *cb_data.mutable_primal_solution_vector(); + SparseVectorFilterPredicate predicate(*filter); + for (const auto [var_id, scip_var] : *constraint_data.variables) { + const double value = context.VariableValue(scip_var); + if (predicate.AcceptsAndUpdate(var_id, value)) { + var_values.add_ids(var_id); + var_values.add_values(value); + } + } + const GScipCallbackStats& stats = context.stats(); + CallbackDataProto::MipStats& cb_stats = *cb_data.mutable_mip_stats(); + cb_stats.set_primal_bound(stats.primal_bound); + cb_stats.set_dual_bound(stats.dual_bound); + cb_stats.set_explored_nodes(stats.num_processed_nodes_total); + cb_stats.set_open_nodes(stats.num_nodes_left); + // TODO(b/314630175): maybe this should include diving/probing iterations + // and strong branching iterations as well, see SCIPgetNDivingLPIterations + // and SCIPgetNStrongbranchLPIterations + cb_stats.set_simplex_iterations(stats.primal_simplex_iterations + + stats.dual_simplex_iterations); + cb_stats.set_number_of_solutions_found(stats.num_solutions_found); + cb_stats.set_cutting_planes_in_lp(stats.num_cuts_in_lp); + const absl::Duration elapsed = absl::Now() - constraint_data.solve_start_time; + ASSIGN_OR_RETURN(*cb_data.mutable_runtime(), + util_time::EncodeGoogleApiProto(elapsed)); + return cb_data; +} + +absl::StatusOr GScipSolverConstraintHandler::ApplyCallback( + const CallbackResultProto& result, GScipConstraintHandlerContext& context, + const GScipSolverConstraintData& constraint_data, + ConstraintHandlerCallbackType scip_cb_type) { + if (!result.suggested_solutions().empty()) { + return absl::UnimplementedError( + "suggested solution is not yet implemented for SCIP callbacks in " + "MathOpt"); + } + GScipCallbackResult cb_result = GScipCallbackResult::kFeasible; + for (const CallbackResultProto::GeneratedLinearConstraint& cut : + result.cuts()) { + GScipLinearRange scip_constraint{.lower_bound = cut.lower_bound(), + .upper_bound = cut.upper_bound()}; + for (int i = 0; i < cut.linear_expression().ids_size(); ++i) { + scip_constraint.variables.push_back( + constraint_data.variables->at(cut.linear_expression().ids(i))); + scip_constraint.coefficients.push_back(cut.linear_expression().values(i)); + } + if (cut.is_lazy()) { + RETURN_IF_ERROR(context.AddLazyLinearConstraint(scip_constraint, "")); + cb_result = MergeConstraintHandlerResults( + cb_result, GScipCallbackResult::kConstraintAdded, scip_cb_type); + } else { + ASSIGN_OR_RETURN(const GScipCallbackResult cut_result, + context.AddCut(scip_constraint, "")); + cb_result = + MergeConstraintHandlerResults(cb_result, cut_result, scip_cb_type); + } + } + if (result.terminate()) { + // NOTE: we do not know what the current stage is, this is safer than + // calling SCIPinterruptSolve() directly. + constraint_data.interrupter->Interrupt(); + } + return cb_result; +} + +std::vector> +GScipSolverConstraintHandler::RoundingLock( + GScip* gscip, const GScipSolverConstraintData& constraint_data, + const bool lock_type_is_model) { + // Warning: we do not call constraint_data.Validate() because this function + // cannot propagate status errors. As implemented, this function does not + // access the members of constraint_data checked by Validate(). + const bool generates_constraints = + constraint_data.adds_cuts || constraint_data.adds_lazy_constraints; + if (constraint_data.user_callback == nullptr || !generates_constraints) { + return {}; + } + std::vector> result; + for (SCIP_VAR* var : gscip->variables()) { + result.push_back({var, RoundingLockDirection::kBoth}); + } + return result; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.h b/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.h new file mode 100644 index 0000000000..f28435b83f --- /dev/null +++ b/ortools/math_opt/solvers/gscip/gscip_solver_constraint_handler.h @@ -0,0 +1,100 @@ +// Copyright 2010-2024 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_GSCIP_SOLVER_CONSTRAINT_HANDLER_H_ +#define OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_GSCIP_SOLVER_CONSTRAINT_HANDLER_H_ + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "ortools/base/linked_hash_map.h" +#include "ortools/gscip/gscip.h" +#include "ortools/gscip/gscip_callback_result.h" +#include "ortools/gscip/gscip_constraint_handler.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "scip/type_var.h" + +namespace operations_research::math_opt { + +struct GScipSolverConstraintData { + SolverInterface::Callback user_callback = nullptr; + const gtl::linked_hash_map* variables = nullptr; + const SparseVectorFilterProto* variable_node_filter = nullptr; + const SparseVectorFilterProto* variable_solution_filter = nullptr; + absl::Time solve_start_time = absl::UnixEpoch(); + bool run_at_nodes = false; + bool run_at_solutions = false; + bool adds_cuts = false; + bool adds_lazy_constraints = false; + GScip::Interrupter* interrupter = nullptr; + + void SetWhenRunAndAdds(const CallbackRegistrationProto& registration); + + // Ensures that when GScipSolverConstraintData::user_callback != nullptr, we + // also have that variables, variable_node_filter, variable_solution_filter, + // and interrupter are not nullptr as well. In a callback, when user_callback + // is nullptr, do not access these fields! + absl::Status Validate() const; +}; + +class GScipSolverConstraintHandler + : public GScipConstraintHandler { + public: + GScipSolverConstraintHandler(); + + private: + absl::StatusOr EnforceLp( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data, + bool solution_infeasible) override; + + absl::StatusOr CheckIsFeasible( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data, bool check_integrality, + bool check_lp_rows, bool print_reason, bool check_completely) override; + + absl::StatusOr SeparateLp( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data) override; + + absl::StatusOr SeparateSolution( + GScipConstraintHandlerContext context, + const GScipSolverConstraintData& constraint_data) override; + + std::vector> RoundingLock( + GScip* gscip, const GScipSolverConstraintData& constraint_data, + bool lock_type_is_model) override; + + // Requires that constraint_data.Validate() has already been called. + absl::StatusOr MakeCbData( + GScipConstraintHandlerContext& context, + const GScipSolverConstraintData& constraint_data, + CallbackEventProto event); + + // If ok, returned value will be one of {cutoff, lazy, cut, feasible}. + // + // Requires that constraint_data.Validate() has already been called. + absl::StatusOr ApplyCallback( + const CallbackResultProto& result, GScipConstraintHandlerContext& context, + const GScipSolverConstraintData& constraint_data, + ConstraintHandlerCallbackType scip_cb_type); +}; + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_GSCIP_SOLVER_CONSTRAINT_HANDLER_H_ diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index b40aa32dd0..7ec575c15e 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -63,7 +62,7 @@ #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/gscip_solver_callback.h" +#include "ortools/math_opt/solvers/gscip/gscip_solver_constraint_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" @@ -991,7 +990,7 @@ absl::StatusOr> GScipSolver::New( RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize())); RETURN_IF_ERROR(gscip->SetObjectiveOffset(model.objective().offset())); auto solver = absl::WrapUnique(new GScipSolver(std::move(gscip))); - + RETURN_IF_ERROR(solver->constraint_handler_.Register(solver->gscip_.get())); RETURN_IF_ERROR(solver->AddVariables( model.variables(), SparseDoubleVectorAsMap(model.objective().linear_coefficients()))); @@ -1006,7 +1005,6 @@ absl::StatusOr> GScipSolver::New( solver->AddIndicatorConstraints(model.indicator_constraints())); RETURN_IF_ERROR(solver->AddSos1Constraints(model.sos1_constraints())); RETURN_IF_ERROR(solver->AddSos2Constraints(model.sos2_constraints())); - return solver; } @@ -1014,16 +1012,52 @@ absl::StatusOr GScipSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, const MessageCallback message_cb, - const CallbackRegistrationProto& callback_registration, const Callback cb, + const CallbackRegistrationProto& callback_registration, Callback cb, SolveInterrupter* const interrupter) { const absl::Time start = absl::Now(); - RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, - /*supported_events=*/{})); + GScip::Interrupter gscip_interrupter; + const ScopedSolveInterrupterCallback scoped_interrupt_cb( + interrupter, [&]() { gscip_interrupter.Interrupt(); }); + const bool use_interrupter = interrupter != nullptr || cb != nullptr; - const std::unique_ptr callback_handler = - GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb, - start, gscip_->scip()); + RETURN_IF_ERROR(CheckRegisteredCallbackEvents( + callback_registration, + /*supported_events=*/{CALLBACK_EVENT_MIP_SOLUTION, + CALLBACK_EVENT_MIP_NODE})); + if (constraint_data_ != nullptr) { + return absl::InternalError( + "constraint_data_ should always be null at the start of " + "GScipSolver::Solver()"); + } + SCIP_CONS* callback_cons = nullptr; + if (cb != nullptr) { + // NOTE: we must meet the invariant on GScipSolverConstraintData, that when + // user_callback != nullptr, all fields are filled in. + constraint_data_ = std::make_unique(); + constraint_data_->user_callback = std::move(cb); + constraint_data_->SetWhenRunAndAdds(callback_registration); + constraint_data_->solve_start_time = absl::Now(); + constraint_data_->variables = &variables_; + constraint_data_->variable_node_filter = + &callback_registration.mip_node_filter(); + constraint_data_->variable_solution_filter = + &callback_registration.mip_solution_filter(); + constraint_data_->interrupter = &gscip_interrupter; + // NOTE: it is critical that this constraint is added after all other + // constraints, as otherwise, due to what appears to be a bug in SCIP, we + // may run our callback before checking all the constraints in the model, + // see https://listserv.zib.de/pipermail/scip/2023-November/004785.html. + ASSIGN_OR_RETURN(callback_cons, + constraint_handler_.AddCallbackConstraint( + gscip_.get(), "mathopt_callback_constraint", + constraint_data_.get())); + } + const auto cleanup_constraint_data = absl::Cleanup([this]() { + if (constraint_data_ != nullptr) { + *constraint_data_ = {}; + } + }); BufferedMessageCallback buffered_message_callback(std::move(message_cb)); auto message_cb_cleanup = absl::MakeCleanup( @@ -1043,12 +1077,6 @@ absl::StatusOr GScipSolver::Solve( RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), value)); } - // Before calling solve, set the interrupter on the event handler that calls - // SCIPinterruptSolve(). - GScip::Interrupter gscip_interrupter; - const ScopedSolveInterrupterCallback scoped_interrupt_cb( - interrupter, [&]() { gscip_interrupter.Interrupt(); }); - // SCIP returns "infeasible" when the model contain invalid bounds. RETURN_IF_ERROR(ListInvertedBounds().ToStatus()); RETURN_IF_ERROR(ListInvalidIndicators().ToStatus()); @@ -1065,13 +1093,14 @@ absl::StatusOr GScipSolver::Solve( GScipResult gscip_result, gscip_->Solve(gscip_parameters, /*legacy_params=*/"", std::move(gscip_msg_cb), - interrupter == nullptr ? nullptr : &gscip_interrupter)); + use_interrupter ? &gscip_interrupter : nullptr)); // Flush the potential last unfinished line. std::move(message_cb_cleanup).Invoke(); - if (callback_handler) { - RETURN_IF_ERROR(callback_handler->Flush()); + if (callback_cons != nullptr) { + RETURN_IF_ERROR(gscip_->DeleteConstraint(callback_cons)); + constraint_data_.reset(); } ASSIGN_OR_RETURN( @@ -1080,6 +1109,14 @@ absl::StatusOr GScipSolver::Solve( parameters.has_cutoff_limit() ? std::make_optional(parameters.cutoff_limit()) : std::nullopt)); + + // Reset solve-specific model parameters so that they do not leak into the + // next solve. Note that 0 is the default branching priority. + for (const auto [id, unused] : + MakeView(model_parameters.branching_priorities())) { + RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), 0)); + } + CHECK_OK(util_time::EncodeGoogleApiProto( absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time())); return result; diff --git a/ortools/math_opt/solvers/gscip_solver.h b/ortools/math_opt/solvers/gscip_solver.h index d8ee5f1041..cefd68e001 100644 --- a/ortools/math_opt/solvers/gscip_solver.h +++ b/ortools/math_opt/solvers/gscip_solver.h @@ -40,6 +40,7 @@ #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/gscip/gscip_solver_constraint_handler.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "scip/type_cons.h" #include "scip/type_var.h" @@ -155,7 +156,13 @@ class GScipSolver : public SolverInterface { // Returns the indicator constraints with non-binary indicator variables. InvalidIndicators ListInvalidIndicators() const; + // Warning: it is critical that GScipConstraintHandlerData outlive its + // associated SCIP_CONS*. When GScip fails, we want this to be cleaned up + // after gscip_. See documentation on + // GScipConstraintHandler::AddCallbackConstraint(). + std::unique_ptr constraint_data_; const std::unique_ptr gscip_; + GScipSolverConstraintHandler constraint_handler_; gtl::linked_hash_map variables_; bool has_quadratic_objective_ = false; diff --git a/ortools/math_opt/solvers/gscip_solver_callback.cc b/ortools/math_opt/solvers/gscip_solver_callback.cc deleted file mode 100644 index c89ff89b3a..0000000000 --- a/ortools/math_opt/solvers/gscip_solver_callback.cc +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2010-2024 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_callback.h" - -#include -#include -#include - -#include "absl/container/flat_hash_set.h" -#include "absl/memory/memory.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/synchronization/mutex.h" -#include "absl/time/time.h" -#include "ortools/base/logging.h" -#include "ortools/linear_solver/scip_helper_macros.h" -#include "ortools/math_opt/callback.pb.h" -#include "ortools/math_opt/core/math_opt_proto_utils.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "scip/scip.h" -#include "scip/type_scip.h" - -namespace operations_research { -namespace math_opt { - -std::unique_ptr -GScipSolverCallbackHandler::RegisterIfNeeded( - const CallbackRegistrationProto& callback_registration, - const SolverInterface::Callback callback, const absl::Time solve_start, - SCIP* const scip) { - // TODO(b/180617976): Don't ignore unknown callbacks. - return nullptr; -} - -GScipSolverCallbackHandler::GScipSolverCallbackHandler( - SolverInterface::Callback callback, absl::Time solve_start, - SCIP* const scip) - : callback_(std::move(ABSL_DIE_IF_NULL(callback))), - solve_start_(std::move(solve_start)), - scip_(ABSL_DIE_IF_NULL(scip)) {} - -absl::Status GScipSolverCallbackHandler::Flush() { - const absl::MutexLock lock(&callback_mutex_); - return status_; -} - -std::optional GScipSolverCallbackHandler::CallUserCallback( - const CallbackDataProto& callback_data) { - // We hold the lock during the call of the user callback to ensure only one - // call execute at a time. Having multiple calls at once may be an issue when - // the user asks for termination since it may ask for it in one call while - // another thread is about to make its call for another callback. - // - // We don't expect any valid actions taken by the user is a callback to lead - // to another callback. - absl::MutexLock lock(&callback_mutex_); - if (!status_.ok()) { - return std::nullopt; - } - - absl::StatusOr result_or = callback_(callback_data); - status_ = result_or.status(); - if (!result_or.ok() || result_or->terminate()) { - // TODO(b/182919884): Make sure it is correct to use SCIPinterruptSolve() - // here and maybe migrate to the same architecture as the one used to - // interrupt the solve from foreign threads.. - const auto interrupt_status = SCIP_TO_STATUS(SCIPinterruptSolve(scip_)); - if (!interrupt_status.ok()) { - if (status_.ok()) { - status_ = interrupt_status; - } else { - LOG(ERROR) << "Failed to interrupt the solve on error: " - << interrupt_status; - } - } - return std::nullopt; - } - - return *std::move(result_or); -} - -} // namespace math_opt -} // namespace operations_research diff --git a/ortools/math_opt/solvers/gscip_solver_callback.h b/ortools/math_opt/solvers/gscip_solver_callback.h deleted file mode 100644 index bfe3a34b49..0000000000 --- a/ortools/math_opt/solvers/gscip_solver_callback.h +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2010-2024 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_CALLBACK_H_ -#define OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_CALLBACK_H_ - -#include -#include - -#include "absl/base/thread_annotations.h" -#include "absl/status/status.h" -#include "absl/synchronization/mutex.h" -#include "absl/time/time.h" -#include "ortools/math_opt/callback.pb.h" -#include "ortools/math_opt/core/solver_interface.h" -#include "scip/type_scip.h" - -namespace operations_research { -namespace math_opt { - -// Handler for user callbacks for GScipSolver. -// -// It deals with solve interruption when the user request it or when an error -// occurs during the call of the user callback. Any such error is returned by -// Flush(). -// -// TODO(b/193537362): see if we need to share code with the handling of -// SolveInterrupter. It is likely that it could the case to make sure the -// `userinterrupt` flag is not lost. It may require sharing the same SCIP event -// handler to make sure the user callback is called first; but maybe that is not -// necessary. -class GScipSolverCallbackHandler { - public: - // Returns a non null handler if needed (there are supported events that we - // register to). - // - // The caller will also have to use MessageHandler() when calling - // GScip::Solve() when the result is not nullptr. - // - // At the end of the solve, Flush() must be called (when everything else - // succeeded) to make the final user callback calls and return the first error - // that occurred when calling the user callback. - static std::unique_ptr RegisterIfNeeded( - const CallbackRegistrationProto& callback_registration, - SolverInterface::Callback callback, absl::Time solve_start, SCIP* scip); - - GScipSolverCallbackHandler(const GScipSolverCallbackHandler&) = delete; - GScipSolverCallbackHandler& operator=(const GScipSolverCallbackHandler&) = - delete; - - // Makes any last pending calls and returns the first error that occurred - // while calling the user callback. Returns OkStatus if no error has occurred. - absl::Status Flush(); - - private: - GScipSolverCallbackHandler(SolverInterface::Callback callback, - absl::Time solve_start, SCIP* scip); - - // Makes a call to the user callback, updating the status_ and interrupting - // the solve if needed (in case of error or if requested by the user). - // - // This function will ignores calls when status_ is not ok. It returns the - // result of the call of the callback when the call has successfully been made - // and the user has not requested the termination of the solve. - // - // This function will hold the callback_mutex_ while making the call to the - // user callback to serialize calls. - std::optional CallUserCallback( - const CallbackDataProto& callback_data) - ABSL_LOCKS_EXCLUDED(callback_mutex_); - - // The user callback. Should be called via CallUserCallback(). - const SolverInterface::Callback callback_; - - // Start time of the solve. - const absl::Time solve_start_; - - // The SCIP solver, used for interruptions. - SCIP* const scip_; - - // Mutex serializing calls to the user callback and the access to status_. - absl::Mutex callback_mutex_; - - // The first error status returned by the user callback. - absl::Status status_ ABSL_GUARDED_BY(callback_mutex_); -}; - -} // namespace math_opt -} // namespace operations_research - -#endif // OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_CALLBACK_H_ diff --git a/ortools/math_opt/solvers/gurobi/BUILD.bazel b/ortools/math_opt/solvers/gurobi/BUILD.bazel index 489d81293e..a0b045be6b 100644 --- a/ortools/math_opt/solvers/gurobi/BUILD.bazel +++ b/ortools/math_opt/solvers/gurobi/BUILD.bazel @@ -34,8 +34,10 @@ cc_library( "//ortools/base:source_location", "//ortools/base:status_macros", "//ortools/gurobi:environment", + "//ortools/gurobi/isv/public:gurobi_isv", "//ortools/math_opt/solvers:gurobi_cc_proto", - "@com_google_absl//absl/cleanup", + "@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", diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index 5ce9706d7f..648a09d9ca 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -16,20 +16,22 @@ #include #include #include -#include #include #include -#include "absl/cleanup/cleanup.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" #include "absl/strings/str_format.h" +#include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/base/source_location.h" #include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" #include "ortools/gurobi/environment.h" +#include "ortools/gurobi/isv/public/gurobi_isv.h" #include "ortools/math_opt/solvers/gurobi.pb.h" namespace operations_research::math_opt { @@ -151,33 +153,23 @@ void GurobiFreeEnv::operator()(GRBenv* const env) const { absl::StatusOr GurobiNewPrimaryEnv( const std::optional& isv_key) { - GRBenv* naked_primary_env = nullptr; - int err; - std::string_view init_env_method; if (isv_key.has_value()) { - err = GRBisqp(&naked_primary_env, /*logfilename=*/ - nullptr, isv_key->name.c_str(), - isv_key->application_name.c_str(), isv_key->expiration, - isv_key->key.c_str()); - init_env_method = "GRBisqp()"; - } else { - err = GRBloadenv(&naked_primary_env, /*logfilename=*/nullptr); - init_env_method = "GRBloadenv()"; + ASSIGN_OR_RETURN(GRBenv* const naked_primary_env, + NewPrimaryEnvFromISVKey(*isv_key)); + return GRBenvUniquePtr(naked_primary_env); } - if (err != kGrbOk) { - // Surprisingly, even when Gurobi fails to load the environment, it still - // creates one. Here we make sure to free it properly. - // - // We can also use it with GRBgeterrormsg() to get the associated error - // message that goes with the error and the contains additional data like - // the user, the host and the hostid. - const GRBenvUniquePtr primary_env(naked_primary_env); - return util::InvalidArgumentErrorBuilder() - << "failed to create Gurobi primary environment, " << init_env_method - << " returned the error (" << err - << "): " << GRBgeterrormsg(primary_env.get()); + GRBenv* naked_primary_env = nullptr; + const int err = GRBloadenv(&naked_primary_env, /*logfilename=*/nullptr); + // Surprisingly, Gurobi will still create an environment if initialization + // fails, so we want this wrapper even in the error case to free it properly. + GRBenvUniquePtr primary_env(naked_primary_env); + if (err == kGrbOk) { + return primary_env; } - return GRBenvUniquePtr(naked_primary_env); + return util::InvalidArgumentErrorBuilder() + << "failed to create Gurobi primary environment, GRBloadenv() " + "returned the error (" + << err << "): " << GRBgeterrormsg(primary_env.get()); } absl::StatusOr> Gurobi::NewWithSharedPrimaryEnv( diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.h b/ortools/math_opt/solvers/gurobi/g_gurobi.h index 82cd353554..2e31d29cd4 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.h +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.h @@ -33,7 +33,6 @@ #ifndef OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_G_GUROBI_H_ #define OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_G_GUROBI_H_ -#include #include #include #include @@ -45,19 +44,10 @@ #include "absl/types/span.h" #include "ortools/base/source_location.h" #include "ortools/gurobi/environment.h" +#include "ortools/gurobi/isv/public/gurobi_isv.h" namespace operations_research::math_opt { -// An ISV key for the Gurobi solver, an alternative to using a license file. -// -// See http://www.gurobi.com/products/licensing-pricing/isv-program. -struct GurobiIsvKey { - std::string name; - std::string application_name; - int32_t expiration = 0; - std::string key; -}; - // Functor to use as deleter for std::unique_ptr that stores a primary GRBenv, // used by GRBenvUniquePtr. Most users will not use this directly. struct GurobiFreeEnv { diff --git a/ortools/math_opt/solvers/gurobi_init_arguments.cc b/ortools/math_opt/solvers/gurobi_init_arguments.cc index cd560ecb9f..2009b5ac29 100644 --- a/ortools/math_opt/solvers/gurobi_init_arguments.cc +++ b/ortools/math_opt/solvers/gurobi_init_arguments.cc @@ -17,6 +17,7 @@ #include #include "absl/status/statusor.h" +#include "ortools/gurobi/isv/public/gurobi_isv.h" #include "ortools/math_opt/solvers/gurobi.pb.h" #include "ortools/math_opt/solvers/gurobi/g_gurobi.h" diff --git a/ortools/math_opt/solvers/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index f2ca76b316..e60688a3c8 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -469,7 +469,7 @@ constexpr int kUnsetIndex = -2; // are contiguous starting at 0. The elements in the output point to the new // shifted index, or `kDeletedIndex` if the starting index was deleted. std::vector IndexUpdateMap(const int size_before_delete, - const std::vector& deletes) { + absl::Span deletes) { std::vector result(size_before_delete, kUnsetIndex); for (const int del : deletes) { result[del] = kDeletedIndex; @@ -1158,7 +1158,12 @@ absl::StatusOr GurobiSolver::GetMipSolutions( } } } - primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + // Gurobi v9 provides a feasibility status for the instance as a whole but + // not for each solution, and pool entries may be infeasible. To be + // conservative, we only label the first ("best") solution as primal + // feasible. + primal_solution.set_feasibility_status( + i == 0 ? SOLUTION_STATUS_FEASIBLE : SOLUTION_STATUS_UNDETERMINED); ASSIGN_OR_RETURN( const std::vector grb_var_values, gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_XN, num_gurobi_variables_)); @@ -2853,6 +2858,29 @@ absl::Status GurobiSolver::SetMultiObjectiveTolerances( return absl::OkStatus(); } +absl::Status GurobiSolver::ResetModelParameters( + const ModelSolveParametersProto& model_parameters) { + for (int i = 0; i < model_parameters.branching_priorities().ids_size(); ++i) { + const int64_t var_id = model_parameters.branching_priorities().ids(i); + const GurobiVariableIndex grb_index = variables_map_.at(var_id); + RETURN_IF_ERROR( + gurobi_->SetIntAttrElement(GRB_INT_ATTR_BRANCHPRIORITY, grb_index, 0)) + << "failed to reset branching priority for variable ID " << var_id + << " (Gurobi index = " << grb_index << ")"; + } + for (const int64_t lazy_constraint_id : + model_parameters.lazy_linear_constraint_ids()) { + const GurobiLinearConstraintIndex lazy_constraint_index = + linear_constraints_map_.at(lazy_constraint_id).constraint_index; + RETURN_IF_ERROR( + gurobi_->SetIntAttrElement(GRB_INT_ATTR_LAZY, lazy_constraint_index, 0)) + << "failed to reset lazy constraint for lazy constraint ID " + << lazy_constraint_id << " (Gurobi index = " << lazy_constraint_index + << ")"; + } + return absl::OkStatus(); +} + absl::StatusOr GurobiSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, @@ -2933,6 +2961,18 @@ absl::StatusOr GurobiSolver::Solve( if (is_multi_objective_mode()) { RETURN_IF_ERROR(SetMultiObjectiveTolerances(model_parameters)); } + for (const int64_t lazy_constraint_id : + model_parameters.lazy_linear_constraint_ids()) { + const GurobiLinearConstraintIndex lazy_constraint_index = + linear_constraints_map_.at(lazy_constraint_id).constraint_index; + // We select a value of "1" here, which means that the lazy constraints will + // be separated at feasible solutions, and that Gurobi has latitude to + // select which violated constraints to add to the model if multiple are + // violated. This seems like a reasonable default choice for us, but we may + // want to revisit later (or expose this choice to the user). + RETURN_IF_ERROR(gurobi_->SetIntAttrElement(GRB_INT_ATTR_LAZY, + lazy_constraint_index, 1)); + } // Here we register the callback when we either have a user callback or a // local interrupter. The rationale for doing so when we have only an @@ -2966,6 +3006,7 @@ absl::StatusOr GurobiSolver::Solve( // TODO(b/277246682): ensure that resetting parameters does not degrade // incrementalism performance. RETURN_IF_ERROR(gurobi_->ResetParameters()); + RETURN_IF_ERROR(ResetModelParameters(model_parameters)); return solve_result; } diff --git a/ortools/math_opt/solvers/gurobi_solver.h b/ortools/math_opt/solvers/gurobi_solver.h index 63b7c14809..38c273ab89 100644 --- a/ortools/math_opt/solvers/gurobi_solver.h +++ b/ortools/math_opt/solvers/gurobi_solver.h @@ -387,6 +387,9 @@ class GurobiSolver : public SolverInterface { absl::Status SetMultiObjectiveTolerances( const ModelSolveParametersProto& model_parameters); + absl::Status ResetModelParameters( + const ModelSolveParametersProto& model_parameters); + const std::unique_ptr gurobi_; // Note that we use linked_hash_map for the indices of the gurobi_model_ diff --git a/ortools/math_opt/solvers/pdlp_bridge.cc b/ortools/math_opt/solvers/pdlp_bridge.cc index a2c55b982b..1ead2670c1 100644 --- a/ortools/math_opt/solvers/pdlp_bridge.cc +++ b/ortools/math_opt/solvers/pdlp_bridge.cc @@ -24,6 +24,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" +#include "absl/types/span.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" @@ -41,7 +42,7 @@ constexpr SupportedProblemStructures kPdlpSupportedStructures = { .quadratic_objectives = SupportType::kSupported}; absl::StatusOr ExtractSolution( - const Eigen::VectorXd& values, const std::vector& pdlp_index_to_id, + const Eigen::VectorXd& values, absl::Span pdlp_index_to_id, const SparseVectorFilterProto& filter, const double scale) { if (values.size() != pdlp_index_to_id.size()) { return absl::InternalError( diff --git a/ortools/math_opt/storage/linear_constraint_storage.cc b/ortools/math_opt/storage/linear_constraint_storage.cc index e5da1201ec..6ebab47575 100644 --- a/ortools/math_opt/storage/linear_constraint_storage.cc +++ b/ortools/math_opt/storage/linear_constraint_storage.cc @@ -22,6 +22,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/core/sorted.h" #include "ortools/math_opt/model.pb.h" @@ -122,7 +123,7 @@ void LinearConstraintStorage::AdvanceCheckpointInDiff( LinearConstraintStorage::UpdateResult LinearConstraintStorage::Update( const Diff& diff, const absl::flat_hash_set& deleted_variables, - const std::vector& new_variables) const { + absl::Span new_variables) const { UpdateResult result; for (const LinearConstraintId c : diff.deleted) { result.deleted.Add(c.value()); diff --git a/ortools/math_opt/storage/linear_constraint_storage.h b/ortools/math_opt/storage/linear_constraint_storage.h index 9f6e9d18c4..4369008ff5 100644 --- a/ortools/math_opt/storage/linear_constraint_storage.h +++ b/ortools/math_opt/storage/linear_constraint_storage.h @@ -26,6 +26,7 @@ #include "absl/log/check.h" #include "absl/meta/type_traits.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" @@ -159,7 +160,7 @@ class LinearConstraintStorage { UpdateResult Update(const Diff& diff, const absl::flat_hash_set& deleted_variables, - const std::vector& new_variables) const; + absl::Span new_variables) const; // Updates the checkpoint and clears all stored changes in diff. void AdvanceCheckpointInDiff(VariableId variable_checkpoint, diff --git a/ortools/math_opt/storage/objective_storage.cc b/ortools/math_opt/storage/objective_storage.cc index 7bba0eefc0..b035ff5e2d 100644 --- a/ortools/math_opt/storage/objective_storage.cc +++ b/ortools/math_opt/storage/objective_storage.cc @@ -24,6 +24,7 @@ #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/core/sorted.h" @@ -113,7 +114,7 @@ void EnsureHasValue(std::optional& update) { std::optional ObjectiveStorage::ObjectiveData::Update( const Diff::SingleObjective& diff_data, const absl::flat_hash_set& deleted_variables, - const std::vector& new_variables) const { + absl::Span new_variables) const { std::optional update_proto; if (diff_data.direction) { diff --git a/ortools/math_opt/storage/objective_storage.h b/ortools/math_opt/storage/objective_storage.h index 1e3d42d089..0296dcffff 100644 --- a/ortools/math_opt/storage/objective_storage.h +++ b/ortools/math_opt/storage/objective_storage.h @@ -23,6 +23,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/types/span.h" #include "google/protobuf/map.h" #include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" @@ -205,7 +206,7 @@ class ObjectiveStorage { std::optional Update( const Diff::SingleObjective& diff_data, const absl::flat_hash_set& deleted_variables, - const std::vector& new_variables) const; + absl::Span new_variables) const; inline void DeleteVariable(VariableId variable);