diff --git a/ortools/math_opt/core/BUILD.bazel b/ortools/math_opt/core/BUILD.bazel index 1b5cbd099bc..ec0ea62832e 100644 --- a/ortools/math_opt/core/BUILD.bazel +++ b/ortools/math_opt/core/BUILD.bazel @@ -22,12 +22,14 @@ cc_library( "//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:result_cc_proto", "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", + "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", @@ -44,6 +46,7 @@ cc_library( ":sparse_vector", "//ortools/base", "//ortools/base:map_util", + "//ortools/base:types", "//ortools/math_opt:sparse_containers_cc_proto", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/types:span", @@ -60,7 +63,6 @@ cc_library( "//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", @@ -103,6 +105,7 @@ cc_library( hdrs = ["solver.h"], deps = [ ":concurrent_calls_guard", + ":math_opt_proto_utils", ":model_summary", ":non_streamable_solver_init_arguments", ":solve_interrupter", diff --git a/ortools/math_opt/core/c_api/cpp_example.cc b/ortools/math_opt/core/c_api/cpp_example.cc new file mode 100644 index 00000000000..76fce04c728 --- /dev/null +++ b/ortools/math_opt/core/c_api/cpp_example.cc @@ -0,0 +1,107 @@ +// 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. + +// Demonstrates how to call the MathOpt C API defined in solver.h from C++. +// +// At a high level, the example: +// * builds a ModelProto in C++, +// * serializes the model to binary, +// * calls MathOptSolve() from the C-API on the model binary, which outputs a +// SolveResultProto in binary, +// * parses a C++ SolveResultProto from the binary, +// * prints some key parts of the SolveResultProto. +// +// Actual C++ users should use MathOpt's various C++ APIs. This is just a +// demonstration of how the C API is intended to be used (from any language that +// an interoperate with C). + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "ortools/base/init_google.h" +#include "ortools/math_opt/core/c_api/solver.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" + +// This example solves the optimization problem: +// max x +// x in [0, 1] +// and then prints out the termination reason and objective value. +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // Create a serialized ModelProto for the problem. + operations_research::math_opt::ModelProto model; + model.mutable_variables()->add_ids(0); + model.mutable_variables()->add_lower_bounds(0.0); + model.mutable_variables()->add_upper_bounds(1.0); + model.mutable_variables()->add_names("x"); + model.mutable_variables()->add_integers(false); + model.mutable_objective()->set_maximize(true); + model.mutable_objective()->mutable_linear_coefficients()->add_ids(0); + model.mutable_objective()->mutable_linear_coefficients()->add_values(1.0); + const std::string model_str = model.SerializeAsString(); + const void* model_bin = model_str.data(); + const size_t model_bin_size = model_str.size(); + + // Pick a solver. + const int solver_type = + static_cast(operations_research::math_opt::SOLVER_TYPE_GLOP); + + // Set up the output arguments for MathOptSolve() + void* result_bin = nullptr; + size_t result_bin_size = 0; + char* status_msg = nullptr; + + // Call the C API to do solve the model and populate the output arguments. + const int status_code = MathOptSolve(model_bin, model_bin_size, solver_type, + /*interrupter=*/nullptr, &result_bin, + &result_bin_size, &status_msg); + + // If MathOptSolve() failed, print the error and abort. + if (status_code != 0) { + std::cerr << absl::Status(static_cast(status_code), + status_msg) + << std::endl; + // If you handle the error instead of crashing, be sure to free status_msg. + std::abort(); + } + + // Recover the SolveResultProto from the output arguments (stored as a + // serialized proto). + operations_research::math_opt::SolveResultProto result; + if (!result.ParseFromArray(result_bin, static_cast(result_bin_size))) { + std::cout << "failed to parse SolveResultProto" << std::endl; + std::abort(); + } + + // Print out the desired output. + std::cout << "Termination is optimal: " + << (result.termination().reason() == + operations_research::math_opt::TERMINATION_REASON_OPTIMAL) + << std::endl; + std::cout << "Objective value: " + << result.termination().objective_bounds().primal_bound() + << std::endl; + + // Clean up any memory allocated by MathOptSolve(). Note that invoking these + // functions on nullptr is safe. + MathOptFree(result_bin); + MathOptFree(status_msg); + return 0; +} diff --git a/ortools/math_opt/core/c_api/cpp_example_test.py b/ortools/math_opt/core/c_api/cpp_example_test.py new file mode 100644 index 00000000000..26e70324cff --- /dev/null +++ b/ortools/math_opt/core/c_api/cpp_example_test.py @@ -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. + +"""Tests that cpp_example.cc solves a small mip and prints out the answer. + +This is done by running the program in a subprocess and then asserting against +what was printed to standard out. +""" + +import unittest +from ortools.math_opt.examples import log_scraping +from ortools.math_opt.testing import binary_testing + + +class CppExampleTest( + binary_testing.BinaryAssertions, + log_scraping.LogScraping, + unittest.TestCase, +): + def test_regression(self): + result = self.assert_binary_succeeds("ortools/math_opt/core/c_api/cpp_example") + is_optimal = self.assert_has_line_with_prefixed_number( + "Termination is optimal: ", result.stdout + ) + self.assertEqual(is_optimal, 1) + objective_value = self.assert_has_line_with_prefixed_number( + "Objective value: ", result.stdout + ) + self.assertAlmostEqual(objective_value, 1.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/ortools/math_opt/core/c_api/solver.cc b/ortools/math_opt/core/c_api/solver.cc new file mode 100644 index 00000000000..3aaddf634b6 --- /dev/null +++ b/ortools/math_opt/core/c_api/solver.cc @@ -0,0 +1,147 @@ +// 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/c_api/solver.h" + +#include + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/core/solve_interrupter.h" +#include "ortools/math_opt/core/solver.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" + +struct MathOptInterrupter { + operations_research::math_opt::SolveInterrupter cpp_interrupter; +}; + +namespace operations_research::math_opt { +namespace { + +// Returns a serialized SolveResultProto and its size. The caller is responsible +// for freeing the result. +absl::StatusOr> SolveImpl( + const void* model_bytes, const size_t model_size, const int solver_type, + MathOptInterrupter* const interrupter, const bool build_result) { + if (model_bytes == nullptr && model_size != 0) { + return absl::InvalidArgumentError( + "model cannot be null unless model_size is zero"); + } + if (model_size > std::numeric_limits::max()) { + return util::InvalidArgumentErrorBuilder() + << "model_size must be at most max int, was: " << model_size; + } + ModelProto model; + if (model_size > 0) { + if (!model.ParseFromArray(model_bytes, static_cast(model_size))) { + return absl::InvalidArgumentError("bad model proto"); + } + } + Solver::InitArgs init_args; + Solver::SolveArgs solve_args; + if (interrupter != nullptr) { + solve_args.interrupter = &interrupter->cpp_interrupter; + } + ASSIGN_OR_RETURN(const SolveResultProto result, + Solver::NonIncrementalSolve( + model, static_cast(solver_type), + init_args, solve_args)); + const size_t result_size_bytes = result.ByteSizeLong(); + if (result_size_bytes > std::numeric_limits::max()) { + return util::InvalidArgumentErrorBuilder() + << "cannot serialize a SolveResultProto with more than INT_MAX = " + << std::numeric_limits::max() << "(0x" << std::hex + << std::numeric_limits::max() << std::dec + << ") bytes, but solve result proto needed " << result_size_bytes + << " bytes in binary format"; + } + void* result_bin = nullptr; + if (build_result) { + result_bin = malloc(result_size_bytes); + // For current implementation, only fails on proto3 when the size is bigger + // than 2 gigs. + const bool serialize_ok = result.SerializeToArray( + result_bin, static_cast(result_size_bytes)); + if (!serialize_ok) { + free(result_bin); + return absl::InternalError("fail to serialize SolveResultProto"); + } + } + return std::make_pair(result_bin, result_size_bytes); +} + +} // namespace +} // namespace operations_research::math_opt + +MathOptInterrupter* MathOptNewInterrupter() { return new MathOptInterrupter(); } + +void MathOptFreeInterrupter(MathOptInterrupter* interrupter) { + delete interrupter; +} + +void MathOptInterrupt(MathOptInterrupter* interrupter) { + CHECK(interrupter != nullptr); + interrupter->cpp_interrupter.Interrupt(); +} +int MathOptIsInterrupted(const MathOptInterrupter* interrupter) { + CHECK(interrupter != nullptr); + return static_cast(interrupter->cpp_interrupter.IsInterrupted()); +} + +int MathOptSolve(const void* model, const size_t model_size, + const int solver_type, MathOptInterrupter* const interrupter, + void** solve_result, size_t* solve_result_size, + char** status_msg) { + const absl::StatusOr> result = + operations_research::math_opt::SolveImpl( + model, model_size, solver_type, interrupter, + /*build_result=*/solve_result != nullptr); + if (result.ok()) { + if (solve_result_size != nullptr) { + *solve_result_size = result->second; + } + if (solve_result != nullptr) { + *solve_result = result->first; + } + if (status_msg != nullptr) { + *status_msg = nullptr; + } + return 0; + } + // WARNING: failure could be caused by null arguments! + if (status_msg != nullptr) { + const size_t num_bytes = result.status().message().size() + 1; + *status_msg = static_cast(malloc(num_bytes)); + std::memcpy(*status_msg, result.status().message().data(), num_bytes); + } + if (solve_result != nullptr) { + *solve_result = nullptr; + } + if (solve_result_size != nullptr) { + *solve_result_size = 0; + } + return result.status().raw_code(); +} + +void MathOptFree(void* ptr) { free(ptr); } diff --git a/ortools/math_opt/core/c_api/solver.h b/ortools/math_opt/core/c_api/solver.h new file mode 100644 index 00000000000..e0d1f056a1a --- /dev/null +++ b/ortools/math_opt/core/c_api/solver.h @@ -0,0 +1,117 @@ +// 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. + +// MathOpt's C API for solving optimization models given as serialized protocol +// buffers. +// +// The MathOpt protocol buffers are used as inputs and outputs for many +// functions in this API, defined at ortools/math_opt/.*.proto. +// Protocol buffers have a language and machine independent binary format, and +// each supported language can serialize a message into this format. This API +// represents these serialized messages as void* and a size_t saying how many +// bytes long the void* is. +// +// Every language has a different mechanism for constructing a protocol buffer +// and serializing it. This API consumes the serialized proto directly, as it is +// designed for providing access to MathOpt from non-C languages that cannot +// call C++ functions directly, but can call C functions. Callers are expected +// to build protos in their language, serialize them, call these C functions, +// and then deserialize the returned bytes in their language. +// +// See cpp_example.cc for a minimal example of creating and serializing these +// protos from C++, calling the C API to solve the model, and then deserializing +// the returned protos. +#ifndef OR_TOOLS_MATH_OPT_CORE_C_API_SOLVER_H_ +#define OR_TOOLS_MATH_OPT_CORE_C_API_SOLVER_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Notifies MathOptSolve() if the user has requested that the solve stop early. +// +// This is passed as an argument to MathOptSolve(). From any thread, before or +// after the solve begins, you can trigger interruption with MathOptInterrupt(). +// +// This is an opaque type you create with MathOptNewInterrupter(), pass by +// pointer, and then delete with MathOptFreeInterrupter() when done. You cannot +// copy or stack allocate this type. +struct MathOptInterrupter; + +// Returns a new interrupter that has not been triggered. The caller must free +// this with MathOptFreeInterrupter(). +struct MathOptInterrupter* MathOptNewInterrupter(); + +// Frees interrupter, has no effect when interrupter is NULL. +void MathOptFreeInterrupter(struct MathOptInterrupter* interrupter); + +// Triggers the interrupter. +// +// Will CHECK fail if interrupter is NULL. This is threadsafe. +void MathOptInterrupt(struct MathOptInterrupter* interrupter); + +// Checks if the interrupter is triggered. +// +// Will CHECK fail if interrupter is NULL. This is threadsafe. +int MathOptIsInterrupted(const struct MathOptInterrupter* interrupter); + +// Solves an optimization model with MathOpt and returns the result. +// +// Arguments: +// * model: a serialized ModelProto to solve. The function fails if this +// cannot be parsed, or if this is NULL and model_size > 0. +// * model_size: the size of model in bytes. Must be at most MAX_INT or the +// the function fails. +// * solver_type: which solver to use, see SolverTypeProto for numeric values. +// * interrupter: ignored if NULL. If interrupted before the solve begins, or +// from another thread while the solve is running, the solve will +// terminate early with whatever results are available. MathOptSolve() +// will not change the state (interrupted or not) of interrupter. It is +// safe for concurrent calls to MathOptSolve() to share a single +// interrupter. The interrupter must survive all calls to MathOptSolve(). +// * solve_result: an output argument, ignored if NULL. On success, +// `*solve_result` is filled with a serialized SolveResultProto from +// solving `model`. The caller must free `*solve_result` in this case with +// MathOptFree(). On failure, `*solve_result` is set to NULL. +// * solve_result_size: an output argument, ignored if NULL. On success, +// `*solve_result_size` has the size in bytes of the serialized +// SolveResultProto from solving `model` (the size of `*solve_result` if +// set). On failure, `*solve_result_size` is set to zero. +// * status_msg: an output argument. If NULL, this output is ignored. On +// success, `*status_msg` is set to NULL. On failure, `*status_msg` is set +// to a null terminated string describing the error. The caller must free +// `*status_msg` with MathOptFree() in this case. +// +// Note that `solve_result_size` holds the size of the serialized proto returned +// in `solve_result`. Typically, you should make `solve_result` and +// `solve_result_size` either both NULL or both not NULL. You cannot safely +// consume `solve_result` without `solve_result_size`. +// +// Returns 0 if successful and a nonzero value on failure (the value is an +// absl::StatusCode enum). +int MathOptSolve(const void* model, size_t model_size, int solver_type, + struct MathOptInterrupter* interrupter, void** solve_result, + size_t* solve_result_size, char** status_msg); + +// Frees memory allocated by the MathOpt C API, e.g. the solve_result or +// status_msg output arguments from MathOptSolve(). If `ptr` is NULL, has no +// effect. +void MathOptFree(void* ptr); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // OR_TOOLS_MATH_OPT_CORE_C_API_SOLVER_H_ diff --git a/ortools/math_opt/core/c_api/solver_port_c_test.c b/ortools/math_opt/core/c_api/solver_port_c_test.c new file mode 100644 index 00000000000..a2c8121a695 --- /dev/null +++ b/ortools/math_opt/core/c_api/solver_port_c_test.c @@ -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. + +// A minimal use of MathOptSolve() from C to ensure the code compiles in C. +// NOTE: this file is .c, not .cc, so the blaze will test this compiles as C. + +#include +#include +#include +#include + +#include "ortools/math_opt/core/c_api/solver.h" + +void TestSolveEmptyModel() { + // Empty model + const size_t model_size = 0; + const void* model = NULL; + // Solve with glop + const int solver_type = 3; + void* solve_result = NULL; + size_t solve_result_size = 0; + char* status_msg = NULL; + const int error = MathOptSolve(model, model_size, solver_type, /*interrupter=*/NULL, &solve_result, + &solve_result_size, &status_msg); + if (error) { + printf("error on MathOptSolve, status code: %d, status message: %s", error, status_msg); + // If you handle the error instead of crashing, be sure to free status_msg. + abort(); + } + MathOptFree(status_msg); + MathOptFree(solve_result); +} + +void TestInterruptSolveEmptyModel() { + // Empty model + const size_t model_size = 0; + const void* model = NULL; + // Solve with glop + const int solver_type = 3; + void* solve_result = NULL; + size_t solve_result_size = 0; + char* status_msg = NULL; + struct MathOptInterrupter* interrupter = MathOptNewInterrupter(); + MathOptInterrupt(interrupter); + const int error = MathOptSolve(model, model_size, solver_type, /*interrupter=*/NULL, &solve_result, + &solve_result_size, &status_msg); + if (error) { + printf("error on MathOptSolve, status code: %d, status message: %s", error, status_msg); + // If you handle the error instead of crashing, be sure to free status_msg. + abort(); + } + if(MathOptIsInterrupted(interrupter) == 0) { + printf("interrupter should be interrupted"); + abort(); + } + MathOptFreeInterrupter(interrupter); + MathOptFree(status_msg); + MathOptFree(solve_result); +} + +int main(int argc, char** argv) { + TestSolveEmptyModel(); + TestInterruptSolveEmptyModel(); + return EXIT_SUCCESS; +} diff --git a/ortools/math_opt/core/math_opt_proto_utils.cc b/ortools/math_opt/core/math_opt_proto_utils.cc index a19359c2f9d..88661534cad 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.cc +++ b/ortools/math_opt/core/math_opt_proto_utils.cc @@ -14,8 +14,11 @@ #include "ortools/math_opt/core/math_opt_proto_utils.h" #include +#include #include #include +#include +#include #include #include "absl/container/flat_hash_set.h" @@ -35,6 +38,34 @@ namespace operations_research { namespace math_opt { +namespace { +constexpr double kInf = std::numeric_limits::infinity(); +} // namespace + +ObjectiveBoundsProto GetObjectiveBounds(const SolveResultProto& solve_result) { + if (solve_result.termination().has_objective_bounds()) { + return solve_result.termination().objective_bounds(); + } + ObjectiveBoundsProto objective_bounds; + objective_bounds.set_primal_bound( + solve_result.solve_stats().best_primal_bound()); + objective_bounds.set_dual_bound(solve_result.solve_stats().best_dual_bound()); + return objective_bounds; +} + +ProblemStatusProto GetProblemStatus(const SolveResultProto& solve_result) { + if (solve_result.termination().has_problem_status()) { + return solve_result.termination().problem_status(); + } + ProblemStatusProto problem_status; + problem_status.set_primal_status( + solve_result.solve_stats().problem_status().primal_status()); + problem_status.set_dual_status( + solve_result.solve_stats().problem_status().dual_status()); + problem_status.set_primal_or_dual_infeasible( + solve_result.solve_stats().problem_status().primal_or_dual_infeasible()); + return problem_status; +} void RemoveSparseDoubleVectorZeros(SparseDoubleVectorProto& sparse_vector) { CHECK_EQ(sparse_vector.ids_size(), sparse_vector.values_size()); @@ -154,6 +185,205 @@ TerminationProto TerminateForReason(const TerminationReasonProto reason, return result; } +ObjectiveBoundsProto MakeTrivialBounds(const bool is_maximize) { + ObjectiveBoundsProto bounds; + bounds.set_primal_bound(is_maximize ? -kInf : +kInf); + bounds.set_dual_bound(is_maximize ? +kInf : -kInf); + return bounds; +} + +namespace { +ObjectiveBoundsProto MakeUnboundedBounds(const bool is_maximize) { + ObjectiveBoundsProto bounds; + bounds.set_primal_bound(is_maximize ? +kInf : -kInf); + bounds.set_dual_bound(bounds.primal_bound()); + return bounds; +} +} // namespace + +TerminationProto TerminateForReason(const bool is_maximize, + const TerminationReasonProto reason, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(reason); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_UNDETERMINED); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_UNDETERMINED); + *result.mutable_objective_bounds() = MakeTrivialBounds(is_maximize); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto OptimalTerminationProto(const double finite_primal_objective, + const double dual_objective, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_OPTIMAL); + result.mutable_objective_bounds()->set_primal_bound(finite_primal_objective); + result.mutable_objective_bounds()->set_dual_bound(dual_objective); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_problem_status()->set_dual_status(FEASIBILITY_STATUS_FEASIBLE); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto UnboundedTerminationProto(const bool is_maximize, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_UNBOUNDED); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_INFEASIBLE); + *result.mutable_objective_bounds() = MakeUnboundedBounds(is_maximize); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto InfeasibleTerminationProto( + bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_INFEASIBLE); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_INFEASIBLE); + result.mutable_problem_status()->set_dual_status(dual_feasibility_status); + *result.mutable_objective_bounds() = MakeTrivialBounds(is_maximize); + if (dual_feasibility_status == FEASIBILITY_STATUS_FEASIBLE) { + result.mutable_objective_bounds()->set_dual_bound( + result.objective_bounds().primal_bound()); + } + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto LimitTerminationProto( + const bool is_maximize, const LimitProto limit, + const std::optional optional_finite_primal_objective, + const std::optional optional_dual_objective, + const absl::string_view detail) { + if (optional_finite_primal_objective.has_value()) { + return FeasibleTerminationProto(is_maximize, limit, + *optional_finite_primal_objective, + optional_dual_objective, detail); + } + return NoSolutionFoundTerminationProto(is_maximize, limit, + optional_dual_objective, detail); +} + +TerminationProto LimitTerminationProto( + LimitProto limit, const double primal_objective, + const double dual_objective, const bool claim_dual_feasible_solution_exists, + const absl::string_view detail) { + TerminationProto result; + if (std::isfinite(primal_objective)) { + result.set_reason(TERMINATION_REASON_FEASIBLE); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + } else { + result.set_reason(TERMINATION_REASON_NO_SOLUTION_FOUND); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_UNDETERMINED); + } + if (claim_dual_feasible_solution_exists) { + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + } else { + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_UNDETERMINED); + } + result.mutable_objective_bounds()->set_primal_bound(primal_objective); + result.mutable_objective_bounds()->set_dual_bound(dual_objective); + result.set_limit(limit); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto CutoffTerminationProto(bool is_maximize, + const absl::string_view detail) { + return NoSolutionFoundTerminationProto( + is_maximize, LIMIT_CUTOFF, /*optional_dual_objective=*/std::nullopt, + detail); +} + +TerminationProto NoSolutionFoundTerminationProto( + const bool is_maximize, const LimitProto limit, + const std::optional optional_dual_objective, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_NO_SOLUTION_FOUND); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_UNDETERMINED); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_UNDETERMINED); + *result.mutable_objective_bounds() = MakeTrivialBounds(is_maximize); + if (optional_dual_objective.has_value()) { + result.mutable_objective_bounds()->set_dual_bound(*optional_dual_objective); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + } + result.set_limit(limit); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto FeasibleTerminationProto( + const bool is_maximize, const LimitProto limit, + const double primal_objective, + const std::optional optional_dual_objective, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_FEASIBLE); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_UNDETERMINED); + *result.mutable_objective_bounds() = MakeTrivialBounds(is_maximize); + result.mutable_objective_bounds()->set_primal_bound(primal_objective); + if (optional_dual_objective.has_value()) { + result.mutable_objective_bounds()->set_dual_bound(*optional_dual_objective); + result.mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + } + result.set_limit(limit); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + +TerminationProto InfeasibleOrUnboundedTerminationProto( + bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, + const absl::string_view detail) { + TerminationProto result; + result.set_reason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED); + result.mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_UNDETERMINED); + result.mutable_problem_status()->set_dual_status(dual_feasibility_status); + if (dual_feasibility_status == FEASIBILITY_STATUS_UNDETERMINED) { + result.mutable_problem_status()->set_primal_or_dual_infeasible(true); + } + *result.mutable_objective_bounds() = MakeTrivialBounds(is_maximize); + if (!detail.empty()) { + result.set_detail(std::string(detail)); + } + return result; +} + absl::Status ModelIsSupported(const ModelProto& model, const SupportedProblemStructures& support_menu, const absl::string_view solver_name) { @@ -312,5 +542,19 @@ bool UpdateIsSupported(const ModelUpdateProto& update, return true; } +void UpgradeSolveResultProtoForStatsMigration( + SolveResultProto& solve_result_proto) { + *solve_result_proto.mutable_termination()->mutable_problem_status() = + GetProblemStatus(solve_result_proto); + *solve_result_proto.mutable_solve_stats()->mutable_problem_status() = + GetProblemStatus(solve_result_proto); + *solve_result_proto.mutable_termination()->mutable_objective_bounds() = + GetObjectiveBounds(solve_result_proto); + solve_result_proto.mutable_solve_stats()->set_best_primal_bound( + solve_result_proto.termination().objective_bounds().primal_bound()); + solve_result_proto.mutable_solve_stats()->set_best_dual_bound( + solve_result_proto.termination().objective_bounds().dual_bound()); +} + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/core/math_opt_proto_utils.h b/ortools/math_opt/core/math_opt_proto_utils.h index 30c767985d6..d38a109ab33 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.h +++ b/ortools/math_opt/core/math_opt_proto_utils.h @@ -16,13 +16,14 @@ #include #include -#include +#include "absl/base/attributes.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/status/status.h" #include "absl/strings/string_view.h" #include "ortools/math_opt/callback.pb.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" @@ -32,6 +33,19 @@ namespace operations_research::math_opt { +// Returns solve_result.termination.objective_bounds if present. Otherwise, +// it builds ObjectiveBoundsProto from +// solve_result.solve_stats.best_primal/dual_bound +// TODO(b/290091715): Remove once solve_stats.best_primal/dual_bound is removed +// and we know termination.objective_bounds will always be present. +ObjectiveBoundsProto GetObjectiveBounds(const SolveResultProto& solve_result); + +// Returns solve_result.termination.problem_status if present. Otherwise, +// it returns solve_result.solve_stats.problem_status +// TODO(b/290091715): Remove once solve_stats.problem_status is removed and we +// know termination.problem_status will always be present. +ProblemStatusProto GetProblemStatus(const SolveResultProto& solve_result); + inline int NumVariables(const VariablesProto& variables) { return variables.ids_size(); } @@ -132,18 +146,113 @@ absl::flat_hash_set EventSet( // Sets the reason to TERMINATION_REASON_FEASIBLE if feasible = true and // TERMINATION_REASON_NO_SOLUTION_FOUND otherwise. +ABSL_DEPRECATED("Use LimitTerminationProto() instead") TerminationProto TerminateForLimit(LimitProto limit, bool feasible, absl::string_view detail = {}); +ABSL_DEPRECATED("Use FeasibleTerminationProto() instead") TerminationProto FeasibleTermination(LimitProto limit, absl::string_view detail = {}); +ABSL_DEPRECATED("Use NoSolutionFound() instead") TerminationProto NoSolutionFoundTermination(LimitProto limit, absl::string_view detail = {}); +ABSL_DEPRECATED( + "Use TerminateForReason(bool, TerminationReasonProto, absl::string_view) " + "instead") TerminationProto TerminateForReason(TerminationReasonProto reason, absl::string_view detail = {}); +ObjectiveBoundsProto MakeTrivialBounds(bool is_maximize); + +// Sets problem statuses to undetermined and sets trivial bounds independently +// of the selected reason. +TerminationProto TerminateForReason(bool is_maximize, + TerminationReasonProto reason, + absl::string_view detail = {}); + +// finite_primal_objective must be finite for a valid TerminationProto to be +// returned. +// TODO(b/290359402): additionally require dual_objective to be finite. +TerminationProto OptimalTerminationProto(double finite_primal_objective, + double dual_objective, + absl::string_view detail = {}); + +// The convention for infeasible MIPs is that dual_feasibility_status is +// feasible (There always exist a dual feasible convex relaxation of an +// infeasible MIP). +// dual_feasibility_status must not be unspecified for a valid TerminationProto +// to be returned. +TerminationProto InfeasibleTerminationProto( + bool is_maximize, FeasibilityStatusProto dual_feasibility_status, + absl::string_view detail = {}); + +// dual_feasibility_status must be infeasible or undetermined for a valid +// TerminationProto to be returned. +TerminationProto InfeasibleOrUnboundedTerminationProto( + bool is_maximize, FeasibilityStatusProto dual_feasibility_status, + absl::string_view detail = {}); + +TerminationProto UnboundedTerminationProto(bool is_maximize, + absl::string_view detail = {}); + +// Assumes dual solution exists if optional_dual_objective is set even if +// infinite (some solvers return feasible dual solutions without an objective +// value). optional_dual_objective should not be set when limit is LIMIT_CUTOFF +// for a valid TerminationProto to be returned (use +// LimitCutoffTerminationProto() below instead). +// TODO(b/290359402): Consider improving to require a finite dual bound when +// dual feasible solutions are returned. +TerminationProto NoSolutionFoundTerminationProto( + bool is_maximize, LimitProto limit, + std::optional optional_dual_objective = std::nullopt, + absl::string_view detail = {}); + +// finite_primal_objective should be finite and limit should not be LIMIT_CUTOFF +// for a valid TerminationProto to be returned (use +// LimitCutoffTerminationProto() below instead). +// Assumes dual solution exists if optional_dual_objective is set even if +// infinite (some solvers return feasible dual solutions without an objective +// value) +// TODO(b/290359402): Consider improving to require a finite dual bound when +// dual feasible solutions are returned. +TerminationProto FeasibleTerminationProto( + bool is_maximize, LimitProto limit, double finite_primal_objective, + std::optional optional_dual_objective = std::nullopt, + absl::string_view detail = {}); + +// Assumes primal solution exists if optional_finite_primal_objective is set. +// If set, optional_finite_primal_objective should be finite if set for a valid +// TerminationProto to be returned. +// Assumes dual solution exists if optional_dual_objective is set even if +// infinite (some solvers return feasible dual solutions without an objective +// value) +// TODO(b/290359402): Consider improving to require a finite dual bound when +// dual feasible solutions are returned. +TerminationProto LimitTerminationProto( + bool is_maximize, LimitProto limit, + std::optional optional_finite_primal_objective, + std::optional optional_dual_objective = std::nullopt, + absl::string_view detail = {}); + +// Assumes primal solution exists if primal_objective is finite. +// Assumes dual solution exists if claim_dual_feasible_solution_exists is true +// even if dual_objective is infinite (some solvers return feasible dual +// solutions without an objective value). If dual_objective is finite then +// claim_dual_feasible_solution_exists must be true for a valid termination to +// be returned. +// TODO(b/290359402): Consider improving to require a finite dual bound when +// dual feasible solutions are returned. +TerminationProto LimitTerminationProto(LimitProto limit, + double primal_objective, + double dual_objective, + bool claim_dual_feasible_solution_exists, + absl::string_view detail = {}); + +TerminationProto CutoffTerminationProto(bool is_maximize, + absl::string_view detail = {}); + enum class SupportType { kNotSupported = 1, kSupported = 2, @@ -173,6 +282,9 @@ absl::Status ModelIsSupported(const ModelProto& model, bool UpdateIsSupported(const ModelUpdateProto& update, const SupportedProblemStructures& support_menu); +void UpgradeSolveResultProtoForStatsMigration( + SolveResultProto& solve_result_proto); + //////////////////////////////////////////////////////////////////////////////// // Inline functions implementations. //////////////////////////////////////////////////////////////////////////////// diff --git a/ortools/math_opt/core/model_summary.cc b/ortools/math_opt/core/model_summary.cc index 0eff520108e..e08c7750e3b 100644 --- a/ortools/math_opt/core/model_summary.cc +++ b/ortools/math_opt/core/model_summary.cc @@ -31,7 +31,6 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" -#include "ortools/util/status_macros.h" namespace operations_research { namespace math_opt { @@ -130,6 +129,7 @@ ModelSummary::ModelSummary(const bool check_names) absl::StatusOr ModelSummary::Create(const ModelProto& model, const bool check_names) { ModelSummary summary(check_names); + summary.maximize = model.objective().maximize(); RETURN_IF_ERROR(summary.variables.BulkUpdate({}, model.variables().ids(), model.variables().names())) << "ModelProto.variables are invalid"; @@ -167,6 +167,9 @@ absl::StatusOr ModelSummary::Create(const ModelProto& model, } absl::Status ModelSummary::Update(const ModelUpdateProto& model_update) { + if (model_update.objective_updates().has_direction_update()) { + maximize = model_update.objective_updates().direction_update(); + } RETURN_IF_ERROR(variables.BulkUpdate(model_update.deleted_variable_ids(), model_update.new_variables().ids(), model_update.new_variables().names())) diff --git a/ortools/math_opt/core/model_summary.h b/ortools/math_opt/core/model_summary.h index 49f19eded66..da37a05bc22 100644 --- a/ortools/math_opt/core/model_summary.h +++ b/ortools/math_opt/core/model_summary.h @@ -140,6 +140,7 @@ struct ModelSummary { IdNameBiMap sos1_constraints; IdNameBiMap sos2_constraints; IdNameBiMap indicator_constraints; + bool maximize = false; }; //////////////////////////////////////////////////////////////////////////////// diff --git a/ortools/math_opt/core/non_streamable_solver_init_arguments.cc b/ortools/math_opt/core/non_streamable_solver_init_arguments.cc new file mode 100644 index 00000000000..eb974b8371a --- /dev/null +++ b/ortools/math_opt/core/non_streamable_solver_init_arguments.cc @@ -0,0 +1,51 @@ +// 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/non_streamable_solver_init_arguments.h" + +#include + +namespace operations_research::math_opt { +namespace { + +// Returns non_streamable->Clone() if non_streamable is not nullptr, else +// returns nullptr. +std::unique_ptr CloneIfNotNull( + const NonStreamableSolverInitArguments* const non_streamable) { + return non_streamable != nullptr ? non_streamable->Clone() : nullptr; +} + +} // namespace + +NonStreamableSolverInitArgumentsValue::NonStreamableSolverInitArgumentsValue( + const NonStreamableSolverInitArguments& non_streamable) + : non_streamable_(non_streamable.Clone()) {} + +NonStreamableSolverInitArgumentsValue::NonStreamableSolverInitArgumentsValue( + const NonStreamableSolverInitArgumentsValue& other) + : non_streamable_(CloneIfNotNull(other.non_streamable_.get())) {} + +NonStreamableSolverInitArgumentsValue& +NonStreamableSolverInitArgumentsValue::operator=( + const NonStreamableSolverInitArgumentsValue& other) { + // Assignment to self is possible. + if (&other == this) { + return *this; + } + + non_streamable_ = CloneIfNotNull(other.non_streamable_.get()); + + return *this; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/core/non_streamable_solver_init_arguments.h b/ortools/math_opt/core/non_streamable_solver_init_arguments.h index d97f202cf92..376b0d55b27 100644 --- a/ortools/math_opt/core/non_streamable_solver_init_arguments.h +++ b/ortools/math_opt/core/non_streamable_solver_init_arguments.h @@ -132,6 +132,47 @@ struct NonStreamableSolverInitArgumentsHelper } }; +// Value-like class holding an optional NonStreamableSolverInitArguments. On +// copy it clones it. +// +// NonStreamableSolverInitArgumentsValue::get() gives access the pointed +// arguments. The implicit constructor from NonStreamableSolverInitArguments is +// used to build new instance. +class NonStreamableSolverInitArgumentsValue { + public: + // Initializes with no value. + NonStreamableSolverInitArgumentsValue() = default; + + // Initializes with the provided value, cloning it. + NonStreamableSolverInitArgumentsValue( // NOLINT + const NonStreamableSolverInitArguments& non_streamable); + + // Clones other.get() if not nullptr. + NonStreamableSolverInitArgumentsValue( + const NonStreamableSolverInitArgumentsValue& other); + + // Clones other.get() if not nullptr. + NonStreamableSolverInitArgumentsValue& operator=( + const NonStreamableSolverInitArgumentsValue& other); + + // Steals other.get() and resets it to nullptr. + NonStreamableSolverInitArgumentsValue( + NonStreamableSolverInitArgumentsValue&& other) = default; + + // Steals other.get() and resets it to nullptr. + NonStreamableSolverInitArgumentsValue& operator=( + NonStreamableSolverInitArgumentsValue&& other) = default; + + // Return a pointer on the value; nullptr if unset (default value). + const NonStreamableSolverInitArguments* get() const { + return non_streamable_.get(); + } + + private: + // The pointed value. + std::unique_ptr non_streamable_; +}; + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/core/solver.cc b/ortools/math_opt/core/solver.cc index b8cd0c04fac..23b88668999 100644 --- a/ortools/math_opt/core/solver.cc +++ b/ortools/math_opt/core/solver.cc @@ -13,10 +13,7 @@ #include "ortools/math_opt/core/solver.h" -#include -#include #include -#include #include #include "absl/log/check.h" @@ -27,6 +24,7 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/concurrent_calls_guard.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/non_streamable_solver_init_arguments.h" #include "ortools/math_opt/core/solver_debug.h" @@ -63,8 +61,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(), InfeasibleSubsystem(), or Update() failed, " - "the Solver can't be used anymore"); + "a previous call to Solve(), ComputeInfeasibleSubsystem(), or Update() " + "failed, the Solver can't be used anymore"); } } // namespace @@ -138,13 +136,15 @@ absl::StatusOr Solver::Solve(const SolveArgs& arguments) { }; } - ASSIGN_OR_RETURN(const SolveResultProto result, + ASSIGN_OR_RETURN(SolveResultProto result, underlying_solver_->Solve(arguments.parameters, arguments.model_parameters, arguments.message_callback, arguments.callback_registration, cb, arguments.interrupter)); - + // TODO(b/290091715): Remove once language specific structs can use new + // messages. + UpgradeSolveResultProtoForStatsMigration(result); // We consider errors in `result` to be internal errors, but // `ValidateResult()` will return an InvalidArgumentError. So here we convert // the error. @@ -184,8 +184,9 @@ absl::StatusOr Solver::Update(const ModelUpdateProto& model_update) { return true; } -absl::StatusOr Solver::InfeasibleSubsystem( - const InfeasibleSubsystemArgs& infeasible_subsystem_args) { +absl::StatusOr +Solver::ComputeInfeasibleSubsystem( + const ComputeInfeasibleSubsystemArgs& arguments) { ASSIGN_OR_RETURN(const auto guard, ConcurrentCallsGuard::TryAcquire(concurrent_calls_tracker_)); @@ -197,33 +198,32 @@ absl::StatusOr Solver::InfeasibleSubsystem( // We will reset it in code paths where no error occur. fatal_failure_occurred_ = true; - RETURN_IF_ERROR(ValidateSolveParameters(infeasible_subsystem_args.parameters)) + RETURN_IF_ERROR(ValidateSolveParameters(arguments.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)); + ASSIGN_OR_RETURN(const ComputeInfeasibleSubsystemResultProto result, + underlying_solver_->ComputeInfeasibleSubsystem( + arguments.parameters, arguments.message_callback, + arguments.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_))); + ValidateComputeInfeasibleSubsystemResult(result, model_summary_))); fatal_failure_occurred_ = false; return result; } -absl::StatusOr -Solver::NonIncrementalInfeasibleSubsystem( +absl::StatusOr +Solver::NonIncrementalComputeInfeasibleSubsystem( const ModelProto& model, const SolverTypeProto solver_type, const InitArgs& init_args, - const InfeasibleSubsystemArgs& infeasible_subsystem_args) { + const ComputeInfeasibleSubsystemArgs& compute_infeasible_subsystem_args) { ASSIGN_OR_RETURN(std::unique_ptr solver, Solver::New(solver_type, model, init_args)); - return solver->InfeasibleSubsystem(infeasible_subsystem_args); + return solver->ComputeInfeasibleSubsystem(compute_infeasible_subsystem_args); } namespace internal { diff --git a/ortools/math_opt/core/solver.h b/ortools/math_opt/core/solver.h index efd7b17b432..b83f967100a 100644 --- a/ortools/math_opt/core/solver.h +++ b/ortools/math_opt/core/solver.h @@ -102,8 +102,8 @@ class Solver { SolveInterrupter* interrupter = nullptr; }; - // Arguments used when calling InfeasibleSubsystem(). - struct InfeasibleSubsystemArgs { + // Arguments used when calling ComputeInfeasibleSubsystem(). + struct ComputeInfeasibleSubsystemArgs { SolveParametersProto parameters; // An optional callback for messages emitted by the solver. @@ -151,16 +151,16 @@ class Solver { absl::StatusOr Update(const ModelUpdateProto& model_update); // Computes an infeasible subsystem of `model`. - absl::StatusOr InfeasibleSubsystem( - const InfeasibleSubsystemArgs& infeasible_subsystem_args); + absl::StatusOr + ComputeInfeasibleSubsystem(const ComputeInfeasibleSubsystemArgs& arguments); // A shortcut for calling Solver::New() and then - // Solver()::InfeasibleSubsystem() - static absl::StatusOr - NonIncrementalInfeasibleSubsystem( + // Solver()::ComputeInfeasibleSubsystem() + static absl::StatusOr + NonIncrementalComputeInfeasibleSubsystem( const ModelProto& model, SolverTypeProto solver_type, const InitArgs& init_args, - const InfeasibleSubsystemArgs& infeasible_subsystem_args); + const ComputeInfeasibleSubsystemArgs& compute_infeasible_subsystem_args); private: Solver(std::unique_ptr underlying_solver, diff --git a/ortools/math_opt/core/solver_interface.h b/ortools/math_opt/core/solver_interface.h index 757c4a87d75..3a72eb79cd0 100644 --- a/ortools/math_opt/core/solver_interface.h +++ b/ortools/math_opt/core/solver_interface.h @@ -36,14 +36,6 @@ namespace operations_research { namespace math_opt { -namespace internal { - -// The message of the InvalidArgumentError returned by solvers that are passed a -// non null message callback when they don't support it. -inline constexpr absl::string_view kMessageCallbackNotSupported = - "This solver does not support message callbacks."; - -} // namespace internal // Interface implemented by actual solvers. // @@ -113,9 +105,12 @@ class SolverInterface { // Parameters `message_cb`, `cb` and `interrupter` are optional. They are // nullptr when not set. // + // When parameter `message_cb` is not null the value of + // parameters.enable_output should be ignored the solver should behave as it + // is was false (i.e. not print anything). + // // 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. + // supports message callbacks, it should ignore it. // // Solvers should return a InvalidArgumentError when called with events on // callback_registration that are not supported by the solver for the type of @@ -147,12 +142,16 @@ class SolverInterface { // The parameters `message_cb` and `interrupter` are optional. They are // nullptr when not set. // + // When parameter `message_cb` is not null the value of + // parameters.enable_output should be ignored the solver should behave as it + // is was false (i.e. not print anything). + // // 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; + // supports message callbacks, it should ignore it. + virtual absl::StatusOr + ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* interrupter) = 0; }; class AllSolversRegistry { diff --git a/ortools/math_opt/core/sparse_vector_view.h b/ortools/math_opt/core/sparse_vector_view.h index faf4995b446..b92c28fd055 100644 --- a/ortools/math_opt/core/sparse_vector_view.h +++ b/ortools/math_opt/core/sparse_vector_view.h @@ -54,9 +54,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/types/span.h" #include "google/protobuf/message.h" -#include "ortools/base/types.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" +#include "ortools/base/types.h" #include "ortools/math_opt/core/arrow_operator_proxy.h" // IWYU pragma: export #include "ortools/math_opt/core/sparse_vector.h" #include "ortools/math_opt/sparse_containers.pb.h" diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index b495aeb1377..68a18bd3890 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -277,12 +277,8 @@ cc_library( cc_library( name = "message_callback", - srcs = [ - "message_callback.cc", - ], - hdrs = [ - "message_callback.h", - ], + srcs = ["message_callback.cc"], + hdrs = ["message_callback.h"], deps = [ "//ortools/base", "//ortools/base:source_location", @@ -295,12 +291,7 @@ cc_library( cc_library( name = "solver_init_arguments", - srcs = [ - "solver_init_arguments.cc", - ], - hdrs = [ - "solver_init_arguments.h", - ], + hdrs = ["solver_init_arguments.h"], deps = [ ":streamable_solver_init_arguments", "//ortools/math_opt/core:non_streamable_solver_init_arguments", @@ -310,9 +301,7 @@ cc_library( cc_library( name = "solve_arguments", srcs = ["solve_arguments.cc"], - hdrs = [ - "solve_arguments.h", - ], + hdrs = ["solve_arguments.h"], deps = [ ":callback", ":message_callback", @@ -332,9 +321,9 @@ cc_library( hdrs = ["solve.h"], deps = [ ":callback", + ":compute_infeasible_subsystem_arguments", + ":compute_infeasible_subsystem_result", ":enums", - ":infeasible_subsystem_arguments", - ":infeasible_subsystem_result", ":model", ":model_solve_parameters", ":parameters", @@ -445,9 +434,9 @@ cc_library( ) cc_library( - name = "infeasible_subsystem_result", - srcs = ["infeasible_subsystem_result.cc"], - hdrs = ["infeasible_subsystem_result.h"], + name = "compute_infeasible_subsystem_result", + srcs = ["compute_infeasible_subsystem_result.cc"], + hdrs = ["compute_infeasible_subsystem_result.h"], deps = [ ":enums", ":key_types", @@ -462,6 +451,7 @@ cc_library( "//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/core:math_opt_proto_utils", "//ortools/math_opt/storage:model_storage", "//ortools/math_opt/validators:infeasible_subsystem_validator", "//ortools/util:status_macros", @@ -476,8 +466,8 @@ cc_library( ) cc_library( - name = "infeasible_subsystem_arguments", - hdrs = ["infeasible_subsystem_arguments.h"], + name = "compute_infeasible_subsystem_arguments", + hdrs = ["compute_infeasible_subsystem_arguments.h"], deps = [ ":message_callback", ":parameters", diff --git a/ortools/math_opt/cpp/infeasible_subsystem_arguments.h b/ortools/math_opt/cpp/compute_infeasible_subsystem_arguments.h similarity index 77% rename from ortools/math_opt/cpp/infeasible_subsystem_arguments.h rename to ortools/math_opt/cpp/compute_infeasible_subsystem_arguments.h index 8fb9e660f6b..9a1da6ce8bf 100644 --- a/ortools/math_opt/cpp/infeasible_subsystem_arguments.h +++ b/ortools/math_opt/cpp/compute_infeasible_subsystem_arguments.h @@ -11,8 +11,8 @@ // 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_ +#ifndef OR_TOOLS_MATH_OPT_CPP_COMPUTE_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ +#define OR_TOOLS_MATH_OPT_CPP_COMPUTE_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 @@ -20,8 +20,8 @@ namespace operations_research::math_opt { -// Arguments passed to InfeasibleSubsystem() to control the solver. -struct InfeasibleSubsystemArguments { +// Arguments passed to ComputeInfeasibleSubsystem() to control the solver. +struct ComputeInfeasibleSubsystemArguments { // Model independent parameters, e.g. time limit. SolveParameters parameters; @@ -37,8 +37,8 @@ struct InfeasibleSubsystemArguments { // // // To print messages to stdout with a prefix. // ASSIGN_OR_RETURN( - // const InfeasibleSubsystemResult result, - // InfeasibleSubsystem(model, SolverType::kGurobi, + // const ComputeInfeasibleSubsystemResult result, + // ComputeInfeasibleSubsystem(model, SolverType::kGurobi, // { .message_callback = PrinterMessageCallback(std::cout, // "logs| "); }); MessageCallback message_callback = nullptr; @@ -55,8 +55,8 @@ struct InfeasibleSubsystemArguments { // interrupter->Interrupt(); // }); // - // ASSIGN_OR_RETURN(const InfeasibleSubsystemResult result, - // InfeasibleSubsystem(model, SolverType::kGurobi, + // ASSIGN_OR_RETURN(const ComputeInfeasibleSubsystemResult result, + // ComputeInfeasibleSubsystem(model, SolverType::kGurobi, // { .interrupter = interrupter.get() }); // SolveInterrupter* interrupter = nullptr; @@ -64,4 +64,4 @@ struct InfeasibleSubsystemArguments { } // namespace operations_research::math_opt -#endif // OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ +#endif // OR_TOOLS_MATH_OPT_CPP_COMPUTE_INFEASIBLE_SUBSYSTEM_ARGUMENTS_H_ diff --git a/ortools/math_opt/cpp/infeasible_subsystem_result.cc b/ortools/math_opt/cpp/compute_infeasible_subsystem_result.cc similarity index 74% rename from ortools/math_opt/cpp/infeasible_subsystem_result.cc rename to ortools/math_opt/cpp/compute_infeasible_subsystem_result.cc index 7e1d36431a7..f1829097d8f 100644 --- a/ortools/math_opt/cpp/infeasible_subsystem_result.cc +++ b/ortools/math_opt/cpp/compute_infeasible_subsystem_result.cc @@ -11,12 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/math_opt/cpp/infeasible_subsystem_result.h" +#include "ortools/math_opt/cpp/compute_infeasible_subsystem_result.h" #include +#include #include #include +#include #include +#include #include "absl/algorithm/container.h" #include "absl/container/flat_hash_map.h" @@ -32,6 +35,7 @@ #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/core/math_opt_proto_utils.h" #include "ortools/math_opt/cpp/enums.h" #include "ortools/math_opt/cpp/key_types.h" #include "ortools/math_opt/cpp/linear_constraint.h" @@ -45,6 +49,8 @@ namespace operations_research::math_opt { +constexpr double kInf = std::numeric_limits::infinity(); + ModelSubset::Bounds ModelSubset::Bounds::FromProto( const ModelSubsetProto::Bounds& bounds_proto) { return {.lower = bounds_proto.lower(), .upper = bounds_proto.upper()}; @@ -220,6 +226,83 @@ bool ModelSubset::empty() const { sos2_constraints.empty() && indicator_constraints.empty(); } +std::string ModelSubset::ToString() const { + std::stringstream str; + str << "Model Subset:\n"; + const auto stream_object = [&str](const auto& object) { + str << " " << object << ": " << object.ToString() << "\n"; + }; + const auto stream_bounded_object = + [&str](const auto& object, const BoundedQuadraticExpression& as_expr, + const Bounds& bounds) { + if (bounds.empty()) { + return; + } + // We only want to only print the bounds appearing in the subset. The << + // operator for `BoundedQuadraticExpression`s will ignore -/+inf bound + // values for the lower/upper bounds, respectively (assuming that at + // least one is finite, otherwise it chooses to print one bound + // arbitrarily). So, to suppress bounds not in the subset, it suffices + // to set their value to the appropriate infinity. + const double lb = bounds.lower ? as_expr.lower_bound : -kInf; + const double ub = bounds.upper ? as_expr.upper_bound : kInf; + str << " " << object << ": " << (lb <= as_expr.expression <= ub) + << "\n"; + }; + + str << " Variable bounds:\n"; + for (const Variable variable : SortedKeys(variable_bounds)) { + stream_bounded_object( + variable, variable.lower_bound() <= variable <= variable.upper_bound(), + variable_bounds.at(variable)); + } + str << " Variable integrality:\n"; + for (const Variable variable : SortedElements(variable_integrality)) { + str << " " << variable << "\n"; + } + str << " Linear constraints:\n"; + for (const LinearConstraint constraint : SortedKeys(linear_constraints)) { + stream_bounded_object(constraint, constraint.AsBoundedLinearExpression(), + linear_constraints.at(constraint)); + } + if (!quadratic_constraints.empty()) { + str << " Quadratic constraints:\n"; + for (const QuadraticConstraint constraint : + SortedKeys(quadratic_constraints)) { + stream_bounded_object(constraint, + constraint.AsBoundedQuadraticExpression(), + quadratic_constraints.at(constraint)); + } + } + if (!second_order_cone_constraints.empty()) { + str << " Second-order cone constraints:\n"; + for (const SecondOrderConeConstraint constraint : + SortedElements(second_order_cone_constraints)) { + stream_object(constraint); + } + } + if (!sos1_constraints.empty()) { + str << " SOS1 constraints:\n"; + for (const Sos1Constraint constraint : SortedElements(sos1_constraints)) { + stream_object(constraint); + } + } + if (!sos2_constraints.empty()) { + str << " SOS2 constraints:\n"; + for (const Sos2Constraint constraint : SortedElements(sos2_constraints)) { + stream_object(constraint); + } + } + if (!indicator_constraints.empty()) { + str << " Indicator constraints:\n"; + for (const IndicatorConstraint constraint : + SortedElements(indicator_constraints)) { + stream_object(constraint); + } + } + return str.str(); +} + std::ostream& operator<<(std::ostream& out, const ModelSubset& model_subset) { const auto stream_bounds_map = [&out](const auto& map, const absl::string_view name) { @@ -261,30 +344,34 @@ std::ostream& operator<<(std::ostream& out, const ModelSubset& model_subset) { return out; } -absl::StatusOr InfeasibleSubsystemResult::FromProto( +absl::StatusOr +ComputeInfeasibleSubsystemResult::FromProto( const ModelStorage* const model, - const InfeasibleSubsystemResultProto& result_proto) { - InfeasibleSubsystemResult result; + const ComputeInfeasibleSubsystemResultProto& result_proto) { + ComputeInfeasibleSubsystemResult result; const std::optional feasibility = EnumFromProto(result_proto.feasibility()); if (!feasibility.has_value()) { return absl::InvalidArgumentError( - "InfeasibleSubsystemResultProto.feasibility must be specified"); + "ComputeInfeasibleSubsystemResultProto.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)); + RETURN_IF_ERROR( + ValidateComputeInfeasibleSubsystemResultNoModel(result_proto)); result.feasibility = *feasibility; OR_ASSIGN_OR_RETURN3( result.infeasible_subsystem, ModelSubset::FromProto(model, result_proto.infeasible_subsystem()), - _ << "invalid InfeasibleSubsystemResultProto.infeasible_subsystem"); + _ << "invalid " + "ComputeInfeasibleSubsystemResultProto.infeasible_subsystem"); result.is_minimal = result_proto.is_minimal(); return result; } -InfeasibleSubsystemResultProto InfeasibleSubsystemResult::Proto() const { - InfeasibleSubsystemResultProto proto; +ComputeInfeasibleSubsystemResultProto ComputeInfeasibleSubsystemResult::Proto() + const { + ComputeInfeasibleSubsystemResultProto proto; proto.set_feasibility(EnumToProto(feasibility)); if (!infeasible_subsystem.empty()) { *proto.mutable_infeasible_subsystem() = infeasible_subsystem.Proto(); @@ -293,13 +380,13 @@ InfeasibleSubsystemResultProto InfeasibleSubsystemResult::Proto() const { return proto; } -absl::Status InfeasibleSubsystemResult::CheckModelStorage( +absl::Status ComputeInfeasibleSubsystemResult::CheckModelStorage( const ModelStorage* const expected_storage) const { return infeasible_subsystem.CheckModelStorage(expected_storage); } std::ostream& operator<<(std::ostream& out, - const InfeasibleSubsystemResult& result) { + const ComputeInfeasibleSubsystemResult& result) { out << "{feasibility: " << result.feasibility << ", infeasible_subsystem: " << result.infeasible_subsystem << ", is_minimal: " << (result.is_minimal ? "true" : "false") << "}"; diff --git a/ortools/math_opt/cpp/infeasible_subsystem_result.h b/ortools/math_opt/cpp/compute_infeasible_subsystem_result.h similarity index 86% rename from ortools/math_opt/cpp/infeasible_subsystem_result.h rename to ortools/math_opt/cpp/compute_infeasible_subsystem_result.h index 3b7cc016dae..af79ea154c3 100644 --- a/ortools/math_opt/cpp/infeasible_subsystem_result.h +++ b/ortools/math_opt/cpp/compute_infeasible_subsystem_result.h @@ -13,10 +13,11 @@ // IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h" // IWYU pragma: friend "ortools/math_opt/cpp/.*" -#ifndef OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ -#define OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ +#ifndef OR_TOOLS_MATH_OPT_CPP_COMPUTE_INFEASIBLE_SUBSYSTEM_RESULT_H_ +#define OR_TOOLS_MATH_OPT_CPP_COMPUTE_INFEASIBLE_SUBSYSTEM_RESULT_H_ #include +#include #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" @@ -72,6 +73,10 @@ struct ModelSubset { // True if this object corresponds to the empty subset. bool empty() const; + // Returns a detailed string description of the contents of the model subset. + // (not the component names, use `<<` for that instead). + std::string ToString() const; + absl::flat_hash_map variable_bounds; absl::flat_hash_set variable_integrality; absl::flat_hash_map linear_constraints; @@ -85,23 +90,23 @@ struct ModelSubset { 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`. +struct ComputeInfeasibleSubsystemResult { + // Returns the `ComputeInfeasibleSubsystemResult` 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( + // * ValidateComputeInfeasibleSubsystemResultNoModel(result_proto) fails. + static absl::StatusOr FromProto( const ModelStorage* model, - const InfeasibleSubsystemResultProto& result_proto); + const ComputeInfeasibleSubsystemResultProto& 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; + ComputeInfeasibleSubsystemResultProto Proto() const; // Returns a failure if this object contains references to a model other than // `expected_storage` (which must not be nullptr). @@ -126,8 +131,8 @@ struct InfeasibleSubsystemResult { }; std::ostream& operator<<(std::ostream& out, - const InfeasibleSubsystemResult& result); + const ComputeInfeasibleSubsystemResult& result); } // namespace operations_research::math_opt -#endif // OR_TOOLS_MATH_OPT_CPP_INFEASIBLE_SUBSYSTEM_RESULT_H_ +#endif // OR_TOOLS_MATH_OPT_CPP_COMPUTE_INFEASIBLE_SUBSYSTEM_RESULT_H_ diff --git a/ortools/math_opt/cpp/model.cc b/ortools/math_opt/cpp/model.cc index 48bdd0b5ce6..407d5f8507b 100644 --- a/ortools/math_opt/cpp/model.cc +++ b/ortools/math_opt/cpp/model.cc @@ -271,7 +271,9 @@ void Model::AddToObjective(Objective objective, } } -ModelProto Model::ExportModel() const { return storage()->ExportModel(); } +ModelProto Model::ExportModel(const bool remove_names) const { + return storage()->ExportModel(remove_names); +} std::unique_ptr Model::NewUpdateTracker() { return std::make_unique(storage_); diff --git a/ortools/math_opt/cpp/model.h b/ortools/math_opt/cpp/model.h index 7db252813f7..94699da1253 100644 --- a/ortools/math_opt/cpp/model.h +++ b/ortools/math_opt/cpp/model.h @@ -849,7 +849,7 @@ class Model { // Returns a proto representation of the optimization model. // // See FromModelProto() to build a Model from a proto. - ModelProto ExportModel() const; + ModelProto ExportModel(bool remove_names = false) const; // Returns a tracker that can be used to generate a ModelUpdateProto with the // updates that happened since the last checkpoint. The tracker initial diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 52742fdb1ce..1ceaaae1db3 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -254,9 +254,8 @@ struct SolveParameters { // Enables printing the solver implementation traces. These traces are sent // to the standard output stream. // - // Note that if the solver supports message callback and the user registers a - // callback for it, then this parameter value is ignored and no traces are - // printed. + // Note that if the user registers a message callback, then this parameter + // value is ignored and no traces are printed. bool enable_output = false; // Maximum time a solver should spend on the problem. diff --git a/ortools/math_opt/cpp/solve.cc b/ortools/math_opt/cpp/solve.cc index 0f0d9bf678a..7d7a59b4458 100644 --- a/ortools/math_opt/cpp/solve.cc +++ b/ortools/math_opt/cpp/solve.cc @@ -26,9 +26,9 @@ #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/compute_infeasible_subsystem_arguments.h" +#include "ortools/math_opt/cpp/compute_infeasible_subsystem_result.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" @@ -47,13 +47,10 @@ namespace math_opt { namespace { Solver::InitArgs ToSolverInitArgs(const SolverInitArguments& arguments) { - Solver::InitArgs solver_init_args; - solver_init_args.streamable = arguments.streamable.Proto(); - if (arguments.non_streamable != nullptr) { - solver_init_args.non_streamable = arguments.non_streamable.get(); - } - - return solver_init_args; + return { + .streamable = arguments.streamable.Proto(), + .non_streamable = arguments.non_streamable.get(), + }; } absl::StatusOr CallSolve( @@ -114,24 +111,26 @@ absl::StatusOr Solve(const Model& model, const SolveArguments& solve_args, const SolverInitArguments& init_args) { ASSIGN_OR_RETURN(const std::unique_ptr solver, - Solver::New(EnumToProto(solver_type), model.ExportModel(), + Solver::New(EnumToProto(solver_type), + model.ExportModel(init_args.remove_names), ToSolverInitArgs(init_args))); return CallSolve(*solver, model.storage(), solve_args); } -absl::StatusOr InfeasibleSubsystem( +absl::StatusOr ComputeInfeasibleSubsystem( const Model& model, const SolverType solver_type, - const InfeasibleSubsystemArguments& infeasible_subsystem_args, + const ComputeInfeasibleSubsystemArguments& infeasible_subsystem_args, const SolverInitArguments& init_args) { ASSIGN_OR_RETURN( - const InfeasibleSubsystemResultProto result_proto, - Solver::NonIncrementalInfeasibleSubsystem( - model.ExportModel(), EnumToProto(solver_type), + const ComputeInfeasibleSubsystemResultProto result_proto, + Solver::NonIncrementalComputeInfeasibleSubsystem( + model.ExportModel(init_args.remove_names), 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); + return ComputeInfeasibleSubsystemResult::FromProto(model.storage(), + result_proto); } absl::StatusOr> IncrementalSolver::New( @@ -141,7 +140,8 @@ absl::StatusOr> IncrementalSolver::New( return absl::InvalidArgumentError("input model can't be null"); } std::unique_ptr update_tracker = model->NewUpdateTracker(); - ASSIGN_OR_RETURN(const ModelProto model_proto, update_tracker->ExportModel()); + ASSIGN_OR_RETURN(const ModelProto model_proto, + update_tracker->ExportModel(arguments.remove_names)); ASSIGN_OR_RETURN(std::unique_ptr solver, Solver::New(EnumToProto(solver_type), model_proto, ToSolverInitArgs(arguments))); @@ -169,7 +169,7 @@ absl::StatusOr IncrementalSolver::Solve( absl::StatusOr IncrementalSolver::Update() { ASSIGN_OR_RETURN(std::optional model_update, - update_tracker_->ExportModelUpdate()); + update_tracker_->ExportModelUpdate(init_args_.remove_names)); if (!model_update) { return UpdateResult(true, std::move(model_update)); } @@ -183,7 +183,7 @@ absl::StatusOr IncrementalSolver::Update() { } ASSIGN_OR_RETURN(const ModelProto model_proto, - update_tracker_->ExportModel()); + update_tracker_->ExportModel(init_args_.remove_names)); OR_ASSIGN_OR_RETURN3(solver_, Solver::New(EnumToProto(solver_type_), model_proto, ToSolverInitArgs(init_args_)), diff --git a/ortools/math_opt/cpp/solve.h b/ortools/math_opt/cpp/solve.h index 8770437ff5d..b105f42033b 100644 --- a/ortools/math_opt/cpp/solve.h +++ b/ortools/math_opt/cpp/solve.h @@ -29,8 +29,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/compute_infeasible_subsystem_arguments.h" // IWYU pragma: export +#include "ortools/math_opt/cpp/compute_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 @@ -92,21 +92,21 @@ using SolveFunction = // // 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, +// errors. Otherwise, check ComputeInfeasibleSubsystemResult::feasibility to see +// if an infeasible subsystem was found. +// +// Memory model: the returned ComputeInfeasibleSubsystemResult 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 ComputeInfeasibleSubsystemResult, +// * Avoid unnecessarily copying ComputeInfeasibleSubsystemResult, // * 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( +absl::StatusOr ComputeInfeasibleSubsystem( const Model& model, SolverType solver_type, - const InfeasibleSubsystemArguments& infeasible_subsystem_args = {}, + const ComputeInfeasibleSubsystemArguments& infeasible_subsystem_args = {}, const SolverInitArguments& init_args = {}); // Incremental solve of a model. @@ -222,7 +222,7 @@ class IncrementalSolver { SolverType solver_type() const { return solver_type_; } - // TODO(b/273961536): Add InfeasibleSubsystem() member function. + // TODO(b/273961536): Add ComputeInfeasibleSubsystem() member function. private: IncrementalSolver(SolverType solver_type, SolverInitArguments init_args, diff --git a/ortools/math_opt/cpp/solver_init_arguments.cc b/ortools/math_opt/cpp/solver_init_arguments.cc deleted file mode 100644 index 44fc806f521..00000000000 --- a/ortools/math_opt/cpp/solver_init_arguments.cc +++ /dev/null @@ -1,55 +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/solver_init_arguments.h" - -#include -#include - -namespace operations_research::math_opt { - -SolverInitArguments::SolverInitArguments( - StreamableSolverInitArguments streamable) - : streamable(std::move(streamable)) {} - -SolverInitArguments::SolverInitArguments( - const NonStreamableSolverInitArguments& non_streamable) - : non_streamable(non_streamable.Clone()) {} - -SolverInitArguments::SolverInitArguments( - StreamableSolverInitArguments streamable, - const NonStreamableSolverInitArguments& non_streamable) - : streamable(std::move(streamable)), - non_streamable(non_streamable.Clone()) {} - -SolverInitArguments::SolverInitArguments(const SolverInitArguments& other) - : streamable(other.streamable), - non_streamable(other.non_streamable != nullptr - ? other.non_streamable->Clone() - : nullptr) {} - -SolverInitArguments& SolverInitArguments::operator=( - const SolverInitArguments& other) { - // Assignment to self is possible. - if (&other == this) { - return *this; - } - - streamable = other.streamable; - non_streamable = - other.non_streamable != nullptr ? other.non_streamable->Clone() : nullptr; - - return *this; -} - -} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/solver_init_arguments.h b/ortools/math_opt/cpp/solver_init_arguments.h index bd6cee1bc0b..02a7edf0733 100644 --- a/ortools/math_opt/cpp/solver_init_arguments.h +++ b/ortools/math_opt/cpp/solver_init_arguments.h @@ -27,20 +27,18 @@ namespace operations_research::math_opt { // Arguments passed to Solve() and IncrementalSolver::New() to control the // instantiation of the solver. // -// For convenience, constructors with streamable or/and non-streamable arguments -// are provided. The non-streamable arguments are cloned so any change made -// after passing them to this class are ignored. -// // Usage with streamable arguments: // // Solve(model, SOLVER_TYPE_GUROBI, /*solver_args=*/{}, -// SolverInitArguments({ -// .gurobi = StreamableGurobiInitArguments{ -// .isv_key = GurobiISVKey{ -// .name = "some name", -// .application_name = "some app name", -// .expiration = -1, -// .key = "random", +// SolverInitArguments{ +// .streamable = { +// .gurobi = StreamableGurobiInitArguments{ +// .isv_key = GurobiISVKey{ +// .name = "some name", +// .application_name = "some app name", +// .expiration = -1, +// .key = "random", +// } // } // } // }); @@ -51,46 +49,24 @@ namespace operations_research::math_opt { // gurobi_args.primary_env = primary_env.get(); // // Solve(model, SOLVER_TYPE_GUROBI, /*solver_args=*/{}, -// SolverInitArguments(gurobi_args)); +// SolverInitArguments{.non_streamable = gurobi_args}); // struct SolverInitArguments { - SolverInitArguments() = default; - - // Initializes this class with the provided streamable arguments. - explicit SolverInitArguments(StreamableSolverInitArguments streamable); - - // Initializes this class with a clone of the provided non-streamable - // arguments. - // - // Note that since this constructors calls Clone() to initialize the - // non_streamable_solver_init_arguments field, changes made after calling it - // to the input non_streamable are ignored. - explicit SolverInitArguments( - const NonStreamableSolverInitArguments& non_streamable); - - // Initializes this class with both the provided streamable arguments and a - // clone of the non-streamable ones. - SolverInitArguments(StreamableSolverInitArguments streamable, - const NonStreamableSolverInitArguments& non_streamable); - - // Initializes this class as a copy of the provided arguments. The - // non_streamable field is cloned if not nullptr. - SolverInitArguments(const SolverInitArguments& other); - - // Sets this class as a copy of the provided arguments. The non_streamable - // field is cloned if not nullptr. - SolverInitArguments& operator=(const SolverInitArguments& other); - - SolverInitArguments(SolverInitArguments&&) = default; - SolverInitArguments& operator=(SolverInitArguments&&) = default; - + // Solver initialization parameters that can be streamed to be exchanged with + // another process. StreamableSolverInitArguments streamable; - // This should either be the solver specific class or nullptr. + // Solver specific initialization parameters that can't be streamed. This + // should either be the solver specific class or be unset. // // Solvers will fail (by returning an absl::Status) if called with arguments // for another solver. - std::unique_ptr non_streamable; + NonStreamableSolverInitArgumentsValue non_streamable; + + // If true, the names of variables and constraints are discarded before + // sending them to the solver. This is particularly useful for models that + // need to be serialized and are near the two gigabyte limit in proto form. + bool remove_names = false; }; } // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/update_tracker.cc b/ortools/math_opt/cpp/update_tracker.cc index dbddd145024..8525e913d5b 100644 --- a/ortools/math_opt/cpp/update_tracker.cc +++ b/ortools/math_opt/cpp/update_tracker.cc @@ -40,12 +40,12 @@ UpdateTracker::UpdateTracker(const std::shared_ptr& storage) update_tracker_(storage->NewUpdateTracker()) {} absl::StatusOr> -UpdateTracker::ExportModelUpdate() { +UpdateTracker::ExportModelUpdate(const bool remove_names) { const std::shared_ptr storage = storage_.lock(); if (storage == nullptr) { return absl::InvalidArgumentError(internal::kModelIsDestroyed); } - return storage->ExportModelUpdate(update_tracker_); + return storage->ExportModelUpdate(update_tracker_, remove_names); } absl::Status UpdateTracker::AdvanceCheckpoint() { @@ -57,12 +57,13 @@ absl::Status UpdateTracker::AdvanceCheckpoint() { return absl::OkStatus(); } -absl::StatusOr UpdateTracker::ExportModel() const { +absl::StatusOr UpdateTracker::ExportModel( + const bool remove_names) const { const std::shared_ptr storage = storage_.lock(); if (storage == nullptr) { return absl::InvalidArgumentError(internal::kModelIsDestroyed); } - return storage->ExportModel(); + return storage->ExportModel(remove_names); } } // namespace math_opt diff --git a/ortools/math_opt/cpp/update_tracker.h b/ortools/math_opt/cpp/update_tracker.h index 5f0d114b5ae..0041e4c61df 100644 --- a/ortools/math_opt/cpp/update_tracker.h +++ b/ortools/math_opt/cpp/update_tracker.h @@ -80,7 +80,8 @@ class UpdateTracker { // if the update would have been empty. // // If fails if the Model has been destroyed. - absl::StatusOr> ExportModelUpdate(); + absl::StatusOr> ExportModelUpdate( + bool remove_names = false); // Uses the current model state as the starting point to calculate the // ModelUpdateProto next time ExportModelUpdate() is called. @@ -95,7 +96,7 @@ class UpdateTracker { // can avoid having to keep a reference to the Model model. // // If fails if the Model has been destroyed. - absl::StatusOr ExportModel() const; + absl::StatusOr ExportModel(bool remove_names = false) const; private: const std::weak_ptr storage_; diff --git a/ortools/math_opt/infeasible_subsystem.proto b/ortools/math_opt/infeasible_subsystem.proto index f2a1dcc4d9b..9bf4d6db90c 100644 --- a/ortools/math_opt/infeasible_subsystem.proto +++ b/ortools/math_opt/infeasible_subsystem.proto @@ -65,7 +65,7 @@ message ModelSubsetProto { repeated int64 indicator_constraints = 8; } -message InfeasibleSubsystemResultProto { +message ComputeInfeasibleSubsystemResultProto { // The primal feasibility status of the model, as determined by the solver. FeasibilityStatusProto feasibility = 1; diff --git a/ortools/math_opt/io/BUILD.bazel b/ortools/math_opt/io/BUILD.bazel index a09f3088804..12c89a3560f 100644 --- a/ortools/math_opt/io/BUILD.bazel +++ b/ortools/math_opt/io/BUILD.bazel @@ -43,6 +43,7 @@ cc_library( hdrs = ["mps_converter.h"], deps = [ ":proto_converter", + "//ortools/base:status_macros", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_exporter", "//ortools/lp_data:mps_reader", diff --git a/ortools/math_opt/labs/solution_improvement.cc b/ortools/math_opt/labs/solution_improvement.cc new file mode 100644 index 00000000000..dffdb0b8f26 --- /dev/null +++ b/ortools/math_opt/labs/solution_improvement.cc @@ -0,0 +1,273 @@ +// 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_improvement.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/validators/model_validator.h" +#include "ortools/util/fp_roundtrip_conv.h" + +namespace operations_research::math_opt { + +namespace { + +// Returns an error if: +// * the solution contains variables not in the correct model +// * or the solution does not have a value for each variable in the model +// * or some of the solution values are not finite. +absl::Status ValidateFullFiniteSolution(const Model& model, + const VariableMap& solution) { + for (const auto& [v, value] : solution) { + RETURN_IF_ERROR(model.ValidateExistingVariableOfThisModel(v)); + if (!std::isfinite(value)) { + return util::InvalidArgumentErrorBuilder() + << "the solution contains non-finite value " << value + << " for variable " << v; + } + } + for (const Variable v : model.SortedVariables()) { + if (!solution.contains(v)) { + return util::InvalidArgumentErrorBuilder() + << "the solution does not contain a value for variable " << v; + } + } + return absl::OkStatus(); +} + +// Returns the constraints' value based on the input full-solution. +// +// This CHECKs if the input solution does not contain values for every variable +// in the constraint. +double ConstraintValue(const LinearConstraint c, + const VariableMap& solution) { + const BoundedLinearExpression c_bexpr = c.AsBoundedLinearExpression(); + CHECK_EQ(c_bexpr.expression.offset(), 0.0); + // Evaluate() CHECKs that the input solution has values for each variable. + return c_bexpr.expression.Evaluate(solution); +} + +absl::Status ValidateOptions( + const MoveVariablesToTheirBestFeasibleValueOptions& options) { + if (!std::isfinite(options.integrality_tolerance) || + options.integrality_tolerance < 0.0 || + options.integrality_tolerance > kMaxIntegralityTolerance) { + return util::InvalidArgumentErrorBuilder() + << "integrality_tolerance = " + << RoundTripDoubleFormat(options.integrality_tolerance) + << " is not in [0, " + << RoundTripDoubleFormat(kMaxIntegralityTolerance) << "] range"; + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr> MoveVariablesToTheirBestFeasibleValue( + const Model& model, const VariableMap& input_solution, + const std::vector& variables, + const MoveVariablesToTheirBestFeasibleValueOptions& options) { + // Validate the inputs. + { + // TODO(b/193121090): here we build the proto as the APIs of MathOpt only + // works with the proto and can't use the C++ Model (or ModelStorage). + const ModelProto model_proto = model.ExportModel(); + RETURN_IF_ERROR(ValidateModel(model_proto).status()) << "invalid model"; + RETURN_IF_ERROR(ModelIsSupported( + model_proto, + SupportedProblemStructures{ + .integer_variables = SupportType::kSupported, + }, + /*solver_name=*/"MoveVariablesToTheirBestFeasibleValue")); + } + for (const Variable v : variables) { + RETURN_IF_ERROR(model.ValidateExistingVariableOfThisModel(v)) + << "invalid `variables`"; + if (v.lower_bound() > v.upper_bound()) { + return util::InvalidArgumentErrorBuilder() + << "variable " << v << " bounds [" + << RoundTripDoubleFormat(v.lower_bound()) << ", " + << RoundTripDoubleFormat(v.upper_bound()) + << "] integer: " << std::boolalpha << v.is_integer() + << " are inverted"; + } + if (RoundedLowerBound(v, options.integrality_tolerance) > + RoundedUpperBound(v, options.integrality_tolerance)) { + return util::InvalidArgumentErrorBuilder() + << "integer variable " << v << " has bounds [" + << RoundTripDoubleFormat(v.lower_bound()) << ", " + << RoundTripDoubleFormat(v.upper_bound()) + << "] that contain no integer value"; + } + } + RETURN_IF_ERROR(ValidateFullFiniteSolution(model, input_solution)) + << "invalid `input_solution`"; + RETURN_IF_ERROR(ValidateOptions(options)) << "invalid `options`"; + + // We maintain a solution with updated value for each variable in the order of + // traversal. + // + // Invariant: values are finite. + VariableMap new_solution = input_solution; + // We also maintain the values of each constraint in sync with the values in + // new_solution. + // + // Invariant: constraints_value.at(c) == ConstraintValue(c, new_solution) + LinearConstraintMap constraint_values; + for (const LinearConstraint c : model.LinearConstraints()) { + constraint_values.try_emplace(c, ConstraintValue(c, new_solution)); + } + for (const Variable v : variables) { + const double obj_coeff = model.objective_coefficient(v); + + // The variable can't change the objective. We ignore it. + if (obj_coeff == 0.0) continue; + + const double v_current_value = new_solution.at(v); + + // We will then compute the best bound of the variable value based on its + // own bounds and all the constraints it belongs to. This best bound is + // based on the sign of the objective coefficient and the objective + // direction (min or max). The positive_v_change value tracks which + // direction this is. + const bool positive_v_change = model.is_maximize() == (obj_coeff > 0.0); + + // The best_v_bound is the maximum variable's value. We initialize it with + // the variable's bounds, we use +/-inf if bounds are infinite. + double best_v_bound = + positive_v_change ? RoundedUpperBound(v, options.integrality_tolerance) + : RoundedLowerBound(v, options.integrality_tolerance); + + // Now iterate on constraints that contain the variable to find the most + // limiting one. + // + // For reason explained below we also keep track of the fact that we found a + // limiting constraint, i.e. a constraint with a finite bound in the + // direction of improvement of v. + bool some_constraints_are_limiting = false; + for (const LinearConstraint& c : model.ColumnNonzeros(v)) { + const double c_coeff = c.coefficient(v); + // The ValidateModel() should have failed. + CHECK(std::isfinite(c_coeff)) << v << ": " << c_coeff; + + // The variable has no influence on the constraint. + if (c_coeff == 0.0) continue; + + // Based on the constraint coefficient's sign and the variable change + // sign, we compute which constraint bound we need to consider. + const bool use_constraint_upper_bound = + (c_coeff >= 0.0) == positive_v_change; + + // If the bound is not finite, ignore this constraint. + const double used_bound = + use_constraint_upper_bound ? c.upper_bound() : c.lower_bound(); + if (!std::isfinite(used_bound)) continue; + + // We have one constraint with a finite bound if we reach this point. + some_constraints_are_limiting = true; + + // Compute the bound that the constraint put on the variable. + const double c_v_bound = [&]() { + const double c_value = constraint_values.at(c); + + // If the constraint value is not finite (could be +/-inf or NaN due to + // computation), we consider we can't improve the value of v. + // + // We could here recompute the constraint value without v and do + // something if the value is finite but in practice it is likely to be + // an issue. + if (!std::isfinite(c_value)) return v_current_value; + + // If we are out of the bounds of the constraint, we can't improve v. + // + // Note that when use_constraint_upper_bound is false we return when + // `c_value < used_bound`, i.e. we use < instead of <=. In practice + // though the case c_value == used_bound is covered by the computation + // and will return v_current_value. + if ((c_value >= used_bound) == use_constraint_upper_bound) { + return v_current_value; + } + // Can be +/-inf; see comment about some_constraints_are_limiting below. + return v_current_value + ((used_bound - c_value) / c_coeff); + }(); + + // Update best_v_bound based on the constraint. + if (positive_v_change) { + best_v_bound = std::fmin(best_v_bound, c_v_bound); + } else { + best_v_bound = std::fmax(best_v_bound, c_v_bound); + } + } + + if (!std::isfinite(best_v_bound)) { + if (some_constraints_are_limiting) { + // Here we don't fail if constraints have finite bounds but computations + // lead to infinite values. This typically occurs when the limiting + // constraint has a huge bound and the variable coefficient in the + // constraint is small. We could improve the algorithm to pick a finite + // value for the variable that does not lead to an overflow but this is + // non trivial. + continue; + } + // If there is no limiting constraint with a finite bound and the variable + // own bound is infinite, the model is actually unbounded. + return util::FailedPreconditionErrorBuilder() + << "the model is unbounded regarding variable " << v; + } + + const double v_improved_value = [&]() { + if (!v.is_integer()) { + return best_v_bound; + } + // Make sure the value is integral for integer variables. If we have a + // constraint limiting x <= 1.5 we want to use x = 1. + // + // Note that since best_v_bound is finite, floor or ceil also are. + return positive_v_change ? std::floor(best_v_bound) + : std::ceil(best_v_bound); + }(); + + // If we have find no improvement; skip this variable. + if (positive_v_change ? v_improved_value <= v_current_value + : v_improved_value >= v_current_value) + continue; + + // Apply the change to new_solution. + // + // As v_improved_value is finite the invariant holds. + new_solution.at(v) = v_improved_value; + // Restore the invariant of constraint_values based on the new_solution. + for (const LinearConstraint& c : model.ColumnNonzeros(v)) { + // Here we could incrementally update values based on the change of + // new_solution.at(v) and the coefficient for (c, v). But since we are + // doing floating point computation, we may introduce some errors for each + // variable being changed. It is easier to recompute the constraints + // values from scratch instead. + constraint_values.at(c) = ConstraintValue(c, new_solution); + } + } + + return new_solution; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/labs/solution_improvement.h b/ortools/math_opt/labs/solution_improvement.h new file mode 100644 index 00000000000..99c8f59eac5 --- /dev/null +++ b/ortools/math_opt/labs/solution_improvement.h @@ -0,0 +1,118 @@ +// 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 file contains primal solution improvement heuristics. +#ifndef OR_TOOLS_MATH_OPT_LABS_SOLUTION_IMPROVEMENT_H_ +#define OR_TOOLS_MATH_OPT_LABS_SOLUTION_IMPROVEMENT_H_ + +#include +#include +#include + +#include "absl/status/statusor.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +// Maximum value for `integrality_tolerance` and for RoundedLowerBound() and +// RoundedUpperBound(). +inline constexpr double kMaxIntegralityTolerance = 0.25; + +// Options for MoveVariablesToTheirBestFeasibleValue. +struct MoveVariablesToTheirBestFeasibleValueOptions { + // An absolute tolerance used for rounding the bounds of integer variables. + // + // It should be in [0, kMaxIntegralityTolerance] range; an error is returned + // if the input tolerance is outside this range. + // + // See RoundedLowerBound() and RoundedUpperBound() for details. + double integrality_tolerance = 0.0; +}; + +// Returns a solution that improves the objective value of the input model by +// moving the input variables' values to the their best feasible value (as +// defined by the objective) based on the constraints and other variables' +// values. +// +// The `input_solution` has to contain a value for each variable in the +// `model`. The input model must not be unbounded (and error is returned if this +// is the case). +// +// Only the value of the variables listed in `variables` are modified. The +// variables are considered in the order they appear in the vector. Thus the end +// result depends on this ordering: +// +// - If multiple variables appear in the same constraint, the first variable may +// use up all the constraint's slack; preventing next variables to improve the +// objective as much as they could. +// +// This issue can be fixed by sorting variables by their objective +// coefficient. But this may conflict with the order picked to solve +// dependencies as explained below. +// +// - A variable improvement may be limited by another variable it depends on. If +// it appears first and the second variable's value changes, we may end up +// with some slack that the first variable could use. +// +// This issue can be solved by either: +// +// * Calling this function multiple times until no more variables are changed. +// +// * Sorting the input `variables` in a correct order so that the limiting +// variable appear first. +// +// The variables' values are changed in the direction that improves the +// objective. Variables that are not in the objective are not modified. +// +// This function is typically useful when solving MIP with a non-zero gap or +// when the time limit interrupts the solve early. In those cases a MIP solver +// can return a solution where some variables can trivially be changed to +// improve the objective but since the solution fits in the termination criteria +// (either the gap or the time limit) the solver did not do it. +absl::StatusOr> MoveVariablesToTheirBestFeasibleValue( + const Model& model, const VariableMap& input_solution, + const std::vector& variables, + const MoveVariablesToTheirBestFeasibleValueOptions& options = {}); + +// Returns the lower bound of the variable, rounding it up when the variable is +// integral and the bound's fractional value is outside the tolerance. +// +// For example if the lower bound of an integer variable is 1.0000000000000002 +// and the tolerance is 0.0 this function will return 2.0. If the tolerance is +// 1e-6 though this function will return 1.0. +// +// Tolerance should be a non-negative value < kMaxIntegralityTolerance (usually +// much smaller). A negative input value (or NaN) will be considered 0.0, a +// value >= kMaxIntegralityTolerance will be considered kMaxIntegralityTolerance +// (using a tolerance like 0.5 would lead to odd behavior for ties as integral +// bounds could be rounded to the next integer. For example with the integer +// 2^53 - 1, 2^53 - 1 + 0.5 = 2^53) +inline double RoundedLowerBound(const Variable v, const double tolerance) { + // We use std::fmax() to treat NaN as 0.0. + const double offset = + std::min(std::fmax(0.0, tolerance), kMaxIntegralityTolerance); + return v.is_integer() ? std::ceil(v.lower_bound() - offset) : v.lower_bound(); +} + +// Same as RoundedLowerBound() but for upper-bound. +inline double RoundedUpperBound(const Variable v, const double tolerance) { + // See comment in RoundedLowerBound(). + const double offset = + std::min(std::fmax(0.0, tolerance), kMaxIntegralityTolerance); + return v.is_integer() ? std::floor(v.upper_bound() + offset) + : v.upper_bound(); +} + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_LABS_SOLUTION_IMPROVEMENT_H_ diff --git a/ortools/math_opt/model.proto b/ortools/math_opt/model.proto index 9851b14ab64..3bc897b35d9 100644 --- a/ortools/math_opt/model.proto +++ b/ortools/math_opt/model.proto @@ -43,6 +43,8 @@ message VariablesProto { message ObjectiveProto { // false is minimize, true is maximize bool maximize = 1; + + // Must be finite and not NaN. double offset = 2; // ObjectiveProto terms that are linear in the decision variables. diff --git a/ortools/math_opt/result.proto b/ortools/math_opt/result.proto index 392a499cc81..ded850e3a5b 100644 --- a/ortools/math_opt/result.proto +++ b/ortools/math_opt/result.proto @@ -257,6 +257,63 @@ enum LimitProto { LIMIT_OTHER = 11; } +// Bounds on the optimal objective value. +message ObjectiveBoundsProto { + // Solver claims the optimal value is equal or better (smaller for + // minimization and larger for maximization) than primal_bound up to the + // solvers primal feasibility tolerance (see warning below): + // * primal_bound is trivial (+inf for minimization and -inf + // maximization) when the solver does not claim to have such bound. + // * primal_bound can be closer to the optimal value than the objective + // of the best primal feasible solution. In particular, primal_bound + // may be non-trivial even when no primal feasible solutions are returned. + // Warning: The precise claim is that there exists a primal solution that: + // * is numerically feasible (i.e. feasible up to the solvers tolerance), and + // * has an objective value primal_bound. + // This numerically feasible solution could be slightly infeasible, in which + // case primal_bound could be strictly better than the optimal value. + // Translating a primal feasibility tolerance to a tolerance on + // primal_bound is non-trivial, specially when the feasibility tolerance + // is relatively large (e.g. when solving with PDLP). + double primal_bound = 2; + + // Solver claims the optimal value is equal or worse (larger for + // minimization and smaller for maximization) than dual_bound up to the + // solvers dual feasibility tolerance (see warning below): + // * dual_bound is trivial (-inf for minimization and +inf + // maximization) when the solver does not claim to have such bound. + // Similarly to primal_bound, this may happen for some solvers even + // when returning optimal. MIP solvers will typically report a bound even + // if it is imprecise. + // * for continuous problems dual_bound can be closer to the optimal + // value than the objective of the best dual feasible solution. For MIP + // one of the first non-trivial values for dual_bound is often the + // optimal value of the LP relaxation of the MIP. + // * dual_bound should be better (smaller for minimization and larger + // for maximization) than primal_bound up to the solvers tolerances + // (see warning below). + // Warning: + // * For continuous problems, the precise claim is that there exists a + // dual solution that: + // * is numerically feasible (i.e. feasible up to the solvers tolerance), + // and + // * has an objective value dual_bound. + // This numerically feasible solution could be slightly infeasible, in + // which case dual_bound could be strictly worse than the optimal + // value and primal_bound. Similar to the primal case, translating a + // dual feasibility tolerance to a tolerance on dual_bound is + // non-trivial, specially when the feasibility tolerance is relatively + // large. However, some solvers provide a corrected version of + // dual_bound that can be numerically safer. This corrected version + // can be accessed through the solver's specific output (e.g. for PDLP, + // pdlp_output.convergence_information.corrected_dual_objective). + // * For MIP solvers, dual_bound may be associated to a dual solution + // for some continuous relaxation (e.g. LP relaxation), but it is often a + // complex consequence of the solvers execution and is typically more + // imprecise than the bounds reported by LP solvers. + double dual_bound = 3; +} + // All information regarding why a call to Solve() terminated. message TerminationProto { // Additional information in `limit` when value is TERMINATION_REASON_FEASIBLE @@ -271,6 +328,19 @@ message TerminationProto { // Additional typically solver specific information about termination. string detail = 3; + + // Feasibility statuses for primal and dual problems. + // As of July 18, 2023 this message may be missing. If missing, problem_status + // can be found in SolveResultProto.solve_stats. + ProblemStatusProto problem_status = 4; + + // Bounds on the optimal objective value. + // As of July 18, 2023 this message may be missing. If missing, + // objective_bounds.primal_bound can be found in + // SolveResultProto.solve.stats.best_primal_bound and + // objective_bounds.dual_bound can be found in + // SolveResultProto.solve.stats.best_dual_bound + ObjectiveBoundsProto objective_bounds = 5; } // The contract of when primal/dual solutions/rays is complex, see diff --git a/ortools/math_opt/samples/BUILD.bazel b/ortools/math_opt/samples/BUILD.bazel index 3dcf21eddf2..9cef4dc98c3 100644 --- a/ortools/math_opt/samples/BUILD.bazel +++ b/ortools/math_opt/samples/BUILD.bazel @@ -14,8 +14,8 @@ package(default_visibility = ["//visibility:public"]) cc_binary( - name = "basic_example_mo", - srcs = ["basic_example_mo.cc"], + name = "basic_example", + srcs = ["basic_example.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -27,8 +27,8 @@ cc_binary( ) cc_binary( - name = "cocktail_hour_mo", - srcs = ["cocktail_hour_mo.cc"], + name = "cocktail_hour", + srcs = ["cocktail_hour.cc"], deps = [ "//ortools/base", "//ortools/base:map_util", @@ -44,8 +44,8 @@ cc_binary( ) cc_binary( - name = "linear_programming_mo", - srcs = ["linear_programming_mo.cc"], + name = "linear_programming", + srcs = ["linear_programming.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -58,8 +58,8 @@ cc_binary( ) cc_binary( - name = "integer_programming_mo", - srcs = ["integer_programming_mo.cc"], + name = "integer_programming", + srcs = ["integer_programming.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -71,8 +71,8 @@ cc_binary( ) cc_binary( - name = "cutting_stock_mo", - srcs = ["cutting_stock_mo.cc"], + name = "cutting_stock", + srcs = ["cutting_stock.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -85,8 +85,8 @@ cc_binary( ) cc_binary( - name = "facility_lp_benders_mo", - srcs = ["facility_lp_benders_mo.cc"], + name = "facility_lp_benders", + srcs = ["facility_lp_benders.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -109,8 +109,8 @@ cc_binary( ) cc_binary( - name = "lagrangian_relaxation_mo", - srcs = ["lagrangian_relaxation_mo.cc"], + name = "lagrangian_relaxation", + srcs = ["lagrangian_relaxation.cc"], deps = [ "//ortools/base", "//ortools/base:container_logging", @@ -129,8 +129,8 @@ cc_binary( ) cc_binary( - name = "tsp_mo", - srcs = ["tsp_mo.cc"], + name = "tsp", + srcs = ["tsp.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -145,8 +145,8 @@ cc_binary( ) cc_binary( - name = "linear_regression_mo", - srcs = ["linear_regression_mo.cc"], + name = "linear_regression", + srcs = ["linear_regression.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -159,8 +159,8 @@ cc_binary( ) cc_binary( - name = "advanced_linear_programming_mo", - srcs = ["advanced_linear_programming_mo.cc"], + name = "advanced_linear_programming", + srcs = ["advanced_linear_programming.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -173,8 +173,8 @@ cc_binary( ) cc_binary( - name = "time_indexed_scheduling_mo", - srcs = ["time_indexed_scheduling_mo.cc"], + name = "time_indexed_scheduling", + srcs = ["time_indexed_scheduling.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", @@ -190,8 +190,8 @@ cc_binary( ) cc_binary( - name = "graph_coloring_mo", - srcs = ["graph_coloring_mo.cc"], + name = "graph_coloring", + srcs = ["graph_coloring.cc"], deps = [ "//ortools/base", "//ortools/base:status_macros", diff --git a/ortools/math_opt/samples/advanced_linear_programming_mo.cc b/ortools/math_opt/samples/advanced_linear_programming.cc similarity index 100% rename from ortools/math_opt/samples/advanced_linear_programming_mo.cc rename to ortools/math_opt/samples/advanced_linear_programming.cc diff --git a/ortools/math_opt/samples/area_socp_mo.cc b/ortools/math_opt/samples/area_socp.cc similarity index 100% rename from ortools/math_opt/samples/area_socp_mo.cc rename to ortools/math_opt/samples/area_socp.cc diff --git a/ortools/math_opt/samples/basic_example_mo.cc b/ortools/math_opt/samples/basic_example.cc similarity index 100% rename from ortools/math_opt/samples/basic_example_mo.cc rename to ortools/math_opt/samples/basic_example.cc diff --git a/ortools/math_opt/samples/cocktail_hour_mo.cc b/ortools/math_opt/samples/cocktail_hour.cc similarity index 100% rename from ortools/math_opt/samples/cocktail_hour_mo.cc rename to ortools/math_opt/samples/cocktail_hour.cc diff --git a/ortools/math_opt/samples/cutting_stock_mo.cc b/ortools/math_opt/samples/cutting_stock.cc similarity index 100% rename from ortools/math_opt/samples/cutting_stock_mo.cc rename to ortools/math_opt/samples/cutting_stock.cc diff --git a/ortools/math_opt/samples/facility_lp_benders_mo.cc b/ortools/math_opt/samples/facility_lp_benders.cc similarity index 99% rename from ortools/math_opt/samples/facility_lp_benders_mo.cc rename to ortools/math_opt/samples/facility_lp_benders.cc index e6a7eb441b8..5796a24e979 100644 --- a/ortools/math_opt/samples/facility_lp_benders_mo.cc +++ b/ortools/math_opt/samples/facility_lp_benders.cc @@ -71,8 +71,8 @@ #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(int, num_facilities, 750, "Number of facilities."); +ABSL_FLAG(int, num_locations, 12, "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."); diff --git a/ortools/math_opt/samples/graph_coloring_mo.cc b/ortools/math_opt/samples/graph_coloring.cc similarity index 100% rename from ortools/math_opt/samples/graph_coloring_mo.cc rename to ortools/math_opt/samples/graph_coloring.cc diff --git a/ortools/math_opt/samples/integer_programming_mo.cc b/ortools/math_opt/samples/integer_programming.cc similarity index 100% rename from ortools/math_opt/samples/integer_programming_mo.cc rename to ortools/math_opt/samples/integer_programming.cc diff --git a/ortools/math_opt/samples/lagrangian_relaxation_mo.cc b/ortools/math_opt/samples/lagrangian_relaxation.cc similarity index 100% rename from ortools/math_opt/samples/lagrangian_relaxation_mo.cc rename to ortools/math_opt/samples/lagrangian_relaxation.cc diff --git a/ortools/math_opt/samples/linear_programming_mo.cc b/ortools/math_opt/samples/linear_programming.cc similarity index 100% rename from ortools/math_opt/samples/linear_programming_mo.cc rename to ortools/math_opt/samples/linear_programming.cc diff --git a/ortools/math_opt/samples/linear_regression_mo.cc b/ortools/math_opt/samples/linear_regression.cc similarity index 100% rename from ortools/math_opt/samples/linear_regression_mo.cc rename to ortools/math_opt/samples/linear_regression.cc diff --git a/ortools/math_opt/samples/time_indexed_scheduling_mo.cc b/ortools/math_opt/samples/time_indexed_scheduling.cc similarity index 100% rename from ortools/math_opt/samples/time_indexed_scheduling_mo.cc rename to ortools/math_opt/samples/time_indexed_scheduling.cc diff --git a/ortools/math_opt/samples/tsp_mo.cc b/ortools/math_opt/samples/tsp.cc similarity index 100% rename from ortools/math_opt/samples/tsp_mo.cc rename to ortools/math_opt/samples/tsp.cc diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index 32b8e728fb1..26578864f1c 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -14,11 +14,10 @@ #include "ortools/math_opt/solvers/cp_sat_solver.h" #include -#include #include #include -#include #include +#include #include #include #include @@ -65,8 +64,6 @@ namespace math_opt { namespace { -constexpr double kInf = std::numeric_limits::infinity(); - constexpr SupportedProblemStructures kCpSatSupportedStructures = { .integer_variables = SupportType::kSupported, .quadratic_objectives = SupportType::kNotImplemented, @@ -249,42 +246,23 @@ std::vector SetSolveParameters( return warnings; } -absl::StatusOr> -GetTerminationAndStats(const bool is_interrupted, const bool maximize, - const bool used_cutoff, - const MPSolutionResponse& response) { - SolveStatsProto solve_stats; - TerminationProto termination; - - // Set default status and bounds. - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_UNDETERMINED); - solve_stats.set_best_primal_bound(maximize ? -kInf : kInf); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_UNDETERMINED); - solve_stats.set_best_dual_bound(maximize ? kInf : -kInf); - - // Set terminations and update status and bounds as appropriate. +absl::StatusOr GetTermination( + const bool is_interrupted, const bool maximize, const bool used_cutoff, + const MPSolutionResponse& response) { switch (response.status()) { case MPSOLVER_OPTIMAL: - termination = - TerminateForReason(TERMINATION_REASON_OPTIMAL, response.status_str()); - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_primal_bound(response.objective_value()); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_dual_bound(response.best_objective_bound()); + return OptimalTerminationProto(response.objective_value(), + response.best_objective_bound(), + response.status_str()); break; case MPSOLVER_INFEASIBLE: if (used_cutoff) { - termination = - NoSolutionFoundTermination(LIMIT_CUTOFF, response.status_str()); + return CutoffTerminationProto(maximize, response.status_str()); } else { - termination = TerminateForReason(TERMINATION_REASON_INFEASIBLE, - response.status_str()); - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_INFEASIBLE); + // By convention infeasible MIPs are always dual feasible. + return InfeasibleTerminationProto( + maximize, /*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE, + response.status_str()); } break; case MPSOLVER_UNKNOWN_STATUS: @@ -302,32 +280,25 @@ GetTerminationAndStats(const bool is_interrupted, const bool maximize, // TODO(b/202159173): A better solution would be to use CP-SAT API // directly which may help further improve the statuses. if (absl::StrContains(response.status_str(), "infeasible or unbounded")) { - termination = TerminateForReason( - TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, response.status_str()); - solve_stats.mutable_problem_status()->set_primal_or_dual_infeasible( - true); + return InfeasibleOrUnboundedTerminationProto( + maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + response.status_str()); } else { - termination = TerminateForReason(TERMINATION_REASON_OTHER_ERROR, - response.status_str()); + return TerminateForReason(maximize, TERMINATION_REASON_OTHER_ERROR, + response.status_str()); } break; case MPSOLVER_FEASIBLE: - termination = FeasibleTermination( - is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED, + return FeasibleTerminationProto( + maximize, is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED, + response.objective_value(), response.best_objective_bound(), response.status_str()); - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_primal_bound(response.objective_value()); - solve_stats.set_best_dual_bound(response.best_objective_bound()); - if (std::isfinite(response.best_objective_bound())) { - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_FEASIBLE); - } break; case MPSOLVER_NOT_SOLVED: - termination = NoSolutionFoundTermination( - is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED, - response.status_str()); + return NoSolutionFoundTerminationProto( + maximize, is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED, + /*optional_dual_objective=*/std::nullopt, response.status_str()); break; case MPSOLVER_MODEL_INVALID: return absl::InternalError( @@ -337,13 +308,14 @@ GetTerminationAndStats(const bool is_interrupted, const bool maximize, return absl::InternalError( absl::StrCat("unexpected solve status: ", response.status())); } - return std::make_pair(std::move(solve_stats), std::move(termination)); + return absl::InternalError( + absl::StrCat("unimplemented solve status: ", response.status())); } } // namespace absl::StatusOr> CpSatSolver::New( - const ModelProto& model, const InitArgs& init_args) { + const ModelProto& model, const InitArgs&) { RETURN_IF_ERROR(ModelIsSupported(model, kCpSatSupportedStructures, "CP-SAT")); ASSIGN_OR_RETURN(MPModelProto cp_sat_model, MathOptModelToMPModelProto(model)); @@ -469,13 +441,10 @@ absl::StatusOr CpSatSolver::Solve( SatSolveProto(std::move(req), &interrupt_solve, logging_callback, solution_callback)); RETURN_IF_ERROR(callback_error) << "error in callback"; - ASSIGN_OR_RETURN( - (auto [solve_stats, termination]), - GetTerminationAndStats(local_interrupter.IsInterrupted(), - /*maximize=*/cp_sat_model_.maximize(), - /*used_cutoff=*/used_cutoff, response)); - *result.mutable_solve_stats() = std::move(solve_stats); - *result.mutable_termination() = std::move(termination); + ASSIGN_OR_RETURN(*result.mutable_termination(), + GetTermination(local_interrupter.IsInterrupted(), + /*maximize=*/cp_sat_model_.maximize(), + /*used_cutoff=*/used_cutoff, response)); const SparseVectorFilterProto& var_values_filter = model_parameters.variable_values_filter(); auto add_solution = @@ -504,7 +473,7 @@ absl::StatusOr CpSatSolver::Solve( return result; } -absl::StatusOr CpSatSolver::Update(const ModelUpdateProto& model_update) { +absl::StatusOr CpSatSolver::Update(const ModelUpdateProto&) { return false; } @@ -553,9 +522,10 @@ InvertedBounds CpSatSolver::ListInvertedBounds() const { return inverted_bounds; } -absl::StatusOr CpSatSolver::InfeasibleSubsystem( - const SolveParametersProto& parameters, MessageCallback message_cb, - SolveInterrupter* const interrupter) { +absl::StatusOr +CpSatSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, + MessageCallback, + SolveInterrupter* const) { return absl::UnimplementedError( "CPSAT does not provide a method to compute an infeasible subsystem"); } diff --git a/ortools/math_opt/solvers/cp_sat_solver.h b/ortools/math_opt/solvers/cp_sat_solver.h index 9e4ff678084..5cee1ade0ae 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.h +++ b/ortools/math_opt/solvers/cp_sat_solver.h @@ -48,9 +48,10 @@ 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; + absl::StatusOr + ComputeInfeasibleSubsystem(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 dab090a9d60..72b17f4fae6 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -17,10 +17,8 @@ #include #include #include -#include #include #include -#include #include #include "absl/container/flat_hash_map.h" @@ -71,8 +69,6 @@ namespace math_opt { namespace { -constexpr double kInf = std::numeric_limits::infinity(); - constexpr SupportedProblemStructures kGlopSupportedStructures = {}; absl::string_view SafeName(const VariablesProto& variables, int index) { @@ -91,42 +87,65 @@ absl::string_view SafeName(const LinearConstraintsProto& linear_constraints, } absl::StatusOr BuildTermination( - const glop::ProblemStatus status, - const SolveInterrupter* const interrupter) { + const glop::ProblemStatus status, const SolveInterrupter* const interrupter, + const bool is_maximize, const double objective_value) { switch (status) { case glop::ProblemStatus::OPTIMAL: - return TerminateForReason(TERMINATION_REASON_OPTIMAL); + return OptimalTerminationProto(objective_value, objective_value); case glop::ProblemStatus::PRIMAL_INFEASIBLE: + return InfeasibleTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); case glop::ProblemStatus::DUAL_UNBOUNDED: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE); + return InfeasibleTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE); case glop::ProblemStatus::PRIMAL_UNBOUNDED: - return TerminateForReason(TERMINATION_REASON_UNBOUNDED); + return UnboundedTerminationProto(is_maximize); case glop::ProblemStatus::DUAL_INFEASIBLE: + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE); case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED); + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); case glop::ProblemStatus::INIT: + // Glop may flip the `interrupt_solve` atomic when it is terminated for a + // reason other than interruption so we should ignore its value. Instead + // we use the interrupter. + // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see + // comments in FillSolution). + return NoSolutionFoundTerminationProto( + is_maximize, interrupter != nullptr && interrupter->IsInterrupted() + ? LIMIT_INTERRUPTED + : LIMIT_UNDETERMINED); case glop::ProblemStatus::DUAL_FEASIBLE: // Glop may flip the `interrupt_solve` atomic when it is terminated for a // reason other than interruption so we should ignore its value. Instead // we use the interrupter. // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see // comments in FillSolution). - return NoSolutionFoundTermination(interrupter != nullptr && - interrupter->IsInterrupted() - ? LIMIT_INTERRUPTED - : LIMIT_UNDETERMINED); + return NoSolutionFoundTerminationProto( + is_maximize, + interrupter != nullptr && interrupter->IsInterrupted() + ? LIMIT_INTERRUPTED + : LIMIT_UNDETERMINED, + objective_value); case glop::ProblemStatus::PRIMAL_FEASIBLE: // Glop may flip the `interrupt_solve` atomic when it is terminated for a // reason other than interruption so we should ignore its value. Instead // we use the interrupter. // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see // comments in FillSolution). - return FeasibleTermination(interrupter != nullptr && - interrupter->IsInterrupted() - ? LIMIT_INTERRUPTED - : LIMIT_UNDETERMINED); + return FeasibleTerminationProto( + is_maximize, + interrupter != nullptr && interrupter->IsInterrupted() + ? LIMIT_INTERRUPTED + : LIMIT_UNDETERMINED, + objective_value); case glop::ProblemStatus::IMPRECISE: - return TerminateForReason(TERMINATION_REASON_IMPRECISE); + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); case glop::ProblemStatus::ABNORMAL: case glop::ProblemStatus::INVALID_PROBLEM: return absl::InternalError( @@ -594,7 +613,7 @@ InvertedBounds GlopSolver::ListInvertedBounds() const { void GlopSolver::FillSolution(const glop::ProblemStatus status, const ModelSolveParametersProto& model_parameters, SolveResultProto& solve_result) { - // Meaningfull solutions are available if optimality is proven in + // Meaningful solutions are available if optimality is proven in // preprocessing or after 1 simplex iteration. // TODO(b/195295177): Discuss what to do with glop::ProblemStatus::IMPRECISE // looks like it may be set also when rays are imprecise. @@ -708,76 +727,8 @@ void GlopSolver::FillSolution(const glop::ProblemStatus status, } } -absl::Status GlopSolver::FillSolveStats(const glop::ProblemStatus status, - const absl::Duration solve_time, +absl::Status GlopSolver::FillSolveStats(const absl::Duration solve_time, SolveStatsProto& solve_stats) { - const bool is_maximize = linear_program_.IsMaximizationProblem(); - - // Set default status and bounds. - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_UNDETERMINED); - solve_stats.set_best_primal_bound(is_maximize ? -kInf : kInf); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_UNDETERMINED); - solve_stats.set_best_dual_bound(is_maximize ? kInf : -kInf); - - // Update status and bounds as appropriate. - switch (status) { - case glop::ProblemStatus::OPTIMAL: - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_primal_bound(lp_solver_.GetObjectiveValue()); - solve_stats.set_best_dual_bound(lp_solver_.GetObjectiveValue()); - break; - case glop::ProblemStatus::PRIMAL_INFEASIBLE: - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_INFEASIBLE); - break; - case glop::ProblemStatus::DUAL_UNBOUNDED: - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_INFEASIBLE); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_dual_bound(is_maximize ? -kInf : kInf); - break; - case glop::ProblemStatus::PRIMAL_UNBOUNDED: - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_INFEASIBLE); - solve_stats.set_best_primal_bound(is_maximize ? kInf : -kInf); - break; - case glop::ProblemStatus::DUAL_INFEASIBLE: - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_INFEASIBLE); - break; - case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED: - solve_stats.mutable_problem_status()->set_primal_or_dual_infeasible(true); - break; - case glop::ProblemStatus::PRIMAL_FEASIBLE: - solve_stats.mutable_problem_status()->set_primal_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_primal_bound(lp_solver_.GetObjectiveValue()); - break; - case glop::ProblemStatus::DUAL_FEASIBLE: - solve_stats.mutable_problem_status()->set_dual_status( - FEASIBILITY_STATUS_FEASIBLE); - solve_stats.set_best_dual_bound(lp_solver_.GetObjectiveValue()); - break; - case glop::ProblemStatus::INIT: - case glop::ProblemStatus::IMPRECISE: - // TODO(b/195295177): Discuss what to do with - // glop::ProblemStatus::IMPRECISE - break; - case glop::ProblemStatus::ABNORMAL: - case glop::ProblemStatus::INVALID_PROBLEM: - return absl::InternalError( - absl::StrCat("Unexpected GLOP termination reason: ", - glop::GetProblemStatusString(status))); - } - // Fill remaining stats solve_stats.set_simplex_iterations(lp_solver_.GetNumberOfSimplexIterations()); RETURN_IF_ERROR(util_time::EncodeGoogleApiProto( @@ -793,10 +744,12 @@ absl::StatusOr GlopSolver::MakeSolveResult( const absl::Duration solve_time) { SolveResultProto solve_result; ASSIGN_OR_RETURN(*solve_result.mutable_termination(), - BuildTermination(status, interrupter)); + BuildTermination(status, interrupter, + linear_program_.IsMaximizationProblem(), + lp_solver_.GetObjectiveValue())); FillSolution(status, model_parameters, solve_result); RETURN_IF_ERROR( - FillSolveStats(status, solve_time, *solve_result.mutable_solve_stats())); + FillSolveStats(solve_time, *solve_result.mutable_solve_stats())); return solve_result; } @@ -821,7 +774,7 @@ absl::StatusOr GlopSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, const MessageCallback message_cb, - const CallbackRegistrationProto& callback_registration, const Callback cb, + const CallbackRegistrationProto& callback_registration, const Callback, SolveInterrupter* const interrupter) { RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, /*supported_events=*/{})); @@ -884,7 +837,7 @@ absl::StatusOr GlopSolver::Solve( } absl::StatusOr> GlopSolver::New( - const ModelProto& model, const InitArgs& init_args) { + const ModelProto& model, const InitArgs&) { RETURN_IF_ERROR(ModelIsSupported(model, kGlopSupportedStructures, "Glop")); auto solver = absl::WrapUnique(new GlopSolver); // By default Glop CHECKs that bounds are always consistent (lb < ub); thus it @@ -938,9 +891,9 @@ absl::StatusOr GlopSolver::Update(const ModelUpdateProto& model_update) { return true; } -absl::StatusOr GlopSolver::InfeasibleSubsystem( - const SolveParametersProto& parameters, MessageCallback message_cb, - SolveInterrupter* interrupter) { +absl::StatusOr +GlopSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, + MessageCallback, SolveInterrupter*) { return absl::UnimplementedError( "GLOP does not implement a method to compute an infeasible subsystem"); } diff --git a/ortools/math_opt/solvers/glop_solver.h b/ortools/math_opt/solvers/glop_solver.h index fbffe444533..0f7e7cdecb8 100644 --- a/ortools/math_opt/solvers/glop_solver.h +++ b/ortools/math_opt/solvers/glop_solver.h @@ -55,9 +55,10 @@ 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; + absl::StatusOr + ComputeInfeasibleSubsystem(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. @@ -94,8 +95,7 @@ class GlopSolver : public SolverInterface { const ModelSolveParametersProto& model_parameters, const SolveInterrupter* interrupter, absl::Duration solve_time); - absl::Status FillSolveStats(glop::ProblemStatus status, - absl::Duration solve_time, + absl::Status FillSolveStats(absl::Duration solve_time, SolveStatsProto& solve_stats); void SetGlopBasis(const BasisProto& basis); diff --git a/ortools/math_opt/solvers/glpk_solver.cc b/ortools/math_opt/solvers/glpk_solver.cc index de98721bafe..21ff0e2902d 100644 --- a/ortools/math_opt/solvers/glpk_solver.cc +++ b/ortools/math_opt/solvers/glpk_solver.cc @@ -736,8 +736,7 @@ absl::StatusOr BuildTermination( const std::function(glp_prob*)> termination_on_success, MipCallbackData* const mip_cb_data, const bool has_feasible_solution, - const std::vector& variable_ids, - const std::vector& linear_constraint_ids) { + const std::vector&, const std::vector&) { if (mip_cb_data != nullptr && mip_cb_data->HasBeenInterruptedByInterrupter()) { return TerminateForLimit(LIMIT_INTERRUPTED, @@ -1223,6 +1222,14 @@ absl::StatusOr GlpkSolver::Solve( /*variable_ids=*/variables_.ids, /*linear_constraint_ids=*/linear_constraints_.ids)); + // If the primal is proven infeasible and the dual is feasible, the dual + // is unbounded. Thus we can compute a better dual bound rather than the + // default value. + if (glp_get_prim_stat(problem_) == GLP_NOFEAS && + glp_get_dual_stat(problem_) == GLP_FEAS) { + best_dual_bound = maximize ? -kInf : +kInf; + } + ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_problem_status(), GetSimplexProblemStatusProto( /*glp_simplex_rc=*/glp_simplex_rc, @@ -1239,14 +1246,22 @@ absl::StatusOr GlpkSolver::Solve( // 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_)) { - case GLP_OPT: // OPT is returned by glp_ipt_status & glp_mip_status. - case GLP_FEAS: // FEAS is returned by glp_mip_status & glp_get_prim_stat. - best_primal_bound = obj_val(problem_); + switch (result.termination().reason()) { + case TERMINATION_REASON_OPTIMAL: + case TERMINATION_REASON_FEASIBLE: + result.mutable_solve_stats()->set_best_primal_bound(obj_val(problem_)); + break; + case TERMINATION_REASON_UNBOUNDED: + // Here we can't use obj_val(problem_) as it would be a finite value of + // the feasible solution found. + result.mutable_solve_stats()->set_best_primal_bound(maximize ? +kInf + : -kInf); + break; + default: + result.mutable_solve_stats()->set_best_primal_bound(maximize ? -kInf + : kInf); break; } - result.mutable_solve_stats()->set_best_primal_bound(best_primal_bound); // TODO(b/187027049): compute the dual value when the dual is feasible (or // problem optimal for interior point) based on the bounds and the dual values // for LPs. @@ -1719,9 +1734,10 @@ std::optional GlpkSolver::EmptyIntegerBoundsResult() { return std::nullopt; } -absl::StatusOr GlpkSolver::InfeasibleSubsystem( - const SolveParametersProto& parameters, MessageCallback message_cb, - SolveInterrupter* const interrupter) { +absl::StatusOr +GlpkSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* const interrupter) { return absl::UnimplementedError( "GLPK does not provide a method to compute an infeasible subsystem"); } diff --git a/ortools/math_opt/solvers/glpk_solver.h b/ortools/math_opt/solvers/glpk_solver.h index 27c17157df8..457d4321277 100644 --- a/ortools/math_opt/solvers/glpk_solver.h +++ b/ortools/math_opt/solvers/glpk_solver.h @@ -57,9 +57,10 @@ 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; + absl::StatusOr + ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* interrupter) override; private: // The columns of the GPLK problem. diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index e8bca728cc8..1d507e17893 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -66,7 +66,6 @@ #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/validators/callback_validator.h" #include "ortools/port/proto_utils.h" -#include "scip/scip.h" #include "scip/type_cons.h" #include "scip/type_event.h" #include "scip/type_var.h" @@ -818,130 +817,116 @@ std::string JoinDetails(absl::string_view gscip_detail, return absl::StrCat(gscip_detail, "; ", math_opt_detail); } -ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status, - const bool has_feasible_solution, - const bool has_finite_dual_bound, - const bool was_cutoff) { - ProblemStatusProto problem_status; - if (has_feasible_solution) { - problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE); - } else { - problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); - } - problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); - - switch (gscip_status) { - case GScipOutput::OPTIMAL: - problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); - break; - case GScipOutput::INFEASIBLE: - if (!was_cutoff) { - problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE); - } - break; - case GScipOutput::UNBOUNDED: - problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE); - break; - case GScipOutput::INF_OR_UNBD: - problem_status.set_primal_or_dual_infeasible(true); - break; - default: - break; - } - if (has_finite_dual_bound) { - problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); - } - return problem_status; -} - absl::StatusOr ConvertTerminationReason( - const GScipOutput::Status gscip_status, - absl::string_view gscip_status_detail, const bool has_feasible_solution, - const bool had_cutoff) { + const bool is_maximize, const GScipOutput::Status gscip_status, + absl::string_view gscip_status_detail, const GScipSolvingStats& gscip_stats, + const bool has_feasible_solution, const bool had_cutoff) { + const std::optional optional_finite_primal_objective = + has_feasible_solution ? std::make_optional(gscip_stats.best_objective()) + : std::nullopt; + // For SCIP, the only indicator for the existence of a dual feasible solution + // is a finite dual bound. + const std::optional optional_dual_objective = + std::isfinite(gscip_stats.best_bound()) + ? std::make_optional(gscip_stats.best_bound()) + : std::nullopt; switch (gscip_status) { case GScipOutput::USER_INTERRUPT: - return TerminateForLimit( - LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_INTERRUPTED, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: USER_INTERRUPT")); case GScipOutput::NODE_LIMIT: - return TerminateForLimit( - LIMIT_NODE, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_NODE, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: NODE_LIMIT")); case GScipOutput::TOTAL_NODE_LIMIT: - return TerminateForLimit( - LIMIT_NODE, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_NODE, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: TOTAL_NODE_LIMIT")); case GScipOutput::STALL_NODE_LIMIT: - return TerminateForLimit(LIMIT_SLOW_PROGRESS, - /*feasible=*/has_feasible_solution, - gscip_status_detail); + return LimitTerminationProto( + is_maximize, LIMIT_SLOW_PROGRESS, optional_finite_primal_objective, + optional_dual_objective, gscip_status_detail); case GScipOutput::TIME_LIMIT: - return TerminateForLimit(LIMIT_TIME, /*feasible=*/has_feasible_solution, - gscip_status_detail); + return LimitTerminationProto( + is_maximize, LIMIT_TIME, optional_finite_primal_objective, + optional_dual_objective, gscip_status_detail); case GScipOutput::MEM_LIMIT: - return TerminateForLimit(LIMIT_MEMORY, /*feasible=*/has_feasible_solution, - gscip_status_detail); + return LimitTerminationProto( + is_maximize, LIMIT_MEMORY, optional_finite_primal_objective, + optional_dual_objective, gscip_status_detail); case GScipOutput::SOL_LIMIT: - return TerminateForLimit( - LIMIT_SOLUTION, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_SOLUTION, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: SOL_LIMIT")); case GScipOutput::BEST_SOL_LIMIT: - return TerminateForLimit( - LIMIT_SOLUTION, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_SOLUTION, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: BEST_SOL_LIMIT")); case GScipOutput::RESTART_LIMIT: - return TerminateForLimit( - LIMIT_OTHER, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_OTHER, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: RESTART_LIMIT")); case GScipOutput::OPTIMAL: - return TerminateForReason( - TERMINATION_REASON_OPTIMAL, + return OptimalTerminationProto( + /*finite_primal_objective=*/gscip_stats.best_objective(), + /*dual_objective=*/gscip_stats.best_bound(), JoinDetails(gscip_status_detail, "underlying gSCIP status: OPTIMAL")); case GScipOutput::GAP_LIMIT: - return TerminateForReason( - TERMINATION_REASON_OPTIMAL, + return OptimalTerminationProto( + /*finite_primal_objective=*/gscip_stats.best_objective(), + /*dual_objective=*/gscip_stats.best_bound(), JoinDetails(gscip_status_detail, "underlying gSCIP status: GAP_LIMIT")); case GScipOutput::INFEASIBLE: if (had_cutoff) { - return TerminateForLimit(LIMIT_CUTOFF, - /*feasible=*/false, gscip_status_detail); + return CutoffTerminationProto(is_maximize, gscip_status_detail); } else { - return TerminateForReason(TERMINATION_REASON_INFEASIBLE, - gscip_status_detail); + // By convention infeasible MIPs are always dual feasible. + const FeasibilityStatusProto dual_feasibility_status = + FEASIBILITY_STATUS_FEASIBLE; + return InfeasibleTerminationProto(is_maximize, dual_feasibility_status, + gscip_status_detail); } case GScipOutput::UNBOUNDED: { if (has_feasible_solution) { - return TerminateForReason( - TERMINATION_REASON_UNBOUNDED, + return UnboundedTerminationProto( + is_maximize, JoinDetails(gscip_status_detail, "underlying gSCIP status was UNBOUNDED, both primal " "ray and feasible solution are present")); } else { - return TerminateForReason( - TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE, JoinDetails( gscip_status_detail, "underlying gSCIP status was UNBOUNDED, but only primal ray " "was given, no feasible solution was found")); } } - case GScipOutput::INF_OR_UNBD: - return TerminateForReason( - TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, JoinDetails(gscip_status_detail, "underlying gSCIP status: INF_OR_UNBD")); - case GScipOutput::TERMINATE: - return TerminateForLimit( - LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution, + return LimitTerminationProto( + is_maximize, LIMIT_INTERRUPTED, optional_finite_primal_objective, + optional_dual_objective, JoinDetails(gscip_status_detail, "underlying gSCIP status: TERMINATE")); case GScipOutput::INVALID_SOLVER_PARAMETERS: @@ -1009,26 +994,13 @@ absl::StatusOr GScipSolver::CreateSolveResultProto( const bool has_feasible_solution = solve_result.solutions_size() > 0; ASSIGN_OR_RETURN( *solve_result.mutable_termination(), - ConvertTerminationReason(gscip_result.gscip_output.status(), + ConvertTerminationReason(is_maximize, gscip_result.gscip_output.status(), gscip_result.gscip_output.status_detail(), + gscip_result.gscip_output.stats(), /*has_feasible_solution=*/has_feasible_solution, /*had_cutoff=*/cutoff.has_value())); - *solve_result.mutable_solve_stats()->mutable_problem_status() = - GetProblemStatusProto( - gscip_result.gscip_output.status(), - /*has_feasible_solution=*/has_feasible_solution, - /*has_finite_dual_bound=*/ - std::isfinite(gscip_result.gscip_output.stats().best_bound()), - /*was_cutoff=*/solve_result.termination().limit() == LIMIT_CUTOFF); SolveStatsProto* const common_stats = solve_result.mutable_solve_stats(); const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats(); - common_stats->set_best_dual_bound(gscip_stats.best_bound()); - // If we found no solutions meeting the cutoff, we have no primal bound. - if (has_feasible_solution) { - common_stats->set_best_primal_bound(gscip_stats.best_objective()); - } else { - common_stats->set_best_primal_bound(is_maximize ? -kInf : kInf); - } common_stats->set_node_count(gscip_stats.node_count()); common_stats->set_simplex_iterations(gscip_stats.primal_simplex_iterations() + @@ -1043,7 +1015,7 @@ GScipSolver::GScipSolver(std::unique_ptr gscip) : gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) {} absl::StatusOr> GScipSolver::New( - const ModelProto& model, const InitArgs& init_args) { + const ModelProto& model, const InitArgs&) { RETURN_IF_ERROR(ModelIsSupported(model, kGscipSupportedStructures, "SCIP")); ASSIGN_OR_RETURN(std::unique_ptr gscip, GScip::Create(model.name())); RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize())); @@ -1433,9 +1405,10 @@ SCIP_RETCODE GScipSolver::InterruptEventHandler::TryCallInterruptIfNeeded( } } -absl::StatusOr GScipSolver::InfeasibleSubsystem( - const SolveParametersProto& parameters, MessageCallback message_cb, - SolveInterrupter* const interrupter) { +absl::StatusOr +GScipSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, + MessageCallback, + SolveInterrupter* const) { return absl::UnimplementedError( "SCIP does not provide a method to compute an infeasible subsystem"); } diff --git a/ortools/math_opt/solvers/gscip_solver.h b/ortools/math_opt/solvers/gscip_solver.h index 8bf04a02f74..c6c6a32ee10 100644 --- a/ortools/math_opt/solvers/gscip_solver.h +++ b/ortools/math_opt/solvers/gscip_solver.h @@ -58,9 +58,10 @@ 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; + absl::StatusOr + ComputeInfeasibleSubsystem(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/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index 6077cc98053..a01f368a922 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -580,55 +579,75 @@ GurobiSolver::SosConstraintData::DependentElements() const { absl::StatusOr GurobiSolver::ConvertTerminationReason( const int gurobi_status, const SolutionClaims solution_claims) { + ASSIGN_OR_RETURN(const double best_primal_bound, + GetBestPrimalBound( + /*has_primal_feasible_solution=*/solution_claims + .primal_feasible_solution_exists)); + + ASSIGN_OR_RETURN(double best_dual_bound, GetBestDualBound()); + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); switch (gurobi_status) { case GRB_OPTIMAL: - return TerminateForReason(TERMINATION_REASON_OPTIMAL); + // TODO(b/290359402): it appears Gurobi could return an infinite + // best_dual_bound (e.g in Qp/Qc/Socp/multi-obj tests). If so, we could + // improve the bound by using the target absolute and relative GAPs (and + // best_primal_bound). + return OptimalTerminationProto(best_primal_bound, best_dual_bound); case GRB_INFEASIBLE: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE); + return InfeasibleTerminationProto( + is_maximize, solution_claims.dual_feasible_solution_exists + ? FEASIBILITY_STATUS_FEASIBLE + : FEASIBILITY_STATUS_UNDETERMINED); case GRB_UNBOUNDED: + // GRB_UNBOUNDED does necessarily imply the primal is feasible + // https://www.gurobi.com/documentation/9.1/refman/optimization_status_codes.html if (solution_claims.primal_feasible_solution_exists) { - return TerminateForReason(TERMINATION_REASON_UNBOUNDED); + return UnboundedTerminationProto(is_maximize); } - return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, - "Gurobi status GRB_UNBOUNDED"); + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE, + "Gurobi status GRB_UNBOUNDED"); case GRB_INF_OR_UNBD: - return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, - "Gurobi status GRB_INF_OR_UNBD"); + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + "Gurobi status GRB_UNBOUNDED"); case GRB_CUTOFF: - return TerminateForLimit(LIMIT_CUTOFF, - /*feasible=*/false, "Gurobi status GRB_CUTOFF"); + return CutoffTerminationProto(is_maximize, "Gurobi status GRB_CUTOFF"); case GRB_ITERATION_LIMIT: - return TerminateForLimit( - LIMIT_ITERATION, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_ITERATION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_NODE_LIMIT: - return TerminateForLimit( - LIMIT_NODE, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_NODE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_TIME_LIMIT: - return TerminateForLimit( - LIMIT_TIME, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_TIME, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_SOLUTION_LIMIT: - return TerminateForLimit( - LIMIT_SOLUTION, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_SOLUTION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_INTERRUPTED: - return TerminateForLimit( - LIMIT_INTERRUPTED, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_INTERRUPTED, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_NUMERIC: - return TerminateForReason(TERMINATION_REASON_NUMERICAL_ERROR); + return TerminateForReason(is_maximize, + TERMINATION_REASON_NUMERICAL_ERROR); case GRB_SUBOPTIMAL: - return TerminateForReason(TERMINATION_REASON_IMPRECISE); + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); case GRB_USER_OBJ_LIMIT: - // TODO(b/214567536): maybe we should override + // Note: maybe we should override // solution_claims.primal_feasible_solution_exists to true or false // depending on whether objective_limit and best_bound_limit triggered // this. Not sure if it's possible to detect this though. - return TerminateForLimit( - LIMIT_OBJECTIVE, - /*feasible=*/solution_claims.primal_feasible_solution_exists); + return LimitTerminationProto( + LIMIT_OBJECTIVE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); case GRB_LOADED: return absl::InternalError( "Error creating termination reason, unexpected gurobi status code " @@ -852,59 +871,6 @@ absl::StatusOr GurobiSolver::GetGurobiDualRay( return dual_ray; } -absl::StatusOr GurobiSolver::GetProblemStatus( - const int grb_termination, const SolutionClaims solution_claims) { - ProblemStatusProto problem_status; - - // Set default statuses - problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); - problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); - - // Set feasibility statuses - if (solution_claims.primal_feasible_solution_exists) { - problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE); - } - if (solution_claims.dual_feasible_solution_exists) { - problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); - } - - // Process infeasible conclusions from grb_termination. - switch (grb_termination) { - case GRB_INFEASIBLE: - problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE); - if (solution_claims.primal_feasible_solution_exists) { - return absl::InternalError( - "GRB_INT_ATTR_STATUS == GRB_INFEASIBLE, but a primal feasible " - "solution was returned."); - } - break; - case GRB_UNBOUNDED: - // GRB_UNBOUNDED does necessarily imply the primal is feasible - // https://www.gurobi.com/documentation/9.1/refman/optimization_status_codes.html - problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE); - if (solution_claims.dual_feasible_solution_exists) { - return absl::InternalError( - "GRB_INT_ATTR_STATUS == GRB_UNBOUNDED, but a dual feasible " - "solution was returned or exists."); - } - break; - case GRB_INF_OR_UNBD: - problem_status.set_primal_or_dual_infeasible(true); - if (solution_claims.primal_feasible_solution_exists) { - return absl::InternalError( - "GRB_INT_ATTR_STATUS == GRB_INF_OR_UNBD, but a primal feasible " - "solution was returned."); - } - if (solution_claims.dual_feasible_solution_exists) { - return absl::InternalError( - "GRB_INT_ATTR_STATUS == GRB_INF_OR_UNBD, but a dual feasible " - "solution was returned or exists."); - } - break; - } - return problem_status; -} - absl::StatusOr GurobiSolver::ExtractSolveResultProto( const absl::Time start, const ModelSolveParametersProto& model_parameters) { SolveResultProto result; @@ -933,11 +899,10 @@ absl::StatusOr GurobiSolver::ExtractSolveResultProto( } } - ASSIGN_OR_RETURN(*result.mutable_solve_stats(), - GetSolveStats(start, solution_claims)); - ASSIGN_OR_RETURN(*result.mutable_termination(), ConvertTerminationReason(grb_termination, solution_claims)); + + ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start)); return std::move(result); } @@ -1033,10 +998,10 @@ GurobiSolver::QuadraticConstraintInIIS( return result; } -absl::StatusOr -GurobiSolver::ExtractInfeasibleSubsystemResultProto( +absl::StatusOr +GurobiSolver::ExtractComputeInfeasibleSubsystemResultProto( const bool proven_infeasible) { - InfeasibleSubsystemResultProto result; + ComputeInfeasibleSubsystemResultProto result; if (!proven_infeasible) { result.set_feasibility(FEASIBILITY_STATUS_UNDETERMINED); return result; @@ -1133,26 +1098,12 @@ absl::StatusOr GurobiSolver::GetSolutions( } absl::StatusOr GurobiSolver::GetSolveStats( - const absl::Time start, const SolutionClaims solution_claims) { + const absl::Time start) const { SolveStatsProto solve_stats; CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start, solve_stats.mutable_solve_time())); - ASSIGN_OR_RETURN(const double best_primal_bound, - GetBestPrimalBound( - /*has_primal_feasible_solution=*/solution_claims - .primal_feasible_solution_exists)); - solve_stats.set_best_primal_bound(best_primal_bound); - - ASSIGN_OR_RETURN(double best_dual_bound, GetBestDualBound()); - solve_stats.set_best_dual_bound(best_dual_bound); - - ASSIGN_OR_RETURN(const int grb_termination, - gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); - ASSIGN_OR_RETURN((*solve_stats.mutable_problem_status()), - GetProblemStatus(grb_termination, solution_claims)); - if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_ITERCOUNT)) { ASSIGN_OR_RETURN(const double simplex_iters_double, gurobi_->GetDoubleAttr(GRB_DBL_ATTR_ITERCOUNT)); @@ -1229,10 +1180,11 @@ absl::StatusOr GurobiSolver::GetMipSolutions( // // If this is a multi-objective model, Gurobi v10 does not expose ObjBound. // Instead, we fake its existence for optimal solves only. + // By convention infeasible MIPs are always dual feasible. const SolutionClaims solution_claims = { .primal_feasible_solution_exists = num_solutions > 0, .dual_feasible_solution_exists = - std::isfinite(best_dual_bound) || + std::isfinite(best_dual_bound) || grb_termination == GRB_INFEASIBLE || (is_multi_objective_mode() && grb_termination == GRB_OPTIMAL)}; // Check consistency of solutions, bounds and statuses. @@ -1351,7 +1303,7 @@ absl::StatusOr GurobiSolver::GetBestPrimalBound( // primal infeasible solutions. if (has_primal_feasible_solution && gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL)) { - // TODO(b/195295177): Discuss if this should be removed. Unlike the dual + // TODO(b/290359402): Discuss if this should be removed. Unlike the dual // case below, it appears infesible models do not return GRB_DBL_ATTR_OBJVAL // equal to GRB_INFINITY (GRB_DBL_ATTR_OBJVAL is just unavailable). Hence, // this may not be needed and may not be consistent (e.g. we should explore @@ -1475,7 +1427,7 @@ GurobiSolver::GetLpDualSolutionIfAvailable( gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); dual_solution.set_objective_value(obj_val); } - // TODO(b/195295177): explore using GRB_DBL_ATTR_OBJBOUND to set the dual + // TODO(b/290359402): explore using GRB_DBL_ATTR_OBJBOUND to set the dual // objective. As described in go/gurobi-objval-bug, this could provide the // dual objective in some cases. @@ -1486,7 +1438,7 @@ GurobiSolver::GetLpDualSolutionIfAvailable( } else if (grb_termination == GRB_UNBOUNDED) { dual_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); } - // TODO(b/195295177): We could use gurobi's dual solution quality measures + // TODO(b/290359402): We could use gurobi's dual solution quality measures // for further upgrade the dual feasibility but it likely is only useful // for phase II of dual simplex because: // * the quality measures seem to evaluate if the basis is dual feasible @@ -2972,10 +2924,10 @@ absl::StatusOr GurobiSolver::Solve( } // TODO(b/277339044): Remove code duplication with GurobiSolver::Solve(). -absl::StatusOr -GurobiSolver::InfeasibleSubsystem(const SolveParametersProto& parameters, - MessageCallback message_cb, - SolveInterrupter* const interrupter) { +absl::StatusOr +GurobiSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* const interrupter) { const absl::Time start = absl::Now(); // Need to run GRBupdatemodel before: @@ -3051,8 +3003,9 @@ GurobiSolver::InfeasibleSubsystem(const SolveParametersProto& parameters, gurobi_cb_data->message_callback_data); } - ASSIGN_OR_RETURN(InfeasibleSubsystemResultProto iis_result, - ExtractInfeasibleSubsystemResultProto(proven_infeasible)); + ASSIGN_OR_RETURN( + ComputeInfeasibleSubsystemResultProto iis_result, + ExtractComputeInfeasibleSubsystemResultProto(proven_infeasible)); // Reset Gurobi parameters. // TODO(b/277246682): ensure that resetting parameters does not degrade // incrementalism performance. diff --git a/ortools/math_opt/solvers/gurobi_solver.h b/ortools/math_opt/solvers/gurobi_solver.h index e234a3b5af2..af03c4806d4 100644 --- a/ortools/math_opt/solvers/gurobi_solver.h +++ b/ortools/math_opt/solvers/gurobi_solver.h @@ -62,9 +62,10 @@ 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; + absl::StatusOr + ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, + MessageCallback message_cb, + SolveInterrupter* interrupter) override; private: struct GurobiCallbackData { @@ -189,8 +190,6 @@ class GurobiSolver : public SolverInterface { using IdHashMap = gtl::linked_hash_map; - absl::StatusOr GetProblemStatus( - int grb_termination, SolutionClaims solution_claims); absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters); absl::Status FillRays(const ModelSolveParametersProto& model_parameters, @@ -198,8 +197,7 @@ class GurobiSolver : public SolverInterface { SolveResultProto& result); absl::StatusOr GetSolutions( const ModelSolveParametersProto& model_parameters); - absl::StatusOr GetSolveStats(absl::Time start, - SolutionClaims solution_claims); + absl::StatusOr GetSolveStats(absl::Time start) const; absl::StatusOr GetBestDualBound(); absl::StatusOr GetBestPrimalBound(bool has_primal_feasible_solution); @@ -221,13 +219,13 @@ class GurobiSolver : public SolverInterface { // not. This occurs because Gurobi does not return variable integrality IIS // attributes, and because internally we apply model transformations to handle // ranged constraints and linear expressions in nonlinear constraints. - absl::StatusOr - ExtractInfeasibleSubsystemResultProto(bool proven_infeasible); + absl::StatusOr + ExtractComputeInfeasibleSubsystemResultProto(bool proven_infeasible); // Warning: is read from gurobi, take care with gurobi update. absl::StatusOr IsMaximize() const; - static absl::StatusOr ConvertTerminationReason( + absl::StatusOr ConvertTerminationReason( int gurobi_status, SolutionClaims solution_claims); // Returns solution information appropriate and available for an LP (linear diff --git a/ortools/math_opt/storage/BUILD.bazel b/ortools/math_opt/storage/BUILD.bazel index dbee63fed7e..bd639096985 100644 --- a/ortools/math_opt/storage/BUILD.bazel +++ b/ortools/math_opt/storage/BUILD.bazel @@ -181,6 +181,7 @@ cc_library( "//ortools/math_opt/constraints/sos:storage", "//ortools/math_opt/core:model_summary", "//ortools/math_opt/core:sparse_vector_view", + "//ortools/math_opt/io:names_removal", "//ortools/math_opt/validators:model_validator", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", diff --git a/ortools/math_opt/storage/model_storage.cc b/ortools/math_opt/storage/model_storage.cc index 30dc2cfc758..148a563e0d2 100644 --- a/ortools/math_opt/storage/model_storage.cc +++ b/ortools/math_opt/storage/model_storage.cc @@ -32,6 +32,7 @@ #include "ortools/base/strong_int.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/io/names_removal.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" @@ -278,7 +279,7 @@ void ModelStorage::AddAuxiliaryObjectives( } } -ModelProto ModelStorage::ExportModel() const { +ModelProto ModelStorage::ExportModel(const bool remove_names) const { ModelProto result; result.set_name(name_); *result.mutable_variables() = variables_.Proto(); @@ -297,12 +298,18 @@ ModelProto ModelStorage::ExportModel() const { *result.mutable_sos1_constraints() = sos1_constraints_.Proto(); *result.mutable_sos2_constraints() = sos2_constraints_.Proto(); *result.mutable_indicator_constraints() = indicator_constraints_.Proto(); + // Performance can be improved when remove_names is true by just not + // extracting the names above instead of clearing them below, but this will + // be more code, see discussion on cl/549469633 and prototype in cl/549369764. + if (remove_names) { + RemoveNames(result); + } return result; } std::optional ModelStorage::UpdateTrackerData::ExportModelUpdate( - const ModelStorage& storage) const { + const ModelStorage& storage, const bool remove_names) const { // We must detect the empty case to prevent unneeded copies and merging in // ExportModelUpdate(). @@ -372,6 +379,9 @@ ModelStorage::UpdateTrackerData::ExportModelUpdate( *result.mutable_objective_updates() = std::move(primary); *result.mutable_auxiliary_objectives_updates() = std::move(auxiliary); } + if (remove_names) { + RemoveNames(result); + } // Note: Named returned value optimization (NRVO) does not apply here. return {std::move(result)}; } @@ -404,8 +414,9 @@ void ModelStorage::DeleteUpdateTracker(const UpdateTrackerId update_tracker) { } std::optional ModelStorage::ExportModelUpdate( - const UpdateTrackerId update_tracker) const { - return update_trackers_.GetData(update_tracker).ExportModelUpdate(*this); + const UpdateTrackerId update_tracker, const bool remove_names) const { + return update_trackers_.GetData(update_tracker) + .ExportModelUpdate(*this, remove_names); } void ModelStorage::AdvanceCheckpoint(UpdateTrackerId update_tracker) { diff --git a/ortools/math_opt/storage/model_storage.h b/ortools/math_opt/storage/model_storage.h index c8df9433c7b..965b68271fa 100644 --- a/ortools/math_opt/storage/model_storage.h +++ b/ortools/math_opt/storage/model_storage.h @@ -537,7 +537,7 @@ class ModelStorage { // Returns a proto representation of the optimization model. // // See FromModelProto() to build a ModelStorage from a proto. - ModelProto ExportModel() const; + ModelProto ExportModel(bool remove_names = false) const; // Creates a tracker that can be used to generate a ModelUpdateProto with the // updates that happened since the last checkpoint. The tracker initial @@ -611,7 +611,7 @@ class ModelStorage { // // It can be called concurrently for different update trackers though. std::optional ExportModelUpdate( - UpdateTrackerId update_tracker) const; + UpdateTrackerId update_tracker, bool remove_names = false) const; // Uses the current model state as the starting point to calculate the // ModelUpdateProto next time ExportModelUpdate() is called. @@ -662,7 +662,7 @@ class ModelStorage { // // Thread-safety: this method is threadsafe. std::optional ExportModelUpdate( - const ModelStorage& storage) const; + const ModelStorage& storage, bool remove_names = false) const; // Use the current model state as the starting point to calculate the // ModelUpdateProto next time ExportSharedModelUpdate() is called. diff --git a/ortools/math_opt/tools/BUILD.bazel b/ortools/math_opt/tools/BUILD.bazel index 8175237a9b8..e2306a813b4 100644 --- a/ortools/math_opt/tools/BUILD.bazel +++ b/ortools/math_opt/tools/BUILD.bazel @@ -24,7 +24,6 @@ cc_binary( "//ortools/math_opt/core:solver_interface", "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/cpp:statistics", - "//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", diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index e45b42976ee..2bd5935d3da 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -195,8 +195,8 @@ cc_library( ":ids_validator", ":solution_validator", ":sparse_vector_validator", - "//ortools/base", "//ortools/base:status_macros", + "//ortools/base:types", "//ortools/math_opt:model_parameters_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", "//ortools/math_opt/core:model_summary", diff --git a/ortools/math_opt/validators/infeasible_subsystem_validator.cc b/ortools/math_opt/validators/infeasible_subsystem_validator.cc index 056ef440751..858d90a91c6 100644 --- a/ortools/math_opt/validators/infeasible_subsystem_validator.cc +++ b/ortools/math_opt/validators/infeasible_subsystem_validator.cc @@ -78,10 +78,10 @@ absl::Status ValidateModelSubset(const ModelSubsetProto& model_subset, return absl::OkStatus(); } -absl::Status ValidateInfeasibleSubsystemResult( - const InfeasibleSubsystemResultProto& result, +absl::Status ValidateComputeInfeasibleSubsystemResult( + const ComputeInfeasibleSubsystemResultProto& result, const ModelSummary& model_summary) { - RETURN_IF_ERROR(ValidateInfeasibleSubsystemResultNoModel(result)); + RETURN_IF_ERROR(ValidateComputeInfeasibleSubsystemResultNoModel(result)); if (result.feasibility() == FEASIBILITY_STATUS_INFEASIBLE) { RETURN_IF_ERROR( ValidateModelSubset(result.infeasible_subsystem(), model_summary)); @@ -89,10 +89,10 @@ absl::Status ValidateInfeasibleSubsystemResult( return absl::OkStatus(); } -absl::Status ValidateInfeasibleSubsystemResultNoModel( - const InfeasibleSubsystemResultProto& result) { +absl::Status ValidateComputeInfeasibleSubsystemResultNoModel( + const ComputeInfeasibleSubsystemResultProto& result) { RETURN_IF_ERROR(ValidateFeasibilityStatus(result.feasibility())) - << "bad InfeasibleSubsystemResultProto.feasibility"; + << "bad ComputeInfeasibleSubsystemResultProto.feasibility"; if (result.feasibility() != FEASIBILITY_STATUS_INFEASIBLE) { // Check that the `infeasible_subsystem` is empty by validating against an // empty ModelSummary. diff --git a/ortools/math_opt/validators/infeasible_subsystem_validator.h b/ortools/math_opt/validators/infeasible_subsystem_validator.h index adf759fb709..02022e90762 100644 --- a/ortools/math_opt/validators/infeasible_subsystem_validator.h +++ b/ortools/math_opt/validators/infeasible_subsystem_validator.h @@ -23,11 +23,12 @@ namespace operations_research::math_opt { absl::Status ValidateModelSubset(const ModelSubsetProto& model_subset, const ModelSummary& summary); -absl::Status ValidateInfeasibleSubsystemResult( - const InfeasibleSubsystemResultProto& result, const ModelSummary& summary); +absl::Status ValidateComputeInfeasibleSubsystemResult( + const ComputeInfeasibleSubsystemResultProto& result, + const ModelSummary& summary); // Validates the internal consistency of the fields. -absl::Status ValidateInfeasibleSubsystemResultNoModel( - const InfeasibleSubsystemResultProto& result); +absl::Status ValidateComputeInfeasibleSubsystemResultNoModel( + const ComputeInfeasibleSubsystemResultProto& result); } // namespace operations_research::math_opt diff --git a/ortools/math_opt/validators/model_parameters_validator.cc b/ortools/math_opt/validators/model_parameters_validator.cc index 52ff0780075..d364c82b176 100644 --- a/ortools/math_opt/validators/model_parameters_validator.cc +++ b/ortools/math_opt/validators/model_parameters_validator.cc @@ -14,8 +14,8 @@ #include "ortools/math_opt/validators/model_parameters_validator.h" #include "absl/status/status.h" -#include "ortools/base/types.h" #include "ortools/base/status_macros.h" +#include "ortools/base/types.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/model_parameters.pb.h"