From 68e56e8456866fdcc518d14d11e0cc3e13985754 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 07:05:11 -0700 Subject: [PATCH 001/491] fix --- ortools/flatzinc/cp_model_fz_solver.cc | 14 +++++++------- ortools/flatzinc/parser_main.cc | 11 ++++------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/ortools/flatzinc/cp_model_fz_solver.cc b/ortools/flatzinc/cp_model_fz_solver.cc index cd4a25fb468..3ea0cc58dc7 100644 --- a/ortools/flatzinc/cp_model_fz_solver.cc +++ b/ortools/flatzinc/cp_model_fz_solver.cc @@ -413,22 +413,22 @@ void CpModelProtoWithMapping::AddTermToLinearConstraint( // We need to update the domain of the constraint. for (int i = 0; i < ct->domain_size(); ++i) { const int64_t b = ct->domain(i); - // We account for infinity like bounds being INT_MAX, INT_MIN, and - // -INT_MAX. - if (b <= -std::numeric_limits::max() || + if (b == std::numeric_limits::min() || b == std::numeric_limits::max()) { continue; } - ct->set_domain(i, ct->domain(i) - coeff); + ct->set_domain(i, b - coeff); } } } int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqValue(int var, int64_t value) { + CHECK_GE(var, 0); const std::pair key = {var, value}; const auto it = var_eq_value_to_literal.find(key); if (it != var_eq_value_to_literal.end()) return it->second; + const IntegerVariableProto& var_proto = proto.variables(var); if (var_proto.domain_size() == 2 && var_proto.domain(0) == var_proto.domain(1)) { @@ -439,8 +439,8 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqValue(int var, if (var_proto.domain_size() == 2 && var_proto.domain(0) == 0 && var_proto.domain(1) == 1) { - var_eq_value_to_literal[std::make_pair(var, 0)] = NegatedRef(var); - var_eq_value_to_literal[std::make_pair(var, 1)] = var; + var_eq_value_to_literal[{var, 0}] = NegatedRef(var); + var_eq_value_to_literal[{var, 1}] = var; return value == 1 ? var : NegatedRef(var); } @@ -869,7 +869,6 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, fz_ct.arguments[1].values.end()}) .Complement(), arg); - AddTermToLinearConstraint(LookupVar(fz_ct.arguments[0]), 1, arg); } else if (fz_ct.arguments[1].type == fz::Argument::INT_INTERVAL) { FillDomainInProto( Domain(fz_ct.arguments[1].values[0], fz_ct.arguments[1].values[1]) @@ -878,6 +877,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, } else { LOG(FATAL) << "Wrong format"; } + AddTermToLinearConstraint(LookupVar(fz_ct.arguments[0]), 1, arg); } else if (fz_ct.type == "int_min") { auto* arg = ct->mutable_lin_max(); *arg->add_exprs() = LookupExpr(fz_ct.arguments[0], /*negate=*/true); diff --git a/ortools/flatzinc/parser_main.cc b/ortools/flatzinc/parser_main.cc index eb479748b6b..47dd1a68484 100644 --- a/ortools/flatzinc/parser_main.cc +++ b/ortools/flatzinc/parser_main.cc @@ -18,12 +18,10 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" -#include "absl/flags/parse.h" -#include "absl/flags/usage.h" #include "absl/log/check.h" -#include "absl/log/flags.h" -#include "absl/log/initialize.h" +#include "absl/log/globals.h" #include "absl/strings/match.h" #include "ortools/base/init_google.h" #include "ortools/base/timer.h" @@ -77,9 +75,8 @@ int main(int argc, char** argv) { const char kUsage[] = "Parses a flatzinc .fzn file, optionally presolve it, and prints it in " "human-readable format"; - absl::SetProgramUsageMessage(kUsage); - absl::ParseCommandLine(argc, argv); - absl::InitializeLog(); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + InitGoogle(kUsage, &argc, &argv, /*remove_flags=*/true); operations_research::fz::ParseFile(absl::GetFlag(FLAGS_input)); return 0; } From 144f782727281925b75a26b7540320692bdfb5d1 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 13:02:40 -0700 Subject: [PATCH 002/491] remove num_search_workers from example --- examples/python/task_allocation_sat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/python/task_allocation_sat.py b/examples/python/task_allocation_sat.py index c35b9c6e20e..381ebe2cf1c 100644 --- a/examples/python/task_allocation_sat.py +++ b/examples/python/task_allocation_sat.py @@ -284,7 +284,7 @@ def task_allocation_sat() -> None: solver = cp_model.CpSolver() # Uses the portfolion of heuristics. solver.parameters.log_search_progress = True - solver.parameters.num_search_workers = 16 + solver.parameters.num_workers = 16 solver.solve(model) From f479af4254b98da28aa4fab55b711b910f664ccc Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 13:02:52 -0700 Subject: [PATCH 003/491] fix port/os.h --- ortools/port/BUILD.bazel | 5 +++++ ortools/port/os.h | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 ortools/port/os.h diff --git a/ortools/port/BUILD.bazel b/ortools/port/BUILD.bazel index 6c5639dee5a..5a619731a98 100644 --- a/ortools/port/BUILD.bazel +++ b/ortools/port/BUILD.bazel @@ -15,6 +15,11 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) +cc_library( + name = "os", + hdrs = ["os.h"], +) + cc_library( name = "sysinfo", srcs = ["sysinfo.cc"], diff --git a/ortools/port/os.h b/ortools/port/os.h new file mode 100644 index 00000000000..a4827a4bc2f --- /dev/null +++ b/ortools/port/os.h @@ -0,0 +1,42 @@ +// Copyright 2010-2025 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. + +// Defines macros about target OS and whether it supports threads. +#ifndef OR_TOOLS_PORT_OS_H_ +#define OR_TOOLS_PORT_OS_H_ + +#if defined(__ANDROID__) +#define ORTOOLS_TARGET_OS_IS_ANDROID +#endif + +#if (defined(__apple__) || defined(__APPLE__) || defined(__MACH__)) +// From https://stackoverflow.com/a/49560690 +#include "TargetConditionals.h" +#if TARGET_OS_IPHONE == 1 +#define ORTOOLS_TARGET_OS_IS_IOS +#endif +#endif + +#if defined(__EMSCRIPTEN__) +#define ORTOOLS_TARGET_OS_IS_EMSCRIPTEN +#endif + +#if defined(ORTOOLS_TARGET_OS_IS_ANDROID) || \ + defined(ORTOOLS_TARGET_OS_IS_IOS) || \ + defined(ORTOOLS_TARGET_OS_IS_EMSCRIPTEN) +#define ORTOOLS_TARGET_OS_SUPPORTS_THREADS 0 +#else +#define ORTOOLS_TARGET_OS_SUPPORTS_THREADS 1 +#endif + +#endif // OR_TOOLS_PORT_OS_H_ From 223796dc65e867f00b42520b64ea2ccd867142d6 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 13:04:07 -0700 Subject: [PATCH 004/491] fix #4759 --- ortools/sat/python/cp_model.py | 1 - ortools/sat/python/cp_model_helper.cc | 9 +++++++++ ortools/sat/python/cp_model_test.py | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ortools/sat/python/cp_model.py b/ortools/sat/python/cp_model.py index 035ef78f22b..3eefdfbdbde 100644 --- a/ortools/sat/python/cp_model.py +++ b/ortools/sat/python/cp_model.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/sat/python/cp_model_helper.cc b/ortools/sat/python/cp_model_helper.cc index 6675d7486e2..1734bd40082 100644 --- a/ortools/sat/python/cp_model_helper.cc +++ b/ortools/sat/python/cp_model_helper.cc @@ -1544,6 +1544,15 @@ PYBIND11_MODULE(cp_model_helper, m) { }, py::arg("other").none(false), DOC(operations_research, sat, python, LinearExpr, AddFloat)) + .def( + "__radd__", + [](std::shared_ptr expr, + std::shared_ptr other) -> std::shared_ptr { + const int num_uses = Py_REFCNT(py::cast(expr).ptr()); + return (num_uses == 4) ? expr->AddInPlace(other) : expr->Add(other); + }, + py::arg("other").none(false), + DOC(operations_research, sat, python, LinearExpr, Add)) .def( "__radd__", [](std::shared_ptr expr, diff --git a/ortools/sat/python/cp_model_test.py b/ortools/sat/python/cp_model_test.py index 92a807759c6..382fdb426c0 100644 --- a/ortools/sat/python/cp_model_test.py +++ b/ortools/sat/python/cp_model_test.py @@ -2691,6 +2691,11 @@ def test_simplification10(self): self.assertEqual(-2, prod.coefficient) self.assertEqual(2, prod.offset) + def test_issue4759(self): + model = cp_model.CpModel() + a = model.new_bool_var("a") + self.assertNotEmpty(str(0 * a + sum([1 * a, 2 * a]))) + def test_pre_pep8(self): model = cp_model.CpModel() x = [model.NewBoolVar(f"x{i}") for i in range(5)] From d8821be2f9290f4980e4fe4597e41d50b87a88dd Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 13:05:03 -0700 Subject: [PATCH 005/491] remove python shebangs --- ortools/sat/colab/flags.py | 1 - ortools/sat/colab/visualization.py | 1 - 2 files changed, 2 deletions(-) diff --git a/ortools/sat/colab/flags.py b/ortools/sat/colab/flags.py index 48d2cebe176..7057db3684a 100644 --- a/ortools/sat/colab/flags.py +++ b/ortools/sat/colab/flags.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/sat/colab/visualization.py b/ortools/sat/colab/visualization.py index 10bcb82bc99..725042fb273 100644 --- a/ortools/sat/colab/visualization.py +++ b/ortools/sat/colab/visualization.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 6e872e95b7fa688f757b733b2386477bff5aa866 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 13:05:30 -0700 Subject: [PATCH 006/491] [CP-SAT] change OS detection and usage --- ortools/sat/BUILD.bazel | 2 ++ ortools/sat/cp_model_solver.cc | 23 +++++++++++------------ ortools/sat/cp_model_solver_test.cc | 7 +++++-- ortools/sat/drat_proof_handler.h | 4 ++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index bc5c0039e84..6bdd9ffd2fe 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -791,6 +791,7 @@ cc_library( "//ortools/base:types", "//ortools/graph:connected_components", "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/port:os", "//ortools/port:proto_utils", "//ortools/util:logging", "//ortools/util:random_engine", @@ -836,6 +837,7 @@ cc_test( "//ortools/base:gmock_main", "//ortools/base:parse_test_proto", "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/port:os", "//ortools/util:logging", "@abseil-cpp//absl/log", "@abseil-cpp//absl/strings", diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 38db8b87a54..7f263e2f3c4 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -53,6 +53,8 @@ #include "ortools/base/logging.h" #include "ortools/base/options.h" #include "ortools/base/timer.h" +#include "ortools/base/version.h" +#include "ortools/port/os.h" #include "ortools/port/proto_utils.h" #include "ortools/sat/combine_solutions.h" #include "ortools/sat/cp_model.pb.h" @@ -91,10 +93,7 @@ #include "ortools/sat/work_assignment.h" #include "ortools/util/logging.h" #include "ortools/util/random_engine.h" -#if !defined(__EMBEDDED_PLATFORM__) #include "ortools/util/sigint.h" -#endif // __EMBEDDED_PLATFORM__ -#include "ortools/base/version.h" #include "ortools/util/sorted_interval_list.h" #include "ortools/util/time_limit.h" @@ -1185,7 +1184,7 @@ class FullProblemSolver : public SubSolver { bool previous_task_is_completed_ ABSL_GUARDED_BY(mutex_) = true; }; -#if !defined(__EMBEDDED_PLATFORM__) +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS class FeasibilityPumpSolver : public SubSolver { public: @@ -2191,7 +2190,7 @@ void SolveCpModelParallel(SharedClasses* shared, Model* global_model) { LaunchSubsolvers(params, shared, subsolvers, name_filter.AllIgnored()); } -#endif // !defined(__EMBEDDED_PLATFORM__) +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS // If the option use_sat_inprocessing is true, then before post-solving a // solution, we need to make sure we add any new clause required for postsolving @@ -2439,13 +2438,13 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { // Initialize the time limit from the parameters. model->GetOrCreate()->ResetLimitFromParameters(params); -#if !defined(__EMBEDDED_PLATFORM__) +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS // Register SIGINT handler if requested by the parameters. if (params.catch_sigint_signal()) { model->GetOrCreate()->Register( [shared_time_limit]() { shared_time_limit->Stop(); }); } -#endif // __EMBEDDED_PLATFORM__ +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS SOLVER_LOG(logger, ""); SOLVER_LOG(logger, "Starting ", CpSatSolverVersion()); @@ -2940,15 +2939,15 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { LoadDebugSolution(*new_cp_model_proto, model); if (!model->GetOrCreate()->LimitReached()) { -#if defined(__EMBEDDED_PLATFORM__) - if (/* DISABLES CODE */ (false)) { - // We ignore the multithreading parameter in this case. -#else // __EMBEDDED_PLATFORM__ +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS if (params.num_workers() > 1 || params.interleave_search() || !params.subsolvers().empty() || !params.filter_subsolvers().empty() || params.use_ls_only()) { SolveCpModelParallel(&shared, model); -#endif // __EMBEDDED_PLATFORM__ +#else // ORTOOLS_TARGET_OS_SUPPORTS_THREADS + if (/* DISABLES CODE */ (false)) { + // We ignore the multithreading parameter in this case. +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS } else { shared_response_manager->SetUpdateGapIntegralOnEachChange(true); diff --git a/ortools/sat/cp_model_solver_test.cc b/ortools/sat/cp_model_solver_test.cc index a1ba69d2f11..6a978b070db 100644 --- a/ortools/sat/cp_model_solver_test.cc +++ b/ortools/sat/cp_model_solver_test.cc @@ -23,6 +23,7 @@ #include "ortools/base/gmock.h" #include "ortools/base/parse_test_proto.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/port/os.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_checker.h" #include "ortools/sat/cp_model_test_utils.h" @@ -330,7 +331,8 @@ TEST(SolveCpModelTest, TrivialModelWithCore) { response.solution().end()))); } -#if !defined(__EMBEDDED_PLATFORM__) +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS + TEST(SolveCpModelTest, TrivialLinearTranslatedModel) { const CpModelProto model_proto = ParseTestProto(R"pb( variables { domain: -10 domain: 10 } @@ -4956,7 +4958,8 @@ TEST(PresolveCpModelTest, CumulativeBug4) { response = SolveWithParameters(cp_model, params); EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); } -#endif // !defined(__EMBEDDED_PLATFORM__) + +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS } // namespace } // namespace sat diff --git a/ortools/sat/drat_proof_handler.h b/ortools/sat/drat_proof_handler.h index 65d32e3239a..f2b28fdbd53 100644 --- a/ortools/sat/drat_proof_handler.h +++ b/ortools/sat/drat_proof_handler.h @@ -51,11 +51,11 @@ class DratProofHandler { ~DratProofHandler() = default; // During the presolve step, variable get deleted and the set of non-deleted - // variable is remaped in a dense set. This allows to keep track of that and + // variable is remapped in a dense set. This allows to keep track of that and // always output the DRAT clauses in term of the original variables. Must be // called before adding or deleting clauses AddClause() or DeleteClause(). // - // TODO(user): This is exactly the same mecanism as in the SatPostsolver + // TODO(user): This is exactly the same mechanism as in the SatPostsolver // class. Factor out the code. void ApplyMapping(const util_intops::StrongVector& mapping); From a0a2ae1e0305548f78589c0c0561ff3eb83e1a81 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 26 Aug 2025 23:09:33 -0700 Subject: [PATCH 007/491] [FZ] supports lex_less{eq} natively --- ortools/flatzinc/checker.cc | 30 ++-- ortools/flatzinc/cp_model_fz_solver.cc | 132 ++++++++++++++++++ ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn | 14 ++ ortools/flatzinc/mznlib/fzn_lex_less_int.mzn | 14 ++ .../flatzinc/mznlib/fzn_lex_lesseq_bool.mzn | 14 ++ .../flatzinc/mznlib/fzn_lex_lesseq_int.mzn | 14 ++ ortools/flatzinc/mznlib/fzn_nvalue_reif.mzn | 2 +- 7 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_lex_less_int.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn diff --git a/ortools/flatzinc/checker.cc b/ortools/flatzinc/checker.cc index 7e8d529f438..f23a8838d11 100644 --- a/ortools/flatzinc/checker.cc +++ b/ortools/flatzinc/checker.cc @@ -1132,11 +1132,12 @@ bool CheckOrtoolsInverse( return true; } -bool CheckLexLessInt( +bool CheckOrtoolsLexLessInt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { - CHECK_EQ(Length(ct.arguments[0]), Length(ct.arguments[1])); - for (int i = 0; i < Length(ct.arguments[0]); ++i) { + const int min_size = + std::min(Length(ct.arguments[0]), Length(ct.arguments[1])); + for (int i = 0; i < min_size; ++i) { const int64_t x = EvalAt(ct.arguments[0], i, evaluator); const int64_t y = EvalAt(ct.arguments[1], i, evaluator); if (x < y) { @@ -1146,15 +1147,16 @@ bool CheckLexLessInt( return false; } } - // We are at the end of the list. The two chains are equals. - return false; + // We are at the end of the common list. We compare the lengths of the lists. + return Length(ct.arguments[1]) > Length(ct.arguments[0]); } -bool CheckLexLesseqInt( +bool CheckOrtoolsLexLesseqInt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { - CHECK_EQ(Length(ct.arguments[0]), Length(ct.arguments[1])); - for (int i = 0; i < Length(ct.arguments[0]); ++i) { + const int min_size = + std::min(Length(ct.arguments[0]), Length(ct.arguments[1])); + for (int i = 0; i < min_size; ++i) { const int64_t x = EvalAt(ct.arguments[0], i, evaluator); const int64_t y = EvalAt(ct.arguments[1], i, evaluator); if (x < y) { @@ -1164,8 +1166,8 @@ bool CheckLexLesseqInt( return false; } } - // We are at the end of the list. The two chains are equals. - return true; + // We are at the end of the common list. We compare the lengths of the lists. + return Length(ct.arguments[1]) >= Length(ct.arguments[0]); } bool CheckMaximumArgInt( @@ -1722,10 +1724,6 @@ CallMap CreateCallMap() { m["int_not_in"] = CheckSetNotIn; m["int_plus"] = CheckIntPlus; m["int_times"] = CheckIntTimes; - m["lex_less_bool"] = CheckLexLessInt; - m["lex_less_int"] = CheckLexLessInt; - m["lex_lesseq_bool"] = CheckLexLesseqInt; - m["lex_lesseq_int"] = CheckLexLesseqInt; m["maximum_arg_int"] = CheckMaximumArgInt; m["maximum_int"] = CheckMaximumInt; m["minimum_arg_int"] = CheckMinimumArgInt; @@ -1743,6 +1741,10 @@ CallMap CreateCallMap() { m["ortools_cumulative_opt"] = CheckOrtoolsCumulativeOpt; m["ortools_disjunctive_strict_opt"] = CheckOrtoolsDisjunctiveStrictOpt; m["ortools_inverse"] = CheckOrtoolsInverse; + m["ortools_lex_less_bool"] = CheckOrtoolsLexLessInt; + m["ortools_lex_less_int"] = CheckOrtoolsLexLessInt; + m["ortools_lex_lesseq_bools"] = CheckOrtoolsLexLesseqInt; + m["ortools_lex_lesseq_int"] = CheckOrtoolsLexLesseqInt; m["ortools_network_flow_cost"] = CheckOrtoolsNetworkFlowCost; m["ortools_network_flow"] = CheckOrtoolsNetworkFlow; m["ortools_nvalue"] = CheckOrtoolsNValue; diff --git a/ortools/flatzinc/cp_model_fz_solver.cc b/ortools/flatzinc/cp_model_fz_solver.cc index 3ea0cc58dc7..b0dc7648fb5 100644 --- a/ortools/flatzinc/cp_model_fz_solver.cc +++ b/ortools/flatzinc/cp_model_fz_solver.cc @@ -132,6 +132,12 @@ struct CpModelProtoWithMapping { // Get or create a literal that is equivalent to var1 == var2. int GetOrCreateLiteralForVarEqVar(int var1, int var2); + // Get or create a literal that is equivalent to var1 < var2. + int GetOrCreateLiteralForVarLtVar(int var1, int var2); + + // Get or create a literal that is equivalent to var1 <= var2. + int GetOrCreateLiteralForVarLeVar(int var1, int var2); + // Returns the list of literals corresponding to the domain of the variable. absl::flat_hash_map FullyEncode(int var); @@ -214,6 +220,8 @@ struct CpModelProtoWithMapping { absl::flat_hash_map var_to_lit_implies_greater_than_zero; absl::flat_hash_map, int> var_eq_value_to_literal; absl::flat_hash_map, int> var_eq_var_to_literal; + absl::flat_hash_map, int> var_lt_var_to_literal; + absl::flat_hash_map, int> var_le_var_to_literal; absl::flat_hash_map> set_variables; }; @@ -509,6 +517,50 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqVar(int var1, int var2) { return bool_var; } +int CpModelProtoWithMapping::GetOrCreateLiteralForVarLtVar(int var1, int var2) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) return LookupConstant(0); + + const std::pair key = {var1, var2}; + const auto it = var_lt_var_to_literal.find(key); + if (it != var_lt_var_to_literal.end()) return it->second; + + const int bool_var = NewBoolVar(); + + AddLinearConstraint({bool_var}, + Domain(std::numeric_limits::min(), -1), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(bool_var)}, + Domain(0, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + + var_lt_var_to_literal[key] = bool_var; + return bool_var; +} + +int CpModelProtoWithMapping::GetOrCreateLiteralForVarLeVar(int var1, int var2) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) return LookupConstant(1); + + const std::pair key = {var1, var2}; + const auto it = var_le_var_to_literal.find(key); + if (it != var_le_var_to_literal.end()) return it->second; + + const int bool_var = NewBoolVar(); + + AddLinearConstraint({bool_var}, + Domain(std::numeric_limits::min(), 0), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(bool_var)}, + Domain(1, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + + var_le_var_to_literal[key] = bool_var; + return bool_var; +} + int CpModelProtoWithMapping::GetOrCreateOptionalInterval(VarOrValue start, VarOrValue size, int opt_var) { @@ -1194,6 +1246,86 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, arg->add_f_inverse(LookupConstant(i - num_variables)); } } + } else if (fz_ct.type == "ortools_lex_less_int" || + fz_ct.type == "ortools_lex_less_bool") { + const std::vector x = LookupVars(fz_ct.arguments[0]); + const std::vector y = LookupVars(fz_ct.arguments[1]); + const int min_size = std::min(x.size(), y.size()); + std::vector is_lt(min_size); + std::vector is_le(min_size); + for (int i = 0; i < min_size; ++i) { + is_le[i] = GetOrCreateLiteralForVarLeVar(x[i], y[i]); + is_lt[i] = GetOrCreateLiteralForVarLtVar(x[i], y[i]); + } + + std::vector hold(min_size + 1); + for (int i = 0; i <= min_size; ++i) { + hold[i] = NewBoolVar(); + } + AddImplication({}, hold[0]); // Root condition. + if (x.size() > y.size()) { + AddImplication({}, hold[min_size]); // true leaf condition. + } else { + AddImplication({}, NegatedRef(hold[min_size])); // false leaf condition. + } + + for (int i = 0; i < min_size; ++i) { + // hold[i] => x[i] <= y[i]. + AddImplication({hold[i]}, is_le[i]); + // hold[i] => x[i] < y[i] || hold[i + 1] + ConstraintProto* chain = AddEnforcedConstraint(hold[i]); + chain->mutable_bool_or()->add_literals(is_lt[i]); + chain->mutable_bool_or()->add_literals(hold[i + 1]); + + // Optimization. + if (i + 1 < min_size) { + // is_lt[i] => no need to look at further hold variables. + AddImplication({is_lt[i]}, NegatedRef(hold[i + 1])); + // Propagate the negation of hold[i] to hold[i + 1] to avoid unnecessary + // branching and disable the chain constraints. + AddImplication({NegatedRef(hold[i])}, NegatedRef(hold[i + 1])); + } + } + } else if (fz_ct.type == "ortools_lex_lesseq_int" || + fz_ct.type == "ortools_lex_lesseq_bool") { + const std::vector x = LookupVars(fz_ct.arguments[0]); + const std::vector y = LookupVars(fz_ct.arguments[1]); + const int min_size = std::min(x.size(), y.size()); + std::vector is_lt(min_size); + std::vector is_le(min_size); + for (int i = 0; i < min_size; ++i) { + is_le[i] = GetOrCreateLiteralForVarLeVar(x[i], y[i]); + is_lt[i] = GetOrCreateLiteralForVarLtVar(x[i], y[i]); + } + + std::vector hold(min_size + 1); + for (int i = 0; i <= min_size; ++i) { + hold[i] = NewBoolVar(); + } + AddImplication({}, hold[0]); // Root condition. + if (x.size() >= y.size()) { + AddImplication({}, hold[min_size]); // true leaf condition. + } else { + AddImplication({}, NegatedRef(hold[min_size])); // false leaf condition. + } + + for (int i = 0; i < min_size; ++i) { + // hold[i] => x[i] <= y[i]. + AddImplication({hold[i]}, is_le[i]); + // hold[i] => x[i] < y[i] || hold[i + 1] + ConstraintProto* chain = AddEnforcedConstraint(hold[i]); + chain->mutable_bool_or()->add_literals(is_lt[i]); + chain->mutable_bool_or()->add_literals(hold[i + 1]); + + // Optimization. + if (i + 1 < min_size) { + // is_lt[i] => no need to look at further hold variables. + AddImplication({is_lt[i]}, NegatedRef(hold[i + 1])); + // Propagate the negation of hold[i] to hold[i + 1] to avoid unnecessary + // branching and disable the chain constraints. + AddImplication({NegatedRef(hold[i])}, NegatedRef(hold[i + 1])); + } + } } else if (fz_ct.type == "fzn_disjunctive") { const std::vector starts = LookupVarsOrValues(fz_ct.arguments[0]); diff --git a/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn b/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn new file mode 100644 index 00000000000..b88045a47cf --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn @@ -0,0 +1,14 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is strictly lexicographically less than array 'y'. +% Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate ortools_lex_less_bool( + array [int] of var bool: x , + array [int] of var bool: y, +); + +predicate fzn_lex_less_bool( + array [int] of var bool: x :: promise_ctx_antitone, + array [int] of var bool: y :: promise_ctx_monotone, +) = ortools_lex_less_bool(x, y); \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn b/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn new file mode 100644 index 00000000000..d2b37449f46 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn @@ -0,0 +1,14 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is strictly lexicographically less than array 'y'. +% Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate ortools_lex_less_int( + array [int] of var int: x , + array [int] of var int: y, +); + +predicate fzn_lex_less_int( + array [int] of var int: x :: promise_ctx_antitone, + array [int] of var int: y :: promise_ctx_monotone, +) = ortools_lex_less_int(x, y); \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn b/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn new file mode 100644 index 00000000000..6fd046cc4e3 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn @@ -0,0 +1,14 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is lexicographically less than or equal to array 'y'. +% Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate ortools_lex_lesseq_bool( + array [int] of var bool: x , + array [int] of var bool: y, +); + +predicate fzn_lex_lesseq_bool( + array [int] of var bool: x :: promise_ctx_antitone, + array [int] of var bool: y :: promise_ctx_monotone, +) = ortools_lex_lesseq_bool(x, y); \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn b/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn new file mode 100644 index 00000000000..648b6de28a4 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn @@ -0,0 +1,14 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is lexicographically less than or equal to array 'y'. +% Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate ortools_lex_lesseq_int( + array [int] of var int: x , + array [int] of var int: y, +); + +predicate fzn_lex_lesseq_int( + array [int] of var int: x :: promise_ctx_antitone, + array [int] of var int: y :: promise_ctx_monotone, +) = ortools_lex_lesseq_int(x, y); \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_nvalue_reif.mzn b/ortools/flatzinc/mznlib/fzn_nvalue_reif.mzn index c1c762b5487..45b1234e9bd 100644 --- a/ortools/flatzinc/mznlib/fzn_nvalue_reif.mzn +++ b/ortools/flatzinc/mznlib/fzn_nvalue_reif.mzn @@ -5,4 +5,4 @@ predicate fzn_nvalue_reif(var int: n, array [int] of var int: x, var bool: b) = int: lx = lb_array(x); int: ux = ub_array(x); var 1..ux-lx+1: c; - } in b <-> n == c /\ ortools_nvalue(c, x) + } in b <-> n == c /\ ortools_nvalue(c, x) \ No newline at end of file From 1d66e3d7f7e6821c5f7d96c5f9b54282b438222d Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 27 Aug 2025 08:13:18 +0200 Subject: [PATCH 008/491] cmake(ci): fix debian python and dotnet build --- cmake/docker/debian/dotnet.Dockerfile | 2 +- cmake/docker/debian/python.Dockerfile | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmake/docker/debian/dotnet.Dockerfile b/cmake/docker/debian/dotnet.Dockerfile index 5cf49e5f8ec..0690fcea24e 100644 --- a/cmake/docker/debian/dotnet.Dockerfile +++ b/cmake/docker/debian/dotnet.Dockerfile @@ -3,7 +3,7 @@ FROM ortools/cmake:debian_swig AS env # see: https://docs.microsoft.com/en-us/dotnet/core/install/linux-debian RUN apt-get update -qq \ && apt-get install -yq wget gpg apt-transport-https \ -&& wget -q "https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb" -O packages-microsoft-prod.deb \ +&& wget -q "https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb" -O packages-microsoft-prod.deb \ && dpkg -i packages-microsoft-prod.deb \ && rm packages-microsoft-prod.deb \ && apt-get update -qq \ diff --git a/cmake/docker/debian/python.Dockerfile b/cmake/docker/debian/python.Dockerfile index a8b75b25c8e..4e51aac8523 100644 --- a/cmake/docker/debian/python.Dockerfile +++ b/cmake/docker/debian/python.Dockerfile @@ -4,7 +4,8 @@ ENV PATH=/root/.local/bin:$PATH RUN apt-get update -qq \ && apt-get install -yq \ python3-dev python3-pip \ - python3-wheel python3-venv python3-virtualenv \ + python3-setuptools python3-wheel \ + python3-venv python3-virtualenv \ python3-numpy python3-pandas \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* From 7ab56a5a0167b6d7d6b7f39a96ed582765bdc201 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 27 Aug 2025 07:52:08 +0200 Subject: [PATCH 009/491] math_opt: cleanup --- ortools/math_opt/elemental/BUILD.bazel | 3 --- ortools/math_opt/python/ipc/BUILD.bazel | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ortools/math_opt/elemental/BUILD.bazel b/ortools/math_opt/elemental/BUILD.bazel index 988e347e8ec..2b03c5d97e7 100644 --- a/ortools/math_opt/elemental/BUILD.bazel +++ b/ortools/math_opt/elemental/BUILD.bazel @@ -399,9 +399,6 @@ cc_library( cc_library( name = "tagged_id", hdrs = ["tagged_id.h"], - # This dependency is not ideal, we should consider moving this to ortools/util, - # see some discussion on b/430226871. - visibility = ["//ortools/apollo:__subpackages__"], deps = [ "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/log:check", diff --git a/ortools/math_opt/python/ipc/BUILD.bazel b/ortools/math_opt/python/ipc/BUILD.bazel index de3cfdb97c1..89205c8cbe2 100644 --- a/ortools/math_opt/python/ipc/BUILD.bazel +++ b/ortools/math_opt/python/ipc/BUILD.bazel @@ -17,7 +17,7 @@ load("@rules_python//python:py_library.bzl", "py_library") py_library( name = "remote_http_solve", srcs = ["remote_http_solve.py"], - visibility = ["//visibility:public"], + visibility = ["//ortools/service:__subpackages__"], deps = [ ":proto_converter", "//ortools/service/v1:optimization_py_pb2", From 128b45a8d36eb2f2d7bfbc4737abce916ddb62f0 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 27 Aug 2025 14:53:28 +0200 Subject: [PATCH 010/491] math_opt: fix visibility issue --- ortools/math_opt/python/ipc/BUILD.bazel | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ortools/math_opt/python/ipc/BUILD.bazel b/ortools/math_opt/python/ipc/BUILD.bazel index 89205c8cbe2..fe6e7e41130 100644 --- a/ortools/math_opt/python/ipc/BUILD.bazel +++ b/ortools/math_opt/python/ipc/BUILD.bazel @@ -17,7 +17,10 @@ load("@rules_python//python:py_library.bzl", "py_library") py_library( name = "remote_http_solve", srcs = ["remote_http_solve.py"], - visibility = ["//ortools/service:__subpackages__"], + visibility = [ + "//examples/service:__subpackages__", + "//ortools/service:__subpackages__", + ], deps = [ ":proto_converter", "//ortools/service/v1:optimization_py_pb2", From bf886b048e5f870985b508fddffdf64927a02108 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 27 Aug 2025 16:47:02 -0700 Subject: [PATCH 011/491] [FZ] - Optimize varlevar and varltvar for Boolean variables - regroup code for lex_less and lex_lesseq - use it in set_le + set_lt - add ortools_lex_less_bool and ortools_lex_lesseq_bool tests - add global redundant constraint to ortools_nvalue --- ortools/flatzinc/checker.cc | 86 +++--- ortools/flatzinc/cp_model_fz_solver.cc | 268 ++++++++---------- ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn | 2 +- ortools/flatzinc/mznlib/fzn_lex_less_int.mzn | 2 +- .../flatzinc/mznlib/fzn_lex_lesseq_bool.mzn | 2 +- .../flatzinc/mznlib/fzn_lex_lesseq_int.mzn | 2 +- 6 files changed, 171 insertions(+), 191 deletions(-) diff --git a/ortools/flatzinc/checker.cc b/ortools/flatzinc/checker.cc index f23a8838d11..687c10228b3 100644 --- a/ortools/flatzinc/checker.cc +++ b/ortools/flatzinc/checker.cc @@ -265,7 +265,7 @@ bool CheckArrayVarIntElement( return element == target; } -bool CheckOrtoolsArrayIntElement( +bool CheckOrToolsArrayIntElement( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int64_t min_index = ct.arguments[1].values[0]; @@ -342,7 +342,7 @@ bool CheckBoolXor( return target == (left + right == 1); } -bool CheckOrtoolsCircuit( +bool CheckOrToolsCircuit( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int size = Length(ct.arguments[0]); @@ -358,7 +358,7 @@ bool CheckOrtoolsCircuit( return visited.size() == size; } -bool CheckOrtoolsBinPacking( +bool CheckOrToolsBinPacking( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int64_t capacity = ct.arguments[0].Value(); @@ -377,7 +377,7 @@ bool CheckOrtoolsBinPacking( return true; } -bool CheckOrtoolsBinPackingCapa( +bool CheckOrToolsBinPackingCapa( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const std::vector& capacities = ct.arguments[0].values; @@ -398,7 +398,7 @@ bool CheckOrtoolsBinPackingCapa( return true; } -bool CheckOrtoolsBinPackingLoad( +bool CheckOrToolsBinPackingLoad( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int num_positions = Length(ct.arguments[1]); @@ -420,7 +420,7 @@ bool CheckOrtoolsBinPackingLoad( return true; } -bool CheckOrtoolsNValue( +bool CheckOrToolsNValue( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int64_t card = Eval(ct.arguments[0], evaluator); @@ -441,7 +441,7 @@ int64_t ComputeCount(const Constraint& ct, return result; } -bool CheckOrtoolsCountEq( +bool CheckOrToolsCountEq( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int64_t count = ComputeCount(ct, evaluator); @@ -521,7 +521,7 @@ bool CheckCumulative( return true; } -bool CheckOrtoolsCumulativeOpt( +bool CheckOrToolsCumulativeOpt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { // TODO: Improve complexity for large durations. @@ -617,7 +617,7 @@ bool CheckDisjunctiveStrict( return true; } -bool CheckOrtoolsDisjunctiveStrictOpt( +bool CheckOrToolsDisjunctiveStrictOpt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int size = Length(ct.arguments[0]); @@ -1104,7 +1104,7 @@ bool CheckIntTimes( return target == left * right; } -bool CheckOrtoolsInverse( +bool CheckOrToolsInverse( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { CHECK_EQ(Length(ct.arguments[0]), Length(ct.arguments[1])); @@ -1132,7 +1132,7 @@ bool CheckOrtoolsInverse( return true; } -bool CheckOrtoolsLexLessInt( +bool CheckOrToolsLexLessInt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int min_size = @@ -1151,7 +1151,7 @@ bool CheckOrtoolsLexLessInt( return Length(ct.arguments[1]) > Length(ct.arguments[0]); } -bool CheckOrtoolsLexLesseqInt( +bool CheckOrToolsLexLesseqInt( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const int min_size = @@ -1253,7 +1253,7 @@ bool CheckNetworkFlowConservation( return true; } -bool CheckOrtoolsNetworkFlow( +bool CheckOrToolsNetworkFlow( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { return CheckNetworkFlowConservation(ct.arguments[0], ct.arguments[1], @@ -1261,7 +1261,7 @@ bool CheckOrtoolsNetworkFlow( evaluator); } -bool CheckOrtoolsNetworkFlowCost( +bool CheckOrToolsNetworkFlowCost( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { if (!CheckNetworkFlowConservation(ct.arguments[0], ct.arguments[1], @@ -1281,7 +1281,7 @@ bool CheckOrtoolsNetworkFlowCost( return total_cost == Eval(ct.arguments[5], evaluator); } -bool CheckOrtoolsRegular( +bool CheckOrToolsRegular( const Constraint& /*ct*/, const std::function& /*evaluator*/, const std::function(Variable*)>& /*set_evaluator*/) { @@ -1548,7 +1548,7 @@ bool CheckSort( return true; } -bool CheckOrtoolsSubCircuit( +bool CheckOrToolsSubCircuit( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { absl::flat_hash_set visited; @@ -1579,7 +1579,7 @@ bool CheckOrtoolsSubCircuit( return visited.size() == Length(ct.arguments[0]); } -bool CheckOrtoolsTableInt( +bool CheckOrToolsTableInt( const Constraint& /*ct*/, const std::function& /*evaluator*/, const std::function(Variable*)>& /*set_evaluator*/) { @@ -1660,14 +1660,14 @@ CallMap CreateCallMap() { m["bool_right_imp"] = CheckIntGe; m["bool_xor"] = CheckBoolXor; m["bool2int"] = CheckIntEq; - m["count_eq"] = CheckOrtoolsCountEq; + m["count_eq"] = CheckOrToolsCountEq; m["count_geq"] = CheckCountGeq; m["count_gt"] = CheckCountGt; m["count_leq"] = CheckCountLeq; m["count_lt"] = CheckCountLt; m["count_neq"] = CheckCountNeq; m["count_reif"] = CheckCountReif; - m["count"] = CheckOrtoolsCountEq; + m["count"] = CheckOrToolsCountEq; m["diffn_k_with_sizes"] = CheckDiffnK; m["diffn_nonstrict_k_with_sizes"] = CheckDiffnNonStrictK; m["false_constraint"] = CheckFalseConstraint; @@ -1728,30 +1728,30 @@ CallMap CreateCallMap() { m["maximum_int"] = CheckMaximumInt; m["minimum_arg_int"] = CheckMinimumArgInt; m["minimum_int"] = CheckMinimumInt; - m["ortools_array_bool_element"] = CheckOrtoolsArrayIntElement; - m["ortools_array_int_element"] = CheckOrtoolsArrayIntElement; - m["ortools_array_var_bool_element"] = CheckOrtoolsArrayIntElement; - m["ortools_array_var_int_element"] = CheckOrtoolsArrayIntElement; - m["ortools_bin_packing_capa"] = CheckOrtoolsBinPackingCapa; - m["ortools_bin_packing_load"] = CheckOrtoolsBinPackingLoad; - m["ortools_bin_packing"] = CheckOrtoolsBinPacking; - m["ortools_circuit"] = CheckOrtoolsCircuit; - m["ortools_count_eq_cst"] = CheckOrtoolsCountEq; - m["ortools_count_eq"] = CheckOrtoolsCountEq; - m["ortools_cumulative_opt"] = CheckOrtoolsCumulativeOpt; - m["ortools_disjunctive_strict_opt"] = CheckOrtoolsDisjunctiveStrictOpt; - m["ortools_inverse"] = CheckOrtoolsInverse; - m["ortools_lex_less_bool"] = CheckOrtoolsLexLessInt; - m["ortools_lex_less_int"] = CheckOrtoolsLexLessInt; - m["ortools_lex_lesseq_bools"] = CheckOrtoolsLexLesseqInt; - m["ortools_lex_lesseq_int"] = CheckOrtoolsLexLesseqInt; - m["ortools_network_flow_cost"] = CheckOrtoolsNetworkFlowCost; - m["ortools_network_flow"] = CheckOrtoolsNetworkFlow; - m["ortools_nvalue"] = CheckOrtoolsNValue; - m["ortools_regular"] = CheckOrtoolsRegular; - m["ortools_subcircuit"] = CheckOrtoolsSubCircuit; - m["ortools_table_bool"] = CheckOrtoolsTableInt; - m["ortools_table_int"] = CheckOrtoolsTableInt; + m["ortools_array_bool_element"] = CheckOrToolsArrayIntElement; + m["ortools_array_int_element"] = CheckOrToolsArrayIntElement; + m["ortools_array_var_bool_element"] = CheckOrToolsArrayIntElement; + m["ortools_array_var_int_element"] = CheckOrToolsArrayIntElement; + m["ortools_bin_packing_capa"] = CheckOrToolsBinPackingCapa; + m["ortools_bin_packing_load"] = CheckOrToolsBinPackingLoad; + m["ortools_bin_packing"] = CheckOrToolsBinPacking; + m["ortools_circuit"] = CheckOrToolsCircuit; + m["ortools_count_eq_cst"] = CheckOrToolsCountEq; + m["ortools_count_eq"] = CheckOrToolsCountEq; + m["ortools_cumulative_opt"] = CheckOrToolsCumulativeOpt; + m["ortools_disjunctive_strict_opt"] = CheckOrToolsDisjunctiveStrictOpt; + m["ortools_inverse"] = CheckOrToolsInverse; + m["ortools_lex_less_bool"] = CheckOrToolsLexLessInt; + m["ortools_lex_less_int"] = CheckOrToolsLexLessInt; + m["ortools_lex_lesseq_bool"] = CheckOrToolsLexLesseqInt; + m["ortools_lex_lesseq_int"] = CheckOrToolsLexLesseqInt; + m["ortools_network_flow_cost"] = CheckOrToolsNetworkFlowCost; + m["ortools_network_flow"] = CheckOrToolsNetworkFlow; + m["ortools_nvalue"] = CheckOrToolsNValue; + m["ortools_regular"] = CheckOrToolsRegular; + m["ortools_subcircuit"] = CheckOrToolsSubCircuit; + m["ortools_table_bool"] = CheckOrToolsTableInt; + m["ortools_table_int"] = CheckOrToolsTableInt; m["regular_nfa"] = CheckRegularNfa; m["set_card"] = CheckSetCard; m["set_diff"] = CheckSetDiff; diff --git a/ortools/flatzinc/cp_model_fz_solver.cc b/ortools/flatzinc/cp_model_fz_solver.cc index b0dc7648fb5..df0c25d8ac6 100644 --- a/ortools/flatzinc/cp_model_fz_solver.cc +++ b/ortools/flatzinc/cp_model_fz_solver.cc @@ -116,9 +116,11 @@ struct CpModelProtoWithMapping { bool negate = false); std::vector LookupVars(const fz::Argument& argument); VarOrValue LookupVarOrValue(const fz::Argument& argument); - std::vector LookupVarsOrValues(const fz::Argument& argument); + // Checks is the domain of the variable is included in [0, 1]. + bool VariableIsBoolean(int var) const; + // Set variables. void ExtractSetConstraint(const fz::Constraint& fz_ct); bool ConstraintContainsSetVariables(const fz::Constraint& constraint) const; @@ -178,7 +180,9 @@ struct CpModelProtoWithMapping { absl::Span enforcement_literals, const Domain& domain, absl::Span> terms = {}); - // Helpers to fill a ConstraintProto. + // Adds a lex_less{eq} constraint to the model. + void AddLexOrdering(absl::Span x, absl::Span y, + bool accepts_equals); // This one must be called after the domain has been set. void AddTermToLinearConstraint(int var, int64_t coeff, @@ -338,6 +342,13 @@ std::vector CpModelProtoWithMapping::LookupVarsOrValues( return result; } +bool CpModelProtoWithMapping::VariableIsBoolean(int var) const { + if (var < 0) var = NegatedRef(var); + const IntegerVariableProto& var_proto = proto.variables(var); + return var_proto.domain_size() == 2 && var_proto.domain(0) >= 0 && + var_proto.domain(1) <= 1; +} + bool CpModelProtoWithMapping::ConstraintContainsSetVariables( const fz::Constraint& constraint) const { for (const fz::Argument& argument : constraint.arguments) { @@ -430,6 +441,47 @@ void CpModelProtoWithMapping::AddTermToLinearConstraint( } } +void CpModelProtoWithMapping::AddLexOrdering(absl::Span x, + absl::Span y, + bool accepts_equals) { + const int min_size = std::min(x.size(), y.size()); + std::vector is_lt(min_size); + std::vector is_le(min_size); + for (int i = 0; i < min_size; ++i) { + is_le[i] = GetOrCreateLiteralForVarLeVar(x[i], y[i]); + is_lt[i] = GetOrCreateLiteralForVarLtVar(x[i], y[i]); + } + + std::vector hold(min_size + 1); + for (int i = 0; i < min_size; ++i) { + hold[i] = NewBoolVar(); + } + AddImplication({}, hold[0]); // Root condition. + const bool hold_sentinel_is_true = + x.size() > y.size() || (x.size() == y.size() && accepts_equals); + hold[min_size] = LookupConstant(hold_sentinel_is_true ? 1 : 0); + + for (int i = 0; i < min_size; ++i) { + // hold[i] => x[i] <= y[i]. + AddImplication({hold[i]}, is_le[i]); + + // hold[i] => x[i] < y[i] || hold[i + 1] + ConstraintProto* chain = AddEnforcedConstraint(hold[i]); + chain->mutable_bool_or()->add_literals(is_lt[i]); + chain->mutable_bool_or()->add_literals(hold[i + 1]); + + // Optimization. + if (i + 1 < min_size) { + // is_lt[i] => no need to look at further hold variables. + AddImplication({is_lt[i]}, NegatedRef(hold[i + 1])); + + // Propagate the negation of hold[i] to hold[i + 1] to avoid unnecessary + // branching and disable the chain constraints. + AddImplication({NegatedRef(hold[i])}, NegatedRef(hold[i + 1])); + } + } +} + int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqValue(int var, int64_t value) { CHECK_GE(var, 0); @@ -527,15 +579,31 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarLtVar(int var1, int var2) { if (it != var_lt_var_to_literal.end()) return it->second; const int bool_var = NewBoolVar(); + var_lt_var_to_literal[key] = bool_var; + var_le_var_to_literal[{var2, var1}] = NegatedRef(bool_var); - AddLinearConstraint({bool_var}, - Domain(std::numeric_limits::min(), -1), - {{var1, 1}, {var2, -1}}); - AddLinearConstraint({NegatedRef(bool_var)}, - Domain(0, std::numeric_limits::max()), - {{var1, 1}, {var2, -1}}); + if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { + // bool_var => var1 < var2 => !var1 && var2 + BoolArgumentProto* is_lt = + AddEnforcedConstraint(bool_var)->mutable_bool_and(); + is_lt->add_literals(NegatedRef(var1)); + is_lt->add_literals(var2); + + // !bool_var => var1 >= var2 => var1 || !var2 + BoolArgumentProto* is_ge = + AddEnforcedConstraint(NegatedRef(bool_var))->mutable_bool_or(); + is_ge->add_literals(var1); + is_ge->add_literals(NegatedRef(var2)); + + } else { + AddLinearConstraint({bool_var}, + Domain(std::numeric_limits::min(), -1), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(bool_var)}, + Domain(0, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + } - var_lt_var_to_literal[key] = bool_var; return bool_var; } @@ -549,15 +617,30 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarLeVar(int var1, int var2) { if (it != var_le_var_to_literal.end()) return it->second; const int bool_var = NewBoolVar(); - - AddLinearConstraint({bool_var}, - Domain(std::numeric_limits::min(), 0), - {{var1, 1}, {var2, -1}}); - AddLinearConstraint({NegatedRef(bool_var)}, - Domain(1, std::numeric_limits::max()), - {{var1, 1}, {var2, -1}}); - var_le_var_to_literal[key] = bool_var; + var_lt_var_to_literal[{var2, var1}] = NegatedRef(bool_var); + + if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { + // bool_var => var1 <= var2 <=> !var1 || var2 + BoolArgumentProto* is_le = + AddEnforcedConstraint(bool_var)->mutable_bool_or(); + is_le->add_literals(NegatedRef(var1)); + is_le->add_literals(var2); + + // !bool_var => var1 > var2 <=> var1 && !var2 + BoolArgumentProto* is_gt = + AddEnforcedConstraint(NegatedRef(bool_var))->mutable_bool_and(); + is_gt->add_literals(var1); + is_gt->add_literals(NegatedRef(var2)); + } else { + AddLinearConstraint({bool_var}, + Domain(std::numeric_limits::min(), 0), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(bool_var)}, + Domain(1, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + } + return bool_var; } @@ -1247,85 +1330,14 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, } } } else if (fz_ct.type == "ortools_lex_less_int" || - fz_ct.type == "ortools_lex_less_bool") { - const std::vector x = LookupVars(fz_ct.arguments[0]); - const std::vector y = LookupVars(fz_ct.arguments[1]); - const int min_size = std::min(x.size(), y.size()); - std::vector is_lt(min_size); - std::vector is_le(min_size); - for (int i = 0; i < min_size; ++i) { - is_le[i] = GetOrCreateLiteralForVarLeVar(x[i], y[i]); - is_lt[i] = GetOrCreateLiteralForVarLtVar(x[i], y[i]); - } - - std::vector hold(min_size + 1); - for (int i = 0; i <= min_size; ++i) { - hold[i] = NewBoolVar(); - } - AddImplication({}, hold[0]); // Root condition. - if (x.size() > y.size()) { - AddImplication({}, hold[min_size]); // true leaf condition. - } else { - AddImplication({}, NegatedRef(hold[min_size])); // false leaf condition. - } - - for (int i = 0; i < min_size; ++i) { - // hold[i] => x[i] <= y[i]. - AddImplication({hold[i]}, is_le[i]); - // hold[i] => x[i] < y[i] || hold[i + 1] - ConstraintProto* chain = AddEnforcedConstraint(hold[i]); - chain->mutable_bool_or()->add_literals(is_lt[i]); - chain->mutable_bool_or()->add_literals(hold[i + 1]); - - // Optimization. - if (i + 1 < min_size) { - // is_lt[i] => no need to look at further hold variables. - AddImplication({is_lt[i]}, NegatedRef(hold[i + 1])); - // Propagate the negation of hold[i] to hold[i + 1] to avoid unnecessary - // branching and disable the chain constraints. - AddImplication({NegatedRef(hold[i])}, NegatedRef(hold[i + 1])); - } - } - } else if (fz_ct.type == "ortools_lex_lesseq_int" || + fz_ct.type == "ortools_lex_less_bool" || + fz_ct.type == "ortools_lex_lesseq_int" || fz_ct.type == "ortools_lex_lesseq_bool") { const std::vector x = LookupVars(fz_ct.arguments[0]); const std::vector y = LookupVars(fz_ct.arguments[1]); - const int min_size = std::min(x.size(), y.size()); - std::vector is_lt(min_size); - std::vector is_le(min_size); - for (int i = 0; i < min_size; ++i) { - is_le[i] = GetOrCreateLiteralForVarLeVar(x[i], y[i]); - is_lt[i] = GetOrCreateLiteralForVarLtVar(x[i], y[i]); - } - - std::vector hold(min_size + 1); - for (int i = 0; i <= min_size; ++i) { - hold[i] = NewBoolVar(); - } - AddImplication({}, hold[0]); // Root condition. - if (x.size() >= y.size()) { - AddImplication({}, hold[min_size]); // true leaf condition. - } else { - AddImplication({}, NegatedRef(hold[min_size])); // false leaf condition. - } - - for (int i = 0; i < min_size; ++i) { - // hold[i] => x[i] <= y[i]. - AddImplication({hold[i]}, is_le[i]); - // hold[i] => x[i] < y[i] || hold[i + 1] - ConstraintProto* chain = AddEnforcedConstraint(hold[i]); - chain->mutable_bool_or()->add_literals(is_lt[i]); - chain->mutable_bool_or()->add_literals(hold[i + 1]); - - // Optimization. - if (i + 1 < min_size) { - // is_lt[i] => no need to look at further hold variables. - AddImplication({is_lt[i]}, NegatedRef(hold[i + 1])); - // Propagate the negation of hold[i] to hold[i + 1] to avoid unnecessary - // branching and disable the chain constraints. - AddImplication({NegatedRef(hold[i])}, NegatedRef(hold[i + 1])); - } - } + const bool accepts_equals = fz_ct.type == "ortools_lex_lesseq_bool" || + fz_ct.type == "ortools_lex_lesseq_int"; + AddLexOrdering(x, y, accepts_equals); } else if (fz_ct.type == "fzn_disjunctive") { const std::vector starts = LookupVarsOrValues(fz_ct.arguments[0]); @@ -1595,14 +1607,22 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, const int card = LookupVar(fz_ct.arguments[0]); const std::vector& x = LookupVars(fz_ct.arguments[1]); + LinearConstraintProto* global_cardinality = ct->mutable_linear(); + FillDomainInProto(Domain(x.size()), global_cardinality); + absl::btree_map> value_to_literals; for (int i = 0; i < x.size(); ++i) { const absl::flat_hash_map encoding = FullyEncode(x[i]); for (const auto& [value, literal] : encoding) { value_to_literals[value].push_back(literal); + AddTermToLinearConstraint(literal, 1, global_cardinality); } } + // Constrain the range of the card variable. + const int64_t max_size = std::min(x.size(), value_to_literals.size()); + AddLinearConstraint({}, {1, max_size}, {{card, 1}}); + LinearConstraintProto* lin = AddLinearConstraint({}, Domain(0), {{card, -1}}); for (const auto& [value, literals] : value_to_literals) { @@ -2049,54 +2069,8 @@ void CpModelProtoWithMapping::ExtractSetConstraint( } } - std::swap(x_literals, y_literals); // Reverse the order. - const bool accept_equals = fz_ct.type == "set_le"; - - // Now compare the bit representation using the lexicographic ordering. - std::vector eq_literals; - BoolArgumentProto* le_or_ct = proto.add_constraints()->mutable_bool_or(); - for (int i = 0; i < x_literals.size(); ++i) { - const int lt_lit = NewBoolVar(); - ConstraintProto* lt_ct = proto.add_constraints(); - // (x < y) <=> (~x && y) <=> lt_lit - lt_ct->add_enforcement_literal(lt_lit); - lt_ct->mutable_bool_and()->add_literals(NegatedRef(x_literals[i])); - lt_ct->mutable_bool_and()->add_literals(y_literals[i]); - ConstraintProto* lt_ct_rev = proto.add_constraints(); - lt_ct_rev->add_enforcement_literal(NegatedRef(x_literals[i])); - lt_ct_rev->add_enforcement_literal(y_literals[i]); - lt_ct_rev->mutable_bool_and()->add_literals(lt_lit); - - // lt_for_index => eq[0] && eq[1] && ... && eq[i-1] && lt[i] - const int lt_for_index = NewBoolVar(); - ConstraintProto* lt_for_index_ct = proto.add_constraints(); - lt_for_index_ct->add_enforcement_literal(lt_for_index); - for (const int eq_lit : eq_literals) { - lt_for_index_ct->mutable_bool_and()->add_literals(eq_lit); - } - lt_for_index_ct->mutable_bool_and()->add_literals(lt_lit); - - le_or_ct->add_literals(lt_for_index); - - const int eq_lit = NewBoolVar(); - AddLinearConstraint({eq_lit}, Domain(0), - {{x_literals[i], 1}, {y_literals[i], -1}}); - AddLinearConstraint({NegatedRef(eq_lit)}, Domain(1), - {{x_literals[i], 1}, {y_literals[i], 1}}); - eq_literals.push_back(eq_lit); - } - if (accept_equals) { - // eq_lit => eq[0] && eq[1] && ... && eq[i-1] && eq[i] - const int eq_lit = NewBoolVar(); - ConstraintProto* eq_ct = proto.add_constraints(); - eq_ct->add_enforcement_literal(eq_lit); - for (const int eq_lit : eq_literals) { - eq_ct->mutable_bool_and()->add_literals(eq_lit); - } - - le_or_ct->add_literals(eq_lit); - } + AddLexOrdering(y_literals, x_literals, accept_equals); } else { LOG(FATAL) << "Not supported " << fz_ct.DebugString(); } @@ -2347,11 +2321,17 @@ std::string SolutionString( } result.append("["); for (int i = 0; i < output.flat_variables.size(); ++i) { - const int64_t value = value_func(output.flat_variables[i]); - if (output.display_as_boolean) { - result.append(value ? "true" : "false"); + if (output.flat_variables[i]->domain.is_a_set) { + const std::vector values = + set_evaluator(output.flat_variables[i]); + absl::StrAppend(&result, "{", absl::StrJoin(values, ","), "}"); } else { - absl::StrAppend(&result, value); + const int64_t value = value_func(output.flat_variables[i]); + if (output.display_as_boolean) { + result.append(value ? "true" : "false"); + } else { + absl::StrAppend(&result, value); + } } if (i != output.flat_variables.size() - 1) { result.append(", "); diff --git a/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn b/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn index b88045a47cf..b7843a8e24e 100644 --- a/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn +++ b/ortools/flatzinc/mznlib/fzn_lex_less_bool.mzn @@ -4,7 +4,7 @@ %-----------------------------------------------------------------------------% predicate ortools_lex_less_bool( - array [int] of var bool: x , + array [int] of var bool: x, array [int] of var bool: y, ); diff --git a/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn b/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn index d2b37449f46..ca9993071e6 100644 --- a/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn +++ b/ortools/flatzinc/mznlib/fzn_lex_less_int.mzn @@ -4,7 +4,7 @@ %-----------------------------------------------------------------------------% predicate ortools_lex_less_int( - array [int] of var int: x , + array [int] of var int: x, array [int] of var int: y, ); diff --git a/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn b/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn index 6fd046cc4e3..48a063fd4bf 100644 --- a/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn +++ b/ortools/flatzinc/mznlib/fzn_lex_lesseq_bool.mzn @@ -4,7 +4,7 @@ %-----------------------------------------------------------------------------% predicate ortools_lex_lesseq_bool( - array [int] of var bool: x , + array [int] of var bool: x, array [int] of var bool: y, ); diff --git a/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn b/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn index 648b6de28a4..9fbee923ed1 100644 --- a/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn +++ b/ortools/flatzinc/mznlib/fzn_lex_lesseq_int.mzn @@ -4,7 +4,7 @@ %-----------------------------------------------------------------------------% predicate ortools_lex_lesseq_int( - array [int] of var int: x , + array [int] of var int: x, array [int] of var int: y, ); From 76c44856840fa04ec9eb3b4ff78b34f05d5bd64d Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 27 Aug 2025 17:13:42 -0700 Subject: [PATCH 012/491] [FZ] minor improvements --- ortools/flatzinc/cp_model_fz_solver.cc | 4 ++-- ortools/flatzinc/mznlib/fzn_all_equal_int.mzn | 4 ++++ ortools/flatzinc/mznlib/fzn_all_equal_set.mzn | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 ortools/flatzinc/mznlib/fzn_all_equal_int.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_all_equal_set.mzn diff --git a/ortools/flatzinc/cp_model_fz_solver.cc b/ortools/flatzinc/cp_model_fz_solver.cc index df0c25d8ac6..507ef423a5e 100644 --- a/ortools/flatzinc/cp_model_fz_solver.cc +++ b/ortools/flatzinc/cp_model_fz_solver.cc @@ -1914,8 +1914,8 @@ void CpModelProtoWithMapping::ExtractSetConstraint( } else if (fz_ct.type == "set_superset") { AddImplication({y_lit}, x_lit); } else if (fz_ct.type == "set_eq") { - AddImplication({x_lit}, y_lit); - AddImplication({y_lit}, x_lit); + // We use the linear as it is easier for the presolve. + AddLinearConstraint({}, Domain(0), {{x_lit, 1}, {y_lit, -1}}); } else { LOG(FATAL) << "Unsupported " << fz_ct.type; } diff --git a/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn b/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn new file mode 100644 index 00000000000..7bae41c2abf --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn @@ -0,0 +1,4 @@ +predicate fzn_all_equal_int(array [int] of var int: x) = + if length(x) > 1 then + forall (i in index_set(x) diff {min(index_set(x))}) (x[min(index_set(x))] = x[i]) + endif; \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn b/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn new file mode 100644 index 00000000000..f7f3d78d684 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn @@ -0,0 +1,4 @@ +predicate fzn_all_equal_set(array [int] of var set of int: x) = + if length(x) > 1 then + forall (i in index_set(x) diff {min(index_set(x))}) (x[min(index_set(x))] = x[i]) + endif; \ No newline at end of file From 4f57180cfabd7a31f306557b013cbbebe4cdfdcd Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Thu, 28 Aug 2025 07:12:16 +0200 Subject: [PATCH 013/491] add global cardinality optimizations --- ortools/flatzinc/mznlib/fzn_global_cardinality.mzn | 8 ++++++++ ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 ortools/flatzinc/mznlib/fzn_global_cardinality.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn new file mode 100644 index 00000000000..b9865da2b95 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn @@ -0,0 +1,8 @@ +predicate fzn_global_cardinality( + array [int] of var int: x, + array [int] of int: cover, + array [int] of var int: counts, +) = + forall (i in index_set(cover)) (count(x, cover[i], counts[i])) /\ + % Implied constraint + length(x) >= sum(counts); \ No newline at end of file diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn new file mode 100644 index 00000000000..51deabf573e --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn @@ -0,0 +1,8 @@ +predicate fzn_global_cardinality_closed( + array [int] of var int: x, + array [int] of int: cover, + array [int] of var int: counts, +) = forall (i in index_set(x)) (x[i] in {d | d in cover}) /\ + global_cardinality(x, cover, counts) /\ + sum(counts) = length(x); + \ No newline at end of file From 28a4ef7e40683da7c9cad1b6770daf0cb25f4939 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 29 Aug 2025 15:06:43 +0200 Subject: [PATCH 014/491] julia: export from google3 --- ortools/julia/ORTools.jl/Project.toml | 10 +- ortools/julia/ORTools.jl/src/ORTools.jl | 1 - ortools/julia/ORTools.jl/src/example.jl | 2 +- .../ORTools.jl/src/moi_wrapper/MOI_wrapper.jl | 885 +++++++++++++- .../src/moi_wrapper/Type_wrappers.jl | 264 ++++- .../julia/ORToolsGenerated.jl/Project.toml | 6 +- .../ORToolsGenerated.jl/scripts/gen_pb.jl | 9 + .../src/genproto/google/google.jl | 2 +- .../genproto/google/protobuf/duration_pb.jl | 5 +- .../genproto/google/protobuf/wrappers_pb.jl | 13 +- .../operations_research/assignment_pb.jl | 5 +- .../bop/bop_parameters_pb.jl | 25 +- .../course_scheduling_pb.jl | 5 +- .../operations_research/demon_profiler_pb.jl | 5 +- .../operations_research/glop/parameters_pb.jl | 133 ++- .../genproto/operations_research/gscip_pb.jl | 41 +- .../operations_research/linear_solver_pb.jl | 69 +- .../math_opt/callback_pb.jl | 53 +- .../operations_research/math_opt/glpk_pb.jl | 5 +- .../operations_research/math_opt/gurobi_pb.jl | 5 +- .../operations_research/math_opt/highs_pb.jl | 5 +- .../math_opt/infeasible_subsystem_pb.jl | 5 +- .../operations_research/math_opt/math_opt.jl | 1 + .../math_opt/model_parameters_pb.jl | 79 +- .../operations_research/math_opt/model_pb.jl | 25 +- .../math_opt/model_update_pb.jl | 9 +- .../operations_research/math_opt/osqp_pb.jl | 189 +++ .../math_opt/parameters_pb.jl | 50 +- .../operations_research/math_opt/result_pb.jl | 37 +- .../math_opt/solution_pb.jl | 25 +- .../math_opt/sparse_containers_pb.jl | 9 +- .../operations_research.jl | 3 +- .../optional_boolean_pb.jl | 5 +- .../multiple_dimensions_bin_packing_pb.jl | 5 +- .../packing/vbp/vector_bin_packing_pb.jl | 25 +- .../operations_research/pdlp/solve_log_pb.jl | 209 ++-- .../operations_research/pdlp/solvers_pb.jl | 217 ++-- .../operations_research/routing_enums_pb.jl | 35 +- .../operations_research/routing_ils_pb.jl | 413 +++++++ .../routing_parameters_pb.jl | 179 ++- .../sat/boolean_problem_pb.jl | 13 +- .../operations_research/sat/cp_model_pb.jl | 589 +++++----- .../sat/sat_parameters_pb.jl | 1035 +++++++++++------ .../scheduling/jssp/jobshop_scheduling_pb.jl | 5 +- .../scheduling/rcpsp/rcpsp_pb.jl | 5 +- .../operations_research/search_limit_pb.jl | 5 +- .../operations_research/search_stats_pb.jl | 61 +- .../solver_parameters_pb.jl | 5 +- ortools/julia/docs/index.md | 13 +- 49 files changed, 3590 insertions(+), 1209 deletions(-) create mode 100644 ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/osqp_pb.jl create mode 100644 ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_ils_pb.jl diff --git a/ortools/julia/ORTools.jl/Project.toml b/ortools/julia/ORTools.jl/Project.toml index 8d4f4ba3fb6..056f350a527 100644 --- a/ortools/julia/ORTools.jl/Project.toml +++ b/ortools/julia/ORTools.jl/Project.toml @@ -1,16 +1,20 @@ name = "ORTools" uuid = "b7d69b34-a827-4671-8cfa-f7e1eec930c7" -version = "1.0.0-DEV" +version = "0.0.1" [deps] MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -ORTools_jll = "717719f8-c30c-5086-8f3c-70cd6a1e3a46" ORToolsGenerated = "6b269722-41d3-11ee-be56-0242ac120002" +ORTools_jll = "717719f8-c30c-5086-8f3c-70cd6a1e3a46" ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -julia = "1.10" +julia = "1.9" +ORTools_jll = "9.14.0" +ORToolsGenerated = "0.0.2" +ProtoBuf = "1.0.15" +MathOptInterface = "1.42.0" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/ortools/julia/ORTools.jl/src/ORTools.jl b/ortools/julia/ORTools.jl/src/ORTools.jl index ac3aaf38d97..0b4453e07a2 100644 --- a/ortools/julia/ORTools.jl/src/ORTools.jl +++ b/ortools/julia/ORTools.jl/src/ORTools.jl @@ -7,5 +7,4 @@ include("moi_wrapper/Type_wrappers.jl") include("c_wrapper/c_wrapper.jl") include("moi_wrapper/MOI_wrapper.jl") - end diff --git a/ortools/julia/ORTools.jl/src/example.jl b/ortools/julia/ORTools.jl/src/example.jl index 0d06664702f..5ede045a9f1 100644 --- a/ortools/julia/ORTools.jl/src/example.jl +++ b/ortools/julia/ORTools.jl/src/example.jl @@ -13,7 +13,7 @@ Solving the following optimization problem: """ function main(ARGS) - optimizer = ORTools.Optimizer() + optimizer = ORTools.Optimizer(solver_type = ORTools.SolverType.SOLVER_TYPE_GLOP) c = [1.0, 2.0] # Variables definition diff --git a/ortools/julia/ORTools.jl/src/moi_wrapper/MOI_wrapper.jl b/ortools/julia/ORTools.jl/src/moi_wrapper/MOI_wrapper.jl index 5197a4a23fa..2e76d76ba19 100644 --- a/ortools/julia/ORTools.jl/src/moi_wrapper/MOI_wrapper.jl +++ b/ortools/julia/ORTools.jl/src/moi_wrapper/MOI_wrapper.jl @@ -15,6 +15,7 @@ const SUPPORTED_SOLVER_TYPES = [ SolverType.SOLVER_TYPE_UNSPECIFIED, SolverType.SOLVER_TYPE_GLOP, SolverType.SOLVER_TYPE_CP_SAT, + SolverType.SOLVER_TYPE_PDLP, ] const NON_GOOGLE_SOLVER_WARNING = """ @@ -36,7 +37,9 @@ const ZERO_ONE_CONSTRAINT_KEY = "zero_one" # Solver Type - By default, the optimizer will use the `GLOP` solver. + By default, the solver type is `SOLVER_TYPE_UNSPECIFIED`. + + TODO: b/435482716 - Add support for solver specific Optimizers. """ mutable struct Optimizer <: MOI.AbstractOptimizer solver_type::SolverType.T @@ -55,11 +58,24 @@ mutable struct Optimizer <: MOI.AbstractOptimizer solve_result::Union{SolveResultProto,Nothing} # Constructor with optional parameters - function Optimizer(; - model_name::String = "", - solver_type::SolverType.T = SolverType.SOLVER_TYPE_GLOP, - parameters::Union{SolveParameters,Nothing} = SolveParameters(), - ) + function Optimizer(;kwargs...) + model_name = "" + solver_type = SolverType.SOLVER_TYPE_UNSPECIFIED + parameters = SolveParameters() + + param_keys = keys(kwargs) + + if :model_name in param_keys + model_name = kwargs[:model_name] + end + if :solver_type in param_keys + solver_type = kwargs[:solver_type] + end + if :parameters in param_keys + parameters = kwargs[:parameters] + end + + if !in(solver_type, SUPPORTED_SOLVER_TYPES) @warn NON_GOOGLE_SOLVER_WARNING end @@ -97,13 +113,50 @@ function setproperty!(model::Optimizer, field::Symbol, value) return nothing end +""" + An optimizer that uses Glop as the underlying solver. +""" +struct GlopOptimizer + function GlopOptimizer(;kwargs...) + return Optimizer(;solver_type = SolverType.SOLVER_TYPE_GLOP, kwargs...) + end +end + +""" + An optimizer that uses CP-SAT as the underlying solver. + This optimizer currently uses MathOpt as the underlying interface. + + TODO: b/436879803 - return a different instance once the CPModelProto is implemented. +""" +struct CPSatOptimizer + function CPSatOptimizer(;kwargs...) + return Optimizer(;solver_type = SolverType.SOLVER_TYPE_CP_SAT, kwargs...) + end +end + +""" + An optimizer that uses PDLP as the underlying solver. + This optimizer currently uses MathOpt as the underlying interface. + + TODO: b/436879979 - return a different instance once the PDLP interface is implemented. +""" +struct PDLPOptimizer + function PDLPOptimizer(;kwargs...) + return Optimizer(;solver_type = SolverType.SOLVER_TYPE_PDLP, kwargs...) + end +end + + function MOI.empty!(model::Optimizer) - model.solver_type = SolverType.SOLVER_TYPE_UNSPECIFIED - model.model = nothing - model.parameters = nothing + model.model = Model() # Clear the related metadata model.constraint_types_present = Set{Tuple{Type,Type}}() - model.constraint_indices_dict = Dict() + model.constraint_indices_dict = Dict( + SCALAR_SET_WITH_VARIABLE_INDEX_CONSTRAINT_KEY => [], + SCALAR_SET_WITH_SCALAR_FUNCTION_CONSTRAINT_KEY => [], + INTEGER_CONSTRAINT_KEY => [], + ZERO_ONE_CONSTRAINT_KEY => [], + ) model.objective_set = false model.solve_result = nothing @@ -111,18 +164,49 @@ function MOI.empty!(model::Optimizer) end function MOI.is_empty(model::Optimizer) - return isnothing(model.model) && - isnothing(model.parameters) && - model.solver_type == SolverType.SOLVER_TYPE_UNSPECIFIED && + return isempty(model.model) && !model.objective_set && isnothing(model.solve_result) end +function Base.isempty(model::Model) + # A model with default values is considered empty. + return isnothing(model) || (encoded_model_size(model) == encoded_model_size(Model())) +end + +function Base.isempty(parameters::SolveParameters) + # A SolveParameters with default values is considered empty. + return isnothing(parameters) || ( + encoded_parameters_size(parameters) == encoded_parameters_size(SolveParameters()) + ) +end + +# Solver Type attribute +# NB: Despite this attribute being an optimizer attribute, to allow for the use of +# MOI.default_copy_to method during the optimize! call, the attribute is defined as +# an AbstractModelAttribute. Using an AbstractOptimizerAttribute will require the +# re-implementation of calling MOI.copy_to to allow for the copying of optimizer attributes. +struct BaseSolverType <: MOI.AbstractOptimizerAttribute end +MOI.attribute_value_type(::BaseSolverType) = SolverType.T + +function MOI.set(model::Optimizer, ::BaseSolverType, solver_type::SolverType.T) + optionally_initialize_model_and_parameters!(model) + + model.solver_type = solver_type + + return nothing +end + +function MOI.get(model::Optimizer, ::BaseSolverType) + return model.solver_type +end + +MOI.supports(model::Optimizer, ::BaseSolverType) = true + """ TODO: b/384496265 - implement Base.summary(::IO, ::Optimizer) to print a nice string when someone shows your model """ - function MOI.get(model::Optimizer, ::MOI.SolverName) return "$(model.solver_type)" end @@ -149,12 +233,9 @@ end MOI.supports(model::Optimizer, ::MOI.Name) = true - function optionally_initialize_model_and_parameters!(model::Optimizer)::Nothing if MOI.is_empty(model) - model.solver_type = SolverType.SOLVER_TYPE_UNSPECIFIED model.model = Model() - model.parameters = SolveParameters() # Re-initailize the associated metadata. # TODO: b/392072219 - use emtpy! to do this after resolving this bug. model.constraint_indices_dict = Dict( @@ -831,7 +912,7 @@ internal fields split by the `PARAM_SPLITTER` and the solver name. For example, `gscip_parameters.preprocessing` should be passed as `gscip__preprocessing`. """ function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) - if !MOI.is_empty(model) && !isnothing(model.parameters) + if !isnothing(model.parameters) param_name = param.name if contains(param_name, PARAM_SPLITTER) @@ -882,10 +963,14 @@ function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) push!(model_attributes_set, MOI.Name()) end + solver_type = MOI.get(model, BaseSolverType()) + if solver_type != SolverType.SOLVER_TYPE_UNSPECIFIED + push!(model_attributes_set, BaseSolverType()) + end + return model_attributes_set end - """ Variable overrides @@ -1885,6 +1970,12 @@ function MOI.supports( end function MOI.optimize!(model::Optimizer) + # If the solver type is not specified, set it to GLOP by default. + if model.solver_type == SolverType.SOLVER_TYPE_UNSPECIFIED + @error "The solver type is not specified. Please specify a solver type.\nYou can do so when building an `Optimizer` object or by setting the `BaseSolverType` attribute.\nFor instance: `Optimizer(solver_type=ORTools.SolverType.SOLVER_TYPE_CP_SAT)`.\nWith JuMP, you can do the following: `Model(() -> ORTools.Optimizer(solver_type=ORTools.SolverType.SOLVER_TYPE_CP_SAT))`." + return + end + status_msg = Ref(pointer(zeros(Int8, 1))) # Serialize the model @@ -1925,6 +2016,8 @@ function MOI.optimize!(model::Optimizer) return nothing end +MOI.supports_incremental_interface(model::Optimizer) = true + function MOI.get(model::Optimizer, ::MOI.RawStatusString)::String if !isnothing(model) && !isnothing(model.solve_result) return string(model.solve_result.termination.reason) @@ -2085,3 +2178,757 @@ function MOI.get(model::Optimizer, attr::DualObjectiveBound) return model.solve_result.termination.objective_bounds.dual_bound end + +function MOI.get(model::Optimizer, ::MOI.ResultCount) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return length(model.solve_result.solutions) +end + +# TODO: b/428754197 - assess if we need this custom error or if a generic one can be used. +""" +Error thrown when there's an attempt to retrieved the VariablePrimal value +when the model or solve_result value is `nothing` or when the primal_status +is NO_SOLUTION. +""" +struct GetVariablePrimalNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, index::MOI.VariableIndex) + if isnothing(model) + throw( + GetVariablePrimalNotAllowed( + "No model exists. Initialize the model and call optimize! afterwards before calling this function.", + ), + ) + end + + if isnothing(model.solve_result) + throw( + GetVariablePrimalNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + if MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + throw( + GetVariablePrimalNotAllowed( + "Cannot retrieve VariablePrimal as no primal solution was found when optimizing the defined model.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + variable_value_idx = findfirst( + isequal(index.value), + model.solve_result.solutions[attr.result_index].primal_solution.variable_values.ids, + ) + return model.solve_result.solutions[attr.result_index].primal_solution.variable_values.values[variable_value_idx] +end + +# TODO: b/428754197 - assess if we need this custom error or if a generic one can be used. +""" +Error thrown when there's an attempt to retrieved the objective value +when the model or solve_result value is `nothing` or when the primal_status +is NO_SOLUTION. +""" +struct GetObjectiveValueNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) + if isnothing(model) + throw( + GetObjectiveValueNotAllowed( + "No model exists. Initialize the model and call optimize! afterwards before calling this function.", + ), + ) + end + + if isnothing(model.solve_result) + throw( + GetObjectiveValueNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + if MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + throw( + GetObjectiveValueNotAllowed( + "Cannot retrieve objective value as no primal solution was found when optimizing the defined model.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + return model.solve_result.solutions[attr.result_index].primal_solution.objective_value +end + +# TODO: b/428758462 - offer better documentation for this attribute. +""" +Feasibility status of the primal solution according to the underlying solver. +This attribute indicates whether the solution is feasible in case of an early termination. +""" +struct FeasibilityStatus <: MOI.AbstractOptimizerAttribute + result_index::Int + FeasibilityStatus() = new(1) + FeasibilityStatus(result_index::Int) = new(result_index) +end +MOI.attribute_value_type(::FeasibilityStatus) = MOI.ResultStatusCode + +function MOI.get(model::Optimizer, attr::FeasibilityStatus)::MOI.ResultStatusCode + if isnothing(model) || isnothing(model.solve_result) + return MOI.NO_SOLUTION + end + + MOI.check_result_index_bounds(model, attr) + + solution_status = + model.solve_result.solutions[attr.result_index].primal_solution.feasibility_status + # TODO: b/428760341 - move this mapping to its own function. + if solution_status == SolutionStatusProto.SOLUTION_STATUS_FEASIBLE + return MOI.FEASIBLE_POINT + elseif solution_status == SolutionStatusProto.SOLUTION_STATUS_INFEASIBLE + return MOI.INFEASIBLE_POINT + elseif solution_status == SolutionStatusProto.SOLUTION_STATUS_UNDETERMINED + return MOI.UNKNOWN_RESULT_STATUS + else + # For SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED + # A guard value representing no status. + return MOI.NO_SOLUTION + end +end + +""" +Error thrown when there's an attempt to retrieved the ConstraintDual value +when the model or solve_result value is `nothing` or when the dual_status +is NO_SOLUTION. +""" +struct GetConstraintDualNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + index::MOI.ConstraintIndex{MOI.VariableIndex,<:S}, +) where {S<:SCALAR_SET} + if isnothing(model) || isnothing(model.solve_result) + throw( + GetConstraintDualNotAllowed( + "No model exists. Initialize the model and call optimize! afterwards before calling this function.", + ), + ) + end + + if isnothing(model.solve_result) + throw( + GetConstraintDualNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + if MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + throw( + GetVariableDualNotAllowed( + "Cannot retrieve VariableDual as no dual solution was found when optimizing the defined model.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + constraint_index = findfirst( + isequal(index.value), + model.solve_result.solutions[attr.result_index].dual_solution.dual_values.ids, + ) + return model.solve_result.solutions[attr.result_index].dual_solution.dual_values.values[constraint_index] +end + +""" +A solution to the dual of an optimization problem. + +This attribute is used to get the variable's reduced cost from the dual solution. +""" +struct VariableReducedCost <: MOI.AbstractVariableAttribute + result_index::Int + VariableReducedCost() = new(1) + VariableReducedCost(result_index::Int) = new(result_index) +end +MOI.attribute_value_type(::VariableReducedCost) = Real + +""" +Error thrown when there's an attempt to retrieved the VariableReducedCost value +when the model or solve_result value is `nothing` or when the dual_status +is NO_SOLUTION. +""" +struct GetVariableReducedCostNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get(model::Optimizer, attr::VariableReducedCost, index::MOI.VariableIndex) + if isnothing(model) + throw( + GetVariableReducedCostNotAllowed( + "No model exists. Initialize the model and call optimize! afterwards before calling this function.", + ), + ) + end + + if isnothing(model.solve_result) + throw( + GetVariableReducedCostNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + if MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + throw( + GetVariableDualNotAllowed( + "Cannot retrieve VariableDual as no dual solution was found when optimizing the defined model.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + variable_index = findfirst( + isequal(index.value), + model.solve_result.solutions[attr.result_index].dual_solution.reduced_costs.ids, + ) + return model.solve_result.solutions[attr.result_index].dual_solution.reduced_costs.values[variable_index] +end + +""" +Error thrown when there's an attempt to retrieve the DualObjectiveValue value +when the model or solve_result value is `nothing` or when the dual_status +is NO_SOLUTION. +""" +struct GetDualObjectiveValueNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) + if isnothing(model) + throw( + GetDualObjectiveValueNotAllowed( + "No model exists. Initialize the model and call optimize! afterwards before calling this function.", + ), + ) + end + + if isnothing(model.solve_result) + throw( + GetDualObjectiveValueNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + if MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + throw( + GetVariableDualNotAllowed( + "Cannot retrieve VariableDual as no dual solution was found when optimizing the defined model.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + return model.solve_result.solutions[attr.result_index].dual_solution.objective_value +end + +""" +Feasibility status of the dual solution. +""" +struct DualSolutionStatus <: MOI.AbstractOptimizerAttribute + result_index::Int + DualSolutionStatus() = new(1) + DualSolutionStatus(result_index::Int) = new(result_index) +end +MOI.attribute_value_type(::DualSolutionStatus) = MOI.ResultStatusCode + +function MOI.get(model::Optimizer, attr::DualSolutionStatus)::MOI.ResultStatusCode + if isnothing(model) || isnothing(model.solve_result) + return MOI.NO_SOLUTION + end + + MOI.check_result_index_bounds(model, attr) + + dual_status = + model.solve_result.solutions[attr.result_index].dual_solution.feasibility_status + + if dual_status == SolutionStatusProto.SOLUTION_STATUS_FEASIBLE + return MOI.FEASIBLE_POINT + elseif dual_status == SolutionStatusProto.SOLUTION_STATUS_INFEASIBLE + return MOI.INFEASIBLE_POINT + elseif dual_status == SolutionStatusProto.SOLUTION_STATUS_UNDETERMINED + return MOI.UNKNOWN_RESULT_STATUS + else + # For DualSolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED + # A guard value representing no status. + return MOI.NO_SOLUTION + end +end + +# TODO: b/428759950 - assess what happens when querying basis status for solver +# that doesn't support the simplex method. +""" +Error thrown when the returned BasisStatusProto is BASIS_STATUS_UNSPECIFIED which +is just a guard value representing no status and has no mapping to the statuses +exposed through MOI.BasisStatusCode. +""" +struct UnsupportedBasisStatus <: MOI.UnsupportedError + message::String +end + +function get_MOI_basis_status(basis_status_proto::BasisStatusProto.T)::MOI.BasisStatusCode + if basis_status_proto == BasisStatusProto.BASIS_STATUS_BASIC + return MOI.BASIC + elseif basis_status_proto == BasisStatusProto.BASIS_STATUS_AT_UPPER_BOUND + return MOI.NONBASIC_AT_UPPER + elseif basis_status_proto == BasisStatusProto.BASIS_STATUS_AT_LOWER_BOUND + return MOI.NONBASIC_AT_LOWER + elseif basis_status_proto == BasisStatusProto.BASIS_STATUS_FIXED_VALUE + return MOI.NONBASIC + elseif basis_status_proto == BasisStatusProto.BASIS_STATUS_FREE + return MOI.SUPER_BASIC + else + # For BasisStatusProto.BASIS_STATUS_UNSPECIFIED + throw( + UnsupportedBasisStatus( + "Unsupported BasisStatusProto value: $basis_status_proto", + ), + ) + end +end + +""" +Error thrown when there's an attempt to retrieved the VariableBasisStatus value +when the model or solve_result value is `nothing` or when the primal_status +is NO_SOLUTION. +""" +struct GetVariableBasisStatusNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get(model::Optimizer, attr::MOI.VariableBasisStatus, index::MOI.VariableIndex) + if isnothing(model) || isnothing(model.solve_result) + throw( + GetVariableBasisStatusNotAllowed( + "model or solve_result value is `nothing`. Initialize the model or call optimize! on an initialized model.", + ), + ) + end + + if MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + throw( + GetVariableBasisStatusNotAllowed( + "Cannot retrieve VariableBasisStatus as no primal solution was found.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + variable_index = findfirst( + isequal(index.value), + model.solve_result.solutions[attr.result_index].basis.variable_status.ids, + ) + basis_status = + model.solve_result.solutions[attr.result_index].basis.variable_status.values[variable_index] + + return get_MOI_basis_status(basis_status) +end + + +""" +Error thrown when there's an attempt to retrieved the ConstraintBasisStatus value +when the model or solve_result value is `nothing` or when the primal_status +is NO_SOLUTION. +""" +struct GetConstraintBasisStatusNotAllowed <: MOI.NotAllowedError + message::String +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintBasisStatus, + index::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S<:SCALAR_SET} + if isnothing(model) || isnothing(model.solve_result) + throw( + GetConstraintBasisStatusNotAllowed( + "model or solve_result value is `nothing`. Initialize the model or call optimize! on an initialized model.", + ), + ) + end + + if MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + throw( + GetConstraintBasisStatusNotAllowed( + "Cannot retrieve ConstraintBasisStatus as no primal solution was found.", + ), + ) + end + + MOI.check_result_index_bounds(model, attr) + + constraint_index = findfirst( + isequal(index.value), + model.solve_result.solutions[attr.result_index].basis.constraint_status.ids, + ) + basis_status = + model.solve_result.solutions[attr.result_index].basis.constraint_status.values[constraint_index] + + return get_MOI_basis_status(basis_status) +end + +""" +This is an advanced feature used by MathOpt to characterize feasibility of +suboptimal LP solutions (optimal solutions will always have status +SOLUTION_STATUS_FEASIBLE (MOI.FEASIBLE_POINT)). + +For single-sided LPs it should be equal to the feasibility status of the +associated dual solution. For two-sided LPs it may be different in some +edge cases (e.g. incomplete solves with primal simplex). + +If you are providing a starting basis via +ModelSolveParametersProto.initial_basis, this value is ignored. It is only +relevant for the basis returned by SolutionProto.basis. +""" +struct BasicDualFeasibilityStatus <: MOI.AbstractOptimizerAttribute + result_index::Int + BasicDualFeasibilityStatus() = new(1) + BasicDualFeasibilityStatus(result_index::Int) = new(result_index) +end +MOI.attribute_value_type(::BasicDualFeasibilityStatus) = MOI.ResultStatusCode + +function MOI.get(model::Optimizer, attr::BasicDualFeasibilityStatus) + if isnothing(model) || isnothing(model.solve_result) + return MOI.NO_SOLUTION + end + + MOI.check_result_index_bounds(model, attr) + + if dual_status == SolutionStatusProto.SOLUTION_STATUS_FEASIBLE + return MOI.FEASIBLE_POINT + elseif dual_status == SolutionStatusProto.SOLUTION_STATUS_INFEASIBLE + return MOI.INFEASIBLE_POINT + elseif dual_status == SolutionStatusProto.SOLUTION_STATUS_UNDETERMINED + return MOI.UNKNOWN_RESULT_STATUS + else + # For DualSolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED + # A guard value representing no status. + return MOI.NO_SOLUTION + end +end + +function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) + if isnothing(model) || isnothing(model.solve_result) + return NaN + end + + sec = model.solve_result.solve_stats.solve_time.seconds + nanos = model.solve_result.solve_stats.solve_time.nanos + return sec + (nanos / 1e9) +end + +function MOI.get(model::Optimizer, ::MOI.SimplexIterations) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return model.solve_result.solve_stats.simplex_iterations +end + +function MOI.get(model::Optimizer, ::MOI.BarrierIterations) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return model.solve_result.solve_stats.barrier_iterations +end + +function MOI.get(model::Optimizer, ::MOI.NodeCount) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return model.solve_result.solve_stats.node_count +end + +""" +Attributes that returns the nummber of first order iterations. +""" +struct FirstOrderIterations <: MOI.AbstractOptimizerAttribute end + +MOI.attribute_value_type(::FirstOrderIterations) = Int + +function MOI.get(model::Optimizer, ::FirstOrderIterations)::Int + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return model.solve_result.solve_stats.first_order_iterations +end + +""" +Attribute used to retrieve the size of the primal ray vector. +""" +struct PrimalRaysSize <: MOI.AbstractOptimizerAttribute end +MOI.attribute_value_type(::PrimalRaysSize) = Int + +function MOI.get(model::Optimizer, ::PrimalRaysSize) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + return length(model.solve_result.primal_rays) +end + +""" +Error thrown when GetPrimalRay is called but either the model is not initialized +or the optimize! method has not been called. +""" +struct GetPrimalRayNotAllowed <: MOI.NotAllowedError + message::String +end + +""" +Attribute used to retrieve a `PrimalRay` by index. +A value is returned if the result_index is in the range [1, PrimalRaysSize]. +""" +struct PrimalRay <: MOI.AbstractOptimizerAttribute + result_index::Int + PrimalRay() = new(1) + PrimalRay(result_index::Int) = new(result_index) +end +MOI.attribute_value_type(::PrimalRay) = Real + +function MOI.get(model::Optimizer, attr::PrimalRay, index::MOI.VariableIndex) + if isnothing(model) + throw(GetPrimalRayNotAllowed("No model exists. Initialize the model + and call optimize! afterwards before calling this function.")) + end + + if isnothing(model.solve_result) + throw( + GetPrimalRayNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + primal_ray_size = MOI.get(model, PrimalRaysSize()) + if !(1 <= attr.result_index <= primal_ray_size) + throw(GetPrimalRayNotAllowed("result_index is out of bounds. + Valid values are in the range [1, PrimalRaysSize].")) + end + + variable_index = findfirst( + isequal(index.value), + model.solve_result.primal_rays[attr.result_index].variable_values.ids, + ) + + if isnothing(variable_index) + throw( + GetPrimalRayNotAllowed( + "Variable with the passed index not found in the primal ray.", + ), + ) + end + + return model.solve_result.primal_rays[attr.result_index].variable_values.values[variable_index] +end + +""" +Attribute used to retrieve the number of the dual_ray vectors. +""" +struct DualRaySize <: MOI.AbstractOptimizerAttribute end +MOI.attribute_value_type(::DualRaySize) = Int + +function MOI.get(model::Optimizer, attr::DualRaySize) + if isnothing(model) || isnothing(model.solve_result) + return 0 + end + + # TODO: b/428836099 - Maybe throw an error for CP-SAT as it will never return a ray. + return length(model.solve_result.dual_rays) +end + +""" +Error thrown when DualRay() is called but either the model is not initialized +or the optimize! method has not been called. +""" +struct GetDualRayNotAllowed <: MOI.NotAllowedError + message::String +end + +""" +Attribute used to retrieve a DualRay by index. +A value is returned if the result_index is in the range [1, DualRayDualValuesSize] +""" +struct DualRay <: MOI.AbstractOptimizerAttribute + result_index::Int + DualRay() = new(1) + DualRay(result_index::Int) = new(result_index) +end + +""" +Specifying the index as a `VariableIndex` attempts to retrieve a value from the `reduced_costs` +vector from the respective DualRayProto instance. The value retrieved, if present, is the one +associated with the variable index. +""" +function MOI.get(model::Optimizer, attr::DualRay, index::MOI.VariableIndex) + if isnothing(model) + throw(GetDualRayNotAllowed("No model exists. Initialize the model + and call optimize! afterwards before calling this function.")) + end + + if isnothing(model.solve_result) + throw( + GetDualRayNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + dual_ray_size = MOI.get(model, attr::DualRaySize) + if !(1 <= attr.result_index <= dual_ray_size) + throw(GetDualRayNotAllowed("result_index is out of bounds. + Valid values are in the range [1, DualRaySize].")) + end + + variable_index = findfirst( + isequal(index.value), + model.solve_result.dual_rays[attr.result_index].reduced_costs.ids, + ) + + if isnothing(variable_index) + throw( + GetPrimalRayNotAllowed( + "Variable with the passed index not found in the dual ray reduced costs.", + ), + ) + end + + return model.solve_result.dual_rays[attr.result_index].reduced_costs.values[variable_index] +end + +""" +Specifying the index as a `ConstraintIndex` attempts to retrieve a value from the `dual_values` +vector from the respective DualRayProto instance. The value retrieved, if present, is the one +associated with the constraint index. The allowable set of constraint indices are those that are +linear constraints. +""" +function MOI.get( + model::Optimizer, + attr::DualRay, + index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}, +) where {T<:Real,S<:SCALAR_SET} + if isnothing(model) + throw(GetDualRayNotAllowed("No model exists. Initialize the model + and call optimize! afterwards before calling this function.")) + end + + if isnothing(model.solve_result) + throw( + GetDualRayNotAllowed( + "Call optimize! on your model before calling this function.", + ), + ) + end + + dual_ray_size = MOI.get(model, attr::DualRaySize) + if !(1 <= attr.result_index <= dual_ray_size) + throw(GetDualRayNotAllowed("result_index is out of bounds. + Valid values are in the range [1, DualRaySize].")) + end + + constraint_index = findfirst( + isequal(index.value), + model.solve_result.dual_rays[attr.result_index].dual_values.ids, + ) + + if isnothing(constraint_index) + throw( + GetDualRayNotAllowed( + "Constraint with the passed index not found in the dual ray dual values.", + ), + ) + end + + return model.solve_result.dual_rays[attr.result_index].dual_values.values[constraint_index] +end + +""" +Attibute that returns the `GScipOutput` as part of the solve result. +""" +struct GScipOutputAttribute <: MOI.AbstractOptimizerAttribute end +MOI.attribute_value_type(::GScipOutputAttribute) = Union{Nothing,GScipOutput} + +function MOI.get(model::Optimizer, attr::GScipOutputAttribute) + if isnothing(model) || isnothing(model.solve_result) + throw(MOI.GetAttributeNotAllowed(attr)) + end + + if (model.solver_type != SolverType.SOLVER_TYPE_GSCIP) + throw(error("GScipOutputAttribute is only supported for the GSCIP solver")) + end + + solver_specific_output = model.solve_result.solver_specific_output + + if isnothing(solver_specific_output) || + solver_specific_output.name != :gscip_output + return nothing + end + + return solver_specific_output.value +end + +function MOI.get(model::MOI.Utilities.CachingOptimizer, attr::GScipOutputAttribute) + return MOI.get(unsafe_backend(model), attr) +end + +""" +Attribute that returns the `PdlpOutput` as part of the solve result. +""" +struct PdlpOutputAttribute <: MOI.AbstractOptimizerAttribute end +MOI.attribute_value_type(::PdlpOutputAttribute) = Union{Nothing,PdlpOutput} + +function MOI.get(model::Optimizer, attr::PdlpOutputAttribute) + if isnothing(model) || isnothing(model.solve_result) + throw(MOI.GetAttributeNotAllowed(attr)) + end + + if (model.solver_type != SolverType.SOLVER_TYPE_PDLP) + throw(error("PdlpOutputAttribute is only supported for the PDLP solver")) + end + + solver_specific_output = model.solve_result.solver_specific_output + + if isnothing(solver_specific_output) || + solver_specific_output.name != :pdlp_output + return nothing + end + + return solver_specific_output.value +end + +function MOI.get(model::MOI.Utilities.CachingOptimizer, attr::PdlpOutputAttribute) + return MOI.get(unsafe_backend(model), attr) +end + +function unsafe_backend(model::MOI.Utilities.CachingOptimizer) + return model.optimizer.model +end diff --git a/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl b/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl index 849fe4b393f..c1d09146561 100644 --- a/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl +++ b/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl @@ -1,11 +1,29 @@ using ORToolsGenerated const OperationsResearch = ORToolsGenerated.Proto.operations_research const MathOpt = OperationsResearch.math_opt +const PDLP = OperationsResearch.pdlp const SolverType = MathOpt.SolverTypeProto const SolveResultProto = MathOpt.SolveResultProto const TerminationReasonProto = MathOpt.TerminationReasonProto const LimitProto = MathOpt.LimitProto const FeasibilityStatusProto = MathOpt.FeasibilityStatusProto +const BasisStatusProto = MathOpt.BasisStatusProto +const GScipOutput = OperationsResearch.GScipOutput +const PdlpOutput = MathOpt.var"SolveResultProto.PdlpOutput" +const PdlpTerminationCriteria_SimpleOptimalityCriteria = + PDLP.var"TerminationCriteria.SimpleOptimalityCriteria" +const PdlpTerminationCriteria_DetailedOptimalityCriteria = + PDLP.var"TerminationCriteria.DetailedOptimalityCriteria" +const PdlpHybridGradientParams_RestartStrategy = + PDLP.var"PrimalDualHybridGradientParams.RestartStrategy" +const PdlpHybridGradientParams_PresolveOptions = + PDLP.var"PrimalDualHybridGradientParams.PresolveOptions" +const PdlpHybridGradientParams_LinesearchRule = + PDLP.var"PrimalDualHybridGradientParams.LinesearchRule" +const PdlpOptimalityNorm = PDLP.OptimalityNorm +const PdlpSchedulerType = PDLP.SchedulerType +const PdlpAdaptiveLinesearchParams = PDLP.AdaptiveLinesearchParams +const PdlpMalitskyPockParams = PDLP.MalitskyPockParams const PB = MathOpt.PB """ @@ -71,7 +89,6 @@ const GlopParameters_InitialBasisHeuristic = OperationsResearch.glop.var"GlopParameters.InitialBasisHeuristic" ## Sat parameter types. -const AbstractSatParameters = OperationsResearch.sat.var"##AbstractSatParameters" const SatParameters_VariableOrder = OperationsResearch.sat.var"SatParameters.VariableOrder" const SatParameters_Polarity = OperationsResearch.sat.var"SatParameters.Polarity" const SatParameters_ConflictMinimizationAlgorithm = @@ -779,7 +796,7 @@ end """ Mutable wrapper struct for the SatParameters struct. """ -mutable struct SatParameters <: AbstractSatParameters +mutable struct SatParameters name::String preferred_variable_order::SatParameters_VariableOrder.T initial_polarity::SatParameters_Polarity.T @@ -1716,6 +1733,227 @@ function to_proto_struct(highs_options::HighsOptions)::MathOpt.HighsOptionsProto ) end +mutable struct OsqpSettings + rho::Float64 + sigma::Float64 + scaling::Int64 + adaptive_rho::Bool + adaptive_rho_interval::Int64 + adaptive_rho_tolerance::Float64 + adaptive_rho_fraction::Float64 + max_iter::Int64 + eps_abs::Float64 + eps_rel::Float64 + eps_prim_inf::Float64 + eps_dual_inf::Float64 + alpha::Float64 + delta::Float64 + polish::Bool + polish_refine_iter::Int64 + verbose::Bool + scaled_termination::Bool + check_termination::Int64 + warm_start::Bool + time_limit::Float64 + + function OsqpSettings(; + rho = zero(Float64), + sigma = zero(Float64), + scaling = zero(Int64), + adaptive_rho = false, + adaptive_rho_interval = zero(Int64), + adaptive_rho_tolerance = zero(Float64), + adaptive_rho_fraction = zero(Float64), + max_iter = zero(Int64), + eps_abs = zero(Float64), + eps_rel = zero(Float64), + eps_prim_inf = zero(Float64), + eps_dual_inf = zero(Float64), + alpha = zero(Float64), + delta = zero(Float64), + polish = false, + polish_refine_iter = zero(Int64), + verbose = false, + scaled_termination = false, + check_termination = 0, + warm_start = false, + time_limit = zero(Float64), + ) + + new( + rho, + sigma, + scaling, + adaptive_rho, + adaptive_rho_interval, + adaptive_rho_tolerance, + adaptive_rho_fraction, + max_iter, + eps_abs, + eps_rel, + eps_prim_inf, + eps_dual_inf, + alpha, + delta, + polish, + polish_refine_iter, + verbose, + scaled_termination, + check_termination, + warm_start, + time_limit, + ) + end +end + +function to_proto_struct(osqp_settings::OsqpSettings)::MathOpt.OsqpSettingsProto + return MathOpt.OsqpSettingsProto( + osqp_settings.rho, + osqp_settings.sigma, + osqp_settings.scaling, + osqp_settings.adaptive_rho, + osqp_settings.adaptive_rho_interval, + osqp_settings.adaptive_rho_tolerance, + osqp_settings.adaptive_rho_fraction, + osqp_settings.max_iter, + osqp_settings.eps_abs, + osqp_settings.eps_rel, + osqp_settings.eps_prim_inf, + osqp_settings.eps_dual_inf, + osqp_settings.alpha, + osqp_settings.delta, + osqp_settings.polish, + osqp_settings.polish_refine_iter, + osqp_settings.verbose, + osqp_settings.scaled_termination, + osqp_settings.check_termination, + osqp_settings.warm_start, + osqp_settings.time_limit, + ) +end + +mutable struct OsqpOutput + initialized_underlying_solver::Bool + + function OsqpOutput(; initialized_underlying_solver = false) + new(initialized_underlying_solver) + end +end + +function to_proto_struct(osqp_output::OsqpOutput)::MathOpt.OsqpOutput + return MathOpt.OsqpOutput(osqp_output.initialized_underlying_solver) +end + +mutable struct PdlpTerminationCriteria + optimality_norm::PdlpOptimalityNorm.T + optimality_criteria::Union{ + Nothing, + PB.OneOf{ + <:Union{ + PdlpTerminationCriteria_SimpleOptimalityCriteria, + PdlpTerminationCriteria_DetailedOptimalityCriteria, + }, + }, + } + eps_optimal_absolute::Float64 + eps_optimal_relative::Float64 + eps_primal_infeasible::Float64 + eps_dual_infeasible::Float64 + time_sec_limit::Float64 + iteration_limit::Int32 + kkt_matrix_pass_limit::Float64 +end + +function to_proto_struct( + pdlp_termination_criteria::PdlpTerminationCriteria, +)::PDLP.TerminationCriteria + return PDLP.TerminationCriteria( + pdlp_termination_criteria.optimality_norm, + pdlp_termination_criteria.optimality_criteria, + pdlp_termination_criteria.eps_optimal_absolute, + pdlp_termination_criteria.eps_optimal_relative, + pdlp_termination_criteria.eps_primal_infeasible, + pdlp_termination_criteria.eps_dual_infeasible, + pdlp_termination_criteria.time_sec_limit, + pdlp_termination_criteria.iteration_limit, + pdlp_termination_criteria.kkt_matrix_pass_limit, + ) +end + +mutable struct PdlpHybridGradientParameters + termination_criteria::Union{Nothing,PdlpTerminationCriteria} + num_threads::Int32 + num_shards::Int32 + scheduler_type::PdlpSchedulerType.T + record_iteration_stats::Bool + verbosity_level::Int32 + log_interval_seconds::Float64 + major_iteration_frequency::Int32 + termination_check_frequency::Int32 + restart_strategy::PdlpHybridGradientParams_RestartStrategy.T + primal_weight_update_smoothing::Float64 + initial_primal_weight::Float64 + presolve_options::Union{Nothing,PdlpHybridGradientParams_PresolveOptions} + l_inf_ruiz_iterations::Int32 + l2_norm_rescaling::Bool + sufficient_reduction_for_restart::Float64 + necessary_reduction_for_restart::Float64 + linesearch_rule::PdlpHybridGradientParams_LinesearchRule.T + adaptive_linesearch_parameters::Union{Nothing,PdlpAdaptiveLinesearchParams} + malitsky_pock_parameters::Union{Nothing,PdlpMalitskyPockParams} + initial_step_size_scaling::Float64 + random_projection_seeds::Vector{Int32} + infinite_constraint_bound_threshold::Float64 + handle_some_primal_gradients_on_finite_bounds_as_residuals::Bool + use_diagonal_qp_trust_region_solver::Bool + diagonal_qp_trust_region_solver_tolerance::Float64 + use_feasibility_polishing::Bool + apply_feasibility_polishing_after_limits_reached::Bool + apply_feasibility_polishing_if_solver_is_interrupted::Bool +end + + +function to_proto_struct( + pdlp_hybrid_gradient_parameters::PdlpHybridGradientParameters, +)::PDLP.PrimalDualHybridGradientParams + termination_criteria = nothing + if !isnothing(pdlp_hybrid_gradient_parameters.termination_criteria) + termination_criteria = + to_proto_struct(pdlp_hybrid_gradient_parameters.termination_criteria) + end + + return PDLP.PrimalDualHybridGradientParams( + termination_criteria, + pdlp_hybrid_gradient_parameters.num_threads, + pdlp_hybrid_gradient_parameters.num_shards, + pdlp_hybrid_gradient_parameters.scheduler_type, + pdlp_hybrid_gradient_parameters.record_iteration_stats, + pdlp_hybrid_gradient_parameters.verbosity_level, + pdlp_hybrid_gradient_parameters.log_interval_seconds, + pdlp_hybrid_gradient_parameters.major_iteration_frequency, + pdlp_hybrid_gradient_parameters.termination_check_frequency, + pdlp_hybrid_gradient_parameters.restart_strategy, + pdlp_hybrid_gradient_parameters.primal_weight_update_smoothing, + pdlp_hybrid_gradient_parameters.initial_primal_weight, + pdlp_hybrid_gradient_parameters.presolve_options, + pdlp_hybrid_gradient_parameters.l_inf_ruiz_iterations, + pdlp_hybrid_gradient_parameters.l2_norm_rescaling, + pdlp_hybrid_gradient_parameters.sufficient_reduction_for_restart, + pdlp_hybrid_gradient_parameters.necessary_reduction_for_restart, + pdlp_hybrid_gradient_parameters.linesearch_rule, + pdlp_hybrid_gradient_parameters.adaptive_linesearch_parameters, + pdlp_hybrid_gradient_parameters.malitsky_pock_parameters, + pdlp_hybrid_gradient_parameters.initial_step_size_scaling, + pdlp_hybrid_gradient_parameters.random_projection_seeds, + pdlp_hybrid_gradient_parameters.infinite_constraint_bound_threshold, + pdlp_hybrid_gradient_parameters.handle_some_primal_gradients_on_finite_bounds_as_residuals, + pdlp_hybrid_gradient_parameters.use_diagonal_qp_trust_region_solver, + pdlp_hybrid_gradient_parameters.diagonal_qp_trust_region_solver_tolerance, + pdlp_hybrid_gradient_parameters.use_feasibility_polishing, + pdlp_hybrid_gradient_parameters.apply_feasibility_polishing_after_limits_reached, + pdlp_hybrid_gradient_parameters.apply_feasibility_polishing_if_solver_is_interrupted, + ) +end """ Mutable wrapper struct for the SolveParametersProto struct. @@ -1743,6 +1981,8 @@ mutable struct SolveParameters gurobi::Union{Nothing,GurobiParameters} glop::Union{Nothing,GlopParameters} cp_sat::Union{Nothing,SatParameters} + pdlp::Union{Nothing,PdlpHybridGradientParameters} + osqp::Union{Nothing,OsqpSettings} glpk::Union{Nothing,GlpkParameters} highs::Union{Nothing,HighsOptions} @@ -1769,6 +2009,8 @@ mutable struct SolveParameters gurobi = nothing, glop = nothing, cp_sat = nothing, + pdlp = nothing, + osqp = nothing, glpk = nothing, highs = nothing, ) @@ -1795,6 +2037,8 @@ mutable struct SolveParameters gurobi, glop, cp_sat, + pdlp, + osqp, glpk, highs, ) @@ -1832,6 +2076,16 @@ function to_proto_struct(solve_parameters::SolveParameters)::MathOpt.SolveParame highs_parameters = to_proto_struct(solve_parameters.highs) end + pdlp_parameters = nothing + if !isnothing(solve_parameters.pdlp) + pdlp_parameters = to_proto_struct(solve_parameters.pdlp) + end + + osqp_parameters = nothing + if !isnothing(solve_parameters.osqp) + osqp_parameters = to_proto_struct(solve_parameters.osqp) + end + return MathOpt.SolveParametersProto( solve_parameters.time_limit, solve_parameters.iteration_limit, @@ -1855,7 +2109,13 @@ function to_proto_struct(solve_parameters::SolveParameters)::MathOpt.SolveParame gurobi_parameters, glop_parameters, cp_sat_parameters, + pdlp_parameters, + osqp_parameters, glpk_parameters, highs_parameters, ) end + +# The size of the encoded solve parameters proto. +encoded_parameters_size(parameters::SolveParameters) = + PB._encoded_size(to_proto_struct(parameters)) diff --git a/ortools/julia/ORToolsGenerated.jl/Project.toml b/ortools/julia/ORToolsGenerated.jl/Project.toml index 7e5b6d4e3e5..983d3d4e61a 100644 --- a/ortools/julia/ORToolsGenerated.jl/Project.toml +++ b/ortools/julia/ORToolsGenerated.jl/Project.toml @@ -1,15 +1,15 @@ name = "ORToolsGenerated" uuid = "6b269722-41d3-11ee-be56-0242ac120002" -version = "0.0.1" +version = "0.0.2" [deps] ORTools_jll = "717719f8-c30c-5086-8f3c-70cd6a1e3a46" ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429" [compat] -ORTools_jll = "9.11.0" -ProtoBuf = "1.0.15" julia = "1.9" +ORTools_jll = "9.14.0" +ProtoBuf = "1.0.15" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/ortools/julia/ORToolsGenerated.jl/scripts/gen_pb.jl b/ortools/julia/ORToolsGenerated.jl/scripts/gen_pb.jl index 324c66ae0e5..6425e08b8fc 100644 --- a/ortools/julia/ORToolsGenerated.jl/scripts/gen_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/scripts/gen_pb.jl @@ -1,3 +1,8 @@ +# Run this script as: +# hgd / g4d ... +# cd third_party/ortools/ortools/julia/ORToolsGenerated.jl/scripts/ +# julia gen_pb.jl + using ORTools_jll using ProtoBuf using Pkg @@ -10,6 +15,7 @@ if protobuf_jl_version < v"1.0.15" # MathOpt requires https://github.com/JuliaIO/ProtoBuf.jl/pull/234. # Bop requires https://github.com/JuliaIO/ProtoBuf.jl/pull/244. error("ProtoBuf.jl has a too low version: 1.0.15 is the minimum required") + # Version 1.1 also works. end # Path to the generated Julia package. @@ -31,5 +37,8 @@ proto_var_names = [file for file in names(ORTools_jll) if startswith(String(file proto_abs_paths = eval.(Meta.parse.("ORTools_jll." .* String.(proto_var_names))) proto_rel_paths = replace.(proto_abs_paths, PROTO_ROOT_PATH => "") +println("List of the $(length(proto_var_names)) protos being included:") +println(proto_var_names) + mkpath(OUTPUT_PATH) protojl(proto_rel_paths, PROTO_ROOT_PATH, OUTPUT_PATH) diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/google.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/google.jl index 31fc82b5422..25bf6b5d48d 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/google.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/google.jl @@ -1,5 +1,5 @@ module google -include("google/protobuf/protobuf.jl") +include("protobuf/protobuf.jl") end # module google diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/duration_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/duration_pb.jl index c141f6dda80..9c324c619a2 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/duration_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/duration_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.449 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/google/protobuf/duration.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.221 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/google/protobuf/duration.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export Duration + struct Duration seconds::Int64 nanos::Int32 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/wrappers_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/wrappers_pb.jl index 5f45220edba..629df8ae6e3 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/wrappers_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/google/protobuf/wrappers_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.261 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/google/protobuf/wrappers.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.139 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/google/protobuf/wrappers.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export BoolValue, Int64Value, FloatValue, Int32Value, DoubleValue, UInt64Value, UInt32Value export BytesValue, StringValue + struct BoolValue value::Bool end @@ -89,12 +90,12 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::FloatValue) initpos = position(e.io) - x.value != zero(Float32) && PB.encode(e, 1, x.value) + x.value !== zero(Float32) && PB.encode(e, 1, x.value) return position(e.io) - initpos end function PB._encoded_size(x::FloatValue) encoded_size = 0 - x.value != zero(Float32) && (encoded_size += PB._encoded_size(x.value, 1)) + x.value !== zero(Float32) && (encoded_size += PB._encoded_size(x.value, 1)) return encoded_size end @@ -149,12 +150,12 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::DoubleValue) initpos = position(e.io) - x.value != zero(Float64) && PB.encode(e, 1, x.value) + x.value !== zero(Float64) && PB.encode(e, 1, x.value) return position(e.io) - initpos end function PB._encoded_size(x::DoubleValue) encoded_size = 0 - x.value != zero(Float64) && (encoded_size += PB._encoded_size(x.value, 1)) + x.value !== zero(Float64) && (encoded_size += PB._encoded_size(x.value, 1)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/assignment_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/assignment_pb.jl index bc3ff621524..6e4d6d10f3e 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/assignment_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/assignment_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.920 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/assignment.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.864 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/assignment.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export IntVarAssignment, SequenceVarAssignment, IntervalVarAssignment, WorkerInfo export AssignmentProto + struct IntVarAssignment var_id::String min::Int64 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/bop/bop_parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/bop/bop_parameters_pb.jl index a3e236f2f82..ba7c33d5ee8 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/bop/bop_parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/bop/bop_parameters_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.181 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/bop/bop_parameters.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.124 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/bop/bop_parameters.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export var"BopOptimizerMethod.OptimizerType", var"BopParameters.ThreadSynchronizationType" export BopOptimizerMethod, BopSolverOptimizerSet, BopParameters + @enumx var"BopOptimizerMethod.OptimizerType" SAT_CORE_BASED=0 SAT_LINEAR_SEARCH=15 LINEAR_RELAXATION=1 LOCAL_SEARCH=2 RANDOM_FIRST_SOLUTION=3 RANDOM_CONSTRAINT_LNS=4 RANDOM_VARIABLE_LNS=5 COMPLETE_LNS=7 LP_FIRST_SOLUTION=8 OBJECTIVE_FIRST_SOLUTION=9 USER_GUIDED_FIRST_SOLUTION=14 RANDOM_CONSTRAINT_LNS_GUIDED_BY_LP=11 RANDOM_VARIABLE_LNS_GUIDED_BY_LP=12 RELATION_GRAPH_LNS=16 RELATION_GRAPH_LNS_GUIDED_BY_LP=17 @enumx var"BopParameters.ThreadSynchronizationType" NO_SYNCHRONIZATION=0 SYNCHRONIZE_ALL=1 SYNCHRONIZE_ON_RIGHT=2 @@ -237,11 +238,11 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::BopParameters) initpos = position(e.io) - x.max_time_in_seconds != Float64(Inf) && PB.encode(e, 1, x.max_time_in_seconds) - x.max_deterministic_time != Float64(Inf) && PB.encode(e, 27, x.max_deterministic_time) - x.lp_max_deterministic_time != Float64(1.0) && PB.encode(e, 37, x.lp_max_deterministic_time) + x.max_time_in_seconds !== Float64(Inf) && PB.encode(e, 1, x.max_time_in_seconds) + x.max_deterministic_time !== Float64(Inf) && PB.encode(e, 27, x.max_deterministic_time) + x.lp_max_deterministic_time !== Float64(1.0) && PB.encode(e, 37, x.lp_max_deterministic_time) x.max_number_of_consecutive_failing_optimizer_calls != zero(Int32) && PB.encode(e, 35, x.max_number_of_consecutive_failing_optimizer_calls) - x.relative_gap_limit != Float64(1e-4) && PB.encode(e, 28, x.relative_gap_limit) + x.relative_gap_limit !== Float64(1e-4) && PB.encode(e, 28, x.relative_gap_limit) x.max_num_decisions_in_ls != Int32(4) && PB.encode(e, 2, x.max_num_decisions_in_ls) x.max_num_broken_constraints_in_ls != Int32(2147483647) && PB.encode(e, 38, x.max_num_broken_constraints_in_ls) x.log_search_progress != false && PB.encode(e, 14, x.log_search_progress) @@ -271,18 +272,18 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::BopParameters) x.use_lp_strong_branching != false && PB.encode(e, 29, x.use_lp_strong_branching) x.decomposer_num_variables_threshold != Int32(50) && PB.encode(e, 30, x.decomposer_num_variables_threshold) x.num_bop_solvers_used_by_decomposition != Int32(1) && PB.encode(e, 31, x.num_bop_solvers_used_by_decomposition) - x.decomposed_problem_min_time_in_seconds != Float64(0.0) && PB.encode(e, 36, x.decomposed_problem_min_time_in_seconds) + x.decomposed_problem_min_time_in_seconds !== Float64(0.0) && PB.encode(e, 36, x.decomposed_problem_min_time_in_seconds) x.guided_sat_conflicts_chunk != Int32(1000) && PB.encode(e, 34, x.guided_sat_conflicts_chunk) x.max_lp_solve_for_feasibility_problems != Int32(0) && PB.encode(e, 41, x.max_lp_solve_for_feasibility_problems) return position(e.io) - initpos end function PB._encoded_size(x::BopParameters) encoded_size = 0 - x.max_time_in_seconds != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 1)) - x.max_deterministic_time != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 27)) - x.lp_max_deterministic_time != Float64(1.0) && (encoded_size += PB._encoded_size(x.lp_max_deterministic_time, 37)) + x.max_time_in_seconds !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 1)) + x.max_deterministic_time !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 27)) + x.lp_max_deterministic_time !== Float64(1.0) && (encoded_size += PB._encoded_size(x.lp_max_deterministic_time, 37)) x.max_number_of_consecutive_failing_optimizer_calls != zero(Int32) && (encoded_size += PB._encoded_size(x.max_number_of_consecutive_failing_optimizer_calls, 35)) - x.relative_gap_limit != Float64(1e-4) && (encoded_size += PB._encoded_size(x.relative_gap_limit, 28)) + x.relative_gap_limit !== Float64(1e-4) && (encoded_size += PB._encoded_size(x.relative_gap_limit, 28)) x.max_num_decisions_in_ls != Int32(4) && (encoded_size += PB._encoded_size(x.max_num_decisions_in_ls, 2)) x.max_num_broken_constraints_in_ls != Int32(2147483647) && (encoded_size += PB._encoded_size(x.max_num_broken_constraints_in_ls, 38)) x.log_search_progress != false && (encoded_size += PB._encoded_size(x.log_search_progress, 14)) @@ -312,7 +313,7 @@ function PB._encoded_size(x::BopParameters) x.use_lp_strong_branching != false && (encoded_size += PB._encoded_size(x.use_lp_strong_branching, 29)) x.decomposer_num_variables_threshold != Int32(50) && (encoded_size += PB._encoded_size(x.decomposer_num_variables_threshold, 30)) x.num_bop_solvers_used_by_decomposition != Int32(1) && (encoded_size += PB._encoded_size(x.num_bop_solvers_used_by_decomposition, 31)) - x.decomposed_problem_min_time_in_seconds != Float64(0.0) && (encoded_size += PB._encoded_size(x.decomposed_problem_min_time_in_seconds, 36)) + x.decomposed_problem_min_time_in_seconds !== Float64(0.0) && (encoded_size += PB._encoded_size(x.decomposed_problem_min_time_in_seconds, 36)) x.guided_sat_conflicts_chunk != Int32(1000) && (encoded_size += PB._encoded_size(x.guided_sat_conflicts_chunk, 34)) x.max_lp_solve_for_feasibility_problems != Int32(0) && (encoded_size += PB._encoded_size(x.max_lp_solve_for_feasibility_problems, 41)) return encoded_size diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/course_scheduling_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/course_scheduling_pb.jl index 98a0e9ea7ba..ccf449e464b 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/course_scheduling_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/course_scheduling_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.918 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/scheduling/course_scheduling.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.863 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/scheduling/course_scheduling.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export ClassAssignment, Course, CourseSchedulingResultStatus, StudentAssignment, Student export Room, Teacher, CourseSchedulingResult, CourseSchedulingModel + struct ClassAssignment course_index::Int32 section_number::Int32 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/demon_profiler_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/demon_profiler_pb.jl index 9f813641443..1a79918ae23 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/demon_profiler_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/demon_profiler_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.919 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/demon_profiler.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.863 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/demon_profiler.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export DemonRuns, ConstraintRuns + struct DemonRuns demon_id::String start_time::Vector{Int64} diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/glop/parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/glop/parameters_pb.jl index 8f673a1506c..81ee12b666a 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/glop/parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/glop/parameters_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.164 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/glop/parameters.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.112 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/glop/parameters.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -9,6 +9,7 @@ export var"GlopParameters.PricingRule", var"GlopParameters.SolverBehavior" export var"GlopParameters.CostScalingAlgorithm", var"GlopParameters.ScalingAlgorithm" export var"GlopParameters.InitialBasisHeuristic", GlopParameters + @enumx var"GlopParameters.PricingRule" DANTZIG=0 STEEPEST_EDGE=1 DEVEX=2 @enumx var"GlopParameters.SolverBehavior" ALWAYS_DO=0 NEVER_DO=1 LET_SOLVER_DECIDE=2 @@ -64,6 +65,7 @@ struct GlopParameters objective_upper_limit::Float64 degenerate_ministep_factor::Float64 random_seed::Int32 + use_absl_random::Bool num_omp_threads::Int32 perturb_costs_in_dual_simplex::Bool use_dedicated_dual_feasibility_algorithm::Bool @@ -76,10 +78,11 @@ struct GlopParameters push_to_vertex::Bool use_implied_free_preprocessor::Bool max_valid_magnitude::Float64 + drop_magnitude::Float64 dual_price_prioritize_norm::Bool end -PB.default_values(::Type{GlopParameters}) = (;scaling_method = var"GlopParameters.ScalingAlgorithm".EQUILIBRATION, feasibility_rule = var"GlopParameters.PricingRule".STEEPEST_EDGE, optimization_rule = var"GlopParameters.PricingRule".STEEPEST_EDGE, refactorization_threshold = Float64(1e-9), recompute_reduced_costs_threshold = Float64(1e-8), recompute_edges_norm_threshold = Float64(100.0), primal_feasibility_tolerance = Float64(1e-8), dual_feasibility_tolerance = Float64(1e-8), ratio_test_zero_threshold = Float64(1e-9), harris_tolerance_ratio = Float64(0.5), small_pivot_threshold = Float64(1e-6), minimum_acceptable_pivot = Float64(1e-6), drop_tolerance = Float64(1e-14), use_scaling = true, cost_scaling = var"GlopParameters.CostScalingAlgorithm".CONTAIN_ONE_COST_SCALING, initial_basis = var"GlopParameters.InitialBasisHeuristic".TRIANGULAR, use_transposed_matrix = true, basis_refactorization_period = Int32(64), dynamically_adjust_refactorization_period = true, solve_dual_problem = var"GlopParameters.SolverBehavior".LET_SOLVER_DECIDE, dualizer_threshold = Float64(1.5), solution_feasibility_tolerance = Float64(1e-6), provide_strong_optimal_guarantee = true, change_status_to_imprecise = true, max_number_of_reoptimizations = Float64(40), lu_factorization_pivot_threshold = Float64(0.01), max_time_in_seconds = Float64(Inf), max_deterministic_time = Float64(Inf), max_number_of_iterations = Int64(-1), markowitz_zlatev_parameter = Int32(3), markowitz_singularity_threshold = Float64(1e-15), use_dual_simplex = false, allow_simplex_algorithm_change = false, devex_weights_reset_period = Int32(150), use_preprocessing = true, use_middle_product_form_update = true, initialize_devex_with_column_norms = true, exploit_singleton_column_in_initial_basis = true, dual_small_pivot_threshold = Float64(1e-4), preprocessor_zero_tolerance = Float64(1e-9), objective_lower_limit = Float64(-Inf), objective_upper_limit = Float64(Inf), degenerate_ministep_factor = Float64(0.01), random_seed = Int32(1), num_omp_threads = Int32(1), perturb_costs_in_dual_simplex = false, use_dedicated_dual_feasibility_algorithm = true, relative_cost_perturbation = Float64(1e-5), relative_max_cost_perturbation = Float64(1e-7), initial_condition_number_threshold = Float64(1e50), log_search_progress = false, log_to_stdout = true, crossover_bound_snapping_distance = Float64(Inf), push_to_vertex = true, use_implied_free_preprocessor = true, max_valid_magnitude = Float64(1e30), dual_price_prioritize_norm = false) -PB.field_numbers(::Type{GlopParameters}) = (;scaling_method = 57, feasibility_rule = 1, optimization_rule = 2, refactorization_threshold = 6, recompute_reduced_costs_threshold = 8, recompute_edges_norm_threshold = 9, primal_feasibility_tolerance = 10, dual_feasibility_tolerance = 11, ratio_test_zero_threshold = 12, harris_tolerance_ratio = 13, small_pivot_threshold = 14, minimum_acceptable_pivot = 15, drop_tolerance = 52, use_scaling = 16, cost_scaling = 60, initial_basis = 17, use_transposed_matrix = 18, basis_refactorization_period = 19, dynamically_adjust_refactorization_period = 63, solve_dual_problem = 20, dualizer_threshold = 21, solution_feasibility_tolerance = 22, provide_strong_optimal_guarantee = 24, change_status_to_imprecise = 58, max_number_of_reoptimizations = 56, lu_factorization_pivot_threshold = 25, max_time_in_seconds = 26, max_deterministic_time = 45, max_number_of_iterations = 27, markowitz_zlatev_parameter = 29, markowitz_singularity_threshold = 30, use_dual_simplex = 31, allow_simplex_algorithm_change = 32, devex_weights_reset_period = 33, use_preprocessing = 34, use_middle_product_form_update = 35, initialize_devex_with_column_norms = 36, exploit_singleton_column_in_initial_basis = 37, dual_small_pivot_threshold = 38, preprocessor_zero_tolerance = 39, objective_lower_limit = 40, objective_upper_limit = 41, degenerate_ministep_factor = 42, random_seed = 43, num_omp_threads = 44, perturb_costs_in_dual_simplex = 53, use_dedicated_dual_feasibility_algorithm = 62, relative_cost_perturbation = 54, relative_max_cost_perturbation = 55, initial_condition_number_threshold = 59, log_search_progress = 61, log_to_stdout = 66, crossover_bound_snapping_distance = 64, push_to_vertex = 65, use_implied_free_preprocessor = 67, max_valid_magnitude = 199, dual_price_prioritize_norm = 69) +PB.default_values(::Type{GlopParameters}) = (;scaling_method = var"GlopParameters.ScalingAlgorithm".EQUILIBRATION, feasibility_rule = var"GlopParameters.PricingRule".STEEPEST_EDGE, optimization_rule = var"GlopParameters.PricingRule".STEEPEST_EDGE, refactorization_threshold = Float64(1e-9), recompute_reduced_costs_threshold = Float64(1e-8), recompute_edges_norm_threshold = Float64(100.0), primal_feasibility_tolerance = Float64(1e-8), dual_feasibility_tolerance = Float64(1e-8), ratio_test_zero_threshold = Float64(1e-9), harris_tolerance_ratio = Float64(0.5), small_pivot_threshold = Float64(1e-6), minimum_acceptable_pivot = Float64(1e-6), drop_tolerance = Float64(1e-14), use_scaling = true, cost_scaling = var"GlopParameters.CostScalingAlgorithm".CONTAIN_ONE_COST_SCALING, initial_basis = var"GlopParameters.InitialBasisHeuristic".TRIANGULAR, use_transposed_matrix = true, basis_refactorization_period = Int32(64), dynamically_adjust_refactorization_period = true, solve_dual_problem = var"GlopParameters.SolverBehavior".LET_SOLVER_DECIDE, dualizer_threshold = Float64(1.5), solution_feasibility_tolerance = Float64(1e-6), provide_strong_optimal_guarantee = true, change_status_to_imprecise = true, max_number_of_reoptimizations = Float64(40), lu_factorization_pivot_threshold = Float64(0.01), max_time_in_seconds = Float64(Inf), max_deterministic_time = Float64(Inf), max_number_of_iterations = Int64(-1), markowitz_zlatev_parameter = Int32(3), markowitz_singularity_threshold = Float64(1e-15), use_dual_simplex = false, allow_simplex_algorithm_change = false, devex_weights_reset_period = Int32(150), use_preprocessing = true, use_middle_product_form_update = true, initialize_devex_with_column_norms = true, exploit_singleton_column_in_initial_basis = true, dual_small_pivot_threshold = Float64(1e-4), preprocessor_zero_tolerance = Float64(1e-9), objective_lower_limit = Float64(-Inf), objective_upper_limit = Float64(Inf), degenerate_ministep_factor = Float64(0.01), random_seed = Int32(1), use_absl_random = false, num_omp_threads = Int32(1), perturb_costs_in_dual_simplex = false, use_dedicated_dual_feasibility_algorithm = true, relative_cost_perturbation = Float64(1e-5), relative_max_cost_perturbation = Float64(1e-7), initial_condition_number_threshold = Float64(1e50), log_search_progress = false, log_to_stdout = true, crossover_bound_snapping_distance = Float64(Inf), push_to_vertex = true, use_implied_free_preprocessor = true, max_valid_magnitude = Float64(1e30), drop_magnitude = Float64(1e-30), dual_price_prioritize_norm = false) +PB.field_numbers(::Type{GlopParameters}) = (;scaling_method = 57, feasibility_rule = 1, optimization_rule = 2, refactorization_threshold = 6, recompute_reduced_costs_threshold = 8, recompute_edges_norm_threshold = 9, primal_feasibility_tolerance = 10, dual_feasibility_tolerance = 11, ratio_test_zero_threshold = 12, harris_tolerance_ratio = 13, small_pivot_threshold = 14, minimum_acceptable_pivot = 15, drop_tolerance = 52, use_scaling = 16, cost_scaling = 60, initial_basis = 17, use_transposed_matrix = 18, basis_refactorization_period = 19, dynamically_adjust_refactorization_period = 63, solve_dual_problem = 20, dualizer_threshold = 21, solution_feasibility_tolerance = 22, provide_strong_optimal_guarantee = 24, change_status_to_imprecise = 58, max_number_of_reoptimizations = 56, lu_factorization_pivot_threshold = 25, max_time_in_seconds = 26, max_deterministic_time = 45, max_number_of_iterations = 27, markowitz_zlatev_parameter = 29, markowitz_singularity_threshold = 30, use_dual_simplex = 31, allow_simplex_algorithm_change = 32, devex_weights_reset_period = 33, use_preprocessing = 34, use_middle_product_form_update = 35, initialize_devex_with_column_norms = 36, exploit_singleton_column_in_initial_basis = 37, dual_small_pivot_threshold = 38, preprocessor_zero_tolerance = 39, objective_lower_limit = 40, objective_upper_limit = 41, degenerate_ministep_factor = 42, random_seed = 43, use_absl_random = 72, num_omp_threads = 44, perturb_costs_in_dual_simplex = 53, use_dedicated_dual_feasibility_algorithm = 62, relative_cost_perturbation = 54, relative_max_cost_perturbation = 55, initial_condition_number_threshold = 59, log_search_progress = 61, log_to_stdout = 66, crossover_bound_snapping_distance = 64, push_to_vertex = 65, use_implied_free_preprocessor = 67, max_valid_magnitude = 70, drop_magnitude = 71, dual_price_prioritize_norm = 69) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GlopParameters}) scaling_method = var"GlopParameters.ScalingAlgorithm".EQUILIBRATION @@ -126,6 +129,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GlopParameters}) objective_upper_limit = Float64(Inf) degenerate_ministep_factor = Float64(0.01) random_seed = Int32(1) + use_absl_random = false num_omp_threads = Int32(1) perturb_costs_in_dual_simplex = false use_dedicated_dual_feasibility_algorithm = true @@ -138,6 +142,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GlopParameters}) push_to_vertex = true use_implied_free_preprocessor = true max_valid_magnitude = Float64(1e30) + drop_magnitude = Float64(1e-30) dual_price_prioritize_norm = false while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) @@ -229,6 +234,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GlopParameters}) degenerate_ministep_factor = PB.decode(d, Float64) elseif field_number == 43 random_seed = PB.decode(d, Int32) + elseif field_number == 72 + use_absl_random = PB.decode(d, Bool) elseif field_number == 44 num_omp_threads = PB.decode(d, Int32) elseif field_number == 53 @@ -251,15 +258,17 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GlopParameters}) push_to_vertex = PB.decode(d, Bool) elseif field_number == 67 use_implied_free_preprocessor = PB.decode(d, Bool) - elseif field_number == 199 + elseif field_number == 70 max_valid_magnitude = PB.decode(d, Float64) + elseif field_number == 71 + drop_magnitude = PB.decode(d, Float64) elseif field_number == 69 dual_price_prioritize_norm = PB.decode(d, Bool) else PB.skip(d, wire_type) end end - return GlopParameters(scaling_method, feasibility_rule, optimization_rule, refactorization_threshold, recompute_reduced_costs_threshold, recompute_edges_norm_threshold, primal_feasibility_tolerance, dual_feasibility_tolerance, ratio_test_zero_threshold, harris_tolerance_ratio, small_pivot_threshold, minimum_acceptable_pivot, drop_tolerance, use_scaling, cost_scaling, initial_basis, use_transposed_matrix, basis_refactorization_period, dynamically_adjust_refactorization_period, solve_dual_problem, dualizer_threshold, solution_feasibility_tolerance, provide_strong_optimal_guarantee, change_status_to_imprecise, max_number_of_reoptimizations, lu_factorization_pivot_threshold, max_time_in_seconds, max_deterministic_time, max_number_of_iterations, markowitz_zlatev_parameter, markowitz_singularity_threshold, use_dual_simplex, allow_simplex_algorithm_change, devex_weights_reset_period, use_preprocessing, use_middle_product_form_update, initialize_devex_with_column_norms, exploit_singleton_column_in_initial_basis, dual_small_pivot_threshold, preprocessor_zero_tolerance, objective_lower_limit, objective_upper_limit, degenerate_ministep_factor, random_seed, num_omp_threads, perturb_costs_in_dual_simplex, use_dedicated_dual_feasibility_algorithm, relative_cost_perturbation, relative_max_cost_perturbation, initial_condition_number_threshold, log_search_progress, log_to_stdout, crossover_bound_snapping_distance, push_to_vertex, use_implied_free_preprocessor, max_valid_magnitude, dual_price_prioritize_norm) + return GlopParameters(scaling_method, feasibility_rule, optimization_rule, refactorization_threshold, recompute_reduced_costs_threshold, recompute_edges_norm_threshold, primal_feasibility_tolerance, dual_feasibility_tolerance, ratio_test_zero_threshold, harris_tolerance_ratio, small_pivot_threshold, minimum_acceptable_pivot, drop_tolerance, use_scaling, cost_scaling, initial_basis, use_transposed_matrix, basis_refactorization_period, dynamically_adjust_refactorization_period, solve_dual_problem, dualizer_threshold, solution_feasibility_tolerance, provide_strong_optimal_guarantee, change_status_to_imprecise, max_number_of_reoptimizations, lu_factorization_pivot_threshold, max_time_in_seconds, max_deterministic_time, max_number_of_iterations, markowitz_zlatev_parameter, markowitz_singularity_threshold, use_dual_simplex, allow_simplex_algorithm_change, devex_weights_reset_period, use_preprocessing, use_middle_product_form_update, initialize_devex_with_column_norms, exploit_singleton_column_in_initial_basis, dual_small_pivot_threshold, preprocessor_zero_tolerance, objective_lower_limit, objective_upper_limit, degenerate_ministep_factor, random_seed, use_absl_random, num_omp_threads, perturb_costs_in_dual_simplex, use_dedicated_dual_feasibility_algorithm, relative_cost_perturbation, relative_max_cost_perturbation, initial_condition_number_threshold, log_search_progress, log_to_stdout, crossover_bound_snapping_distance, push_to_vertex, use_implied_free_preprocessor, max_valid_magnitude, drop_magnitude, dual_price_prioritize_norm) end function PB.encode(e::PB.AbstractProtoEncoder, x::GlopParameters) @@ -267,16 +276,16 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::GlopParameters) x.scaling_method != var"GlopParameters.ScalingAlgorithm".EQUILIBRATION && PB.encode(e, 57, x.scaling_method) x.feasibility_rule != var"GlopParameters.PricingRule".STEEPEST_EDGE && PB.encode(e, 1, x.feasibility_rule) x.optimization_rule != var"GlopParameters.PricingRule".STEEPEST_EDGE && PB.encode(e, 2, x.optimization_rule) - x.refactorization_threshold != Float64(1e-9) && PB.encode(e, 6, x.refactorization_threshold) - x.recompute_reduced_costs_threshold != Float64(1e-8) && PB.encode(e, 8, x.recompute_reduced_costs_threshold) - x.recompute_edges_norm_threshold != Float64(100.0) && PB.encode(e, 9, x.recompute_edges_norm_threshold) - x.primal_feasibility_tolerance != Float64(1e-8) && PB.encode(e, 10, x.primal_feasibility_tolerance) - x.dual_feasibility_tolerance != Float64(1e-8) && PB.encode(e, 11, x.dual_feasibility_tolerance) - x.ratio_test_zero_threshold != Float64(1e-9) && PB.encode(e, 12, x.ratio_test_zero_threshold) - x.harris_tolerance_ratio != Float64(0.5) && PB.encode(e, 13, x.harris_tolerance_ratio) - x.small_pivot_threshold != Float64(1e-6) && PB.encode(e, 14, x.small_pivot_threshold) - x.minimum_acceptable_pivot != Float64(1e-6) && PB.encode(e, 15, x.minimum_acceptable_pivot) - x.drop_tolerance != Float64(1e-14) && PB.encode(e, 52, x.drop_tolerance) + x.refactorization_threshold !== Float64(1e-9) && PB.encode(e, 6, x.refactorization_threshold) + x.recompute_reduced_costs_threshold !== Float64(1e-8) && PB.encode(e, 8, x.recompute_reduced_costs_threshold) + x.recompute_edges_norm_threshold !== Float64(100.0) && PB.encode(e, 9, x.recompute_edges_norm_threshold) + x.primal_feasibility_tolerance !== Float64(1e-8) && PB.encode(e, 10, x.primal_feasibility_tolerance) + x.dual_feasibility_tolerance !== Float64(1e-8) && PB.encode(e, 11, x.dual_feasibility_tolerance) + x.ratio_test_zero_threshold !== Float64(1e-9) && PB.encode(e, 12, x.ratio_test_zero_threshold) + x.harris_tolerance_ratio !== Float64(0.5) && PB.encode(e, 13, x.harris_tolerance_ratio) + x.small_pivot_threshold !== Float64(1e-6) && PB.encode(e, 14, x.small_pivot_threshold) + x.minimum_acceptable_pivot !== Float64(1e-6) && PB.encode(e, 15, x.minimum_acceptable_pivot) + x.drop_tolerance !== Float64(1e-14) && PB.encode(e, 52, x.drop_tolerance) x.use_scaling != true && PB.encode(e, 16, x.use_scaling) x.cost_scaling != var"GlopParameters.CostScalingAlgorithm".CONTAIN_ONE_COST_SCALING && PB.encode(e, 60, x.cost_scaling) x.initial_basis != var"GlopParameters.InitialBasisHeuristic".TRIANGULAR && PB.encode(e, 17, x.initial_basis) @@ -284,17 +293,17 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::GlopParameters) x.basis_refactorization_period != Int32(64) && PB.encode(e, 19, x.basis_refactorization_period) x.dynamically_adjust_refactorization_period != true && PB.encode(e, 63, x.dynamically_adjust_refactorization_period) x.solve_dual_problem != var"GlopParameters.SolverBehavior".LET_SOLVER_DECIDE && PB.encode(e, 20, x.solve_dual_problem) - x.dualizer_threshold != Float64(1.5) && PB.encode(e, 21, x.dualizer_threshold) - x.solution_feasibility_tolerance != Float64(1e-6) && PB.encode(e, 22, x.solution_feasibility_tolerance) + x.dualizer_threshold !== Float64(1.5) && PB.encode(e, 21, x.dualizer_threshold) + x.solution_feasibility_tolerance !== Float64(1e-6) && PB.encode(e, 22, x.solution_feasibility_tolerance) x.provide_strong_optimal_guarantee != true && PB.encode(e, 24, x.provide_strong_optimal_guarantee) x.change_status_to_imprecise != true && PB.encode(e, 58, x.change_status_to_imprecise) - x.max_number_of_reoptimizations != Float64(40) && PB.encode(e, 56, x.max_number_of_reoptimizations) - x.lu_factorization_pivot_threshold != Float64(0.01) && PB.encode(e, 25, x.lu_factorization_pivot_threshold) - x.max_time_in_seconds != Float64(Inf) && PB.encode(e, 26, x.max_time_in_seconds) - x.max_deterministic_time != Float64(Inf) && PB.encode(e, 45, x.max_deterministic_time) + x.max_number_of_reoptimizations !== Float64(40) && PB.encode(e, 56, x.max_number_of_reoptimizations) + x.lu_factorization_pivot_threshold !== Float64(0.01) && PB.encode(e, 25, x.lu_factorization_pivot_threshold) + x.max_time_in_seconds !== Float64(Inf) && PB.encode(e, 26, x.max_time_in_seconds) + x.max_deterministic_time !== Float64(Inf) && PB.encode(e, 45, x.max_deterministic_time) x.max_number_of_iterations != Int64(-1) && PB.encode(e, 27, x.max_number_of_iterations) x.markowitz_zlatev_parameter != Int32(3) && PB.encode(e, 29, x.markowitz_zlatev_parameter) - x.markowitz_singularity_threshold != Float64(1e-15) && PB.encode(e, 30, x.markowitz_singularity_threshold) + x.markowitz_singularity_threshold !== Float64(1e-15) && PB.encode(e, 30, x.markowitz_singularity_threshold) x.use_dual_simplex != false && PB.encode(e, 31, x.use_dual_simplex) x.allow_simplex_algorithm_change != false && PB.encode(e, 32, x.allow_simplex_algorithm_change) x.devex_weights_reset_period != Int32(150) && PB.encode(e, 33, x.devex_weights_reset_period) @@ -302,24 +311,26 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::GlopParameters) x.use_middle_product_form_update != true && PB.encode(e, 35, x.use_middle_product_form_update) x.initialize_devex_with_column_norms != true && PB.encode(e, 36, x.initialize_devex_with_column_norms) x.exploit_singleton_column_in_initial_basis != true && PB.encode(e, 37, x.exploit_singleton_column_in_initial_basis) - x.dual_small_pivot_threshold != Float64(1e-4) && PB.encode(e, 38, x.dual_small_pivot_threshold) - x.preprocessor_zero_tolerance != Float64(1e-9) && PB.encode(e, 39, x.preprocessor_zero_tolerance) - x.objective_lower_limit != Float64(-Inf) && PB.encode(e, 40, x.objective_lower_limit) - x.objective_upper_limit != Float64(Inf) && PB.encode(e, 41, x.objective_upper_limit) - x.degenerate_ministep_factor != Float64(0.01) && PB.encode(e, 42, x.degenerate_ministep_factor) + x.dual_small_pivot_threshold !== Float64(1e-4) && PB.encode(e, 38, x.dual_small_pivot_threshold) + x.preprocessor_zero_tolerance !== Float64(1e-9) && PB.encode(e, 39, x.preprocessor_zero_tolerance) + x.objective_lower_limit !== Float64(-Inf) && PB.encode(e, 40, x.objective_lower_limit) + x.objective_upper_limit !== Float64(Inf) && PB.encode(e, 41, x.objective_upper_limit) + x.degenerate_ministep_factor !== Float64(0.01) && PB.encode(e, 42, x.degenerate_ministep_factor) x.random_seed != Int32(1) && PB.encode(e, 43, x.random_seed) + x.use_absl_random != false && PB.encode(e, 72, x.use_absl_random) x.num_omp_threads != Int32(1) && PB.encode(e, 44, x.num_omp_threads) x.perturb_costs_in_dual_simplex != false && PB.encode(e, 53, x.perturb_costs_in_dual_simplex) x.use_dedicated_dual_feasibility_algorithm != true && PB.encode(e, 62, x.use_dedicated_dual_feasibility_algorithm) - x.relative_cost_perturbation != Float64(1e-5) && PB.encode(e, 54, x.relative_cost_perturbation) - x.relative_max_cost_perturbation != Float64(1e-7) && PB.encode(e, 55, x.relative_max_cost_perturbation) - x.initial_condition_number_threshold != Float64(1e50) && PB.encode(e, 59, x.initial_condition_number_threshold) + x.relative_cost_perturbation !== Float64(1e-5) && PB.encode(e, 54, x.relative_cost_perturbation) + x.relative_max_cost_perturbation !== Float64(1e-7) && PB.encode(e, 55, x.relative_max_cost_perturbation) + x.initial_condition_number_threshold !== Float64(1e50) && PB.encode(e, 59, x.initial_condition_number_threshold) x.log_search_progress != false && PB.encode(e, 61, x.log_search_progress) x.log_to_stdout != true && PB.encode(e, 66, x.log_to_stdout) - x.crossover_bound_snapping_distance != Float64(Inf) && PB.encode(e, 64, x.crossover_bound_snapping_distance) + x.crossover_bound_snapping_distance !== Float64(Inf) && PB.encode(e, 64, x.crossover_bound_snapping_distance) x.push_to_vertex != true && PB.encode(e, 65, x.push_to_vertex) x.use_implied_free_preprocessor != true && PB.encode(e, 67, x.use_implied_free_preprocessor) - x.max_valid_magnitude != Float64(1e30) && PB.encode(e, 199, x.max_valid_magnitude) + x.max_valid_magnitude !== Float64(1e30) && PB.encode(e, 70, x.max_valid_magnitude) + x.drop_magnitude !== Float64(1e-30) && PB.encode(e, 71, x.drop_magnitude) x.dual_price_prioritize_norm != false && PB.encode(e, 69, x.dual_price_prioritize_norm) return position(e.io) - initpos end @@ -328,16 +339,16 @@ function PB._encoded_size(x::GlopParameters) x.scaling_method != var"GlopParameters.ScalingAlgorithm".EQUILIBRATION && (encoded_size += PB._encoded_size(x.scaling_method, 57)) x.feasibility_rule != var"GlopParameters.PricingRule".STEEPEST_EDGE && (encoded_size += PB._encoded_size(x.feasibility_rule, 1)) x.optimization_rule != var"GlopParameters.PricingRule".STEEPEST_EDGE && (encoded_size += PB._encoded_size(x.optimization_rule, 2)) - x.refactorization_threshold != Float64(1e-9) && (encoded_size += PB._encoded_size(x.refactorization_threshold, 6)) - x.recompute_reduced_costs_threshold != Float64(1e-8) && (encoded_size += PB._encoded_size(x.recompute_reduced_costs_threshold, 8)) - x.recompute_edges_norm_threshold != Float64(100.0) && (encoded_size += PB._encoded_size(x.recompute_edges_norm_threshold, 9)) - x.primal_feasibility_tolerance != Float64(1e-8) && (encoded_size += PB._encoded_size(x.primal_feasibility_tolerance, 10)) - x.dual_feasibility_tolerance != Float64(1e-8) && (encoded_size += PB._encoded_size(x.dual_feasibility_tolerance, 11)) - x.ratio_test_zero_threshold != Float64(1e-9) && (encoded_size += PB._encoded_size(x.ratio_test_zero_threshold, 12)) - x.harris_tolerance_ratio != Float64(0.5) && (encoded_size += PB._encoded_size(x.harris_tolerance_ratio, 13)) - x.small_pivot_threshold != Float64(1e-6) && (encoded_size += PB._encoded_size(x.small_pivot_threshold, 14)) - x.minimum_acceptable_pivot != Float64(1e-6) && (encoded_size += PB._encoded_size(x.minimum_acceptable_pivot, 15)) - x.drop_tolerance != Float64(1e-14) && (encoded_size += PB._encoded_size(x.drop_tolerance, 52)) + x.refactorization_threshold !== Float64(1e-9) && (encoded_size += PB._encoded_size(x.refactorization_threshold, 6)) + x.recompute_reduced_costs_threshold !== Float64(1e-8) && (encoded_size += PB._encoded_size(x.recompute_reduced_costs_threshold, 8)) + x.recompute_edges_norm_threshold !== Float64(100.0) && (encoded_size += PB._encoded_size(x.recompute_edges_norm_threshold, 9)) + x.primal_feasibility_tolerance !== Float64(1e-8) && (encoded_size += PB._encoded_size(x.primal_feasibility_tolerance, 10)) + x.dual_feasibility_tolerance !== Float64(1e-8) && (encoded_size += PB._encoded_size(x.dual_feasibility_tolerance, 11)) + x.ratio_test_zero_threshold !== Float64(1e-9) && (encoded_size += PB._encoded_size(x.ratio_test_zero_threshold, 12)) + x.harris_tolerance_ratio !== Float64(0.5) && (encoded_size += PB._encoded_size(x.harris_tolerance_ratio, 13)) + x.small_pivot_threshold !== Float64(1e-6) && (encoded_size += PB._encoded_size(x.small_pivot_threshold, 14)) + x.minimum_acceptable_pivot !== Float64(1e-6) && (encoded_size += PB._encoded_size(x.minimum_acceptable_pivot, 15)) + x.drop_tolerance !== Float64(1e-14) && (encoded_size += PB._encoded_size(x.drop_tolerance, 52)) x.use_scaling != true && (encoded_size += PB._encoded_size(x.use_scaling, 16)) x.cost_scaling != var"GlopParameters.CostScalingAlgorithm".CONTAIN_ONE_COST_SCALING && (encoded_size += PB._encoded_size(x.cost_scaling, 60)) x.initial_basis != var"GlopParameters.InitialBasisHeuristic".TRIANGULAR && (encoded_size += PB._encoded_size(x.initial_basis, 17)) @@ -345,17 +356,17 @@ function PB._encoded_size(x::GlopParameters) x.basis_refactorization_period != Int32(64) && (encoded_size += PB._encoded_size(x.basis_refactorization_period, 19)) x.dynamically_adjust_refactorization_period != true && (encoded_size += PB._encoded_size(x.dynamically_adjust_refactorization_period, 63)) x.solve_dual_problem != var"GlopParameters.SolverBehavior".LET_SOLVER_DECIDE && (encoded_size += PB._encoded_size(x.solve_dual_problem, 20)) - x.dualizer_threshold != Float64(1.5) && (encoded_size += PB._encoded_size(x.dualizer_threshold, 21)) - x.solution_feasibility_tolerance != Float64(1e-6) && (encoded_size += PB._encoded_size(x.solution_feasibility_tolerance, 22)) + x.dualizer_threshold !== Float64(1.5) && (encoded_size += PB._encoded_size(x.dualizer_threshold, 21)) + x.solution_feasibility_tolerance !== Float64(1e-6) && (encoded_size += PB._encoded_size(x.solution_feasibility_tolerance, 22)) x.provide_strong_optimal_guarantee != true && (encoded_size += PB._encoded_size(x.provide_strong_optimal_guarantee, 24)) x.change_status_to_imprecise != true && (encoded_size += PB._encoded_size(x.change_status_to_imprecise, 58)) - x.max_number_of_reoptimizations != Float64(40) && (encoded_size += PB._encoded_size(x.max_number_of_reoptimizations, 56)) - x.lu_factorization_pivot_threshold != Float64(0.01) && (encoded_size += PB._encoded_size(x.lu_factorization_pivot_threshold, 25)) - x.max_time_in_seconds != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 26)) - x.max_deterministic_time != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 45)) + x.max_number_of_reoptimizations !== Float64(40) && (encoded_size += PB._encoded_size(x.max_number_of_reoptimizations, 56)) + x.lu_factorization_pivot_threshold !== Float64(0.01) && (encoded_size += PB._encoded_size(x.lu_factorization_pivot_threshold, 25)) + x.max_time_in_seconds !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 26)) + x.max_deterministic_time !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 45)) x.max_number_of_iterations != Int64(-1) && (encoded_size += PB._encoded_size(x.max_number_of_iterations, 27)) x.markowitz_zlatev_parameter != Int32(3) && (encoded_size += PB._encoded_size(x.markowitz_zlatev_parameter, 29)) - x.markowitz_singularity_threshold != Float64(1e-15) && (encoded_size += PB._encoded_size(x.markowitz_singularity_threshold, 30)) + x.markowitz_singularity_threshold !== Float64(1e-15) && (encoded_size += PB._encoded_size(x.markowitz_singularity_threshold, 30)) x.use_dual_simplex != false && (encoded_size += PB._encoded_size(x.use_dual_simplex, 31)) x.allow_simplex_algorithm_change != false && (encoded_size += PB._encoded_size(x.allow_simplex_algorithm_change, 32)) x.devex_weights_reset_period != Int32(150) && (encoded_size += PB._encoded_size(x.devex_weights_reset_period, 33)) @@ -363,24 +374,26 @@ function PB._encoded_size(x::GlopParameters) x.use_middle_product_form_update != true && (encoded_size += PB._encoded_size(x.use_middle_product_form_update, 35)) x.initialize_devex_with_column_norms != true && (encoded_size += PB._encoded_size(x.initialize_devex_with_column_norms, 36)) x.exploit_singleton_column_in_initial_basis != true && (encoded_size += PB._encoded_size(x.exploit_singleton_column_in_initial_basis, 37)) - x.dual_small_pivot_threshold != Float64(1e-4) && (encoded_size += PB._encoded_size(x.dual_small_pivot_threshold, 38)) - x.preprocessor_zero_tolerance != Float64(1e-9) && (encoded_size += PB._encoded_size(x.preprocessor_zero_tolerance, 39)) - x.objective_lower_limit != Float64(-Inf) && (encoded_size += PB._encoded_size(x.objective_lower_limit, 40)) - x.objective_upper_limit != Float64(Inf) && (encoded_size += PB._encoded_size(x.objective_upper_limit, 41)) - x.degenerate_ministep_factor != Float64(0.01) && (encoded_size += PB._encoded_size(x.degenerate_ministep_factor, 42)) + x.dual_small_pivot_threshold !== Float64(1e-4) && (encoded_size += PB._encoded_size(x.dual_small_pivot_threshold, 38)) + x.preprocessor_zero_tolerance !== Float64(1e-9) && (encoded_size += PB._encoded_size(x.preprocessor_zero_tolerance, 39)) + x.objective_lower_limit !== Float64(-Inf) && (encoded_size += PB._encoded_size(x.objective_lower_limit, 40)) + x.objective_upper_limit !== Float64(Inf) && (encoded_size += PB._encoded_size(x.objective_upper_limit, 41)) + x.degenerate_ministep_factor !== Float64(0.01) && (encoded_size += PB._encoded_size(x.degenerate_ministep_factor, 42)) x.random_seed != Int32(1) && (encoded_size += PB._encoded_size(x.random_seed, 43)) + x.use_absl_random != false && (encoded_size += PB._encoded_size(x.use_absl_random, 72)) x.num_omp_threads != Int32(1) && (encoded_size += PB._encoded_size(x.num_omp_threads, 44)) x.perturb_costs_in_dual_simplex != false && (encoded_size += PB._encoded_size(x.perturb_costs_in_dual_simplex, 53)) x.use_dedicated_dual_feasibility_algorithm != true && (encoded_size += PB._encoded_size(x.use_dedicated_dual_feasibility_algorithm, 62)) - x.relative_cost_perturbation != Float64(1e-5) && (encoded_size += PB._encoded_size(x.relative_cost_perturbation, 54)) - x.relative_max_cost_perturbation != Float64(1e-7) && (encoded_size += PB._encoded_size(x.relative_max_cost_perturbation, 55)) - x.initial_condition_number_threshold != Float64(1e50) && (encoded_size += PB._encoded_size(x.initial_condition_number_threshold, 59)) + x.relative_cost_perturbation !== Float64(1e-5) && (encoded_size += PB._encoded_size(x.relative_cost_perturbation, 54)) + x.relative_max_cost_perturbation !== Float64(1e-7) && (encoded_size += PB._encoded_size(x.relative_max_cost_perturbation, 55)) + x.initial_condition_number_threshold !== Float64(1e50) && (encoded_size += PB._encoded_size(x.initial_condition_number_threshold, 59)) x.log_search_progress != false && (encoded_size += PB._encoded_size(x.log_search_progress, 61)) x.log_to_stdout != true && (encoded_size += PB._encoded_size(x.log_to_stdout, 66)) - x.crossover_bound_snapping_distance != Float64(Inf) && (encoded_size += PB._encoded_size(x.crossover_bound_snapping_distance, 64)) + x.crossover_bound_snapping_distance !== Float64(Inf) && (encoded_size += PB._encoded_size(x.crossover_bound_snapping_distance, 64)) x.push_to_vertex != true && (encoded_size += PB._encoded_size(x.push_to_vertex, 65)) x.use_implied_free_preprocessor != true && (encoded_size += PB._encoded_size(x.use_implied_free_preprocessor, 67)) - x.max_valid_magnitude != Float64(1e30) && (encoded_size += PB._encoded_size(x.max_valid_magnitude, 199)) + x.max_valid_magnitude !== Float64(1e30) && (encoded_size += PB._encoded_size(x.max_valid_magnitude, 70)) + x.drop_magnitude !== Float64(1e-30) && (encoded_size += PB._encoded_size(x.drop_magnitude, 71)) x.dual_price_prioritize_norm != false && (encoded_size += PB._encoded_size(x.dual_price_prioritize_norm, 69)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/gscip_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/gscip_pb.jl index 6276faff955..6400cd87656 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/gscip_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/gscip_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.805 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/gscip/gscip.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.771 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/gscip/gscip.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export var"GScipParameters.MetaParamValue", var"GScipParameters.Emphasis" export GScipSolvingStats, var"GScipOutput.Status", GScipParameters, GScipOutput + @enumx var"GScipParameters.MetaParamValue" DEFAULT_META_PARAM_VALUE=0 AGGRESSIVE=1 FAST=2 OFF=3 @enumx var"GScipParameters.Emphasis" DEFAULT_EMPHASIS=0 COUNTER=1 CP_SOLVER=2 EASY_CIP=3 FEASIBILITY=4 HARD_LP=5 OPTIMALITY=6 PHASE_FEAS=7 PHASE_IMPROVE=8 PHASE_PROOF=9 @@ -17,20 +18,22 @@ struct GScipSolvingStats best_bound::Float64 primal_simplex_iterations::Int64 dual_simplex_iterations::Int64 + barrier_iterations::Int64 total_lp_iterations::Int64 node_count::Int64 first_lp_relaxation_bound::Float64 root_node_bound::Float64 deterministic_time::Float64 end -PB.default_values(::Type{GScipSolvingStats}) = (;best_objective = zero(Float64), best_bound = zero(Float64), primal_simplex_iterations = zero(Int64), dual_simplex_iterations = zero(Int64), total_lp_iterations = zero(Int64), node_count = zero(Int64), first_lp_relaxation_bound = zero(Float64), root_node_bound = zero(Float64), deterministic_time = zero(Float64)) -PB.field_numbers(::Type{GScipSolvingStats}) = (;best_objective = 1, best_bound = 2, primal_simplex_iterations = 3, dual_simplex_iterations = 4, total_lp_iterations = 5, node_count = 6, first_lp_relaxation_bound = 7, root_node_bound = 8, deterministic_time = 9) +PB.default_values(::Type{GScipSolvingStats}) = (;best_objective = zero(Float64), best_bound = zero(Float64), primal_simplex_iterations = zero(Int64), dual_simplex_iterations = zero(Int64), barrier_iterations = zero(Int64), total_lp_iterations = zero(Int64), node_count = zero(Int64), first_lp_relaxation_bound = zero(Float64), root_node_bound = zero(Float64), deterministic_time = zero(Float64)) +PB.field_numbers(::Type{GScipSolvingStats}) = (;best_objective = 1, best_bound = 2, primal_simplex_iterations = 3, dual_simplex_iterations = 4, barrier_iterations = 10, total_lp_iterations = 5, node_count = 6, first_lp_relaxation_bound = 7, root_node_bound = 8, deterministic_time = 9) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GScipSolvingStats}) best_objective = zero(Float64) best_bound = zero(Float64) primal_simplex_iterations = zero(Int64) dual_simplex_iterations = zero(Int64) + barrier_iterations = zero(Int64) total_lp_iterations = zero(Int64) node_count = zero(Int64) first_lp_relaxation_bound = zero(Float64) @@ -46,6 +49,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GScipSolvingStats}) primal_simplex_iterations = PB.decode(d, Int64) elseif field_number == 4 dual_simplex_iterations = PB.decode(d, Int64) + elseif field_number == 10 + barrier_iterations = PB.decode(d, Int64) elseif field_number == 5 total_lp_iterations = PB.decode(d, Int64) elseif field_number == 6 @@ -60,33 +65,35 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:GScipSolvingStats}) PB.skip(d, wire_type) end end - return GScipSolvingStats(best_objective, best_bound, primal_simplex_iterations, dual_simplex_iterations, total_lp_iterations, node_count, first_lp_relaxation_bound, root_node_bound, deterministic_time) + return GScipSolvingStats(best_objective, best_bound, primal_simplex_iterations, dual_simplex_iterations, barrier_iterations, total_lp_iterations, node_count, first_lp_relaxation_bound, root_node_bound, deterministic_time) end function PB.encode(e::PB.AbstractProtoEncoder, x::GScipSolvingStats) initpos = position(e.io) - x.best_objective != zero(Float64) && PB.encode(e, 1, x.best_objective) - x.best_bound != zero(Float64) && PB.encode(e, 2, x.best_bound) + x.best_objective !== zero(Float64) && PB.encode(e, 1, x.best_objective) + x.best_bound !== zero(Float64) && PB.encode(e, 2, x.best_bound) x.primal_simplex_iterations != zero(Int64) && PB.encode(e, 3, x.primal_simplex_iterations) x.dual_simplex_iterations != zero(Int64) && PB.encode(e, 4, x.dual_simplex_iterations) + x.barrier_iterations != zero(Int64) && PB.encode(e, 10, x.barrier_iterations) x.total_lp_iterations != zero(Int64) && PB.encode(e, 5, x.total_lp_iterations) x.node_count != zero(Int64) && PB.encode(e, 6, x.node_count) - x.first_lp_relaxation_bound != zero(Float64) && PB.encode(e, 7, x.first_lp_relaxation_bound) - x.root_node_bound != zero(Float64) && PB.encode(e, 8, x.root_node_bound) - x.deterministic_time != zero(Float64) && PB.encode(e, 9, x.deterministic_time) + x.first_lp_relaxation_bound !== zero(Float64) && PB.encode(e, 7, x.first_lp_relaxation_bound) + x.root_node_bound !== zero(Float64) && PB.encode(e, 8, x.root_node_bound) + x.deterministic_time !== zero(Float64) && PB.encode(e, 9, x.deterministic_time) return position(e.io) - initpos end function PB._encoded_size(x::GScipSolvingStats) encoded_size = 0 - x.best_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective, 1)) - x.best_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.best_bound, 2)) + x.best_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective, 1)) + x.best_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_bound, 2)) x.primal_simplex_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.primal_simplex_iterations, 3)) x.dual_simplex_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.dual_simplex_iterations, 4)) + x.barrier_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.barrier_iterations, 10)) x.total_lp_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.total_lp_iterations, 5)) x.node_count != zero(Int64) && (encoded_size += PB._encoded_size(x.node_count, 6)) - x.first_lp_relaxation_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.first_lp_relaxation_bound, 7)) - x.root_node_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.root_node_bound, 8)) - x.deterministic_time != zero(Float64) && (encoded_size += PB._encoded_size(x.deterministic_time, 9)) + x.first_lp_relaxation_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.first_lp_relaxation_bound, 7)) + x.root_node_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.root_node_bound, 8)) + x.deterministic_time !== zero(Float64) && (encoded_size += PB._encoded_size(x.deterministic_time, 9)) return encoded_size end @@ -198,7 +205,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::GScipParameters) !isempty(x.detailed_solving_stats_filename) && PB.encode(e, 15, x.detailed_solving_stats_filename) !isempty(x.scip_model_filename) && PB.encode(e, 16, x.scip_model_filename) x.num_solutions != zero(Int32) && PB.encode(e, 17, x.num_solutions) - x.objective_limit != zero(Float64) && PB.encode(e, 18, x.objective_limit) + x.objective_limit !== zero(Float64) && PB.encode(e, 18, x.objective_limit) return position(e.io) - initpos end function PB._encoded_size(x::GScipParameters) @@ -220,7 +227,7 @@ function PB._encoded_size(x::GScipParameters) !isempty(x.detailed_solving_stats_filename) && (encoded_size += PB._encoded_size(x.detailed_solving_stats_filename, 15)) !isempty(x.scip_model_filename) && (encoded_size += PB._encoded_size(x.scip_model_filename, 16)) x.num_solutions != zero(Int32) && (encoded_size += PB._encoded_size(x.num_solutions, 17)) - x.objective_limit != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_limit, 18)) + x.objective_limit !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_limit, 18)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/linear_solver_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/linear_solver_pb.jl index 3158e34374e..7896bf02cf3 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/linear_solver_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/linear_solver_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.922 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/linear_solver/linear_solver.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.864 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/linear_solver/linear_solver.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -14,6 +14,7 @@ export var"MPModelProto.Annotation.TargetType", MPArrayConstraint, MPIndicatorCo export MPSosConstraint, MPSolverCommonParameters, MPSolutionResponse, MPModelDeltaProto export var"MPModelProto.Annotation", MPGeneralConstraintProto, MPModelProto, MPModelRequest + struct MPQuadraticObjective qvar1_index::Vector{Int32} qvar2_index::Vector{Int32} @@ -108,8 +109,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPQuadraticConstraint) !isempty(x.qvar1_index) && PB.encode(e, 3, x.qvar1_index) !isempty(x.qvar2_index) && PB.encode(e, 4, x.qvar2_index) !isempty(x.qcoefficient) && PB.encode(e, 5, x.qcoefficient) - x.lower_bound != Float64(-Inf) && PB.encode(e, 6, x.lower_bound) - x.upper_bound != Float64(Inf) && PB.encode(e, 7, x.upper_bound) + x.lower_bound !== Float64(-Inf) && PB.encode(e, 6, x.lower_bound) + x.upper_bound !== Float64(Inf) && PB.encode(e, 7, x.upper_bound) return position(e.io) - initpos end function PB._encoded_size(x::MPQuadraticConstraint) @@ -119,8 +120,8 @@ function PB._encoded_size(x::MPQuadraticConstraint) !isempty(x.qvar1_index) && (encoded_size += PB._encoded_size(x.qvar1_index, 3)) !isempty(x.qvar2_index) && (encoded_size += PB._encoded_size(x.qvar2_index, 4)) !isempty(x.qcoefficient) && (encoded_size += PB._encoded_size(x.qcoefficient, 5)) - x.lower_bound != Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 6)) - x.upper_bound != Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 7)) + x.lower_bound !== Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 6)) + x.upper_bound !== Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 7)) return encoded_size end @@ -167,8 +168,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPConstraintProto) initpos = position(e.io) !isempty(x.var_index) && PB.encode(e, 6, x.var_index) !isempty(x.coefficient) && PB.encode(e, 7, x.coefficient) - x.lower_bound != Float64(-Inf) && PB.encode(e, 2, x.lower_bound) - x.upper_bound != Float64(Inf) && PB.encode(e, 3, x.upper_bound) + x.lower_bound !== Float64(-Inf) && PB.encode(e, 2, x.lower_bound) + x.upper_bound !== Float64(Inf) && PB.encode(e, 3, x.upper_bound) x.name != "" && PB.encode(e, 4, x.name) x.is_lazy != false && PB.encode(e, 5, x.is_lazy) return position(e.io) - initpos @@ -177,8 +178,8 @@ function PB._encoded_size(x::MPConstraintProto) encoded_size = 0 !isempty(x.var_index) && (encoded_size += PB._encoded_size(x.var_index, 6)) !isempty(x.coefficient) && (encoded_size += PB._encoded_size(x.coefficient, 7)) - x.lower_bound != Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 2)) - x.upper_bound != Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 3)) + x.lower_bound !== Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 2)) + x.upper_bound !== Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 3)) x.name != "" && (encoded_size += PB._encoded_size(x.name, 4)) x.is_lazy != false && (encoded_size += PB._encoded_size(x.is_lazy, 5)) return encoded_size @@ -205,12 +206,12 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::OptionalDouble) initpos = position(e.io) - x.value != zero(Float64) && PB.encode(e, 1, x.value) + x.value !== zero(Float64) && PB.encode(e, 1, x.value) return position(e.io) - initpos end function PB._encoded_size(x::OptionalDouble) encoded_size = 0 - x.value != zero(Float64) && (encoded_size += PB._encoded_size(x.value, 1)) + x.value !== zero(Float64) && (encoded_size += PB._encoded_size(x.value, 1)) return encoded_size end @@ -281,13 +282,13 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::MPSolution) initpos = position(e.io) - x.objective_value != zero(Float64) && PB.encode(e, 1, x.objective_value) + x.objective_value !== zero(Float64) && PB.encode(e, 1, x.objective_value) !isempty(x.variable_value) && PB.encode(e, 2, x.variable_value) return position(e.io) - initpos end function PB._encoded_size(x::MPSolution) encoded_size = 0 - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 1)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 1)) !isempty(x.variable_value) && (encoded_size += PB._encoded_size(x.variable_value, 2)) return encoded_size end @@ -322,14 +323,14 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::MPArrayWithConstantConstraint) initpos = position(e.io) !isempty(x.var_index) && PB.encode(e, 1, x.var_index) - x.constant != zero(Float64) && PB.encode(e, 2, x.constant) + x.constant !== zero(Float64) && PB.encode(e, 2, x.constant) x.resultant_var_index != zero(Int32) && PB.encode(e, 3, x.resultant_var_index) return position(e.io) - initpos end function PB._encoded_size(x::MPArrayWithConstantConstraint) encoded_size = 0 !isempty(x.var_index) && (encoded_size += PB._encoded_size(x.var_index, 1)) - x.constant != zero(Float64) && (encoded_size += PB._encoded_size(x.constant, 2)) + x.constant !== zero(Float64) && (encoded_size += PB._encoded_size(x.constant, 2)) x.resultant_var_index != zero(Int32) && (encoded_size += PB._encoded_size(x.resultant_var_index, 3)) return encoded_size end @@ -395,14 +396,14 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::MPSolveInfo) initpos = position(e.io) - x.solve_wall_time_seconds != zero(Float64) && PB.encode(e, 1, x.solve_wall_time_seconds) - x.solve_user_time_seconds != zero(Float64) && PB.encode(e, 2, x.solve_user_time_seconds) + x.solve_wall_time_seconds !== zero(Float64) && PB.encode(e, 1, x.solve_wall_time_seconds) + x.solve_user_time_seconds !== zero(Float64) && PB.encode(e, 2, x.solve_user_time_seconds) return position(e.io) - initpos end function PB._encoded_size(x::MPSolveInfo) encoded_size = 0 - x.solve_wall_time_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.solve_wall_time_seconds, 1)) - x.solve_user_time_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.solve_user_time_seconds, 2)) + x.solve_wall_time_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.solve_wall_time_seconds, 1)) + x.solve_user_time_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.solve_user_time_seconds, 2)) return encoded_size end @@ -447,9 +448,9 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::MPVariableProto) initpos = position(e.io) - x.lower_bound != Float64(-Inf) && PB.encode(e, 1, x.lower_bound) - x.upper_bound != Float64(Inf) && PB.encode(e, 2, x.upper_bound) - x.objective_coefficient != Float64(0.0) && PB.encode(e, 3, x.objective_coefficient) + x.lower_bound !== Float64(-Inf) && PB.encode(e, 1, x.lower_bound) + x.upper_bound !== Float64(Inf) && PB.encode(e, 2, x.upper_bound) + x.objective_coefficient !== Float64(0.0) && PB.encode(e, 3, x.objective_coefficient) x.is_integer != false && PB.encode(e, 4, x.is_integer) x.name != "" && PB.encode(e, 5, x.name) x.branching_priority != Int32(0) && PB.encode(e, 6, x.branching_priority) @@ -457,9 +458,9 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPVariableProto) end function PB._encoded_size(x::MPVariableProto) encoded_size = 0 - x.lower_bound != Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 1)) - x.upper_bound != Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 2)) - x.objective_coefficient != Float64(0.0) && (encoded_size += PB._encoded_size(x.objective_coefficient, 3)) + x.lower_bound !== Float64(-Inf) && (encoded_size += PB._encoded_size(x.lower_bound, 1)) + x.upper_bound !== Float64(Inf) && (encoded_size += PB._encoded_size(x.upper_bound, 2)) + x.objective_coefficient !== Float64(0.0) && (encoded_size += PB._encoded_size(x.objective_coefficient, 3)) x.is_integer != false && (encoded_size += PB._encoded_size(x.is_integer, 4)) x.name != "" && (encoded_size += PB._encoded_size(x.name, 5)) x.branching_priority != Int32(0) && (encoded_size += PB._encoded_size(x.branching_priority, 6)) @@ -707,8 +708,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPSolutionResponse) initpos = position(e.io) x.status != MPSolverResponseStatus.MPSOLVER_UNKNOWN_STATUS && PB.encode(e, 1, x.status) !isempty(x.status_str) && PB.encode(e, 7, x.status_str) - x.objective_value != zero(Float64) && PB.encode(e, 2, x.objective_value) - x.best_objective_bound != zero(Float64) && PB.encode(e, 5, x.best_objective_bound) + x.objective_value !== zero(Float64) && PB.encode(e, 2, x.objective_value) + x.best_objective_bound !== zero(Float64) && PB.encode(e, 5, x.best_objective_bound) !isempty(x.variable_value) && PB.encode(e, 3, x.variable_value) !isnothing(x.solve_info) && PB.encode(e, 10, x.solve_info) !isempty(x.solver_specific_info) && PB.encode(e, 11, x.solver_specific_info) @@ -721,8 +722,8 @@ function PB._encoded_size(x::MPSolutionResponse) encoded_size = 0 x.status != MPSolverResponseStatus.MPSOLVER_UNKNOWN_STATUS && (encoded_size += PB._encoded_size(x.status, 1)) !isempty(x.status_str) && (encoded_size += PB._encoded_size(x.status_str, 7)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) - x.best_objective_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective_bound, 5)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) + x.best_objective_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective_bound, 5)) !isempty(x.variable_value) && (encoded_size += PB._encoded_size(x.variable_value, 3)) !isnothing(x.solve_info) && (encoded_size += PB._encoded_size(x.solve_info, 10)) !isempty(x.solver_specific_info) && (encoded_size += PB._encoded_size(x.solver_specific_info, 11)) @@ -972,7 +973,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPModelProto) !isempty(x.constraint) && PB.encode(e, 4, x.constraint) !isempty(x.general_constraint) && PB.encode(e, 7, x.general_constraint) x.maximize != false && PB.encode(e, 1, x.maximize) - x.objective_offset != Float64(0.0) && PB.encode(e, 2, x.objective_offset) + x.objective_offset !== Float64(0.0) && PB.encode(e, 2, x.objective_offset) !isnothing(x.quadratic_objective) && PB.encode(e, 8, x.quadratic_objective) x.name != "" && PB.encode(e, 5, x.name) !isnothing(x.solution_hint) && PB.encode(e, 6, x.solution_hint) @@ -985,7 +986,7 @@ function PB._encoded_size(x::MPModelProto) !isempty(x.constraint) && (encoded_size += PB._encoded_size(x.constraint, 4)) !isempty(x.general_constraint) && (encoded_size += PB._encoded_size(x.general_constraint, 7)) x.maximize != false && (encoded_size += PB._encoded_size(x.maximize, 1)) - x.objective_offset != Float64(0.0) && (encoded_size += PB._encoded_size(x.objective_offset, 2)) + x.objective_offset !== Float64(0.0) && (encoded_size += PB._encoded_size(x.objective_offset, 2)) !isnothing(x.quadratic_objective) && (encoded_size += PB._encoded_size(x.quadratic_objective, 8)) x.name != "" && (encoded_size += PB._encoded_size(x.name, 5)) !isnothing(x.solution_hint) && (encoded_size += PB._encoded_size(x.solution_hint, 6)) @@ -1044,7 +1045,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::MPModelRequest) initpos = position(e.io) !isnothing(x.model) && PB.encode(e, 1, x.model) x.solver_type != var"MPModelRequest.SolverType".GLOP_LINEAR_PROGRAMMING && PB.encode(e, 2, x.solver_type) - x.solver_time_limit_seconds != zero(Float64) && PB.encode(e, 3, x.solver_time_limit_seconds) + x.solver_time_limit_seconds !== zero(Float64) && PB.encode(e, 3, x.solver_time_limit_seconds) x.enable_internal_solver_output != false && PB.encode(e, 4, x.enable_internal_solver_output) !isempty(x.solver_specific_parameters) && PB.encode(e, 5, x.solver_specific_parameters) x.ignore_solver_specific_parameters_failure != false && PB.encode(e, 9, x.ignore_solver_specific_parameters_failure) @@ -1056,7 +1057,7 @@ function PB._encoded_size(x::MPModelRequest) encoded_size = 0 !isnothing(x.model) && (encoded_size += PB._encoded_size(x.model, 1)) x.solver_type != var"MPModelRequest.SolverType".GLOP_LINEAR_PROGRAMMING && (encoded_size += PB._encoded_size(x.solver_type, 2)) - x.solver_time_limit_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.solver_time_limit_seconds, 3)) + x.solver_time_limit_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.solver_time_limit_seconds, 3)) x.enable_internal_solver_output != false && (encoded_size += PB._encoded_size(x.enable_internal_solver_output, 4)) !isempty(x.solver_specific_parameters) && (encoded_size += PB._encoded_size(x.solver_specific_parameters, 5)) x.ignore_solver_specific_parameters_failure != false && (encoded_size += PB._encoded_size(x.ignore_solver_specific_parameters_failure, 9)) diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/callback_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/callback_pb.jl index 65128740e73..47e821fd0b0 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/callback_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/callback_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.188 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/callback.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.127 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/callback.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -11,6 +11,7 @@ export var"CallbackDataProto.MipStats", var"CallbackDataProto.BarrierStats" export var"CallbackDataProto.SimplexStats", CallbackResultProto, CallbackRegistrationProto export CallbackDataProto + struct var"CallbackDataProto.PresolveStats" removed_variables::Int64 removed_constraints::Int64 @@ -93,16 +94,16 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"CallbackResultProto.GeneratedLinearConstraint") initpos = position(e.io) !isnothing(x.linear_expression) && PB.encode(e, 1, x.linear_expression) - x.lower_bound != zero(Float64) && PB.encode(e, 2, x.lower_bound) - x.upper_bound != zero(Float64) && PB.encode(e, 3, x.upper_bound) + x.lower_bound !== zero(Float64) && PB.encode(e, 2, x.lower_bound) + x.upper_bound !== zero(Float64) && PB.encode(e, 3, x.upper_bound) x.is_lazy != false && PB.encode(e, 4, x.is_lazy) return position(e.io) - initpos end function PB._encoded_size(x::var"CallbackResultProto.GeneratedLinearConstraint") encoded_size = 0 !isnothing(x.linear_expression) && (encoded_size += PB._encoded_size(x.linear_expression, 1)) - x.lower_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 2)) - x.upper_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 3)) + x.lower_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 2)) + x.upper_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 3)) x.is_lazy != false && (encoded_size += PB._encoded_size(x.is_lazy, 4)) return encoded_size end @@ -154,8 +155,8 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"CallbackDataProto.MipStats") initpos = position(e.io) - x.primal_bound != zero(Float64) && PB.encode(e, 1, x.primal_bound) - x.dual_bound != zero(Float64) && PB.encode(e, 2, x.dual_bound) + x.primal_bound !== zero(Float64) && PB.encode(e, 1, x.primal_bound) + x.dual_bound !== zero(Float64) && PB.encode(e, 2, x.dual_bound) x.explored_nodes != zero(Int64) && PB.encode(e, 3, x.explored_nodes) x.open_nodes != zero(Int64) && PB.encode(e, 4, x.open_nodes) x.simplex_iterations != zero(Int64) && PB.encode(e, 5, x.simplex_iterations) @@ -165,8 +166,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::var"CallbackDataProto.MipStats end function PB._encoded_size(x::var"CallbackDataProto.MipStats") encoded_size = 0 - x.primal_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_bound, 1)) - x.dual_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_bound, 2)) + x.primal_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_bound, 1)) + x.dual_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_bound, 2)) x.explored_nodes != zero(Int64) && (encoded_size += PB._encoded_size(x.explored_nodes, 3)) x.open_nodes != zero(Int64) && (encoded_size += PB._encoded_size(x.open_nodes, 4)) x.simplex_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.simplex_iterations, 5)) @@ -217,21 +218,21 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"CallbackDataProto.BarrierStats") initpos = position(e.io) x.iteration_count != zero(Int32) && PB.encode(e, 1, x.iteration_count) - x.primal_objective != zero(Float64) && PB.encode(e, 2, x.primal_objective) - x.dual_objective != zero(Float64) && PB.encode(e, 3, x.dual_objective) - x.complementarity != zero(Float64) && PB.encode(e, 4, x.complementarity) - x.primal_infeasibility != zero(Float64) && PB.encode(e, 5, x.primal_infeasibility) - x.dual_infeasibility != zero(Float64) && PB.encode(e, 6, x.dual_infeasibility) + x.primal_objective !== zero(Float64) && PB.encode(e, 2, x.primal_objective) + x.dual_objective !== zero(Float64) && PB.encode(e, 3, x.dual_objective) + x.complementarity !== zero(Float64) && PB.encode(e, 4, x.complementarity) + x.primal_infeasibility !== zero(Float64) && PB.encode(e, 5, x.primal_infeasibility) + x.dual_infeasibility !== zero(Float64) && PB.encode(e, 6, x.dual_infeasibility) return position(e.io) - initpos end function PB._encoded_size(x::var"CallbackDataProto.BarrierStats") encoded_size = 0 x.iteration_count != zero(Int32) && (encoded_size += PB._encoded_size(x.iteration_count, 1)) - x.primal_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_objective, 2)) - x.dual_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_objective, 3)) - x.complementarity != zero(Float64) && (encoded_size += PB._encoded_size(x.complementarity, 4)) - x.primal_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_infeasibility, 5)) - x.dual_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_infeasibility, 6)) + x.primal_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_objective, 2)) + x.dual_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_objective, 3)) + x.complementarity !== zero(Float64) && (encoded_size += PB._encoded_size(x.complementarity, 4)) + x.primal_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_infeasibility, 5)) + x.dual_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_infeasibility, 6)) return encoded_size end @@ -273,18 +274,18 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"CallbackDataProto.SimplexStats") initpos = position(e.io) x.iteration_count != zero(Int64) && PB.encode(e, 1, x.iteration_count) - x.objective_value != zero(Float64) && PB.encode(e, 2, x.objective_value) - x.primal_infeasibility != zero(Float64) && PB.encode(e, 3, x.primal_infeasibility) - x.dual_infeasibility != zero(Float64) && PB.encode(e, 4, x.dual_infeasibility) + x.objective_value !== zero(Float64) && PB.encode(e, 2, x.objective_value) + x.primal_infeasibility !== zero(Float64) && PB.encode(e, 3, x.primal_infeasibility) + x.dual_infeasibility !== zero(Float64) && PB.encode(e, 4, x.dual_infeasibility) x.is_pertubated != false && PB.encode(e, 5, x.is_pertubated) return position(e.io) - initpos end function PB._encoded_size(x::var"CallbackDataProto.SimplexStats") encoded_size = 0 x.iteration_count != zero(Int64) && (encoded_size += PB._encoded_size(x.iteration_count, 1)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) - x.primal_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_infeasibility, 3)) - x.dual_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_infeasibility, 4)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) + x.primal_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_infeasibility, 3)) + x.dual_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_infeasibility, 4)) x.is_pertubated != false && (encoded_size += PB._encoded_size(x.is_pertubated, 5)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/glpk_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/glpk_pb.jl index 1a425d64e82..6d2197148e8 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/glpk_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/glpk_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.184 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/solvers/glpk.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.126 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/solvers/glpk.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export GlpkParametersProto + struct GlpkParametersProto compute_unbound_rays_if_possible::Bool end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/gurobi_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/gurobi_pb.jl index c81eaf64b5e..b015b845ec9 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/gurobi_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/gurobi_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.185 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/solvers/gurobi.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.126 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/solvers/gurobi.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export var"GurobiParametersProto.Parameter", var"GurobiInitializerProto.ISVKey" export GurobiParametersProto, GurobiInitializerProto + struct var"GurobiParametersProto.Parameter" name::String value::String diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/highs_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/highs_pb.jl index 35c123e5349..4620c531c6f 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/highs_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/highs_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.186 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/solvers/highs.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.126 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/solvers/highs.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export HighsOptionsProto + struct HighsOptionsProto string_options::Dict{String,String} double_options::Dict{String,Float64} diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/infeasible_subsystem_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/infeasible_subsystem_pb.jl index 3460b458195..45309daf637 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/infeasible_subsystem_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/infeasible_subsystem_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.231 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/infeasible_subsystem.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.130 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/infeasible_subsystem.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export var"ModelSubsetProto.Bounds", ModelSubsetProto export ComputeInfeasibleSubsystemResultProto + struct var"ModelSubsetProto.Bounds" lower::Bool upper::Bool diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/math_opt.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/math_opt.jl index 9ac1521b7f9..2dfbf58f3d7 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/math_opt.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/math_opt.jl @@ -3,6 +3,7 @@ module math_opt import ..google import ..operations_research +include("osqp_pb.jl") include("sparse_containers_pb.jl") include("glpk_pb.jl") include("gurobi_pb.jl") diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_parameters_pb.jl index 81824a80ee1..c779add365b 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_parameters_pb.jl @@ -1,11 +1,12 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.228 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/model_parameters.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.129 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/model_parameters.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf using ProtoBuf.EnumX: @enumx -export SolutionHintProto, ModelSolveParametersProto +export SolutionHintProto, ObjectiveParametersProto, ModelSolveParametersProto + struct SolutionHintProto variable_values::Union{Nothing,SparseDoubleVectorProto} @@ -43,30 +44,82 @@ function PB._encoded_size(x::SolutionHintProto) return encoded_size end +struct ObjectiveParametersProto + objective_degradation_absolute_tolerance::Float64 + objective_degradation_relative_tolerance::Float64 + time_limit::Union{Nothing,google.protobuf.Duration} +end +PB.default_values(::Type{ObjectiveParametersProto}) = (;objective_degradation_absolute_tolerance = zero(Float64), objective_degradation_relative_tolerance = zero(Float64), time_limit = nothing) +PB.field_numbers(::Type{ObjectiveParametersProto}) = (;objective_degradation_absolute_tolerance = 7, objective_degradation_relative_tolerance = 8, time_limit = 9) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:ObjectiveParametersProto}) + objective_degradation_absolute_tolerance = zero(Float64) + objective_degradation_relative_tolerance = zero(Float64) + time_limit = Ref{Union{Nothing,google.protobuf.Duration}}(nothing) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 7 + objective_degradation_absolute_tolerance = PB.decode(d, Float64) + elseif field_number == 8 + objective_degradation_relative_tolerance = PB.decode(d, Float64) + elseif field_number == 9 + PB.decode!(d, time_limit) + else + PB.skip(d, wire_type) + end + end + return ObjectiveParametersProto(objective_degradation_absolute_tolerance, objective_degradation_relative_tolerance, time_limit[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::ObjectiveParametersProto) + initpos = position(e.io) + x.objective_degradation_absolute_tolerance !== zero(Float64) && PB.encode(e, 7, x.objective_degradation_absolute_tolerance) + x.objective_degradation_relative_tolerance !== zero(Float64) && PB.encode(e, 8, x.objective_degradation_relative_tolerance) + !isnothing(x.time_limit) && PB.encode(e, 9, x.time_limit) + return position(e.io) - initpos +end +function PB._encoded_size(x::ObjectiveParametersProto) + encoded_size = 0 + x.objective_degradation_absolute_tolerance !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_degradation_absolute_tolerance, 7)) + x.objective_degradation_relative_tolerance !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_degradation_relative_tolerance, 8)) + !isnothing(x.time_limit) && (encoded_size += PB._encoded_size(x.time_limit, 9)) + return encoded_size +end + struct ModelSolveParametersProto variable_values_filter::Union{Nothing,SparseVectorFilterProto} dual_values_filter::Union{Nothing,SparseVectorFilterProto} + quadratic_dual_values_filter::Union{Nothing,SparseVectorFilterProto} reduced_costs_filter::Union{Nothing,SparseVectorFilterProto} initial_basis::Union{Nothing,BasisProto} solution_hints::Vector{SolutionHintProto} branching_priorities::Union{Nothing,SparseInt32VectorProto} + primary_objective_parameters::Union{Nothing,ObjectiveParametersProto} + auxiliary_objective_parameters::Dict{Int64,ObjectiveParametersProto} + lazy_linear_constraint_ids::Vector{Int64} end -PB.default_values(::Type{ModelSolveParametersProto}) = (;variable_values_filter = nothing, dual_values_filter = nothing, reduced_costs_filter = nothing, initial_basis = nothing, solution_hints = Vector{SolutionHintProto}(), branching_priorities = nothing) -PB.field_numbers(::Type{ModelSolveParametersProto}) = (;variable_values_filter = 1, dual_values_filter = 2, reduced_costs_filter = 3, initial_basis = 4, solution_hints = 5, branching_priorities = 6) +PB.default_values(::Type{ModelSolveParametersProto}) = (;variable_values_filter = nothing, dual_values_filter = nothing, quadratic_dual_values_filter = nothing, reduced_costs_filter = nothing, initial_basis = nothing, solution_hints = Vector{SolutionHintProto}(), branching_priorities = nothing, primary_objective_parameters = nothing, auxiliary_objective_parameters = Dict{Int64,ObjectiveParametersProto}(), lazy_linear_constraint_ids = Vector{Int64}()) +PB.field_numbers(::Type{ModelSolveParametersProto}) = (;variable_values_filter = 1, dual_values_filter = 2, quadratic_dual_values_filter = 10, reduced_costs_filter = 3, initial_basis = 4, solution_hints = 5, branching_priorities = 6, primary_objective_parameters = 7, auxiliary_objective_parameters = 8, lazy_linear_constraint_ids = 9) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:ModelSolveParametersProto}) variable_values_filter = Ref{Union{Nothing,SparseVectorFilterProto}}(nothing) dual_values_filter = Ref{Union{Nothing,SparseVectorFilterProto}}(nothing) + quadratic_dual_values_filter = Ref{Union{Nothing,SparseVectorFilterProto}}(nothing) reduced_costs_filter = Ref{Union{Nothing,SparseVectorFilterProto}}(nothing) initial_basis = Ref{Union{Nothing,BasisProto}}(nothing) solution_hints = PB.BufferedVector{SolutionHintProto}() branching_priorities = Ref{Union{Nothing,SparseInt32VectorProto}}(nothing) + primary_objective_parameters = Ref{Union{Nothing,ObjectiveParametersProto}}(nothing) + auxiliary_objective_parameters = Dict{Int64,ObjectiveParametersProto}() + lazy_linear_constraint_ids = PB.BufferedVector{Int64}() while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 PB.decode!(d, variable_values_filter) elseif field_number == 2 PB.decode!(d, dual_values_filter) + elseif field_number == 10 + PB.decode!(d, quadratic_dual_values_filter) elseif field_number == 3 PB.decode!(d, reduced_costs_filter) elseif field_number == 4 @@ -75,30 +128,44 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:ModelSolveParametersProt PB.decode!(d, solution_hints) elseif field_number == 6 PB.decode!(d, branching_priorities) + elseif field_number == 7 + PB.decode!(d, primary_objective_parameters) + elseif field_number == 8 + PB.decode!(d, auxiliary_objective_parameters) + elseif field_number == 9 + PB.decode!(d, wire_type, lazy_linear_constraint_ids) else PB.skip(d, wire_type) end end - return ModelSolveParametersProto(variable_values_filter[], dual_values_filter[], reduced_costs_filter[], initial_basis[], solution_hints[], branching_priorities[]) + return ModelSolveParametersProto(variable_values_filter[], dual_values_filter[], quadratic_dual_values_filter[], reduced_costs_filter[], initial_basis[], solution_hints[], branching_priorities[], primary_objective_parameters[], auxiliary_objective_parameters, lazy_linear_constraint_ids[]) end function PB.encode(e::PB.AbstractProtoEncoder, x::ModelSolveParametersProto) initpos = position(e.io) !isnothing(x.variable_values_filter) && PB.encode(e, 1, x.variable_values_filter) !isnothing(x.dual_values_filter) && PB.encode(e, 2, x.dual_values_filter) + !isnothing(x.quadratic_dual_values_filter) && PB.encode(e, 10, x.quadratic_dual_values_filter) !isnothing(x.reduced_costs_filter) && PB.encode(e, 3, x.reduced_costs_filter) !isnothing(x.initial_basis) && PB.encode(e, 4, x.initial_basis) !isempty(x.solution_hints) && PB.encode(e, 5, x.solution_hints) !isnothing(x.branching_priorities) && PB.encode(e, 6, x.branching_priorities) + !isnothing(x.primary_objective_parameters) && PB.encode(e, 7, x.primary_objective_parameters) + !isempty(x.auxiliary_objective_parameters) && PB.encode(e, 8, x.auxiliary_objective_parameters) + !isempty(x.lazy_linear_constraint_ids) && PB.encode(e, 9, x.lazy_linear_constraint_ids) return position(e.io) - initpos end function PB._encoded_size(x::ModelSolveParametersProto) encoded_size = 0 !isnothing(x.variable_values_filter) && (encoded_size += PB._encoded_size(x.variable_values_filter, 1)) !isnothing(x.dual_values_filter) && (encoded_size += PB._encoded_size(x.dual_values_filter, 2)) + !isnothing(x.quadratic_dual_values_filter) && (encoded_size += PB._encoded_size(x.quadratic_dual_values_filter, 10)) !isnothing(x.reduced_costs_filter) && (encoded_size += PB._encoded_size(x.reduced_costs_filter, 3)) !isnothing(x.initial_basis) && (encoded_size += PB._encoded_size(x.initial_basis, 4)) !isempty(x.solution_hints) && (encoded_size += PB._encoded_size(x.solution_hints, 5)) !isnothing(x.branching_priorities) && (encoded_size += PB._encoded_size(x.branching_priorities, 6)) + !isnothing(x.primary_objective_parameters) && (encoded_size += PB._encoded_size(x.primary_objective_parameters, 7)) + !isempty(x.auxiliary_objective_parameters) && (encoded_size += PB._encoded_size(x.auxiliary_objective_parameters, 8)) + !isempty(x.lazy_linear_constraint_ids) && (encoded_size += PB._encoded_size(x.lazy_linear_constraint_ids, 9)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_pb.jl index d5ccb3f9004..4fe0c42bf6a 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.189 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/model.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.128 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/model.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -9,6 +9,7 @@ export QuadraticConstraintProto, LinearConstraintsProto, SecondOrderConeConstrai export IndicatorConstraintProto, VariablesProto, SosConstraintProto, ObjectiveProto export ModelProto + struct QuadraticConstraintProto linear_terms::Union{Nothing,SparseDoubleVectorProto} quadratic_terms::Union{Nothing,SparseDoubleMatrixProto} @@ -48,8 +49,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::QuadraticConstraintProto) initpos = position(e.io) !isnothing(x.linear_terms) && PB.encode(e, 1, x.linear_terms) !isnothing(x.quadratic_terms) && PB.encode(e, 2, x.quadratic_terms) - x.lower_bound != zero(Float64) && PB.encode(e, 3, x.lower_bound) - x.upper_bound != zero(Float64) && PB.encode(e, 4, x.upper_bound) + x.lower_bound !== zero(Float64) && PB.encode(e, 3, x.lower_bound) + x.upper_bound !== zero(Float64) && PB.encode(e, 4, x.upper_bound) !isempty(x.name) && PB.encode(e, 5, x.name) return position(e.io) - initpos end @@ -57,8 +58,8 @@ function PB._encoded_size(x::QuadraticConstraintProto) encoded_size = 0 !isnothing(x.linear_terms) && (encoded_size += PB._encoded_size(x.linear_terms, 1)) !isnothing(x.quadratic_terms) && (encoded_size += PB._encoded_size(x.quadratic_terms, 2)) - x.lower_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 3)) - x.upper_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 4)) + x.lower_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 3)) + x.upper_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 4)) !isempty(x.name) && (encoded_size += PB._encoded_size(x.name, 5)) return encoded_size end @@ -197,8 +198,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::IndicatorConstraintProto) x.indicator_id != zero(Int64) && PB.encode(e, 1, x.indicator_id) x.activate_on_zero != false && PB.encode(e, 6, x.activate_on_zero) !isnothing(x.expression) && PB.encode(e, 2, x.expression) - x.lower_bound != zero(Float64) && PB.encode(e, 3, x.lower_bound) - x.upper_bound != zero(Float64) && PB.encode(e, 4, x.upper_bound) + x.lower_bound !== zero(Float64) && PB.encode(e, 3, x.lower_bound) + x.upper_bound !== zero(Float64) && PB.encode(e, 4, x.upper_bound) !isempty(x.name) && PB.encode(e, 5, x.name) return position(e.io) - initpos end @@ -207,8 +208,8 @@ function PB._encoded_size(x::IndicatorConstraintProto) x.indicator_id != zero(Int64) && (encoded_size += PB._encoded_size(x.indicator_id, 1)) x.activate_on_zero != false && (encoded_size += PB._encoded_size(x.activate_on_zero, 6)) !isnothing(x.expression) && (encoded_size += PB._encoded_size(x.expression, 2)) - x.lower_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 3)) - x.upper_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 4)) + x.lower_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.lower_bound, 3)) + x.upper_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.upper_bound, 4)) !isempty(x.name) && (encoded_size += PB._encoded_size(x.name, 5)) return encoded_size end @@ -351,7 +352,7 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::ObjectiveProto) initpos = position(e.io) x.maximize != false && PB.encode(e, 1, x.maximize) - x.offset != zero(Float64) && PB.encode(e, 2, x.offset) + x.offset !== zero(Float64) && PB.encode(e, 2, x.offset) !isnothing(x.linear_coefficients) && PB.encode(e, 3, x.linear_coefficients) !isnothing(x.quadratic_coefficients) && PB.encode(e, 4, x.quadratic_coefficients) !isempty(x.name) && PB.encode(e, 5, x.name) @@ -361,7 +362,7 @@ end function PB._encoded_size(x::ObjectiveProto) encoded_size = 0 x.maximize != false && (encoded_size += PB._encoded_size(x.maximize, 1)) - x.offset != zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 2)) + x.offset !== zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 2)) !isnothing(x.linear_coefficients) && (encoded_size += PB._encoded_size(x.linear_coefficients, 3)) !isnothing(x.quadratic_coefficients) && (encoded_size += PB._encoded_size(x.quadratic_coefficients, 4)) !isempty(x.name) && (encoded_size += PB._encoded_size(x.name, 5)) diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_update_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_update_pb.jl index 1224287a135..5145251f9c8 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_update_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/model_update_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.230 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/model_update.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.130 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/model_update.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -10,6 +10,7 @@ export VariableUpdatesProto, SosConstraintUpdatesProto export SecondOrderConeConstraintUpdatesProto, QuadraticConstraintUpdatesProto export AuxiliaryObjectivesUpdatesProto, ModelUpdateProto + struct LinearConstraintUpdatesProto lower_bounds::Union{Nothing,SparseDoubleVectorProto} upper_bounds::Union{Nothing,SparseDoubleVectorProto} @@ -84,7 +85,7 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::ObjectiveUpdatesProto) initpos = position(e.io) x.direction_update != false && PB.encode(e, 1, x.direction_update) - x.offset_update != zero(Float64) && PB.encode(e, 2, x.offset_update) + x.offset_update !== zero(Float64) && PB.encode(e, 2, x.offset_update) !isnothing(x.linear_coefficients) && PB.encode(e, 3, x.linear_coefficients) !isnothing(x.quadratic_coefficients) && PB.encode(e, 4, x.quadratic_coefficients) x.priority_update != zero(Int64) && PB.encode(e, 5, x.priority_update) @@ -93,7 +94,7 @@ end function PB._encoded_size(x::ObjectiveUpdatesProto) encoded_size = 0 x.direction_update != false && (encoded_size += PB._encoded_size(x.direction_update, 1)) - x.offset_update != zero(Float64) && (encoded_size += PB._encoded_size(x.offset_update, 2)) + x.offset_update !== zero(Float64) && (encoded_size += PB._encoded_size(x.offset_update, 2)) !isnothing(x.linear_coefficients) && (encoded_size += PB._encoded_size(x.linear_coefficients, 3)) !isnothing(x.quadratic_coefficients) && (encoded_size += PB._encoded_size(x.quadratic_coefficients, 4)) x.priority_update != zero(Int64) && (encoded_size += PB._encoded_size(x.priority_update, 5)) diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/osqp_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/osqp_pb.jl new file mode 100644 index 00000000000..ed12732e6b0 --- /dev/null +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/osqp_pb.jl @@ -0,0 +1,189 @@ +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.125 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/solvers/osqp.proto (proto3 syntax) + +import ProtoBuf as PB +using ProtoBuf: OneOf +using ProtoBuf.EnumX: @enumx + +export OsqpSettingsProto, OsqpOutput + + +struct OsqpSettingsProto + rho::Float64 + sigma::Float64 + scaling::Int64 + adaptive_rho::Bool + adaptive_rho_interval::Int64 + adaptive_rho_tolerance::Float64 + adaptive_rho_fraction::Float64 + max_iter::Int64 + eps_abs::Float64 + eps_rel::Float64 + eps_prim_inf::Float64 + eps_dual_inf::Float64 + alpha::Float64 + delta::Float64 + polish::Bool + polish_refine_iter::Int64 + verbose::Bool + scaled_termination::Bool + check_termination::Int64 + warm_start::Bool + time_limit::Float64 +end +PB.default_values(::Type{OsqpSettingsProto}) = (;rho = zero(Float64), sigma = zero(Float64), scaling = zero(Int64), adaptive_rho = false, adaptive_rho_interval = zero(Int64), adaptive_rho_tolerance = zero(Float64), adaptive_rho_fraction = zero(Float64), max_iter = zero(Int64), eps_abs = zero(Float64), eps_rel = zero(Float64), eps_prim_inf = zero(Float64), eps_dual_inf = zero(Float64), alpha = zero(Float64), delta = zero(Float64), polish = false, polish_refine_iter = zero(Int64), verbose = false, scaled_termination = false, check_termination = zero(Int64), warm_start = false, time_limit = zero(Float64)) +PB.field_numbers(::Type{OsqpSettingsProto}) = (;rho = 1, sigma = 2, scaling = 3, adaptive_rho = 4, adaptive_rho_interval = 5, adaptive_rho_tolerance = 6, adaptive_rho_fraction = 7, max_iter = 8, eps_abs = 9, eps_rel = 10, eps_prim_inf = 11, eps_dual_inf = 12, alpha = 13, delta = 14, polish = 15, polish_refine_iter = 16, verbose = 17, scaled_termination = 18, check_termination = 19, warm_start = 20, time_limit = 21) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:OsqpSettingsProto}) + rho = zero(Float64) + sigma = zero(Float64) + scaling = zero(Int64) + adaptive_rho = false + adaptive_rho_interval = zero(Int64) + adaptive_rho_tolerance = zero(Float64) + adaptive_rho_fraction = zero(Float64) + max_iter = zero(Int64) + eps_abs = zero(Float64) + eps_rel = zero(Float64) + eps_prim_inf = zero(Float64) + eps_dual_inf = zero(Float64) + alpha = zero(Float64) + delta = zero(Float64) + polish = false + polish_refine_iter = zero(Int64) + verbose = false + scaled_termination = false + check_termination = zero(Int64) + warm_start = false + time_limit = zero(Float64) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + rho = PB.decode(d, Float64) + elseif field_number == 2 + sigma = PB.decode(d, Float64) + elseif field_number == 3 + scaling = PB.decode(d, Int64) + elseif field_number == 4 + adaptive_rho = PB.decode(d, Bool) + elseif field_number == 5 + adaptive_rho_interval = PB.decode(d, Int64) + elseif field_number == 6 + adaptive_rho_tolerance = PB.decode(d, Float64) + elseif field_number == 7 + adaptive_rho_fraction = PB.decode(d, Float64) + elseif field_number == 8 + max_iter = PB.decode(d, Int64) + elseif field_number == 9 + eps_abs = PB.decode(d, Float64) + elseif field_number == 10 + eps_rel = PB.decode(d, Float64) + elseif field_number == 11 + eps_prim_inf = PB.decode(d, Float64) + elseif field_number == 12 + eps_dual_inf = PB.decode(d, Float64) + elseif field_number == 13 + alpha = PB.decode(d, Float64) + elseif field_number == 14 + delta = PB.decode(d, Float64) + elseif field_number == 15 + polish = PB.decode(d, Bool) + elseif field_number == 16 + polish_refine_iter = PB.decode(d, Int64) + elseif field_number == 17 + verbose = PB.decode(d, Bool) + elseif field_number == 18 + scaled_termination = PB.decode(d, Bool) + elseif field_number == 19 + check_termination = PB.decode(d, Int64) + elseif field_number == 20 + warm_start = PB.decode(d, Bool) + elseif field_number == 21 + time_limit = PB.decode(d, Float64) + else + PB.skip(d, wire_type) + end + end + return OsqpSettingsProto(rho, sigma, scaling, adaptive_rho, adaptive_rho_interval, adaptive_rho_tolerance, adaptive_rho_fraction, max_iter, eps_abs, eps_rel, eps_prim_inf, eps_dual_inf, alpha, delta, polish, polish_refine_iter, verbose, scaled_termination, check_termination, warm_start, time_limit) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::OsqpSettingsProto) + initpos = position(e.io) + x.rho !== zero(Float64) && PB.encode(e, 1, x.rho) + x.sigma !== zero(Float64) && PB.encode(e, 2, x.sigma) + x.scaling != zero(Int64) && PB.encode(e, 3, x.scaling) + x.adaptive_rho != false && PB.encode(e, 4, x.adaptive_rho) + x.adaptive_rho_interval != zero(Int64) && PB.encode(e, 5, x.adaptive_rho_interval) + x.adaptive_rho_tolerance !== zero(Float64) && PB.encode(e, 6, x.adaptive_rho_tolerance) + x.adaptive_rho_fraction !== zero(Float64) && PB.encode(e, 7, x.adaptive_rho_fraction) + x.max_iter != zero(Int64) && PB.encode(e, 8, x.max_iter) + x.eps_abs !== zero(Float64) && PB.encode(e, 9, x.eps_abs) + x.eps_rel !== zero(Float64) && PB.encode(e, 10, x.eps_rel) + x.eps_prim_inf !== zero(Float64) && PB.encode(e, 11, x.eps_prim_inf) + x.eps_dual_inf !== zero(Float64) && PB.encode(e, 12, x.eps_dual_inf) + x.alpha !== zero(Float64) && PB.encode(e, 13, x.alpha) + x.delta !== zero(Float64) && PB.encode(e, 14, x.delta) + x.polish != false && PB.encode(e, 15, x.polish) + x.polish_refine_iter != zero(Int64) && PB.encode(e, 16, x.polish_refine_iter) + x.verbose != false && PB.encode(e, 17, x.verbose) + x.scaled_termination != false && PB.encode(e, 18, x.scaled_termination) + x.check_termination != zero(Int64) && PB.encode(e, 19, x.check_termination) + x.warm_start != false && PB.encode(e, 20, x.warm_start) + x.time_limit !== zero(Float64) && PB.encode(e, 21, x.time_limit) + return position(e.io) - initpos +end +function PB._encoded_size(x::OsqpSettingsProto) + encoded_size = 0 + x.rho !== zero(Float64) && (encoded_size += PB._encoded_size(x.rho, 1)) + x.sigma !== zero(Float64) && (encoded_size += PB._encoded_size(x.sigma, 2)) + x.scaling != zero(Int64) && (encoded_size += PB._encoded_size(x.scaling, 3)) + x.adaptive_rho != false && (encoded_size += PB._encoded_size(x.adaptive_rho, 4)) + x.adaptive_rho_interval != zero(Int64) && (encoded_size += PB._encoded_size(x.adaptive_rho_interval, 5)) + x.adaptive_rho_tolerance !== zero(Float64) && (encoded_size += PB._encoded_size(x.adaptive_rho_tolerance, 6)) + x.adaptive_rho_fraction !== zero(Float64) && (encoded_size += PB._encoded_size(x.adaptive_rho_fraction, 7)) + x.max_iter != zero(Int64) && (encoded_size += PB._encoded_size(x.max_iter, 8)) + x.eps_abs !== zero(Float64) && (encoded_size += PB._encoded_size(x.eps_abs, 9)) + x.eps_rel !== zero(Float64) && (encoded_size += PB._encoded_size(x.eps_rel, 10)) + x.eps_prim_inf !== zero(Float64) && (encoded_size += PB._encoded_size(x.eps_prim_inf, 11)) + x.eps_dual_inf !== zero(Float64) && (encoded_size += PB._encoded_size(x.eps_dual_inf, 12)) + x.alpha !== zero(Float64) && (encoded_size += PB._encoded_size(x.alpha, 13)) + x.delta !== zero(Float64) && (encoded_size += PB._encoded_size(x.delta, 14)) + x.polish != false && (encoded_size += PB._encoded_size(x.polish, 15)) + x.polish_refine_iter != zero(Int64) && (encoded_size += PB._encoded_size(x.polish_refine_iter, 16)) + x.verbose != false && (encoded_size += PB._encoded_size(x.verbose, 17)) + x.scaled_termination != false && (encoded_size += PB._encoded_size(x.scaled_termination, 18)) + x.check_termination != zero(Int64) && (encoded_size += PB._encoded_size(x.check_termination, 19)) + x.warm_start != false && (encoded_size += PB._encoded_size(x.warm_start, 20)) + x.time_limit !== zero(Float64) && (encoded_size += PB._encoded_size(x.time_limit, 21)) + return encoded_size +end + +struct OsqpOutput + initialized_underlying_solver::Bool +end +PB.default_values(::Type{OsqpOutput}) = (;initialized_underlying_solver = false) +PB.field_numbers(::Type{OsqpOutput}) = (;initialized_underlying_solver = 1) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:OsqpOutput}) + initialized_underlying_solver = false + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + initialized_underlying_solver = PB.decode(d, Bool) + else + PB.skip(d, wire_type) + end + end + return OsqpOutput(initialized_underlying_solver) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::OsqpOutput) + initpos = position(e.io) + x.initialized_underlying_solver != false && PB.encode(e, 1, x.initialized_underlying_solver) + return position(e.io) - initpos +end +function PB._encoded_size(x::OsqpOutput) + encoded_size = 0 + x.initialized_underlying_solver != false && (encoded_size += PB._encoded_size(x.initialized_underlying_solver, 1)) + return encoded_size +end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/parameters_pb.jl index 99b4ff9c9c8..82737cba24c 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/parameters_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.191 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/parameters.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.128 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/parameters.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export EmphasisProto, StrictnessProto, SolverTypeProto, SolverInitializerProto export LPAlgorithmProto, SolveParametersProto + @enumx EmphasisProto EMPHASIS_UNSPECIFIED=0 EMPHASIS_OFF=1 EMPHASIS_LOW=2 EMPHASIS_MEDIUM=3 EMPHASIS_HIGH=4 EMPHASIS_VERY_HIGH=5 struct StrictnessProto @@ -40,12 +41,13 @@ function PB._encoded_size(x::StrictnessProto) return encoded_size end -@enumx SolverTypeProto SOLVER_TYPE_UNSPECIFIED=0 SOLVER_TYPE_GSCIP=1 SOLVER_TYPE_GUROBI=2 SOLVER_TYPE_GLOP=3 SOLVER_TYPE_CP_SAT=4 SOLVER_TYPE_GLPK=6 SOLVER_TYPE_ECOS=8 SOLVER_TYPE_SCS=9 SOLVER_TYPE_HIGHS=10 -PB.reserved_fields(::Type{SolverTypeProto.T}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[5, 7]) +@enumx SolverTypeProto SOLVER_TYPE_UNSPECIFIED=0 SOLVER_TYPE_GSCIP=1 SOLVER_TYPE_GUROBI=2 SOLVER_TYPE_GLOP=3 SOLVER_TYPE_CP_SAT=4 SOLVER_TYPE_PDLP=5 SOLVER_TYPE_GLPK=6 SOLVER_TYPE_OSQP=7 SOLVER_TYPE_ECOS=8 SOLVER_TYPE_SCS=9 SOLVER_TYPE_HIGHS=10 SOLVER_TYPE_SANTORINI=11 SOLVER_TYPE_XPRESS=13 +PB.reserved_fields(::Type{SolverTypeProto.T}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[12]) struct SolverInitializerProto gurobi::Union{Nothing,GurobiInitializerProto} end +PB.reserved_fields(::Type{SolverInitializerProto}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[2]) PB.default_values(::Type{SolverInitializerProto}) = (;gurobi = nothing) PB.field_numbers(::Type{SolverInitializerProto}) = (;gurobi = 1) @@ -98,12 +100,14 @@ struct SolveParametersProto gurobi::Union{Nothing,GurobiParametersProto} glop::Union{Nothing,operations_research.glop.GlopParameters} cp_sat::Union{Nothing,operations_research.sat.SatParameters} + pdlp::Union{Nothing,operations_research.pdlp.PrimalDualHybridGradientParams} + osqp::Union{Nothing,OsqpSettingsProto} glpk::Union{Nothing,GlpkParametersProto} highs::Union{Nothing,HighsOptionsProto} end -PB.reserved_fields(::Type{SolveParametersProto}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[16, 19, 11]) -PB.default_values(::Type{SolveParametersProto}) = (;time_limit = nothing, iteration_limit = zero(Int64), node_limit = zero(Int64), cutoff_limit = zero(Float64), objective_limit = zero(Float64), best_bound_limit = zero(Float64), solution_limit = zero(Int32), enable_output = false, threads = zero(Int32), random_seed = zero(Int32), absolute_gap_tolerance = zero(Float64), relative_gap_tolerance = zero(Float64), solution_pool_size = zero(Int32), lp_algorithm = LPAlgorithmProto.LP_ALGORITHM_UNSPECIFIED, presolve = EmphasisProto.EMPHASIS_UNSPECIFIED, cuts = EmphasisProto.EMPHASIS_UNSPECIFIED, heuristics = EmphasisProto.EMPHASIS_UNSPECIFIED, scaling = EmphasisProto.EMPHASIS_UNSPECIFIED, gscip = nothing, gurobi = nothing, glop = nothing, cp_sat = nothing, glpk = nothing, highs = nothing) -PB.field_numbers(::Type{SolveParametersProto}) = (;time_limit = 1, iteration_limit = 2, node_limit = 24, cutoff_limit = 20, objective_limit = 21, best_bound_limit = 22, solution_limit = 23, enable_output = 3, threads = 4, random_seed = 5, absolute_gap_tolerance = 18, relative_gap_tolerance = 17, solution_pool_size = 25, lp_algorithm = 6, presolve = 7, cuts = 8, heuristics = 9, scaling = 10, gscip = 12, gurobi = 13, glop = 14, cp_sat = 15, glpk = 26, highs = 27) +PB.reserved_fields(::Type{SolveParametersProto}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[11]) +PB.default_values(::Type{SolveParametersProto}) = (;time_limit = nothing, iteration_limit = zero(Int64), node_limit = zero(Int64), cutoff_limit = zero(Float64), objective_limit = zero(Float64), best_bound_limit = zero(Float64), solution_limit = zero(Int32), enable_output = false, threads = zero(Int32), random_seed = zero(Int32), absolute_gap_tolerance = zero(Float64), relative_gap_tolerance = zero(Float64), solution_pool_size = zero(Int32), lp_algorithm = LPAlgorithmProto.LP_ALGORITHM_UNSPECIFIED, presolve = EmphasisProto.EMPHASIS_UNSPECIFIED, cuts = EmphasisProto.EMPHASIS_UNSPECIFIED, heuristics = EmphasisProto.EMPHASIS_UNSPECIFIED, scaling = EmphasisProto.EMPHASIS_UNSPECIFIED, gscip = nothing, gurobi = nothing, glop = nothing, cp_sat = nothing, pdlp = nothing, osqp = nothing, glpk = nothing, highs = nothing) +PB.field_numbers(::Type{SolveParametersProto}) = (;time_limit = 1, iteration_limit = 2, node_limit = 24, cutoff_limit = 20, objective_limit = 21, best_bound_limit = 22, solution_limit = 23, enable_output = 3, threads = 4, random_seed = 5, absolute_gap_tolerance = 18, relative_gap_tolerance = 17, solution_pool_size = 25, lp_algorithm = 6, presolve = 7, cuts = 8, heuristics = 9, scaling = 10, gscip = 12, gurobi = 13, glop = 14, cp_sat = 15, pdlp = 16, osqp = 19, glpk = 26, highs = 27) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveParametersProto}) time_limit = Ref{Union{Nothing,google.protobuf.Duration}}(nothing) @@ -128,6 +132,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveParametersProto}) gurobi = Ref{Union{Nothing,GurobiParametersProto}}(nothing) glop = Ref{Union{Nothing,operations_research.glop.GlopParameters}}(nothing) cp_sat = Ref{Union{Nothing,operations_research.sat.SatParameters}}(nothing) + pdlp = Ref{Union{Nothing,operations_research.pdlp.PrimalDualHybridGradientParams}}(nothing) + osqp = Ref{Union{Nothing,OsqpSettingsProto}}(nothing) glpk = Ref{Union{Nothing,GlpkParametersProto}}(nothing) highs = Ref{Union{Nothing,HighsOptionsProto}}(nothing) while !PB.message_done(d) @@ -176,6 +182,10 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveParametersProto}) PB.decode!(d, glop) elseif field_number == 15 PB.decode!(d, cp_sat) + elseif field_number == 16 + PB.decode!(d, pdlp) + elseif field_number == 19 + PB.decode!(d, osqp) elseif field_number == 26 PB.decode!(d, glpk) elseif field_number == 27 @@ -184,7 +194,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveParametersProto}) PB.skip(d, wire_type) end end - return SolveParametersProto(time_limit[], iteration_limit, node_limit, cutoff_limit, objective_limit, best_bound_limit, solution_limit, enable_output, threads, random_seed, absolute_gap_tolerance, relative_gap_tolerance, solution_pool_size, lp_algorithm, presolve, cuts, heuristics, scaling, gscip[], gurobi[], glop[], cp_sat[], glpk[], highs[]) + return SolveParametersProto(time_limit[], iteration_limit, node_limit, cutoff_limit, objective_limit, best_bound_limit, solution_limit, enable_output, threads, random_seed, absolute_gap_tolerance, relative_gap_tolerance, solution_pool_size, lp_algorithm, presolve, cuts, heuristics, scaling, gscip[], gurobi[], glop[], cp_sat[], pdlp[], osqp[], glpk[], highs[]) end function PB.encode(e::PB.AbstractProtoEncoder, x::SolveParametersProto) @@ -192,15 +202,15 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SolveParametersProto) !isnothing(x.time_limit) && PB.encode(e, 1, x.time_limit) x.iteration_limit != zero(Int64) && PB.encode(e, 2, x.iteration_limit) x.node_limit != zero(Int64) && PB.encode(e, 24, x.node_limit) - x.cutoff_limit != zero(Float64) && PB.encode(e, 20, x.cutoff_limit) - x.objective_limit != zero(Float64) && PB.encode(e, 21, x.objective_limit) - x.best_bound_limit != zero(Float64) && PB.encode(e, 22, x.best_bound_limit) + x.cutoff_limit !== zero(Float64) && PB.encode(e, 20, x.cutoff_limit) + x.objective_limit !== zero(Float64) && PB.encode(e, 21, x.objective_limit) + x.best_bound_limit !== zero(Float64) && PB.encode(e, 22, x.best_bound_limit) x.solution_limit != zero(Int32) && PB.encode(e, 23, x.solution_limit) x.enable_output != false && PB.encode(e, 3, x.enable_output) x.threads != zero(Int32) && PB.encode(e, 4, x.threads) x.random_seed != zero(Int32) && PB.encode(e, 5, x.random_seed) - x.absolute_gap_tolerance != zero(Float64) && PB.encode(e, 18, x.absolute_gap_tolerance) - x.relative_gap_tolerance != zero(Float64) && PB.encode(e, 17, x.relative_gap_tolerance) + x.absolute_gap_tolerance !== zero(Float64) && PB.encode(e, 18, x.absolute_gap_tolerance) + x.relative_gap_tolerance !== zero(Float64) && PB.encode(e, 17, x.relative_gap_tolerance) x.solution_pool_size != zero(Int32) && PB.encode(e, 25, x.solution_pool_size) x.lp_algorithm != LPAlgorithmProto.LP_ALGORITHM_UNSPECIFIED && PB.encode(e, 6, x.lp_algorithm) x.presolve != EmphasisProto.EMPHASIS_UNSPECIFIED && PB.encode(e, 7, x.presolve) @@ -211,6 +221,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SolveParametersProto) !isnothing(x.gurobi) && PB.encode(e, 13, x.gurobi) !isnothing(x.glop) && PB.encode(e, 14, x.glop) !isnothing(x.cp_sat) && PB.encode(e, 15, x.cp_sat) + !isnothing(x.pdlp) && PB.encode(e, 16, x.pdlp) + !isnothing(x.osqp) && PB.encode(e, 19, x.osqp) !isnothing(x.glpk) && PB.encode(e, 26, x.glpk) !isnothing(x.highs) && PB.encode(e, 27, x.highs) return position(e.io) - initpos @@ -220,15 +232,15 @@ function PB._encoded_size(x::SolveParametersProto) !isnothing(x.time_limit) && (encoded_size += PB._encoded_size(x.time_limit, 1)) x.iteration_limit != zero(Int64) && (encoded_size += PB._encoded_size(x.iteration_limit, 2)) x.node_limit != zero(Int64) && (encoded_size += PB._encoded_size(x.node_limit, 24)) - x.cutoff_limit != zero(Float64) && (encoded_size += PB._encoded_size(x.cutoff_limit, 20)) - x.objective_limit != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_limit, 21)) - x.best_bound_limit != zero(Float64) && (encoded_size += PB._encoded_size(x.best_bound_limit, 22)) + x.cutoff_limit !== zero(Float64) && (encoded_size += PB._encoded_size(x.cutoff_limit, 20)) + x.objective_limit !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_limit, 21)) + x.best_bound_limit !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_bound_limit, 22)) x.solution_limit != zero(Int32) && (encoded_size += PB._encoded_size(x.solution_limit, 23)) x.enable_output != false && (encoded_size += PB._encoded_size(x.enable_output, 3)) x.threads != zero(Int32) && (encoded_size += PB._encoded_size(x.threads, 4)) x.random_seed != zero(Int32) && (encoded_size += PB._encoded_size(x.random_seed, 5)) - x.absolute_gap_tolerance != zero(Float64) && (encoded_size += PB._encoded_size(x.absolute_gap_tolerance, 18)) - x.relative_gap_tolerance != zero(Float64) && (encoded_size += PB._encoded_size(x.relative_gap_tolerance, 17)) + x.absolute_gap_tolerance !== zero(Float64) && (encoded_size += PB._encoded_size(x.absolute_gap_tolerance, 18)) + x.relative_gap_tolerance !== zero(Float64) && (encoded_size += PB._encoded_size(x.relative_gap_tolerance, 17)) x.solution_pool_size != zero(Int32) && (encoded_size += PB._encoded_size(x.solution_pool_size, 25)) x.lp_algorithm != LPAlgorithmProto.LP_ALGORITHM_UNSPECIFIED && (encoded_size += PB._encoded_size(x.lp_algorithm, 6)) x.presolve != EmphasisProto.EMPHASIS_UNSPECIFIED && (encoded_size += PB._encoded_size(x.presolve, 7)) @@ -239,6 +251,8 @@ function PB._encoded_size(x::SolveParametersProto) !isnothing(x.gurobi) && (encoded_size += PB._encoded_size(x.gurobi, 13)) !isnothing(x.glop) && (encoded_size += PB._encoded_size(x.glop, 14)) !isnothing(x.cp_sat) && (encoded_size += PB._encoded_size(x.cp_sat, 15)) + !isnothing(x.pdlp) && (encoded_size += PB._encoded_size(x.pdlp, 16)) + !isnothing(x.osqp) && (encoded_size += PB._encoded_size(x.osqp, 19)) !isnothing(x.glpk) && (encoded_size += PB._encoded_size(x.glpk, 26)) !isnothing(x.highs) && (encoded_size += PB._encoded_size(x.highs, 27)) return encoded_size diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/result_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/result_pb.jl index 4fc0e03aa00..f97ae4591b0 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/result_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/result_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.229 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/result.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.129 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/result.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -9,6 +9,7 @@ export var"SolveResultProto.PdlpOutput", ObjectiveBoundsProto, LimitProto export FeasibilityStatusProto, TerminationReasonProto, ProblemStatusProto, SolveStatsProto export TerminationProto, SolveResultProto + struct var"SolveResultProto.PdlpOutput" convergence_information::Union{Nothing,operations_research.pdlp.ConvergenceInformation} end @@ -64,14 +65,14 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::ObjectiveBoundsProto) initpos = position(e.io) - x.primal_bound != zero(Float64) && PB.encode(e, 2, x.primal_bound) - x.dual_bound != zero(Float64) && PB.encode(e, 3, x.dual_bound) + x.primal_bound !== zero(Float64) && PB.encode(e, 2, x.primal_bound) + x.dual_bound !== zero(Float64) && PB.encode(e, 3, x.dual_bound) return position(e.io) - initpos end function PB._encoded_size(x::ObjectiveBoundsProto) encoded_size = 0 - x.primal_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_bound, 2)) - x.dual_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_bound, 3)) + x.primal_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_bound, 2)) + x.dual_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_bound, 3)) return encoded_size end @@ -173,8 +174,8 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::SolveStatsProto) initpos = position(e.io) !isnothing(x.solve_time) && PB.encode(e, 1, x.solve_time) - x.best_primal_bound != zero(Float64) && PB.encode(e, 2, x.best_primal_bound) - x.best_dual_bound != zero(Float64) && PB.encode(e, 3, x.best_dual_bound) + x.best_primal_bound !== zero(Float64) && PB.encode(e, 2, x.best_primal_bound) + x.best_dual_bound !== zero(Float64) && PB.encode(e, 3, x.best_dual_bound) !isnothing(x.problem_status) && PB.encode(e, 4, x.problem_status) x.simplex_iterations != zero(Int64) && PB.encode(e, 5, x.simplex_iterations) x.barrier_iterations != zero(Int64) && PB.encode(e, 6, x.barrier_iterations) @@ -185,8 +186,8 @@ end function PB._encoded_size(x::SolveStatsProto) encoded_size = 0 !isnothing(x.solve_time) && (encoded_size += PB._encoded_size(x.solve_time, 1)) - x.best_primal_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.best_primal_bound, 2)) - x.best_dual_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.best_dual_bound, 3)) + x.best_primal_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_primal_bound, 2)) + x.best_dual_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_dual_bound, 3)) !isnothing(x.problem_status) && (encoded_size += PB._encoded_size(x.problem_status, 4)) x.simplex_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.simplex_iterations, 5)) x.barrier_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.barrier_iterations, 6)) @@ -255,14 +256,14 @@ struct SolveResultProto primal_rays::Vector{PrimalRayProto} dual_rays::Vector{DualRayProto} solve_stats::Union{Nothing,SolveStatsProto} - solver_specific_output::Union{Nothing,OneOf{<:Union{operations_research.GScipOutput,var"SolveResultProto.PdlpOutput"}}} + solver_specific_output::Union{Nothing,OneOf{<:Union{operations_research.GScipOutput,OsqpOutput,var"SolveResultProto.PdlpOutput"}}} end -PB.reserved_fields(::Type{SolveResultProto}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[8, 1]) +PB.reserved_fields(::Type{SolveResultProto}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[1]) PB.oneof_field_types(::Type{SolveResultProto}) = (; - solver_specific_output = (;gscip_output=operations_research.GScipOutput, pdlp_output=var"SolveResultProto.PdlpOutput"), + solver_specific_output = (;gscip_output=operations_research.GScipOutput, osqp_output=OsqpOutput, pdlp_output=var"SolveResultProto.PdlpOutput"), ) -PB.default_values(::Type{SolveResultProto}) = (;termination = nothing, solutions = Vector{SolutionProto}(), primal_rays = Vector{PrimalRayProto}(), dual_rays = Vector{DualRayProto}(), solve_stats = nothing, gscip_output = nothing, pdlp_output = nothing) -PB.field_numbers(::Type{SolveResultProto}) = (;termination = 2, solutions = 3, primal_rays = 4, dual_rays = 5, solve_stats = 6, gscip_output = 7, pdlp_output = 9) +PB.default_values(::Type{SolveResultProto}) = (;termination = nothing, solutions = Vector{SolutionProto}(), primal_rays = Vector{PrimalRayProto}(), dual_rays = Vector{DualRayProto}(), solve_stats = nothing, gscip_output = nothing, osqp_output = nothing, pdlp_output = nothing) +PB.field_numbers(::Type{SolveResultProto}) = (;termination = 2, solutions = 3, primal_rays = 4, dual_rays = 5, solve_stats = 6, gscip_output = 7, osqp_output = 8, pdlp_output = 9) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveResultProto}) termination = Ref{Union{Nothing,TerminationProto}}(nothing) @@ -285,6 +286,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SolveResultProto}) PB.decode!(d, solve_stats) elseif field_number == 7 solver_specific_output = OneOf(:gscip_output, PB.decode(d, Ref{operations_research.GScipOutput})) + elseif field_number == 8 + solver_specific_output = OneOf(:osqp_output, PB.decode(d, Ref{OsqpOutput})) elseif field_number == 9 solver_specific_output = OneOf(:pdlp_output, PB.decode(d, Ref{var"SolveResultProto.PdlpOutput"})) else @@ -304,6 +307,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SolveResultProto) if isnothing(x.solver_specific_output); elseif x.solver_specific_output.name === :gscip_output PB.encode(e, 7, x.solver_specific_output[]::operations_research.GScipOutput) + elseif x.solver_specific_output.name === :osqp_output + PB.encode(e, 8, x.solver_specific_output[]::OsqpOutput) elseif x.solver_specific_output.name === :pdlp_output PB.encode(e, 9, x.solver_specific_output[]::var"SolveResultProto.PdlpOutput") end @@ -319,6 +324,8 @@ function PB._encoded_size(x::SolveResultProto) if isnothing(x.solver_specific_output); elseif x.solver_specific_output.name === :gscip_output encoded_size += PB._encoded_size(x.solver_specific_output[]::operations_research.GScipOutput, 7) + elseif x.solver_specific_output.name === :osqp_output + encoded_size += PB._encoded_size(x.solver_specific_output[]::OsqpOutput, 8) elseif x.solver_specific_output.name === :pdlp_output encoded_size += PB._encoded_size(x.solver_specific_output[]::var"SolveResultProto.PdlpOutput", 9) end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/solution_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/solution_pb.jl index 7259b1e7ed1..c9159590d29 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/solution_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/solution_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.187 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/solution.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.127 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/solution.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -9,6 +9,7 @@ export PrimalRayProto, SolutionStatusProto, BasisStatusProto, DualRayProto export PrimalSolutionProto, DualSolutionProto, SparseBasisStatusVector, BasisProto export SolutionProto + struct PrimalRayProto variable_values::Union{Nothing,SparseDoubleVectorProto} end @@ -113,7 +114,7 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::PrimalSolutionProto) initpos = position(e.io) !isnothing(x.variable_values) && PB.encode(e, 1, x.variable_values) - x.objective_value != zero(Float64) && PB.encode(e, 2, x.objective_value) + x.objective_value !== zero(Float64) && PB.encode(e, 2, x.objective_value) !isempty(x.auxiliary_objective_values) && PB.encode(e, 4, x.auxiliary_objective_values) x.feasibility_status != SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED && PB.encode(e, 3, x.feasibility_status) return position(e.io) - initpos @@ -121,7 +122,7 @@ end function PB._encoded_size(x::PrimalSolutionProto) encoded_size = 0 !isnothing(x.variable_values) && (encoded_size += PB._encoded_size(x.variable_values, 1)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 2)) !isempty(x.auxiliary_objective_values) && (encoded_size += PB._encoded_size(x.auxiliary_objective_values, 4)) x.feasibility_status != SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED && (encoded_size += PB._encoded_size(x.feasibility_status, 3)) return encoded_size @@ -129,15 +130,17 @@ end struct DualSolutionProto dual_values::Union{Nothing,SparseDoubleVectorProto} + quadratic_dual_values::Union{Nothing,SparseDoubleVectorProto} reduced_costs::Union{Nothing,SparseDoubleVectorProto} objective_value::Float64 feasibility_status::SolutionStatusProto.T end -PB.default_values(::Type{DualSolutionProto}) = (;dual_values = nothing, reduced_costs = nothing, objective_value = zero(Float64), feasibility_status = SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED) -PB.field_numbers(::Type{DualSolutionProto}) = (;dual_values = 1, reduced_costs = 2, objective_value = 3, feasibility_status = 4) +PB.default_values(::Type{DualSolutionProto}) = (;dual_values = nothing, quadratic_dual_values = nothing, reduced_costs = nothing, objective_value = zero(Float64), feasibility_status = SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED) +PB.field_numbers(::Type{DualSolutionProto}) = (;dual_values = 1, quadratic_dual_values = 5, reduced_costs = 2, objective_value = 3, feasibility_status = 4) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:DualSolutionProto}) dual_values = Ref{Union{Nothing,SparseDoubleVectorProto}}(nothing) + quadratic_dual_values = Ref{Union{Nothing,SparseDoubleVectorProto}}(nothing) reduced_costs = Ref{Union{Nothing,SparseDoubleVectorProto}}(nothing) objective_value = zero(Float64) feasibility_status = SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED @@ -145,6 +148,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:DualSolutionProto}) field_number, wire_type = PB.decode_tag(d) if field_number == 1 PB.decode!(d, dual_values) + elseif field_number == 5 + PB.decode!(d, quadratic_dual_values) elseif field_number == 2 PB.decode!(d, reduced_costs) elseif field_number == 3 @@ -155,22 +160,24 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:DualSolutionProto}) PB.skip(d, wire_type) end end - return DualSolutionProto(dual_values[], reduced_costs[], objective_value, feasibility_status) + return DualSolutionProto(dual_values[], quadratic_dual_values[], reduced_costs[], objective_value, feasibility_status) end function PB.encode(e::PB.AbstractProtoEncoder, x::DualSolutionProto) initpos = position(e.io) !isnothing(x.dual_values) && PB.encode(e, 1, x.dual_values) + !isnothing(x.quadratic_dual_values) && PB.encode(e, 5, x.quadratic_dual_values) !isnothing(x.reduced_costs) && PB.encode(e, 2, x.reduced_costs) - x.objective_value != zero(Float64) && PB.encode(e, 3, x.objective_value) + x.objective_value !== zero(Float64) && PB.encode(e, 3, x.objective_value) x.feasibility_status != SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED && PB.encode(e, 4, x.feasibility_status) return position(e.io) - initpos end function PB._encoded_size(x::DualSolutionProto) encoded_size = 0 !isnothing(x.dual_values) && (encoded_size += PB._encoded_size(x.dual_values, 1)) + !isnothing(x.quadratic_dual_values) && (encoded_size += PB._encoded_size(x.quadratic_dual_values, 5)) !isnothing(x.reduced_costs) && (encoded_size += PB._encoded_size(x.reduced_costs, 2)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 3)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 3)) x.feasibility_status != SolutionStatusProto.SOLUTION_STATUS_UNSPECIFIED && (encoded_size += PB._encoded_size(x.feasibility_status, 4)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/sparse_containers_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/sparse_containers_pb.jl index 716ce782561..610ed04993e 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/sparse_containers_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/math_opt/sparse_containers_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.183 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/math_opt/sparse_containers.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.125 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/math_opt/sparse_containers.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export LinearExpressionProto, SparseDoubleVectorProto, SparseInt32VectorProto export SparseBoolVectorProto, SparseVectorFilterProto, SparseDoubleMatrixProto + struct LinearExpressionProto ids::Vector{Int64} coefficients::Vector{Float64} @@ -39,14 +40,14 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::LinearExpressionProto) initpos = position(e.io) !isempty(x.ids) && PB.encode(e, 1, x.ids) !isempty(x.coefficients) && PB.encode(e, 2, x.coefficients) - x.offset != zero(Float64) && PB.encode(e, 3, x.offset) + x.offset !== zero(Float64) && PB.encode(e, 3, x.offset) return position(e.io) - initpos end function PB._encoded_size(x::LinearExpressionProto) encoded_size = 0 !isempty(x.ids) && (encoded_size += PB._encoded_size(x.ids, 1)) !isempty(x.coefficients) && (encoded_size += PB._encoded_size(x.coefficients, 2)) - x.offset != zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 3)) + x.offset !== zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 3)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/operations_research.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/operations_research.jl index f37f1760d53..44df6041f4b 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/operations_research.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/operations_research.jl @@ -13,13 +13,14 @@ include("course_scheduling_pb.jl") include("demon_profiler_pb.jl") include("assignment_pb.jl") include("linear_solver_pb.jl") +include("routing_ils_pb.jl") include("sat/sat.jl") include("routing_parameters_pb.jl") include("scheduling/scheduling.jl") include("glop/glop.jl") include("bop/bop.jl") include("packing/packing.jl") -include("ortools/pdlp/pdlp.jl") +include("pdlp/pdlp.jl") include("math_opt/math_opt.jl") end # module operations_research diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/optional_boolean_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/optional_boolean_pb.jl index 594b238c5e2..42fc03aec3f 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/optional_boolean_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/optional_boolean_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.400 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/util/optional_boolean.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.425 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/util/optional_boolean.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,4 +7,5 @@ using ProtoBuf.EnumX: @enumx export OptionalBoolean + @enumx OptionalBoolean BOOL_UNSPECIFIED=0 BOOL_FALSE=2 BOOL_TRUE=3 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/multiple_dimensions_bin_packing_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/multiple_dimensions_bin_packing_pb.jl index 35e9d0dfeaa..99221a839d3 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/multiple_dimensions_bin_packing_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/multiple_dimensions_bin_packing_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.239 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/packing/multiple_dimensions_bin_packing.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.133 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/packing/multiple_dimensions_bin_packing.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export MultipleDimensionsBinPackingShape, MultipleDimensionsBinPackingItem export MultipleDimensionsBinPackingProblem + struct MultipleDimensionsBinPackingShape dimensions::Vector{Int64} end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/vbp/vector_bin_packing_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/vbp/vector_bin_packing_pb.jl index 2249cc30820..20ef0dd5e84 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/vbp/vector_bin_packing_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/packing/vbp/vector_bin_packing_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.242 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/packing/vbp/vector_bin_packing.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.134 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/packing/vbp/vector_bin_packing.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export VectorBinPackingOneBinInSolution, VectorBinPackingSolveStatus, Item export VectorBinPackingSolution, VectorBinPackingProblem + struct VectorBinPackingOneBinInSolution item_indices::Vector{Int32} item_copies::Vector{Int32} @@ -92,7 +93,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::Item) x.num_copies != zero(Int32) && PB.encode(e, 3, x.num_copies) x.num_optional_copies != zero(Int32) && PB.encode(e, 5, x.num_optional_copies) x.max_number_of_copies_per_bin != zero(Int32) && PB.encode(e, 4, x.max_number_of_copies_per_bin) - x.penalty_per_missing_copy != zero(Float64) && PB.encode(e, 6, x.penalty_per_missing_copy) + x.penalty_per_missing_copy !== zero(Float64) && PB.encode(e, 6, x.penalty_per_missing_copy) return position(e.io) - initpos end function PB._encoded_size(x::Item) @@ -102,7 +103,7 @@ function PB._encoded_size(x::Item) x.num_copies != zero(Int32) && (encoded_size += PB._encoded_size(x.num_copies, 3)) x.num_optional_copies != zero(Int32) && (encoded_size += PB._encoded_size(x.num_optional_copies, 5)) x.max_number_of_copies_per_bin != zero(Int32) && (encoded_size += PB._encoded_size(x.max_number_of_copies_per_bin, 4)) - x.penalty_per_missing_copy != zero(Float64) && (encoded_size += PB._encoded_size(x.penalty_per_missing_copy, 6)) + x.penalty_per_missing_copy !== zero(Float64) && (encoded_size += PB._encoded_size(x.penalty_per_missing_copy, 6)) return encoded_size end @@ -150,9 +151,9 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::VectorBinPackingSolution) !isempty(x.solver_info) && PB.encode(e, 1, x.solver_info) !isempty(x.bins) && PB.encode(e, 2, x.bins) x.status != VectorBinPackingSolveStatus.VECTOR_BIN_PACKING_SOLVE_STATUS_UNSPECIFIED && PB.encode(e, 3, x.status) - x.objective_value != zero(Float64) && PB.encode(e, 4, x.objective_value) - x.solve_time_in_seconds != zero(Float64) && PB.encode(e, 5, x.solve_time_in_seconds) - x.arc_flow_time_in_seconds != zero(Float64) && PB.encode(e, 6, x.arc_flow_time_in_seconds) + x.objective_value !== zero(Float64) && PB.encode(e, 4, x.objective_value) + x.solve_time_in_seconds !== zero(Float64) && PB.encode(e, 5, x.solve_time_in_seconds) + x.arc_flow_time_in_seconds !== zero(Float64) && PB.encode(e, 6, x.arc_flow_time_in_seconds) return position(e.io) - initpos end function PB._encoded_size(x::VectorBinPackingSolution) @@ -160,9 +161,9 @@ function PB._encoded_size(x::VectorBinPackingSolution) !isempty(x.solver_info) && (encoded_size += PB._encoded_size(x.solver_info, 1)) !isempty(x.bins) && (encoded_size += PB._encoded_size(x.bins, 2)) x.status != VectorBinPackingSolveStatus.VECTOR_BIN_PACKING_SOLVE_STATUS_UNSPECIFIED && (encoded_size += PB._encoded_size(x.status, 3)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 4)) - x.solve_time_in_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_in_seconds, 5)) - x.arc_flow_time_in_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.arc_flow_time_in_seconds, 6)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 4)) + x.solve_time_in_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_in_seconds, 5)) + x.arc_flow_time_in_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.arc_flow_time_in_seconds, 6)) return encoded_size end @@ -212,7 +213,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::VectorBinPackingProblem) !isempty(x.resource_name) && PB.encode(e, 3, x.resource_name) !isempty(x.item) && PB.encode(e, 4, x.item) x.max_bins != zero(Int32) && PB.encode(e, 5, x.max_bins) - x.cost_per_bin != zero(Float64) && PB.encode(e, 6, x.cost_per_bin) + x.cost_per_bin !== zero(Float64) && PB.encode(e, 6, x.cost_per_bin) return position(e.io) - initpos end function PB._encoded_size(x::VectorBinPackingProblem) @@ -222,6 +223,6 @@ function PB._encoded_size(x::VectorBinPackingProblem) !isempty(x.resource_name) && (encoded_size += PB._encoded_size(x.resource_name, 3)) !isempty(x.item) && (encoded_size += PB._encoded_size(x.item, 4)) x.max_bins != zero(Int32) && (encoded_size += PB._encoded_size(x.max_bins, 5)) - x.cost_per_bin != zero(Float64) && (encoded_size += PB._encoded_size(x.cost_per_bin, 6)) + x.cost_per_bin !== zero(Float64) && (encoded_size += PB._encoded_size(x.cost_per_bin, 6)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solve_log_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solve_log_pb.jl index d353815d340..1cafa2d5daf 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solve_log_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solve_log_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.235 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/pdlp/solve_log.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.132 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/pdlp/solve_log.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -9,6 +9,7 @@ export QuadraticProgramStats, PolishingPhaseType, PointType, TerminationReason export RestartChoice, PointMetadata, InfeasibilityInformation, ConvergenceInformation export IterationStats, FeasibilityPolishingDetails, SolveLog + struct QuadraticProgramStats num_variables::Int64 num_constraints::Int64 @@ -152,70 +153,70 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::QuadraticProgramStats) initpos = position(e.io) x.num_variables != zero(Int64) && PB.encode(e, 1, x.num_variables) x.num_constraints != zero(Int64) && PB.encode(e, 2, x.num_constraints) - x.constraint_matrix_col_min_l_inf_norm != zero(Float64) && PB.encode(e, 3, x.constraint_matrix_col_min_l_inf_norm) - x.constraint_matrix_row_min_l_inf_norm != zero(Float64) && PB.encode(e, 4, x.constraint_matrix_row_min_l_inf_norm) + x.constraint_matrix_col_min_l_inf_norm !== zero(Float64) && PB.encode(e, 3, x.constraint_matrix_col_min_l_inf_norm) + x.constraint_matrix_row_min_l_inf_norm !== zero(Float64) && PB.encode(e, 4, x.constraint_matrix_row_min_l_inf_norm) x.constraint_matrix_num_nonzeros != zero(Int64) && PB.encode(e, 5, x.constraint_matrix_num_nonzeros) - x.constraint_matrix_abs_max != zero(Float64) && PB.encode(e, 6, x.constraint_matrix_abs_max) - x.constraint_matrix_abs_min != zero(Float64) && PB.encode(e, 7, x.constraint_matrix_abs_min) - x.constraint_matrix_abs_avg != zero(Float64) && PB.encode(e, 8, x.constraint_matrix_abs_avg) - x.constraint_matrix_l2_norm != zero(Float64) && PB.encode(e, 25, x.constraint_matrix_l2_norm) - x.combined_bounds_max != zero(Float64) && PB.encode(e, 9, x.combined_bounds_max) - x.combined_bounds_min != zero(Float64) && PB.encode(e, 10, x.combined_bounds_min) - x.combined_bounds_avg != zero(Float64) && PB.encode(e, 11, x.combined_bounds_avg) - x.combined_bounds_l2_norm != zero(Float64) && PB.encode(e, 24, x.combined_bounds_l2_norm) - x.combined_variable_bounds_max != zero(Float64) && PB.encode(e, 28, x.combined_variable_bounds_max) - x.combined_variable_bounds_min != zero(Float64) && PB.encode(e, 29, x.combined_variable_bounds_min) - x.combined_variable_bounds_avg != zero(Float64) && PB.encode(e, 30, x.combined_variable_bounds_avg) - x.combined_variable_bounds_l2_norm != zero(Float64) && PB.encode(e, 31, x.combined_variable_bounds_l2_norm) + x.constraint_matrix_abs_max !== zero(Float64) && PB.encode(e, 6, x.constraint_matrix_abs_max) + x.constraint_matrix_abs_min !== zero(Float64) && PB.encode(e, 7, x.constraint_matrix_abs_min) + x.constraint_matrix_abs_avg !== zero(Float64) && PB.encode(e, 8, x.constraint_matrix_abs_avg) + x.constraint_matrix_l2_norm !== zero(Float64) && PB.encode(e, 25, x.constraint_matrix_l2_norm) + x.combined_bounds_max !== zero(Float64) && PB.encode(e, 9, x.combined_bounds_max) + x.combined_bounds_min !== zero(Float64) && PB.encode(e, 10, x.combined_bounds_min) + x.combined_bounds_avg !== zero(Float64) && PB.encode(e, 11, x.combined_bounds_avg) + x.combined_bounds_l2_norm !== zero(Float64) && PB.encode(e, 24, x.combined_bounds_l2_norm) + x.combined_variable_bounds_max !== zero(Float64) && PB.encode(e, 28, x.combined_variable_bounds_max) + x.combined_variable_bounds_min !== zero(Float64) && PB.encode(e, 29, x.combined_variable_bounds_min) + x.combined_variable_bounds_avg !== zero(Float64) && PB.encode(e, 30, x.combined_variable_bounds_avg) + x.combined_variable_bounds_l2_norm !== zero(Float64) && PB.encode(e, 31, x.combined_variable_bounds_l2_norm) x.variable_bound_gaps_num_finite != zero(Int64) && PB.encode(e, 12, x.variable_bound_gaps_num_finite) - x.variable_bound_gaps_max != zero(Float64) && PB.encode(e, 13, x.variable_bound_gaps_max) - x.variable_bound_gaps_min != zero(Float64) && PB.encode(e, 14, x.variable_bound_gaps_min) - x.variable_bound_gaps_avg != zero(Float64) && PB.encode(e, 15, x.variable_bound_gaps_avg) - x.variable_bound_gaps_l2_norm != zero(Float64) && PB.encode(e, 26, x.variable_bound_gaps_l2_norm) - x.objective_vector_abs_max != zero(Float64) && PB.encode(e, 16, x.objective_vector_abs_max) - x.objective_vector_abs_min != zero(Float64) && PB.encode(e, 17, x.objective_vector_abs_min) - x.objective_vector_abs_avg != zero(Float64) && PB.encode(e, 18, x.objective_vector_abs_avg) - x.objective_vector_l2_norm != zero(Float64) && PB.encode(e, 23, x.objective_vector_l2_norm) + x.variable_bound_gaps_max !== zero(Float64) && PB.encode(e, 13, x.variable_bound_gaps_max) + x.variable_bound_gaps_min !== zero(Float64) && PB.encode(e, 14, x.variable_bound_gaps_min) + x.variable_bound_gaps_avg !== zero(Float64) && PB.encode(e, 15, x.variable_bound_gaps_avg) + x.variable_bound_gaps_l2_norm !== zero(Float64) && PB.encode(e, 26, x.variable_bound_gaps_l2_norm) + x.objective_vector_abs_max !== zero(Float64) && PB.encode(e, 16, x.objective_vector_abs_max) + x.objective_vector_abs_min !== zero(Float64) && PB.encode(e, 17, x.objective_vector_abs_min) + x.objective_vector_abs_avg !== zero(Float64) && PB.encode(e, 18, x.objective_vector_abs_avg) + x.objective_vector_l2_norm !== zero(Float64) && PB.encode(e, 23, x.objective_vector_l2_norm) x.objective_matrix_num_nonzeros != zero(Int64) && PB.encode(e, 19, x.objective_matrix_num_nonzeros) - x.objective_matrix_abs_max != zero(Float64) && PB.encode(e, 20, x.objective_matrix_abs_max) - x.objective_matrix_abs_min != zero(Float64) && PB.encode(e, 21, x.objective_matrix_abs_min) - x.objective_matrix_abs_avg != zero(Float64) && PB.encode(e, 22, x.objective_matrix_abs_avg) - x.objective_matrix_l2_norm != zero(Float64) && PB.encode(e, 27, x.objective_matrix_l2_norm) + x.objective_matrix_abs_max !== zero(Float64) && PB.encode(e, 20, x.objective_matrix_abs_max) + x.objective_matrix_abs_min !== zero(Float64) && PB.encode(e, 21, x.objective_matrix_abs_min) + x.objective_matrix_abs_avg !== zero(Float64) && PB.encode(e, 22, x.objective_matrix_abs_avg) + x.objective_matrix_l2_norm !== zero(Float64) && PB.encode(e, 27, x.objective_matrix_l2_norm) return position(e.io) - initpos end function PB._encoded_size(x::QuadraticProgramStats) encoded_size = 0 x.num_variables != zero(Int64) && (encoded_size += PB._encoded_size(x.num_variables, 1)) x.num_constraints != zero(Int64) && (encoded_size += PB._encoded_size(x.num_constraints, 2)) - x.constraint_matrix_col_min_l_inf_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_col_min_l_inf_norm, 3)) - x.constraint_matrix_row_min_l_inf_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_row_min_l_inf_norm, 4)) + x.constraint_matrix_col_min_l_inf_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_col_min_l_inf_norm, 3)) + x.constraint_matrix_row_min_l_inf_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_row_min_l_inf_norm, 4)) x.constraint_matrix_num_nonzeros != zero(Int64) && (encoded_size += PB._encoded_size(x.constraint_matrix_num_nonzeros, 5)) - x.constraint_matrix_abs_max != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_max, 6)) - x.constraint_matrix_abs_min != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_min, 7)) - x.constraint_matrix_abs_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_avg, 8)) - x.constraint_matrix_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_l2_norm, 25)) - x.combined_bounds_max != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_max, 9)) - x.combined_bounds_min != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_min, 10)) - x.combined_bounds_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_avg, 11)) - x.combined_bounds_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_l2_norm, 24)) - x.combined_variable_bounds_max != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_max, 28)) - x.combined_variable_bounds_min != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_min, 29)) - x.combined_variable_bounds_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_avg, 30)) - x.combined_variable_bounds_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_l2_norm, 31)) + x.constraint_matrix_abs_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_max, 6)) + x.constraint_matrix_abs_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_min, 7)) + x.constraint_matrix_abs_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_abs_avg, 8)) + x.constraint_matrix_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.constraint_matrix_l2_norm, 25)) + x.combined_bounds_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_max, 9)) + x.combined_bounds_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_min, 10)) + x.combined_bounds_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_avg, 11)) + x.combined_bounds_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_bounds_l2_norm, 24)) + x.combined_variable_bounds_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_max, 28)) + x.combined_variable_bounds_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_min, 29)) + x.combined_variable_bounds_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_avg, 30)) + x.combined_variable_bounds_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.combined_variable_bounds_l2_norm, 31)) x.variable_bound_gaps_num_finite != zero(Int64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_num_finite, 12)) - x.variable_bound_gaps_max != zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_max, 13)) - x.variable_bound_gaps_min != zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_min, 14)) - x.variable_bound_gaps_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_avg, 15)) - x.variable_bound_gaps_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_l2_norm, 26)) - x.objective_vector_abs_max != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_max, 16)) - x.objective_vector_abs_min != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_min, 17)) - x.objective_vector_abs_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_avg, 18)) - x.objective_vector_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_l2_norm, 23)) + x.variable_bound_gaps_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_max, 13)) + x.variable_bound_gaps_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_min, 14)) + x.variable_bound_gaps_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_avg, 15)) + x.variable_bound_gaps_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.variable_bound_gaps_l2_norm, 26)) + x.objective_vector_abs_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_max, 16)) + x.objective_vector_abs_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_min, 17)) + x.objective_vector_abs_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_abs_avg, 18)) + x.objective_vector_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_vector_l2_norm, 23)) x.objective_matrix_num_nonzeros != zero(Int64) && (encoded_size += PB._encoded_size(x.objective_matrix_num_nonzeros, 19)) - x.objective_matrix_abs_max != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_max, 20)) - x.objective_matrix_abs_min != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_min, 21)) - x.objective_matrix_abs_avg != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_avg, 22)) - x.objective_matrix_l2_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_l2_norm, 27)) + x.objective_matrix_abs_max !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_max, 20)) + x.objective_matrix_abs_min !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_min, 21)) + x.objective_matrix_abs_avg !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_abs_avg, 22)) + x.objective_matrix_l2_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_matrix_l2_norm, 27)) return encoded_size end @@ -335,21 +336,21 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::InfeasibilityInformation) initpos = position(e.io) - x.max_primal_ray_infeasibility != zero(Float64) && PB.encode(e, 1, x.max_primal_ray_infeasibility) - x.primal_ray_linear_objective != zero(Float64) && PB.encode(e, 2, x.primal_ray_linear_objective) - x.primal_ray_quadratic_norm != zero(Float64) && PB.encode(e, 3, x.primal_ray_quadratic_norm) - x.max_dual_ray_infeasibility != zero(Float64) && PB.encode(e, 4, x.max_dual_ray_infeasibility) - x.dual_ray_objective != zero(Float64) && PB.encode(e, 5, x.dual_ray_objective) + x.max_primal_ray_infeasibility !== zero(Float64) && PB.encode(e, 1, x.max_primal_ray_infeasibility) + x.primal_ray_linear_objective !== zero(Float64) && PB.encode(e, 2, x.primal_ray_linear_objective) + x.primal_ray_quadratic_norm !== zero(Float64) && PB.encode(e, 3, x.primal_ray_quadratic_norm) + x.max_dual_ray_infeasibility !== zero(Float64) && PB.encode(e, 4, x.max_dual_ray_infeasibility) + x.dual_ray_objective !== zero(Float64) && PB.encode(e, 5, x.dual_ray_objective) x.candidate_type != PointType.POINT_TYPE_UNSPECIFIED && PB.encode(e, 6, x.candidate_type) return position(e.io) - initpos end function PB._encoded_size(x::InfeasibilityInformation) encoded_size = 0 - x.max_primal_ray_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.max_primal_ray_infeasibility, 1)) - x.primal_ray_linear_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_ray_linear_objective, 2)) - x.primal_ray_quadratic_norm != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_ray_quadratic_norm, 3)) - x.max_dual_ray_infeasibility != zero(Float64) && (encoded_size += PB._encoded_size(x.max_dual_ray_infeasibility, 4)) - x.dual_ray_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_ray_objective, 5)) + x.max_primal_ray_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.max_primal_ray_infeasibility, 1)) + x.primal_ray_linear_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_ray_linear_objective, 2)) + x.primal_ray_quadratic_norm !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_ray_quadratic_norm, 3)) + x.max_dual_ray_infeasibility !== zero(Float64) && (encoded_size += PB._encoded_size(x.max_dual_ray_infeasibility, 4)) + x.dual_ray_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_ray_objective, 5)) x.candidate_type != PointType.POINT_TYPE_UNSPECIFIED && (encoded_size += PB._encoded_size(x.candidate_type, 6)) return encoded_size end @@ -429,37 +430,37 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::ConvergenceInformation) initpos = position(e.io) x.candidate_type != PointType.POINT_TYPE_UNSPECIFIED && PB.encode(e, 1, x.candidate_type) - x.primal_objective != zero(Float64) && PB.encode(e, 2, x.primal_objective) - x.dual_objective != zero(Float64) && PB.encode(e, 3, x.dual_objective) - x.corrected_dual_objective != zero(Float64) && PB.encode(e, 4, x.corrected_dual_objective) - x.l_inf_primal_residual != zero(Float64) && PB.encode(e, 5, x.l_inf_primal_residual) - x.l2_primal_residual != zero(Float64) && PB.encode(e, 6, x.l2_primal_residual) - x.l_inf_componentwise_primal_residual != zero(Float64) && PB.encode(e, 24, x.l_inf_componentwise_primal_residual) - x.l_inf_dual_residual != zero(Float64) && PB.encode(e, 7, x.l_inf_dual_residual) - x.l2_dual_residual != zero(Float64) && PB.encode(e, 8, x.l2_dual_residual) - x.l_inf_componentwise_dual_residual != zero(Float64) && PB.encode(e, 25, x.l_inf_componentwise_dual_residual) - x.l_inf_primal_variable != zero(Float64) && PB.encode(e, 14, x.l_inf_primal_variable) - x.l2_primal_variable != zero(Float64) && PB.encode(e, 15, x.l2_primal_variable) - x.l_inf_dual_variable != zero(Float64) && PB.encode(e, 16, x.l_inf_dual_variable) - x.l2_dual_variable != zero(Float64) && PB.encode(e, 17, x.l2_dual_variable) + x.primal_objective !== zero(Float64) && PB.encode(e, 2, x.primal_objective) + x.dual_objective !== zero(Float64) && PB.encode(e, 3, x.dual_objective) + x.corrected_dual_objective !== zero(Float64) && PB.encode(e, 4, x.corrected_dual_objective) + x.l_inf_primal_residual !== zero(Float64) && PB.encode(e, 5, x.l_inf_primal_residual) + x.l2_primal_residual !== zero(Float64) && PB.encode(e, 6, x.l2_primal_residual) + x.l_inf_componentwise_primal_residual !== zero(Float64) && PB.encode(e, 24, x.l_inf_componentwise_primal_residual) + x.l_inf_dual_residual !== zero(Float64) && PB.encode(e, 7, x.l_inf_dual_residual) + x.l2_dual_residual !== zero(Float64) && PB.encode(e, 8, x.l2_dual_residual) + x.l_inf_componentwise_dual_residual !== zero(Float64) && PB.encode(e, 25, x.l_inf_componentwise_dual_residual) + x.l_inf_primal_variable !== zero(Float64) && PB.encode(e, 14, x.l_inf_primal_variable) + x.l2_primal_variable !== zero(Float64) && PB.encode(e, 15, x.l2_primal_variable) + x.l_inf_dual_variable !== zero(Float64) && PB.encode(e, 16, x.l_inf_dual_variable) + x.l2_dual_variable !== zero(Float64) && PB.encode(e, 17, x.l2_dual_variable) return position(e.io) - initpos end function PB._encoded_size(x::ConvergenceInformation) encoded_size = 0 x.candidate_type != PointType.POINT_TYPE_UNSPECIFIED && (encoded_size += PB._encoded_size(x.candidate_type, 1)) - x.primal_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_objective, 2)) - x.dual_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.dual_objective, 3)) - x.corrected_dual_objective != zero(Float64) && (encoded_size += PB._encoded_size(x.corrected_dual_objective, 4)) - x.l_inf_primal_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_primal_residual, 5)) - x.l2_primal_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l2_primal_residual, 6)) - x.l_inf_componentwise_primal_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_componentwise_primal_residual, 24)) - x.l_inf_dual_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_dual_residual, 7)) - x.l2_dual_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l2_dual_residual, 8)) - x.l_inf_componentwise_dual_residual != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_componentwise_dual_residual, 25)) - x.l_inf_primal_variable != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_primal_variable, 14)) - x.l2_primal_variable != zero(Float64) && (encoded_size += PB._encoded_size(x.l2_primal_variable, 15)) - x.l_inf_dual_variable != zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_dual_variable, 16)) - x.l2_dual_variable != zero(Float64) && (encoded_size += PB._encoded_size(x.l2_dual_variable, 17)) + x.primal_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_objective, 2)) + x.dual_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.dual_objective, 3)) + x.corrected_dual_objective !== zero(Float64) && (encoded_size += PB._encoded_size(x.corrected_dual_objective, 4)) + x.l_inf_primal_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_primal_residual, 5)) + x.l2_primal_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l2_primal_residual, 6)) + x.l_inf_componentwise_primal_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_componentwise_primal_residual, 24)) + x.l_inf_dual_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_dual_residual, 7)) + x.l2_dual_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l2_dual_residual, 8)) + x.l_inf_componentwise_dual_residual !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_componentwise_dual_residual, 25)) + x.l_inf_primal_variable !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_primal_variable, 14)) + x.l2_primal_variable !== zero(Float64) && (encoded_size += PB._encoded_size(x.l2_primal_variable, 15)) + x.l_inf_dual_variable !== zero(Float64) && (encoded_size += PB._encoded_size(x.l_inf_dual_variable, 16)) + x.l2_dual_variable !== zero(Float64) && (encoded_size += PB._encoded_size(x.l2_dual_variable, 17)) return encoded_size end @@ -525,12 +526,12 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::IterationStats) !isempty(x.convergence_information) && PB.encode(e, 2, x.convergence_information) !isempty(x.infeasibility_information) && PB.encode(e, 3, x.infeasibility_information) !isempty(x.point_metadata) && PB.encode(e, 11, x.point_metadata) - x.cumulative_kkt_matrix_passes != zero(Float64) && PB.encode(e, 4, x.cumulative_kkt_matrix_passes) + x.cumulative_kkt_matrix_passes !== zero(Float64) && PB.encode(e, 4, x.cumulative_kkt_matrix_passes) x.cumulative_rejected_steps != zero(Int32) && PB.encode(e, 5, x.cumulative_rejected_steps) - x.cumulative_time_sec != zero(Float64) && PB.encode(e, 6, x.cumulative_time_sec) + x.cumulative_time_sec !== zero(Float64) && PB.encode(e, 6, x.cumulative_time_sec) x.restart_used != RestartChoice.RESTART_CHOICE_UNSPECIFIED && PB.encode(e, 7, x.restart_used) - x.step_size != zero(Float64) && PB.encode(e, 8, x.step_size) - x.primal_weight != zero(Float64) && PB.encode(e, 9, x.primal_weight) + x.step_size !== zero(Float64) && PB.encode(e, 8, x.step_size) + x.primal_weight !== zero(Float64) && PB.encode(e, 9, x.primal_weight) return position(e.io) - initpos end function PB._encoded_size(x::IterationStats) @@ -539,12 +540,12 @@ function PB._encoded_size(x::IterationStats) !isempty(x.convergence_information) && (encoded_size += PB._encoded_size(x.convergence_information, 2)) !isempty(x.infeasibility_information) && (encoded_size += PB._encoded_size(x.infeasibility_information, 3)) !isempty(x.point_metadata) && (encoded_size += PB._encoded_size(x.point_metadata, 11)) - x.cumulative_kkt_matrix_passes != zero(Float64) && (encoded_size += PB._encoded_size(x.cumulative_kkt_matrix_passes, 4)) + x.cumulative_kkt_matrix_passes !== zero(Float64) && (encoded_size += PB._encoded_size(x.cumulative_kkt_matrix_passes, 4)) x.cumulative_rejected_steps != zero(Int32) && (encoded_size += PB._encoded_size(x.cumulative_rejected_steps, 5)) - x.cumulative_time_sec != zero(Float64) && (encoded_size += PB._encoded_size(x.cumulative_time_sec, 6)) + x.cumulative_time_sec !== zero(Float64) && (encoded_size += PB._encoded_size(x.cumulative_time_sec, 6)) x.restart_used != RestartChoice.RESTART_CHOICE_UNSPECIFIED && (encoded_size += PB._encoded_size(x.restart_used, 7)) - x.step_size != zero(Float64) && (encoded_size += PB._encoded_size(x.step_size, 8)) - x.primal_weight != zero(Float64) && (encoded_size += PB._encoded_size(x.primal_weight, 9)) + x.step_size !== zero(Float64) && (encoded_size += PB._encoded_size(x.step_size, 8)) + x.primal_weight !== zero(Float64) && (encoded_size += PB._encoded_size(x.primal_weight, 9)) return encoded_size end @@ -606,7 +607,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::FeasibilityPolishingDetails) !isnothing(x.params) && PB.encode(e, 3, x.params) x.termination_reason != TerminationReason.TERMINATION_REASON_UNSPECIFIED && PB.encode(e, 4, x.termination_reason) x.iteration_count != zero(Int32) && PB.encode(e, 5, x.iteration_count) - x.solve_time_sec != zero(Float64) && PB.encode(e, 6, x.solve_time_sec) + x.solve_time_sec !== zero(Float64) && PB.encode(e, 6, x.solve_time_sec) !isnothing(x.solution_stats) && PB.encode(e, 7, x.solution_stats) x.solution_type != PointType.POINT_TYPE_UNSPECIFIED && PB.encode(e, 8, x.solution_type) !isempty(x.iteration_stats) && PB.encode(e, 9, x.iteration_stats) @@ -619,7 +620,7 @@ function PB._encoded_size(x::FeasibilityPolishingDetails) !isnothing(x.params) && (encoded_size += PB._encoded_size(x.params, 3)) x.termination_reason != TerminationReason.TERMINATION_REASON_UNSPECIFIED && (encoded_size += PB._encoded_size(x.termination_reason, 4)) x.iteration_count != zero(Int32) && (encoded_size += PB._encoded_size(x.iteration_count, 5)) - x.solve_time_sec != zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_sec, 6)) + x.solve_time_sec !== zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_sec, 6)) !isnothing(x.solution_stats) && (encoded_size += PB._encoded_size(x.solution_stats, 7)) x.solution_type != PointType.POINT_TYPE_UNSPECIFIED && (encoded_size += PB._encoded_size(x.solution_type, 8)) !isempty(x.iteration_stats) && (encoded_size += PB._encoded_size(x.iteration_stats, 9)) @@ -701,8 +702,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SolveLog) x.termination_reason != TerminationReason.TERMINATION_REASON_UNSPECIFIED && PB.encode(e, 3, x.termination_reason) !isempty(x.termination_string) && PB.encode(e, 4, x.termination_string) x.iteration_count != zero(Int32) && PB.encode(e, 5, x.iteration_count) - x.preprocessing_time_sec != zero(Float64) && PB.encode(e, 13, x.preprocessing_time_sec) - x.solve_time_sec != zero(Float64) && PB.encode(e, 6, x.solve_time_sec) + x.preprocessing_time_sec !== zero(Float64) && PB.encode(e, 13, x.preprocessing_time_sec) + x.solve_time_sec !== zero(Float64) && PB.encode(e, 6, x.solve_time_sec) !isnothing(x.solution_stats) && PB.encode(e, 8, x.solution_stats) x.solution_type != PointType.POINT_TYPE_UNSPECIFIED && PB.encode(e, 10, x.solution_type) !isempty(x.iteration_stats) && PB.encode(e, 7, x.iteration_stats) @@ -718,8 +719,8 @@ function PB._encoded_size(x::SolveLog) x.termination_reason != TerminationReason.TERMINATION_REASON_UNSPECIFIED && (encoded_size += PB._encoded_size(x.termination_reason, 3)) !isempty(x.termination_string) && (encoded_size += PB._encoded_size(x.termination_string, 4)) x.iteration_count != zero(Int32) && (encoded_size += PB._encoded_size(x.iteration_count, 5)) - x.preprocessing_time_sec != zero(Float64) && (encoded_size += PB._encoded_size(x.preprocessing_time_sec, 13)) - x.solve_time_sec != zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_sec, 6)) + x.preprocessing_time_sec !== zero(Float64) && (encoded_size += PB._encoded_size(x.preprocessing_time_sec, 13)) + x.solve_time_sec !== zero(Float64) && (encoded_size += PB._encoded_size(x.solve_time_sec, 6)) !isnothing(x.solution_stats) && (encoded_size += PB._encoded_size(x.solution_stats, 8)) x.solution_type != PointType.POINT_TYPE_UNSPECIFIED && (encoded_size += PB._encoded_size(x.solution_type, 10)) !isempty(x.iteration_stats) && (encoded_size += PB._encoded_size(x.iteration_stats, 7)) diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solvers_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solvers_pb.jl index ddd94f06f07..08526cd4c10 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solvers_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/pdlp/solvers_pb.jl @@ -1,17 +1,18 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.233 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/pdlp/solvers.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.131 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/pdlp/solvers.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf using ProtoBuf.EnumX: @enumx -export var"TerminationCriteria.SimpleOptimalityCriteria", MalitskyPockParams -export OptimalityNorm, var"PrimalDualHybridGradientParams.PresolveOptions" -export var"PrimalDualHybridGradientParams.RestartStrategy" +export var"TerminationCriteria.SimpleOptimalityCriteria" +export var"PrimalDualHybridGradientParams.PresolveOptions", MalitskyPockParams +export OptimalityNorm, var"PrimalDualHybridGradientParams.RestartStrategy", SchedulerType export var"PrimalDualHybridGradientParams.LinesearchRule" export var"TerminationCriteria.DetailedOptimalityCriteria", AdaptiveLinesearchParams export TerminationCriteria, PrimalDualHybridGradientParams + struct var"TerminationCriteria.SimpleOptimalityCriteria" eps_optimal_absolute::Float64 eps_optimal_relative::Float64 @@ -37,99 +38,101 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"TerminationCriteria.SimpleOptimalityCriteria") initpos = position(e.io) - x.eps_optimal_absolute != Float64(1.0e-6) && PB.encode(e, 1, x.eps_optimal_absolute) - x.eps_optimal_relative != Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_relative) + x.eps_optimal_absolute !== Float64(1.0e-6) && PB.encode(e, 1, x.eps_optimal_absolute) + x.eps_optimal_relative !== Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_relative) return position(e.io) - initpos end function PB._encoded_size(x::var"TerminationCriteria.SimpleOptimalityCriteria") encoded_size = 0 - x.eps_optimal_absolute != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_absolute, 1)) - x.eps_optimal_relative != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_relative, 2)) + x.eps_optimal_absolute !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_absolute, 1)) + x.eps_optimal_relative !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_relative, 2)) return encoded_size end -struct MalitskyPockParams - step_size_downscaling_factor::Float64 - linesearch_contraction_factor::Float64 - step_size_interpolation::Float64 +struct var"PrimalDualHybridGradientParams.PresolveOptions" + use_glop::Bool + glop_parameters::Union{Nothing,operations_research.glop.GlopParameters} end -PB.default_values(::Type{MalitskyPockParams}) = (;step_size_downscaling_factor = Float64(0.7), linesearch_contraction_factor = Float64(0.99), step_size_interpolation = Float64(1)) -PB.field_numbers(::Type{MalitskyPockParams}) = (;step_size_downscaling_factor = 1, linesearch_contraction_factor = 2, step_size_interpolation = 3) +PB.default_values(::Type{var"PrimalDualHybridGradientParams.PresolveOptions"}) = (;use_glop = false, glop_parameters = nothing) +PB.field_numbers(::Type{var"PrimalDualHybridGradientParams.PresolveOptions"}) = (;use_glop = 1, glop_parameters = 2) -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:MalitskyPockParams}) - step_size_downscaling_factor = Float64(0.7) - linesearch_contraction_factor = Float64(0.99) - step_size_interpolation = Float64(1) +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"PrimalDualHybridGradientParams.PresolveOptions"}) + use_glop = false + glop_parameters = Ref{Union{Nothing,operations_research.glop.GlopParameters}}(nothing) while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 - step_size_downscaling_factor = PB.decode(d, Float64) + use_glop = PB.decode(d, Bool) elseif field_number == 2 - linesearch_contraction_factor = PB.decode(d, Float64) - elseif field_number == 3 - step_size_interpolation = PB.decode(d, Float64) + PB.decode!(d, glop_parameters) else PB.skip(d, wire_type) end end - return MalitskyPockParams(step_size_downscaling_factor, linesearch_contraction_factor, step_size_interpolation) + return var"PrimalDualHybridGradientParams.PresolveOptions"(use_glop, glop_parameters[]) end -function PB.encode(e::PB.AbstractProtoEncoder, x::MalitskyPockParams) +function PB.encode(e::PB.AbstractProtoEncoder, x::var"PrimalDualHybridGradientParams.PresolveOptions") initpos = position(e.io) - x.step_size_downscaling_factor != Float64(0.7) && PB.encode(e, 1, x.step_size_downscaling_factor) - x.linesearch_contraction_factor != Float64(0.99) && PB.encode(e, 2, x.linesearch_contraction_factor) - x.step_size_interpolation != Float64(1) && PB.encode(e, 3, x.step_size_interpolation) + x.use_glop != false && PB.encode(e, 1, x.use_glop) + !isnothing(x.glop_parameters) && PB.encode(e, 2, x.glop_parameters) return position(e.io) - initpos end -function PB._encoded_size(x::MalitskyPockParams) +function PB._encoded_size(x::var"PrimalDualHybridGradientParams.PresolveOptions") encoded_size = 0 - x.step_size_downscaling_factor != Float64(0.7) && (encoded_size += PB._encoded_size(x.step_size_downscaling_factor, 1)) - x.linesearch_contraction_factor != Float64(0.99) && (encoded_size += PB._encoded_size(x.linesearch_contraction_factor, 2)) - x.step_size_interpolation != Float64(1) && (encoded_size += PB._encoded_size(x.step_size_interpolation, 3)) + x.use_glop != false && (encoded_size += PB._encoded_size(x.use_glop, 1)) + !isnothing(x.glop_parameters) && (encoded_size += PB._encoded_size(x.glop_parameters, 2)) return encoded_size end -@enumx OptimalityNorm OPTIMALITY_NORM_UNSPECIFIED=0 OPTIMALITY_NORM_L_INF=1 OPTIMALITY_NORM_L2=2 OPTIMALITY_NORM_L_INF_COMPONENTWISE=3 - -struct var"PrimalDualHybridGradientParams.PresolveOptions" - use_glop::Bool - glop_parameters::Union{Nothing,operations_research.glop.GlopParameters} +struct MalitskyPockParams + step_size_downscaling_factor::Float64 + linesearch_contraction_factor::Float64 + step_size_interpolation::Float64 end -PB.default_values(::Type{var"PrimalDualHybridGradientParams.PresolveOptions"}) = (;use_glop = false, glop_parameters = nothing) -PB.field_numbers(::Type{var"PrimalDualHybridGradientParams.PresolveOptions"}) = (;use_glop = 1, glop_parameters = 2) +PB.default_values(::Type{MalitskyPockParams}) = (;step_size_downscaling_factor = Float64(0.7), linesearch_contraction_factor = Float64(0.99), step_size_interpolation = Float64(1)) +PB.field_numbers(::Type{MalitskyPockParams}) = (;step_size_downscaling_factor = 1, linesearch_contraction_factor = 2, step_size_interpolation = 3) -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"PrimalDualHybridGradientParams.PresolveOptions"}) - use_glop = false - glop_parameters = Ref{Union{Nothing,operations_research.glop.GlopParameters}}(nothing) +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:MalitskyPockParams}) + step_size_downscaling_factor = Float64(0.7) + linesearch_contraction_factor = Float64(0.99) + step_size_interpolation = Float64(1) while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 - use_glop = PB.decode(d, Bool) + step_size_downscaling_factor = PB.decode(d, Float64) elseif field_number == 2 - PB.decode!(d, glop_parameters) + linesearch_contraction_factor = PB.decode(d, Float64) + elseif field_number == 3 + step_size_interpolation = PB.decode(d, Float64) else PB.skip(d, wire_type) end end - return var"PrimalDualHybridGradientParams.PresolveOptions"(use_glop, glop_parameters[]) + return MalitskyPockParams(step_size_downscaling_factor, linesearch_contraction_factor, step_size_interpolation) end -function PB.encode(e::PB.AbstractProtoEncoder, x::var"PrimalDualHybridGradientParams.PresolveOptions") +function PB.encode(e::PB.AbstractProtoEncoder, x::MalitskyPockParams) initpos = position(e.io) - x.use_glop != false && PB.encode(e, 1, x.use_glop) - !isnothing(x.glop_parameters) && PB.encode(e, 2, x.glop_parameters) + x.step_size_downscaling_factor !== Float64(0.7) && PB.encode(e, 1, x.step_size_downscaling_factor) + x.linesearch_contraction_factor !== Float64(0.99) && PB.encode(e, 2, x.linesearch_contraction_factor) + x.step_size_interpolation !== Float64(1) && PB.encode(e, 3, x.step_size_interpolation) return position(e.io) - initpos end -function PB._encoded_size(x::var"PrimalDualHybridGradientParams.PresolveOptions") +function PB._encoded_size(x::MalitskyPockParams) encoded_size = 0 - x.use_glop != false && (encoded_size += PB._encoded_size(x.use_glop, 1)) - !isnothing(x.glop_parameters) && (encoded_size += PB._encoded_size(x.glop_parameters, 2)) + x.step_size_downscaling_factor !== Float64(0.7) && (encoded_size += PB._encoded_size(x.step_size_downscaling_factor, 1)) + x.linesearch_contraction_factor !== Float64(0.99) && (encoded_size += PB._encoded_size(x.linesearch_contraction_factor, 2)) + x.step_size_interpolation !== Float64(1) && (encoded_size += PB._encoded_size(x.step_size_interpolation, 3)) return encoded_size end +@enumx OptimalityNorm OPTIMALITY_NORM_UNSPECIFIED=0 OPTIMALITY_NORM_L_INF=1 OPTIMALITY_NORM_L2=2 OPTIMALITY_NORM_L_INF_COMPONENTWISE=3 + @enumx var"PrimalDualHybridGradientParams.RestartStrategy" RESTART_STRATEGY_UNSPECIFIED=0 NO_RESTARTS=1 EVERY_MAJOR_ITERATION=2 ADAPTIVE_HEURISTIC=3 ADAPTIVE_DISTANCE_BASED=4 +@enumx SchedulerType SCHEDULER_TYPE_UNSPECIFIED=0 SCHEDULER_TYPE_GOOGLE_THREADPOOL=1 SCHEDULER_TYPE_EIGEN_THREADPOOL=3 + @enumx var"PrimalDualHybridGradientParams.LinesearchRule" LINESEARCH_RULE_UNSPECIFIED=0 ADAPTIVE_LINESEARCH_RULE=1 MALITSKY_POCK_LINESEARCH_RULE=2 CONSTANT_STEP_SIZE_RULE=3 struct var"TerminationCriteria.DetailedOptimalityCriteria" @@ -173,22 +176,22 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"TerminationCriteria.DetailedOptimalityCriteria") initpos = position(e.io) - x.eps_optimal_primal_residual_absolute != Float64(1.0e-6) && PB.encode(e, 1, x.eps_optimal_primal_residual_absolute) - x.eps_optimal_primal_residual_relative != Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_primal_residual_relative) - x.eps_optimal_dual_residual_absolute != Float64(1.0e-6) && PB.encode(e, 3, x.eps_optimal_dual_residual_absolute) - x.eps_optimal_dual_residual_relative != Float64(1.0e-6) && PB.encode(e, 4, x.eps_optimal_dual_residual_relative) - x.eps_optimal_objective_gap_absolute != Float64(1.0e-6) && PB.encode(e, 5, x.eps_optimal_objective_gap_absolute) - x.eps_optimal_objective_gap_relative != Float64(1.0e-6) && PB.encode(e, 6, x.eps_optimal_objective_gap_relative) + x.eps_optimal_primal_residual_absolute !== Float64(1.0e-6) && PB.encode(e, 1, x.eps_optimal_primal_residual_absolute) + x.eps_optimal_primal_residual_relative !== Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_primal_residual_relative) + x.eps_optimal_dual_residual_absolute !== Float64(1.0e-6) && PB.encode(e, 3, x.eps_optimal_dual_residual_absolute) + x.eps_optimal_dual_residual_relative !== Float64(1.0e-6) && PB.encode(e, 4, x.eps_optimal_dual_residual_relative) + x.eps_optimal_objective_gap_absolute !== Float64(1.0e-6) && PB.encode(e, 5, x.eps_optimal_objective_gap_absolute) + x.eps_optimal_objective_gap_relative !== Float64(1.0e-6) && PB.encode(e, 6, x.eps_optimal_objective_gap_relative) return position(e.io) - initpos end function PB._encoded_size(x::var"TerminationCriteria.DetailedOptimalityCriteria") encoded_size = 0 - x.eps_optimal_primal_residual_absolute != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_primal_residual_absolute, 1)) - x.eps_optimal_primal_residual_relative != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_primal_residual_relative, 2)) - x.eps_optimal_dual_residual_absolute != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_dual_residual_absolute, 3)) - x.eps_optimal_dual_residual_relative != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_dual_residual_relative, 4)) - x.eps_optimal_objective_gap_absolute != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_objective_gap_absolute, 5)) - x.eps_optimal_objective_gap_relative != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_objective_gap_relative, 6)) + x.eps_optimal_primal_residual_absolute !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_primal_residual_absolute, 1)) + x.eps_optimal_primal_residual_relative !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_primal_residual_relative, 2)) + x.eps_optimal_dual_residual_absolute !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_dual_residual_absolute, 3)) + x.eps_optimal_dual_residual_relative !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_dual_residual_relative, 4)) + x.eps_optimal_objective_gap_absolute !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_objective_gap_absolute, 5)) + x.eps_optimal_objective_gap_relative !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_objective_gap_relative, 6)) return encoded_size end @@ -217,14 +220,14 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::AdaptiveLinesearchParams) initpos = position(e.io) - x.step_size_reduction_exponent != Float64(0.3) && PB.encode(e, 1, x.step_size_reduction_exponent) - x.step_size_growth_exponent != Float64(0.6) && PB.encode(e, 2, x.step_size_growth_exponent) + x.step_size_reduction_exponent !== Float64(0.3) && PB.encode(e, 1, x.step_size_reduction_exponent) + x.step_size_growth_exponent !== Float64(0.6) && PB.encode(e, 2, x.step_size_growth_exponent) return position(e.io) - initpos end function PB._encoded_size(x::AdaptiveLinesearchParams) encoded_size = 0 - x.step_size_reduction_exponent != Float64(0.3) && (encoded_size += PB._encoded_size(x.step_size_reduction_exponent, 1)) - x.step_size_growth_exponent != Float64(0.6) && (encoded_size += PB._encoded_size(x.step_size_growth_exponent, 2)) + x.step_size_reduction_exponent !== Float64(0.3) && (encoded_size += PB._encoded_size(x.step_size_reduction_exponent, 1)) + x.step_size_growth_exponent !== Float64(0.6) && (encoded_size += PB._encoded_size(x.step_size_growth_exponent, 2)) return encoded_size end @@ -293,13 +296,13 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::TerminationCriteria) elseif x.optimality_criteria.name === :detailed_optimality_criteria PB.encode(e, 10, x.optimality_criteria[]::var"TerminationCriteria.DetailedOptimalityCriteria") end - x.eps_optimal_absolute != Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_absolute) - x.eps_optimal_relative != Float64(1.0e-6) && PB.encode(e, 3, x.eps_optimal_relative) - x.eps_primal_infeasible != Float64(1.0e-8) && PB.encode(e, 4, x.eps_primal_infeasible) - x.eps_dual_infeasible != Float64(1.0e-8) && PB.encode(e, 5, x.eps_dual_infeasible) - x.time_sec_limit != Float64(Inf) && PB.encode(e, 6, x.time_sec_limit) + x.eps_optimal_absolute !== Float64(1.0e-6) && PB.encode(e, 2, x.eps_optimal_absolute) + x.eps_optimal_relative !== Float64(1.0e-6) && PB.encode(e, 3, x.eps_optimal_relative) + x.eps_primal_infeasible !== Float64(1.0e-8) && PB.encode(e, 4, x.eps_primal_infeasible) + x.eps_dual_infeasible !== Float64(1.0e-8) && PB.encode(e, 5, x.eps_dual_infeasible) + x.time_sec_limit !== Float64(Inf) && PB.encode(e, 6, x.time_sec_limit) x.iteration_limit != Int32(2147483647) && PB.encode(e, 7, x.iteration_limit) - x.kkt_matrix_pass_limit != Float64(Inf) && PB.encode(e, 8, x.kkt_matrix_pass_limit) + x.kkt_matrix_pass_limit !== Float64(Inf) && PB.encode(e, 8, x.kkt_matrix_pass_limit) return position(e.io) - initpos end function PB._encoded_size(x::TerminationCriteria) @@ -311,13 +314,13 @@ function PB._encoded_size(x::TerminationCriteria) elseif x.optimality_criteria.name === :detailed_optimality_criteria encoded_size += PB._encoded_size(x.optimality_criteria[]::var"TerminationCriteria.DetailedOptimalityCriteria", 10) end - x.eps_optimal_absolute != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_absolute, 2)) - x.eps_optimal_relative != Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_relative, 3)) - x.eps_primal_infeasible != Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.eps_primal_infeasible, 4)) - x.eps_dual_infeasible != Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.eps_dual_infeasible, 5)) - x.time_sec_limit != Float64(Inf) && (encoded_size += PB._encoded_size(x.time_sec_limit, 6)) + x.eps_optimal_absolute !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_absolute, 2)) + x.eps_optimal_relative !== Float64(1.0e-6) && (encoded_size += PB._encoded_size(x.eps_optimal_relative, 3)) + x.eps_primal_infeasible !== Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.eps_primal_infeasible, 4)) + x.eps_dual_infeasible !== Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.eps_dual_infeasible, 5)) + x.time_sec_limit !== Float64(Inf) && (encoded_size += PB._encoded_size(x.time_sec_limit, 6)) x.iteration_limit != Int32(2147483647) && (encoded_size += PB._encoded_size(x.iteration_limit, 7)) - x.kkt_matrix_pass_limit != Float64(Inf) && (encoded_size += PB._encoded_size(x.kkt_matrix_pass_limit, 8)) + x.kkt_matrix_pass_limit !== Float64(Inf) && (encoded_size += PB._encoded_size(x.kkt_matrix_pass_limit, 8)) return encoded_size end @@ -325,6 +328,7 @@ struct PrimalDualHybridGradientParams termination_criteria::Union{Nothing,TerminationCriteria} num_threads::Int32 num_shards::Int32 + scheduler_type::SchedulerType.T record_iteration_stats::Bool verbosity_level::Int32 log_interval_seconds::Float64 @@ -348,15 +352,18 @@ struct PrimalDualHybridGradientParams use_diagonal_qp_trust_region_solver::Bool diagonal_qp_trust_region_solver_tolerance::Float64 use_feasibility_polishing::Bool + apply_feasibility_polishing_after_limits_reached::Bool + apply_feasibility_polishing_if_solver_is_interrupted::Bool end PB.reserved_fields(::Type{PrimalDualHybridGradientParams}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[13, 14, 15, 20, 21]) -PB.default_values(::Type{PrimalDualHybridGradientParams}) = (;termination_criteria = nothing, num_threads = Int32(1), num_shards = Int32(0), record_iteration_stats = false, verbosity_level = Int32(0), log_interval_seconds = Float64(0.0), major_iteration_frequency = Int32(64), termination_check_frequency = Int32(64), restart_strategy = var"PrimalDualHybridGradientParams.RestartStrategy".ADAPTIVE_HEURISTIC, primal_weight_update_smoothing = Float64(0.5), initial_primal_weight = zero(Float64), presolve_options = nothing, l_inf_ruiz_iterations = Int32(5), l2_norm_rescaling = true, sufficient_reduction_for_restart = Float64(0.1), necessary_reduction_for_restart = Float64(0.9), linesearch_rule = var"PrimalDualHybridGradientParams.LinesearchRule".ADAPTIVE_LINESEARCH_RULE, adaptive_linesearch_parameters = nothing, malitsky_pock_parameters = nothing, initial_step_size_scaling = Float64(1.0), random_projection_seeds = Vector{Int32}(), infinite_constraint_bound_threshold = Float64(Inf), handle_some_primal_gradients_on_finite_bounds_as_residuals = true, use_diagonal_qp_trust_region_solver = false, diagonal_qp_trust_region_solver_tolerance = Float64(1.0e-8), use_feasibility_polishing = false) -PB.field_numbers(::Type{PrimalDualHybridGradientParams}) = (;termination_criteria = 1, num_threads = 2, num_shards = 27, record_iteration_stats = 3, verbosity_level = 26, log_interval_seconds = 31, major_iteration_frequency = 4, termination_check_frequency = 5, restart_strategy = 6, primal_weight_update_smoothing = 7, initial_primal_weight = 8, presolve_options = 16, l_inf_ruiz_iterations = 9, l2_norm_rescaling = 10, sufficient_reduction_for_restart = 11, necessary_reduction_for_restart = 17, linesearch_rule = 12, adaptive_linesearch_parameters = 18, malitsky_pock_parameters = 19, initial_step_size_scaling = 25, random_projection_seeds = 28, infinite_constraint_bound_threshold = 22, handle_some_primal_gradients_on_finite_bounds_as_residuals = 29, use_diagonal_qp_trust_region_solver = 23, diagonal_qp_trust_region_solver_tolerance = 24, use_feasibility_polishing = 30) +PB.default_values(::Type{PrimalDualHybridGradientParams}) = (;termination_criteria = nothing, num_threads = Int32(1), num_shards = Int32(0), scheduler_type = SchedulerType.SCHEDULER_TYPE_GOOGLE_THREADPOOL, record_iteration_stats = false, verbosity_level = Int32(0), log_interval_seconds = Float64(0.0), major_iteration_frequency = Int32(64), termination_check_frequency = Int32(64), restart_strategy = var"PrimalDualHybridGradientParams.RestartStrategy".ADAPTIVE_HEURISTIC, primal_weight_update_smoothing = Float64(0.5), initial_primal_weight = zero(Float64), presolve_options = nothing, l_inf_ruiz_iterations = Int32(5), l2_norm_rescaling = true, sufficient_reduction_for_restart = Float64(0.1), necessary_reduction_for_restart = Float64(0.9), linesearch_rule = var"PrimalDualHybridGradientParams.LinesearchRule".ADAPTIVE_LINESEARCH_RULE, adaptive_linesearch_parameters = nothing, malitsky_pock_parameters = nothing, initial_step_size_scaling = Float64(1.0), random_projection_seeds = Vector{Int32}(), infinite_constraint_bound_threshold = Float64(Inf), handle_some_primal_gradients_on_finite_bounds_as_residuals = true, use_diagonal_qp_trust_region_solver = false, diagonal_qp_trust_region_solver_tolerance = Float64(1.0e-8), use_feasibility_polishing = false, apply_feasibility_polishing_after_limits_reached = false, apply_feasibility_polishing_if_solver_is_interrupted = false) +PB.field_numbers(::Type{PrimalDualHybridGradientParams}) = (;termination_criteria = 1, num_threads = 2, num_shards = 27, scheduler_type = 32, record_iteration_stats = 3, verbosity_level = 26, log_interval_seconds = 31, major_iteration_frequency = 4, termination_check_frequency = 5, restart_strategy = 6, primal_weight_update_smoothing = 7, initial_primal_weight = 8, presolve_options = 16, l_inf_ruiz_iterations = 9, l2_norm_rescaling = 10, sufficient_reduction_for_restart = 11, necessary_reduction_for_restart = 17, linesearch_rule = 12, adaptive_linesearch_parameters = 18, malitsky_pock_parameters = 19, initial_step_size_scaling = 25, random_projection_seeds = 28, infinite_constraint_bound_threshold = 22, handle_some_primal_gradients_on_finite_bounds_as_residuals = 29, use_diagonal_qp_trust_region_solver = 23, diagonal_qp_trust_region_solver_tolerance = 24, use_feasibility_polishing = 30, apply_feasibility_polishing_after_limits_reached = 33, apply_feasibility_polishing_if_solver_is_interrupted = 34) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:PrimalDualHybridGradientParams}) termination_criteria = Ref{Union{Nothing,TerminationCriteria}}(nothing) num_threads = Int32(1) num_shards = Int32(0) + scheduler_type = SchedulerType.SCHEDULER_TYPE_GOOGLE_THREADPOOL record_iteration_stats = false verbosity_level = Int32(0) log_interval_seconds = Float64(0.0) @@ -380,6 +387,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:PrimalDualHybridGradient use_diagonal_qp_trust_region_solver = false diagonal_qp_trust_region_solver_tolerance = Float64(1.0e-8) use_feasibility_polishing = false + apply_feasibility_polishing_after_limits_reached = false + apply_feasibility_polishing_if_solver_is_interrupted = false while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 @@ -388,6 +397,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:PrimalDualHybridGradient num_threads = PB.decode(d, Int32) elseif field_number == 27 num_shards = PB.decode(d, Int32) + elseif field_number == 32 + scheduler_type = PB.decode(d, SchedulerType.T) elseif field_number == 3 record_iteration_stats = PB.decode(d, Bool) elseif field_number == 26 @@ -434,11 +445,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:PrimalDualHybridGradient diagonal_qp_trust_region_solver_tolerance = PB.decode(d, Float64) elseif field_number == 30 use_feasibility_polishing = PB.decode(d, Bool) + elseif field_number == 33 + apply_feasibility_polishing_after_limits_reached = PB.decode(d, Bool) + elseif field_number == 34 + apply_feasibility_polishing_if_solver_is_interrupted = PB.decode(d, Bool) else PB.skip(d, wire_type) end end - return PrimalDualHybridGradientParams(termination_criteria[], num_threads, num_shards, record_iteration_stats, verbosity_level, log_interval_seconds, major_iteration_frequency, termination_check_frequency, restart_strategy, primal_weight_update_smoothing, initial_primal_weight, presolve_options[], l_inf_ruiz_iterations, l2_norm_rescaling, sufficient_reduction_for_restart, necessary_reduction_for_restart, linesearch_rule, adaptive_linesearch_parameters[], malitsky_pock_parameters[], initial_step_size_scaling, random_projection_seeds[], infinite_constraint_bound_threshold, handle_some_primal_gradients_on_finite_bounds_as_residuals, use_diagonal_qp_trust_region_solver, diagonal_qp_trust_region_solver_tolerance, use_feasibility_polishing) + return PrimalDualHybridGradientParams(termination_criteria[], num_threads, num_shards, scheduler_type, record_iteration_stats, verbosity_level, log_interval_seconds, major_iteration_frequency, termination_check_frequency, restart_strategy, primal_weight_update_smoothing, initial_primal_weight, presolve_options[], l_inf_ruiz_iterations, l2_norm_rescaling, sufficient_reduction_for_restart, necessary_reduction_for_restart, linesearch_rule, adaptive_linesearch_parameters[], malitsky_pock_parameters[], initial_step_size_scaling, random_projection_seeds[], infinite_constraint_bound_threshold, handle_some_primal_gradients_on_finite_bounds_as_residuals, use_diagonal_qp_trust_region_solver, diagonal_qp_trust_region_solver_tolerance, use_feasibility_polishing, apply_feasibility_polishing_after_limits_reached, apply_feasibility_polishing_if_solver_is_interrupted) end function PB.encode(e::PB.AbstractProtoEncoder, x::PrimalDualHybridGradientParams) @@ -446,29 +461,32 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::PrimalDualHybridGradientParams !isnothing(x.termination_criteria) && PB.encode(e, 1, x.termination_criteria) x.num_threads != Int32(1) && PB.encode(e, 2, x.num_threads) x.num_shards != Int32(0) && PB.encode(e, 27, x.num_shards) + x.scheduler_type != SchedulerType.SCHEDULER_TYPE_GOOGLE_THREADPOOL && PB.encode(e, 32, x.scheduler_type) x.record_iteration_stats != false && PB.encode(e, 3, x.record_iteration_stats) x.verbosity_level != Int32(0) && PB.encode(e, 26, x.verbosity_level) - x.log_interval_seconds != Float64(0.0) && PB.encode(e, 31, x.log_interval_seconds) + x.log_interval_seconds !== Float64(0.0) && PB.encode(e, 31, x.log_interval_seconds) x.major_iteration_frequency != Int32(64) && PB.encode(e, 4, x.major_iteration_frequency) x.termination_check_frequency != Int32(64) && PB.encode(e, 5, x.termination_check_frequency) x.restart_strategy != var"PrimalDualHybridGradientParams.RestartStrategy".ADAPTIVE_HEURISTIC && PB.encode(e, 6, x.restart_strategy) - x.primal_weight_update_smoothing != Float64(0.5) && PB.encode(e, 7, x.primal_weight_update_smoothing) - x.initial_primal_weight != zero(Float64) && PB.encode(e, 8, x.initial_primal_weight) + x.primal_weight_update_smoothing !== Float64(0.5) && PB.encode(e, 7, x.primal_weight_update_smoothing) + x.initial_primal_weight !== zero(Float64) && PB.encode(e, 8, x.initial_primal_weight) !isnothing(x.presolve_options) && PB.encode(e, 16, x.presolve_options) x.l_inf_ruiz_iterations != Int32(5) && PB.encode(e, 9, x.l_inf_ruiz_iterations) x.l2_norm_rescaling != true && PB.encode(e, 10, x.l2_norm_rescaling) - x.sufficient_reduction_for_restart != Float64(0.1) && PB.encode(e, 11, x.sufficient_reduction_for_restart) - x.necessary_reduction_for_restart != Float64(0.9) && PB.encode(e, 17, x.necessary_reduction_for_restart) + x.sufficient_reduction_for_restart !== Float64(0.1) && PB.encode(e, 11, x.sufficient_reduction_for_restart) + x.necessary_reduction_for_restart !== Float64(0.9) && PB.encode(e, 17, x.necessary_reduction_for_restart) x.linesearch_rule != var"PrimalDualHybridGradientParams.LinesearchRule".ADAPTIVE_LINESEARCH_RULE && PB.encode(e, 12, x.linesearch_rule) !isnothing(x.adaptive_linesearch_parameters) && PB.encode(e, 18, x.adaptive_linesearch_parameters) !isnothing(x.malitsky_pock_parameters) && PB.encode(e, 19, x.malitsky_pock_parameters) - x.initial_step_size_scaling != Float64(1.0) && PB.encode(e, 25, x.initial_step_size_scaling) + x.initial_step_size_scaling !== Float64(1.0) && PB.encode(e, 25, x.initial_step_size_scaling) !isempty(x.random_projection_seeds) && PB.encode(e, 28, x.random_projection_seeds) - x.infinite_constraint_bound_threshold != Float64(Inf) && PB.encode(e, 22, x.infinite_constraint_bound_threshold) + x.infinite_constraint_bound_threshold !== Float64(Inf) && PB.encode(e, 22, x.infinite_constraint_bound_threshold) x.handle_some_primal_gradients_on_finite_bounds_as_residuals != true && PB.encode(e, 29, x.handle_some_primal_gradients_on_finite_bounds_as_residuals) x.use_diagonal_qp_trust_region_solver != false && PB.encode(e, 23, x.use_diagonal_qp_trust_region_solver) - x.diagonal_qp_trust_region_solver_tolerance != Float64(1.0e-8) && PB.encode(e, 24, x.diagonal_qp_trust_region_solver_tolerance) + x.diagonal_qp_trust_region_solver_tolerance !== Float64(1.0e-8) && PB.encode(e, 24, x.diagonal_qp_trust_region_solver_tolerance) x.use_feasibility_polishing != false && PB.encode(e, 30, x.use_feasibility_polishing) + x.apply_feasibility_polishing_after_limits_reached != false && PB.encode(e, 33, x.apply_feasibility_polishing_after_limits_reached) + x.apply_feasibility_polishing_if_solver_is_interrupted != false && PB.encode(e, 34, x.apply_feasibility_polishing_if_solver_is_interrupted) return position(e.io) - initpos end function PB._encoded_size(x::PrimalDualHybridGradientParams) @@ -476,28 +494,31 @@ function PB._encoded_size(x::PrimalDualHybridGradientParams) !isnothing(x.termination_criteria) && (encoded_size += PB._encoded_size(x.termination_criteria, 1)) x.num_threads != Int32(1) && (encoded_size += PB._encoded_size(x.num_threads, 2)) x.num_shards != Int32(0) && (encoded_size += PB._encoded_size(x.num_shards, 27)) + x.scheduler_type != SchedulerType.SCHEDULER_TYPE_GOOGLE_THREADPOOL && (encoded_size += PB._encoded_size(x.scheduler_type, 32)) x.record_iteration_stats != false && (encoded_size += PB._encoded_size(x.record_iteration_stats, 3)) x.verbosity_level != Int32(0) && (encoded_size += PB._encoded_size(x.verbosity_level, 26)) - x.log_interval_seconds != Float64(0.0) && (encoded_size += PB._encoded_size(x.log_interval_seconds, 31)) + x.log_interval_seconds !== Float64(0.0) && (encoded_size += PB._encoded_size(x.log_interval_seconds, 31)) x.major_iteration_frequency != Int32(64) && (encoded_size += PB._encoded_size(x.major_iteration_frequency, 4)) x.termination_check_frequency != Int32(64) && (encoded_size += PB._encoded_size(x.termination_check_frequency, 5)) x.restart_strategy != var"PrimalDualHybridGradientParams.RestartStrategy".ADAPTIVE_HEURISTIC && (encoded_size += PB._encoded_size(x.restart_strategy, 6)) - x.primal_weight_update_smoothing != Float64(0.5) && (encoded_size += PB._encoded_size(x.primal_weight_update_smoothing, 7)) - x.initial_primal_weight != zero(Float64) && (encoded_size += PB._encoded_size(x.initial_primal_weight, 8)) + x.primal_weight_update_smoothing !== Float64(0.5) && (encoded_size += PB._encoded_size(x.primal_weight_update_smoothing, 7)) + x.initial_primal_weight !== zero(Float64) && (encoded_size += PB._encoded_size(x.initial_primal_weight, 8)) !isnothing(x.presolve_options) && (encoded_size += PB._encoded_size(x.presolve_options, 16)) x.l_inf_ruiz_iterations != Int32(5) && (encoded_size += PB._encoded_size(x.l_inf_ruiz_iterations, 9)) x.l2_norm_rescaling != true && (encoded_size += PB._encoded_size(x.l2_norm_rescaling, 10)) - x.sufficient_reduction_for_restart != Float64(0.1) && (encoded_size += PB._encoded_size(x.sufficient_reduction_for_restart, 11)) - x.necessary_reduction_for_restart != Float64(0.9) && (encoded_size += PB._encoded_size(x.necessary_reduction_for_restart, 17)) + x.sufficient_reduction_for_restart !== Float64(0.1) && (encoded_size += PB._encoded_size(x.sufficient_reduction_for_restart, 11)) + x.necessary_reduction_for_restart !== Float64(0.9) && (encoded_size += PB._encoded_size(x.necessary_reduction_for_restart, 17)) x.linesearch_rule != var"PrimalDualHybridGradientParams.LinesearchRule".ADAPTIVE_LINESEARCH_RULE && (encoded_size += PB._encoded_size(x.linesearch_rule, 12)) !isnothing(x.adaptive_linesearch_parameters) && (encoded_size += PB._encoded_size(x.adaptive_linesearch_parameters, 18)) !isnothing(x.malitsky_pock_parameters) && (encoded_size += PB._encoded_size(x.malitsky_pock_parameters, 19)) - x.initial_step_size_scaling != Float64(1.0) && (encoded_size += PB._encoded_size(x.initial_step_size_scaling, 25)) + x.initial_step_size_scaling !== Float64(1.0) && (encoded_size += PB._encoded_size(x.initial_step_size_scaling, 25)) !isempty(x.random_projection_seeds) && (encoded_size += PB._encoded_size(x.random_projection_seeds, 28)) - x.infinite_constraint_bound_threshold != Float64(Inf) && (encoded_size += PB._encoded_size(x.infinite_constraint_bound_threshold, 22)) + x.infinite_constraint_bound_threshold !== Float64(Inf) && (encoded_size += PB._encoded_size(x.infinite_constraint_bound_threshold, 22)) x.handle_some_primal_gradients_on_finite_bounds_as_residuals != true && (encoded_size += PB._encoded_size(x.handle_some_primal_gradients_on_finite_bounds_as_residuals, 29)) x.use_diagonal_qp_trust_region_solver != false && (encoded_size += PB._encoded_size(x.use_diagonal_qp_trust_region_solver, 23)) - x.diagonal_qp_trust_region_solver_tolerance != Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.diagonal_qp_trust_region_solver_tolerance, 24)) + x.diagonal_qp_trust_region_solver_tolerance !== Float64(1.0e-8) && (encoded_size += PB._encoded_size(x.diagonal_qp_trust_region_solver_tolerance, 24)) x.use_feasibility_polishing != false && (encoded_size += PB._encoded_size(x.use_feasibility_polishing, 30)) + x.apply_feasibility_polishing_after_limits_reached != false && (encoded_size += PB._encoded_size(x.apply_feasibility_polishing_after_limits_reached, 33)) + x.apply_feasibility_polishing_if_solver_is_interrupted != false && (encoded_size += PB._encoded_size(x.apply_feasibility_polishing_if_solver_is_interrupted, 34)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_enums_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_enums_pb.jl index 071ac336ae1..b55e3350a53 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_enums_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_enums_pb.jl @@ -1,14 +1,16 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.401 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/routing_enums.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.430 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/routing_enums.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf using ProtoBuf.EnumX: @enumx -export LocalSearchMetaheuristic, FirstSolutionStrategy, var"FirstSolutionStrategy.Value" +export LocalSearchMetaheuristic, FirstSolutionStrategy, RoutingSearchStatus +export var"RoutingSearchStatus.Value", var"FirstSolutionStrategy.Value" export var"LocalSearchMetaheuristic.Value" -struct LocalSearchMetaheuristic end + +struct LocalSearchMetaheuristic end function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:LocalSearchMetaheuristic}) while !PB.message_done(d) @@ -27,7 +29,7 @@ function PB._encoded_size(x::LocalSearchMetaheuristic) return encoded_size end -struct FirstSolutionStrategy end +struct FirstSolutionStrategy end function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:FirstSolutionStrategy}) while !PB.message_done(d) @@ -46,6 +48,27 @@ function PB._encoded_size(x::FirstSolutionStrategy) return encoded_size end -@enumx var"FirstSolutionStrategy.Value" UNSET=0 AUTOMATIC=15 PATH_CHEAPEST_ARC=3 PATH_MOST_CONSTRAINED_ARC=4 EVALUATOR_STRATEGY=5 SAVINGS=10 SWEEP=11 CHRISTOFIDES=13 ALL_UNPERFORMED=6 BEST_INSERTION=7 PARALLEL_CHEAPEST_INSERTION=8 SEQUENTIAL_CHEAPEST_INSERTION=14 LOCAL_CHEAPEST_INSERTION=9 LOCAL_CHEAPEST_COST_INSERTION=16 GLOBAL_CHEAPEST_ARC=1 LOCAL_CHEAPEST_ARC=2 FIRST_UNBOUND_MIN_VALUE=12 +struct RoutingSearchStatus end + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchStatus}) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + PB.skip(d, wire_type) + end + return RoutingSearchStatus() +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RoutingSearchStatus) + initpos = position(e.io) + return position(e.io) - initpos +end +function PB._encoded_size(x::RoutingSearchStatus) + encoded_size = 0 + return encoded_size +end + +@enumx var"RoutingSearchStatus.Value" ROUTING_NOT_SOLVED=0 ROUTING_SUCCESS=1 ROUTING_PARTIAL_SUCCESS_LOCAL_OPTIMUM_NOT_REACHED=2 ROUTING_FAIL=3 ROUTING_FAIL_TIMEOUT=4 ROUTING_INVALID=5 ROUTING_INFEASIBLE=6 ROUTING_OPTIMAL=7 + +@enumx var"FirstSolutionStrategy.Value" UNSET=0 AUTOMATIC=15 PATH_CHEAPEST_ARC=3 PATH_MOST_CONSTRAINED_ARC=4 EVALUATOR_STRATEGY=5 SAVINGS=10 PARALLEL_SAVINGS=17 SWEEP=11 CHRISTOFIDES=13 ALL_UNPERFORMED=6 BEST_INSERTION=7 PARALLEL_CHEAPEST_INSERTION=8 SEQUENTIAL_CHEAPEST_INSERTION=14 LOCAL_CHEAPEST_INSERTION=9 LOCAL_CHEAPEST_COST_INSERTION=16 GLOBAL_CHEAPEST_ARC=1 LOCAL_CHEAPEST_ARC=2 FIRST_UNBOUND_MIN_VALUE=12 @enumx var"LocalSearchMetaheuristic.Value" UNSET=0 AUTOMATIC=6 GREEDY_DESCENT=1 GUIDED_LOCAL_SEARCH=2 SIMULATED_ANNEALING=3 TABU_SEARCH=4 GENERIC_TABU_SEARCH=5 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_ils_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_ils_pb.jl new file mode 100644 index 00000000000..16c2d117df4 --- /dev/null +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_ils_pb.jl @@ -0,0 +1,413 @@ +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.070 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/routing_ils.proto (proto3 syntax) + +import ProtoBuf as PB +using ProtoBuf: OneOf +using ProtoBuf.EnumX: @enumx + +export SISRRuinStrategy, var"AcceptanceStrategy.Value", AcceptanceStrategy +export var"PerturbationStrategy.Value", var"RuinCompositionStrategy.Value" +export CoolingScheduleStrategy, RuinCompositionStrategy, var"CoolingScheduleStrategy.Value" +export RandomWalkRuinStrategy, PerturbationStrategy, SpatiallyCloseRoutesRuinStrategy +export SimulatedAnnealingParameters, RuinStrategy, RuinRecreateParameters +export IteratedLocalSearchParameters + + +struct SISRRuinStrategy + max_removed_sequence_size::UInt32 + avg_num_removed_visits::UInt32 + bypass_factor::Float64 +end +PB.default_values(::Type{SISRRuinStrategy}) = (;max_removed_sequence_size = zero(UInt32), avg_num_removed_visits = zero(UInt32), bypass_factor = zero(Float64)) +PB.field_numbers(::Type{SISRRuinStrategy}) = (;max_removed_sequence_size = 1, avg_num_removed_visits = 2, bypass_factor = 3) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SISRRuinStrategy}) + max_removed_sequence_size = zero(UInt32) + avg_num_removed_visits = zero(UInt32) + bypass_factor = zero(Float64) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + max_removed_sequence_size = PB.decode(d, UInt32) + elseif field_number == 2 + avg_num_removed_visits = PB.decode(d, UInt32) + elseif field_number == 3 + bypass_factor = PB.decode(d, Float64) + else + PB.skip(d, wire_type) + end + end + return SISRRuinStrategy(max_removed_sequence_size, avg_num_removed_visits, bypass_factor) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::SISRRuinStrategy) + initpos = position(e.io) + x.max_removed_sequence_size != zero(UInt32) && PB.encode(e, 1, x.max_removed_sequence_size) + x.avg_num_removed_visits != zero(UInt32) && PB.encode(e, 2, x.avg_num_removed_visits) + x.bypass_factor !== zero(Float64) && PB.encode(e, 3, x.bypass_factor) + return position(e.io) - initpos +end +function PB._encoded_size(x::SISRRuinStrategy) + encoded_size = 0 + x.max_removed_sequence_size != zero(UInt32) && (encoded_size += PB._encoded_size(x.max_removed_sequence_size, 1)) + x.avg_num_removed_visits != zero(UInt32) && (encoded_size += PB._encoded_size(x.avg_num_removed_visits, 2)) + x.bypass_factor !== zero(Float64) && (encoded_size += PB._encoded_size(x.bypass_factor, 3)) + return encoded_size +end + +@enumx var"AcceptanceStrategy.Value" UNSET=0 GREEDY_DESCENT=1 SIMULATED_ANNEALING=2 + +struct AcceptanceStrategy end + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:AcceptanceStrategy}) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + PB.skip(d, wire_type) + end + return AcceptanceStrategy() +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::AcceptanceStrategy) + initpos = position(e.io) + return position(e.io) - initpos +end +function PB._encoded_size(x::AcceptanceStrategy) + encoded_size = 0 + return encoded_size +end + +@enumx var"PerturbationStrategy.Value" UNSET=0 RUIN_AND_RECREATE=1 + +@enumx var"RuinCompositionStrategy.Value" UNSET=0 RUN_ALL_SEQUENTIALLY=1 RUN_ALL_RANDOMLY=2 RUN_ONE_RANDOMLY=3 + +struct CoolingScheduleStrategy end + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:CoolingScheduleStrategy}) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + PB.skip(d, wire_type) + end + return CoolingScheduleStrategy() +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::CoolingScheduleStrategy) + initpos = position(e.io) + return position(e.io) - initpos +end +function PB._encoded_size(x::CoolingScheduleStrategy) + encoded_size = 0 + return encoded_size +end + +struct RuinCompositionStrategy end + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RuinCompositionStrategy}) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + PB.skip(d, wire_type) + end + return RuinCompositionStrategy() +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RuinCompositionStrategy) + initpos = position(e.io) + return position(e.io) - initpos +end +function PB._encoded_size(x::RuinCompositionStrategy) + encoded_size = 0 + return encoded_size +end + +@enumx var"CoolingScheduleStrategy.Value" UNSET=0 EXPONENTIAL=1 LINEAR=2 + +struct RandomWalkRuinStrategy + num_removed_visits::UInt32 +end +PB.default_values(::Type{RandomWalkRuinStrategy}) = (;num_removed_visits = zero(UInt32)) +PB.field_numbers(::Type{RandomWalkRuinStrategy}) = (;num_removed_visits = 7) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RandomWalkRuinStrategy}) + num_removed_visits = zero(UInt32) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 7 + num_removed_visits = PB.decode(d, UInt32) + else + PB.skip(d, wire_type) + end + end + return RandomWalkRuinStrategy(num_removed_visits) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RandomWalkRuinStrategy) + initpos = position(e.io) + x.num_removed_visits != zero(UInt32) && PB.encode(e, 7, x.num_removed_visits) + return position(e.io) - initpos +end +function PB._encoded_size(x::RandomWalkRuinStrategy) + encoded_size = 0 + x.num_removed_visits != zero(UInt32) && (encoded_size += PB._encoded_size(x.num_removed_visits, 7)) + return encoded_size +end + +struct PerturbationStrategy end + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:PerturbationStrategy}) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + PB.skip(d, wire_type) + end + return PerturbationStrategy() +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::PerturbationStrategy) + initpos = position(e.io) + return position(e.io) - initpos +end +function PB._encoded_size(x::PerturbationStrategy) + encoded_size = 0 + return encoded_size +end + +struct SpatiallyCloseRoutesRuinStrategy + num_ruined_routes::UInt32 +end +PB.default_values(::Type{SpatiallyCloseRoutesRuinStrategy}) = (;num_ruined_routes = zero(UInt32)) +PB.field_numbers(::Type{SpatiallyCloseRoutesRuinStrategy}) = (;num_ruined_routes = 3) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SpatiallyCloseRoutesRuinStrategy}) + num_ruined_routes = zero(UInt32) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 3 + num_ruined_routes = PB.decode(d, UInt32) + else + PB.skip(d, wire_type) + end + end + return SpatiallyCloseRoutesRuinStrategy(num_ruined_routes) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::SpatiallyCloseRoutesRuinStrategy) + initpos = position(e.io) + x.num_ruined_routes != zero(UInt32) && PB.encode(e, 3, x.num_ruined_routes) + return position(e.io) - initpos +end +function PB._encoded_size(x::SpatiallyCloseRoutesRuinStrategy) + encoded_size = 0 + x.num_ruined_routes != zero(UInt32) && (encoded_size += PB._encoded_size(x.num_ruined_routes, 3)) + return encoded_size +end + +struct SimulatedAnnealingParameters + cooling_schedule_strategy::var"CoolingScheduleStrategy.Value".T + initial_temperature::Float64 + final_temperature::Float64 + automatic_temperatures::Bool +end +PB.default_values(::Type{SimulatedAnnealingParameters}) = (;cooling_schedule_strategy = var"CoolingScheduleStrategy.Value".UNSET, initial_temperature = zero(Float64), final_temperature = zero(Float64), automatic_temperatures = false) +PB.field_numbers(::Type{SimulatedAnnealingParameters}) = (;cooling_schedule_strategy = 1, initial_temperature = 2, final_temperature = 3, automatic_temperatures = 4) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SimulatedAnnealingParameters}) + cooling_schedule_strategy = var"CoolingScheduleStrategy.Value".UNSET + initial_temperature = zero(Float64) + final_temperature = zero(Float64) + automatic_temperatures = false + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + cooling_schedule_strategy = PB.decode(d, var"CoolingScheduleStrategy.Value".T) + elseif field_number == 2 + initial_temperature = PB.decode(d, Float64) + elseif field_number == 3 + final_temperature = PB.decode(d, Float64) + elseif field_number == 4 + automatic_temperatures = PB.decode(d, Bool) + else + PB.skip(d, wire_type) + end + end + return SimulatedAnnealingParameters(cooling_schedule_strategy, initial_temperature, final_temperature, automatic_temperatures) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::SimulatedAnnealingParameters) + initpos = position(e.io) + x.cooling_schedule_strategy != var"CoolingScheduleStrategy.Value".UNSET && PB.encode(e, 1, x.cooling_schedule_strategy) + x.initial_temperature !== zero(Float64) && PB.encode(e, 2, x.initial_temperature) + x.final_temperature !== zero(Float64) && PB.encode(e, 3, x.final_temperature) + x.automatic_temperatures != false && PB.encode(e, 4, x.automatic_temperatures) + return position(e.io) - initpos +end +function PB._encoded_size(x::SimulatedAnnealingParameters) + encoded_size = 0 + x.cooling_schedule_strategy != var"CoolingScheduleStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.cooling_schedule_strategy, 1)) + x.initial_temperature !== zero(Float64) && (encoded_size += PB._encoded_size(x.initial_temperature, 2)) + x.final_temperature !== zero(Float64) && (encoded_size += PB._encoded_size(x.final_temperature, 3)) + x.automatic_temperatures != false && (encoded_size += PB._encoded_size(x.automatic_temperatures, 4)) + return encoded_size +end + +struct RuinStrategy + strategy::Union{Nothing,OneOf{<:Union{SpatiallyCloseRoutesRuinStrategy,RandomWalkRuinStrategy,SISRRuinStrategy}}} +end +PB.oneof_field_types(::Type{RuinStrategy}) = (; + strategy = (;spatially_close_routes=SpatiallyCloseRoutesRuinStrategy, random_walk=RandomWalkRuinStrategy, sisr=SISRRuinStrategy), +) +PB.default_values(::Type{RuinStrategy}) = (;spatially_close_routes = nothing, random_walk = nothing, sisr = nothing) +PB.field_numbers(::Type{RuinStrategy}) = (;spatially_close_routes = 1, random_walk = 2, sisr = 3) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RuinStrategy}) + strategy = nothing + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + strategy = OneOf(:spatially_close_routes, PB.decode(d, Ref{SpatiallyCloseRoutesRuinStrategy})) + elseif field_number == 2 + strategy = OneOf(:random_walk, PB.decode(d, Ref{RandomWalkRuinStrategy})) + elseif field_number == 3 + strategy = OneOf(:sisr, PB.decode(d, Ref{SISRRuinStrategy})) + else + PB.skip(d, wire_type) + end + end + return RuinStrategy(strategy) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RuinStrategy) + initpos = position(e.io) + if isnothing(x.strategy); + elseif x.strategy.name === :spatially_close_routes + PB.encode(e, 1, x.strategy[]::SpatiallyCloseRoutesRuinStrategy) + elseif x.strategy.name === :random_walk + PB.encode(e, 2, x.strategy[]::RandomWalkRuinStrategy) + elseif x.strategy.name === :sisr + PB.encode(e, 3, x.strategy[]::SISRRuinStrategy) + end + return position(e.io) - initpos +end +function PB._encoded_size(x::RuinStrategy) + encoded_size = 0 + if isnothing(x.strategy); + elseif x.strategy.name === :spatially_close_routes + encoded_size += PB._encoded_size(x.strategy[]::SpatiallyCloseRoutesRuinStrategy, 1) + elseif x.strategy.name === :random_walk + encoded_size += PB._encoded_size(x.strategy[]::RandomWalkRuinStrategy, 2) + elseif x.strategy.name === :sisr + encoded_size += PB._encoded_size(x.strategy[]::SISRRuinStrategy, 3) + end + return encoded_size +end + +struct RuinRecreateParameters + ruin_strategies::Vector{RuinStrategy} + ruin_composition_strategy::var"RuinCompositionStrategy.Value".T + recreate_strategy::var"FirstSolutionStrategy.Value".T + route_selection_neighbors_ratio::Float64 + route_selection_min_neighbors::UInt32 + route_selection_max_neighbors::UInt32 +end +PB.default_values(::Type{RuinRecreateParameters}) = (;ruin_strategies = Vector{RuinStrategy}(), ruin_composition_strategy = var"RuinCompositionStrategy.Value".UNSET, recreate_strategy = var"FirstSolutionStrategy.Value".UNSET, route_selection_neighbors_ratio = zero(Float64), route_selection_min_neighbors = zero(UInt32), route_selection_max_neighbors = zero(UInt32)) +PB.field_numbers(::Type{RuinRecreateParameters}) = (;ruin_strategies = 1, ruin_composition_strategy = 2, recreate_strategy = 3, route_selection_neighbors_ratio = 4, route_selection_min_neighbors = 5, route_selection_max_neighbors = 6) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RuinRecreateParameters}) + ruin_strategies = PB.BufferedVector{RuinStrategy}() + ruin_composition_strategy = var"RuinCompositionStrategy.Value".UNSET + recreate_strategy = var"FirstSolutionStrategy.Value".UNSET + route_selection_neighbors_ratio = zero(Float64) + route_selection_min_neighbors = zero(UInt32) + route_selection_max_neighbors = zero(UInt32) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + PB.decode!(d, ruin_strategies) + elseif field_number == 2 + ruin_composition_strategy = PB.decode(d, var"RuinCompositionStrategy.Value".T) + elseif field_number == 3 + recreate_strategy = PB.decode(d, var"FirstSolutionStrategy.Value".T) + elseif field_number == 4 + route_selection_neighbors_ratio = PB.decode(d, Float64) + elseif field_number == 5 + route_selection_min_neighbors = PB.decode(d, UInt32) + elseif field_number == 6 + route_selection_max_neighbors = PB.decode(d, UInt32) + else + PB.skip(d, wire_type) + end + end + return RuinRecreateParameters(ruin_strategies[], ruin_composition_strategy, recreate_strategy, route_selection_neighbors_ratio, route_selection_min_neighbors, route_selection_max_neighbors) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RuinRecreateParameters) + initpos = position(e.io) + !isempty(x.ruin_strategies) && PB.encode(e, 1, x.ruin_strategies) + x.ruin_composition_strategy != var"RuinCompositionStrategy.Value".UNSET && PB.encode(e, 2, x.ruin_composition_strategy) + x.recreate_strategy != var"FirstSolutionStrategy.Value".UNSET && PB.encode(e, 3, x.recreate_strategy) + x.route_selection_neighbors_ratio !== zero(Float64) && PB.encode(e, 4, x.route_selection_neighbors_ratio) + x.route_selection_min_neighbors != zero(UInt32) && PB.encode(e, 5, x.route_selection_min_neighbors) + x.route_selection_max_neighbors != zero(UInt32) && PB.encode(e, 6, x.route_selection_max_neighbors) + return position(e.io) - initpos +end +function PB._encoded_size(x::RuinRecreateParameters) + encoded_size = 0 + !isempty(x.ruin_strategies) && (encoded_size += PB._encoded_size(x.ruin_strategies, 1)) + x.ruin_composition_strategy != var"RuinCompositionStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.ruin_composition_strategy, 2)) + x.recreate_strategy != var"FirstSolutionStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.recreate_strategy, 3)) + x.route_selection_neighbors_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.route_selection_neighbors_ratio, 4)) + x.route_selection_min_neighbors != zero(UInt32) && (encoded_size += PB._encoded_size(x.route_selection_min_neighbors, 5)) + x.route_selection_max_neighbors != zero(UInt32) && (encoded_size += PB._encoded_size(x.route_selection_max_neighbors, 6)) + return encoded_size +end + +struct IteratedLocalSearchParameters + perturbation_strategy::var"PerturbationStrategy.Value".T + ruin_recreate_parameters::Union{Nothing,RuinRecreateParameters} + improve_perturbed_solution::Bool + acceptance_strategy::var"AcceptanceStrategy.Value".T + simulated_annealing_parameters::Union{Nothing,SimulatedAnnealingParameters} +end +PB.default_values(::Type{IteratedLocalSearchParameters}) = (;perturbation_strategy = var"PerturbationStrategy.Value".UNSET, ruin_recreate_parameters = nothing, improve_perturbed_solution = false, acceptance_strategy = var"AcceptanceStrategy.Value".UNSET, simulated_annealing_parameters = nothing) +PB.field_numbers(::Type{IteratedLocalSearchParameters}) = (;perturbation_strategy = 1, ruin_recreate_parameters = 2, improve_perturbed_solution = 3, acceptance_strategy = 4, simulated_annealing_parameters = 5) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:IteratedLocalSearchParameters}) + perturbation_strategy = var"PerturbationStrategy.Value".UNSET + ruin_recreate_parameters = Ref{Union{Nothing,RuinRecreateParameters}}(nothing) + improve_perturbed_solution = false + acceptance_strategy = var"AcceptanceStrategy.Value".UNSET + simulated_annealing_parameters = Ref{Union{Nothing,SimulatedAnnealingParameters}}(nothing) + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + perturbation_strategy = PB.decode(d, var"PerturbationStrategy.Value".T) + elseif field_number == 2 + PB.decode!(d, ruin_recreate_parameters) + elseif field_number == 3 + improve_perturbed_solution = PB.decode(d, Bool) + elseif field_number == 4 + acceptance_strategy = PB.decode(d, var"AcceptanceStrategy.Value".T) + elseif field_number == 5 + PB.decode!(d, simulated_annealing_parameters) + else + PB.skip(d, wire_type) + end + end + return IteratedLocalSearchParameters(perturbation_strategy, ruin_recreate_parameters[], improve_perturbed_solution, acceptance_strategy, simulated_annealing_parameters[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::IteratedLocalSearchParameters) + initpos = position(e.io) + x.perturbation_strategy != var"PerturbationStrategy.Value".UNSET && PB.encode(e, 1, x.perturbation_strategy) + !isnothing(x.ruin_recreate_parameters) && PB.encode(e, 2, x.ruin_recreate_parameters) + x.improve_perturbed_solution != false && PB.encode(e, 3, x.improve_perturbed_solution) + x.acceptance_strategy != var"AcceptanceStrategy.Value".UNSET && PB.encode(e, 4, x.acceptance_strategy) + !isnothing(x.simulated_annealing_parameters) && PB.encode(e, 5, x.simulated_annealing_parameters) + return position(e.io) - initpos +end +function PB._encoded_size(x::IteratedLocalSearchParameters) + encoded_size = 0 + x.perturbation_strategy != var"PerturbationStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.perturbation_strategy, 1)) + !isnothing(x.ruin_recreate_parameters) && (encoded_size += PB._encoded_size(x.ruin_recreate_parameters, 2)) + x.improve_perturbed_solution != false && (encoded_size += PB._encoded_size(x.improve_perturbed_solution, 3)) + x.acceptance_strategy != var"AcceptanceStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.acceptance_strategy, 4)) + !isnothing(x.simulated_annealing_parameters) && (encoded_size += PB._encoded_size(x.simulated_annealing_parameters, 5)) + return encoded_size +end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_parameters_pb.jl index cf6ce76d79d..7dfb8a12ca6 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/routing_parameters_pb.jl @@ -1,15 +1,21 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.151 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/routing_parameters.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.109 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/routing_parameters.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf using ProtoBuf.EnumX: @enumx -export RoutingModelParameters, var"RoutingSearchParameters.PairInsertionStrategy" +export var"RoutingSearchParameters.InsertionSortingProperty" +export var"RoutingSearchParameters.PairInsertionStrategy", RoutingModelParameters export var"RoutingSearchParameters.LocalSearchNeighborhoodOperators" export var"RoutingSearchParameters.ImprovementSearchLimitParameters" export var"RoutingSearchParameters.SchedulingSolver", RoutingSearchParameters + +@enumx var"RoutingSearchParameters.InsertionSortingProperty" SORTING_PROPERTY_UNSPECIFIED=0 SORTING_PROPERTY_ALLOWED_VEHICLES=1 SORTING_PROPERTY_PENALTY=2 SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO=3 SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS=4 SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS=5 SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS=6 SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE=7 SORTING_PROPERTY_RANDOM=8 + +@enumx var"RoutingSearchParameters.PairInsertionStrategy" AUTOMATIC=0 BEST_PICKUP_THEN_BEST_DELIVERY=1 BEST_PICKUP_DELIVERY_PAIR=2 BEST_PICKUP_DELIVERY_PAIR_MULTITOUR=3 + struct RoutingModelParameters solver_parameters::Union{Nothing,ConstraintSolverParameters} reduce_vehicle_cost_model::Bool @@ -52,8 +58,6 @@ function PB._encoded_size(x::RoutingModelParameters) return encoded_size end -@enumx var"RoutingSearchParameters.PairInsertionStrategy" AUTOMATIC=0 BEST_PICKUP_THEN_BEST_DELIVERY=1 BEST_PICKUP_DELIVERY_PAIR=2 BEST_PICKUP_DELIVERY_PAIR_MULTITOUR=3 - struct var"RoutingSearchParameters.LocalSearchNeighborhoodOperators" use_relocate::OptionalBoolean.T use_relocate_pair::OptionalBoolean.T @@ -72,11 +76,15 @@ struct var"RoutingSearchParameters.LocalSearchNeighborhoodOperators" use_tsp_opt::OptionalBoolean.T use_make_active::OptionalBoolean.T use_relocate_and_make_active::OptionalBoolean.T + use_exchange_and_make_active::OptionalBoolean.T + use_exchange_path_start_ends_and_make_active::OptionalBoolean.T use_make_inactive::OptionalBoolean.T use_make_chain_inactive::OptionalBoolean.T use_swap_active::OptionalBoolean.T + use_swap_active_chain::OptionalBoolean.T use_extended_swap_active::OptionalBoolean.T use_shortest_path_swap_active::OptionalBoolean.T + use_shortest_path_two_opt::OptionalBoolean.T use_node_pair_swap_active::OptionalBoolean.T use_path_lns::OptionalBoolean.T use_full_path_lns::OptionalBoolean.T @@ -90,8 +98,8 @@ struct var"RoutingSearchParameters.LocalSearchNeighborhoodOperators" use_global_cheapest_insertion_close_nodes_lns::OptionalBoolean.T use_local_cheapest_insertion_close_nodes_lns::OptionalBoolean.T end -PB.default_values(::Type{var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}) = (;use_relocate = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_light_relocate_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_neighbors = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_subtrip = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_subtrip = OptionalBoolean.BOOL_UNSPECIFIED, use_cross = OptionalBoolean.BOOL_UNSPECIFIED, use_cross_exchange = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_expensive_chain = OptionalBoolean.BOOL_UNSPECIFIED, use_two_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_or_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_lin_kernighan = OptionalBoolean.BOOL_UNSPECIFIED, use_tsp_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_make_inactive = OptionalBoolean.BOOL_UNSPECIFIED, use_make_chain_inactive = OptionalBoolean.BOOL_UNSPECIFIED, use_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_extended_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_shortest_path_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_node_pair_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_full_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_tsp_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_inactive_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_path_global_cheapest_insertion_insert_unperformed = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_expensive_chain_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_expensive_chain_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_close_nodes_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_close_nodes_lns = OptionalBoolean.BOOL_UNSPECIFIED) -PB.field_numbers(::Type{var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}) = (;use_relocate = 1, use_relocate_pair = 2, use_light_relocate_pair = 24, use_relocate_neighbors = 3, use_relocate_subtrip = 25, use_exchange = 4, use_exchange_pair = 22, use_exchange_subtrip = 26, use_cross = 5, use_cross_exchange = 6, use_relocate_expensive_chain = 23, use_two_opt = 7, use_or_opt = 8, use_lin_kernighan = 9, use_tsp_opt = 10, use_make_active = 11, use_relocate_and_make_active = 21, use_make_inactive = 12, use_make_chain_inactive = 13, use_swap_active = 14, use_extended_swap_active = 15, use_shortest_path_swap_active = 34, use_node_pair_swap_active = 20, use_path_lns = 16, use_full_path_lns = 17, use_tsp_lns = 18, use_inactive_lns = 19, use_global_cheapest_insertion_path_lns = 27, use_local_cheapest_insertion_path_lns = 28, use_relocate_path_global_cheapest_insertion_insert_unperformed = 33, use_global_cheapest_insertion_expensive_chain_lns = 29, use_local_cheapest_insertion_expensive_chain_lns = 30, use_global_cheapest_insertion_close_nodes_lns = 31, use_local_cheapest_insertion_close_nodes_lns = 32) +PB.default_values(::Type{var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}) = (;use_relocate = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_light_relocate_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_neighbors = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_subtrip = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_pair = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_subtrip = OptionalBoolean.BOOL_UNSPECIFIED, use_cross = OptionalBoolean.BOOL_UNSPECIFIED, use_cross_exchange = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_expensive_chain = OptionalBoolean.BOOL_UNSPECIFIED, use_two_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_or_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_lin_kernighan = OptionalBoolean.BOOL_UNSPECIFIED, use_tsp_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_exchange_path_start_ends_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED, use_make_inactive = OptionalBoolean.BOOL_UNSPECIFIED, use_make_chain_inactive = OptionalBoolean.BOOL_UNSPECIFIED, use_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_swap_active_chain = OptionalBoolean.BOOL_UNSPECIFIED, use_extended_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_shortest_path_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_shortest_path_two_opt = OptionalBoolean.BOOL_UNSPECIFIED, use_node_pair_swap_active = OptionalBoolean.BOOL_UNSPECIFIED, use_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_full_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_tsp_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_inactive_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_path_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_relocate_path_global_cheapest_insertion_insert_unperformed = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_expensive_chain_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_expensive_chain_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_global_cheapest_insertion_close_nodes_lns = OptionalBoolean.BOOL_UNSPECIFIED, use_local_cheapest_insertion_close_nodes_lns = OptionalBoolean.BOOL_UNSPECIFIED) +PB.field_numbers(::Type{var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}) = (;use_relocate = 1, use_relocate_pair = 2, use_light_relocate_pair = 24, use_relocate_neighbors = 3, use_relocate_subtrip = 25, use_exchange = 4, use_exchange_pair = 22, use_exchange_subtrip = 26, use_cross = 5, use_cross_exchange = 6, use_relocate_expensive_chain = 23, use_two_opt = 7, use_or_opt = 8, use_lin_kernighan = 9, use_tsp_opt = 10, use_make_active = 11, use_relocate_and_make_active = 21, use_exchange_and_make_active = 37, use_exchange_path_start_ends_and_make_active = 38, use_make_inactive = 12, use_make_chain_inactive = 13, use_swap_active = 14, use_swap_active_chain = 35, use_extended_swap_active = 15, use_shortest_path_swap_active = 34, use_shortest_path_two_opt = 36, use_node_pair_swap_active = 20, use_path_lns = 16, use_full_path_lns = 17, use_tsp_lns = 18, use_inactive_lns = 19, use_global_cheapest_insertion_path_lns = 27, use_local_cheapest_insertion_path_lns = 28, use_relocate_path_global_cheapest_insertion_insert_unperformed = 33, use_global_cheapest_insertion_expensive_chain_lns = 29, use_local_cheapest_insertion_expensive_chain_lns = 30, use_global_cheapest_insertion_close_nodes_lns = 31, use_local_cheapest_insertion_close_nodes_lns = 32) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}) use_relocate = OptionalBoolean.BOOL_UNSPECIFIED @@ -111,11 +119,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"RoutingSearchParamet use_tsp_opt = OptionalBoolean.BOOL_UNSPECIFIED use_make_active = OptionalBoolean.BOOL_UNSPECIFIED use_relocate_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED + use_exchange_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED + use_exchange_path_start_ends_and_make_active = OptionalBoolean.BOOL_UNSPECIFIED use_make_inactive = OptionalBoolean.BOOL_UNSPECIFIED use_make_chain_inactive = OptionalBoolean.BOOL_UNSPECIFIED use_swap_active = OptionalBoolean.BOOL_UNSPECIFIED + use_swap_active_chain = OptionalBoolean.BOOL_UNSPECIFIED use_extended_swap_active = OptionalBoolean.BOOL_UNSPECIFIED use_shortest_path_swap_active = OptionalBoolean.BOOL_UNSPECIFIED + use_shortest_path_two_opt = OptionalBoolean.BOOL_UNSPECIFIED use_node_pair_swap_active = OptionalBoolean.BOOL_UNSPECIFIED use_path_lns = OptionalBoolean.BOOL_UNSPECIFIED use_full_path_lns = OptionalBoolean.BOOL_UNSPECIFIED @@ -164,16 +176,24 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"RoutingSearchParamet use_make_active = PB.decode(d, OptionalBoolean.T) elseif field_number == 21 use_relocate_and_make_active = PB.decode(d, OptionalBoolean.T) + elseif field_number == 37 + use_exchange_and_make_active = PB.decode(d, OptionalBoolean.T) + elseif field_number == 38 + use_exchange_path_start_ends_and_make_active = PB.decode(d, OptionalBoolean.T) elseif field_number == 12 use_make_inactive = PB.decode(d, OptionalBoolean.T) elseif field_number == 13 use_make_chain_inactive = PB.decode(d, OptionalBoolean.T) elseif field_number == 14 use_swap_active = PB.decode(d, OptionalBoolean.T) + elseif field_number == 35 + use_swap_active_chain = PB.decode(d, OptionalBoolean.T) elseif field_number == 15 use_extended_swap_active = PB.decode(d, OptionalBoolean.T) elseif field_number == 34 use_shortest_path_swap_active = PB.decode(d, OptionalBoolean.T) + elseif field_number == 36 + use_shortest_path_two_opt = PB.decode(d, OptionalBoolean.T) elseif field_number == 20 use_node_pair_swap_active = PB.decode(d, OptionalBoolean.T) elseif field_number == 16 @@ -202,7 +222,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"RoutingSearchParamet PB.skip(d, wire_type) end end - return var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"(use_relocate, use_relocate_pair, use_light_relocate_pair, use_relocate_neighbors, use_relocate_subtrip, use_exchange, use_exchange_pair, use_exchange_subtrip, use_cross, use_cross_exchange, use_relocate_expensive_chain, use_two_opt, use_or_opt, use_lin_kernighan, use_tsp_opt, use_make_active, use_relocate_and_make_active, use_make_inactive, use_make_chain_inactive, use_swap_active, use_extended_swap_active, use_shortest_path_swap_active, use_node_pair_swap_active, use_path_lns, use_full_path_lns, use_tsp_lns, use_inactive_lns, use_global_cheapest_insertion_path_lns, use_local_cheapest_insertion_path_lns, use_relocate_path_global_cheapest_insertion_insert_unperformed, use_global_cheapest_insertion_expensive_chain_lns, use_local_cheapest_insertion_expensive_chain_lns, use_global_cheapest_insertion_close_nodes_lns, use_local_cheapest_insertion_close_nodes_lns) + return var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"(use_relocate, use_relocate_pair, use_light_relocate_pair, use_relocate_neighbors, use_relocate_subtrip, use_exchange, use_exchange_pair, use_exchange_subtrip, use_cross, use_cross_exchange, use_relocate_expensive_chain, use_two_opt, use_or_opt, use_lin_kernighan, use_tsp_opt, use_make_active, use_relocate_and_make_active, use_exchange_and_make_active, use_exchange_path_start_ends_and_make_active, use_make_inactive, use_make_chain_inactive, use_swap_active, use_swap_active_chain, use_extended_swap_active, use_shortest_path_swap_active, use_shortest_path_two_opt, use_node_pair_swap_active, use_path_lns, use_full_path_lns, use_tsp_lns, use_inactive_lns, use_global_cheapest_insertion_path_lns, use_local_cheapest_insertion_path_lns, use_relocate_path_global_cheapest_insertion_insert_unperformed, use_global_cheapest_insertion_expensive_chain_lns, use_local_cheapest_insertion_expensive_chain_lns, use_global_cheapest_insertion_close_nodes_lns, use_local_cheapest_insertion_close_nodes_lns) end function PB.encode(e::PB.AbstractProtoEncoder, x::var"RoutingSearchParameters.LocalSearchNeighborhoodOperators") @@ -224,11 +244,15 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::var"RoutingSearchParameters.Lo x.use_tsp_opt != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 10, x.use_tsp_opt) x.use_make_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 11, x.use_make_active) x.use_relocate_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 21, x.use_relocate_and_make_active) + x.use_exchange_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 37, x.use_exchange_and_make_active) + x.use_exchange_path_start_ends_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 38, x.use_exchange_path_start_ends_and_make_active) x.use_make_inactive != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 12, x.use_make_inactive) x.use_make_chain_inactive != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 13, x.use_make_chain_inactive) x.use_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 14, x.use_swap_active) + x.use_swap_active_chain != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 35, x.use_swap_active_chain) x.use_extended_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 15, x.use_extended_swap_active) x.use_shortest_path_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 34, x.use_shortest_path_swap_active) + x.use_shortest_path_two_opt != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 36, x.use_shortest_path_two_opt) x.use_node_pair_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 20, x.use_node_pair_swap_active) x.use_path_lns != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 16, x.use_path_lns) x.use_full_path_lns != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 17, x.use_full_path_lns) @@ -262,11 +286,15 @@ function PB._encoded_size(x::var"RoutingSearchParameters.LocalSearchNeighborhood x.use_tsp_opt != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_tsp_opt, 10)) x.use_make_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_make_active, 11)) x.use_relocate_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_relocate_and_make_active, 21)) + x.use_exchange_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_exchange_and_make_active, 37)) + x.use_exchange_path_start_ends_and_make_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_exchange_path_start_ends_and_make_active, 38)) x.use_make_inactive != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_make_inactive, 12)) x.use_make_chain_inactive != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_make_chain_inactive, 13)) x.use_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_swap_active, 14)) + x.use_swap_active_chain != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_swap_active_chain, 35)) x.use_extended_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_extended_swap_active, 15)) x.use_shortest_path_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_shortest_path_swap_active, 34)) + x.use_shortest_path_two_opt != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_shortest_path_two_opt, 36)) x.use_node_pair_swap_active != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_node_pair_swap_active, 20)) x.use_path_lns != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_path_lns, 16)) x.use_full_path_lns != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_full_path_lns, 17)) @@ -307,13 +335,13 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"RoutingSearchParameters.ImprovementSearchLimitParameters") initpos = position(e.io) - x.improvement_rate_coefficient != zero(Float64) && PB.encode(e, 38, x.improvement_rate_coefficient) + x.improvement_rate_coefficient !== zero(Float64) && PB.encode(e, 38, x.improvement_rate_coefficient) x.improvement_rate_solutions_distance != zero(Int32) && PB.encode(e, 39, x.improvement_rate_solutions_distance) return position(e.io) - initpos end function PB._encoded_size(x::var"RoutingSearchParameters.ImprovementSearchLimitParameters") encoded_size = 0 - x.improvement_rate_coefficient != zero(Float64) && (encoded_size += PB._encoded_size(x.improvement_rate_coefficient, 38)) + x.improvement_rate_coefficient !== zero(Float64) && (encoded_size += PB._encoded_size(x.improvement_rate_coefficient, 38)) x.improvement_rate_solutions_distance != zero(Int32) && (encoded_size += PB._encoded_size(x.improvement_rate_solutions_distance, 39)) return encoded_size end @@ -327,7 +355,6 @@ struct RoutingSearchParameters savings_max_memory_usage_bytes::Float64 savings_add_reverse_arcs::Bool savings_arc_coefficient::Float64 - savings_parallel_routes::Bool cheapest_insertion_farthest_seeds_ratio::Float64 cheapest_insertion_first_solution_neighbors_ratio::Float64 cheapest_insertion_first_solution_min_neighbors::Int32 @@ -337,24 +364,32 @@ struct RoutingSearchParameters cheapest_insertion_add_unperformed_entries::Bool local_cheapest_insertion_pickup_delivery_strategy::var"RoutingSearchParameters.PairInsertionStrategy".T local_cheapest_cost_insertion_pickup_delivery_strategy::var"RoutingSearchParameters.PairInsertionStrategy".T + local_cheapest_insertion_sorting_properties::Vector{var"RoutingSearchParameters.InsertionSortingProperty".T} christofides_use_minimum_matching::Bool + first_solution_optimization_period::Int32 local_search_operators::Union{Nothing,var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"} ls_operator_neighbors_ratio::Float64 ls_operator_min_neighbors::Int32 use_multi_armed_bandit_concatenate_operators::Bool multi_armed_bandit_compound_operator_memory_coefficient::Float64 multi_armed_bandit_compound_operator_exploration_coefficient::Float64 + max_swap_active_chain_size::Int32 relocate_expensive_chain_num_arcs_to_consider::Int32 heuristic_expensive_chain_lns_num_arcs_to_consider::Int32 heuristic_close_nodes_lns_num_nodes::Int32 local_search_metaheuristic::var"LocalSearchMetaheuristic.Value".T + local_search_metaheuristics::Vector{var"LocalSearchMetaheuristic.Value".T} + num_max_local_optima_before_metaheuristic_switch::Int32 guided_local_search_lambda_coefficient::Float64 guided_local_search_reset_penalties_on_new_best_solution::Bool + guided_local_search_penalize_with_vehicle_classes::Bool + use_guided_local_search_penalties_in_local_search_operators::Bool use_depth_first_search::Bool use_cp::OptionalBoolean.T use_cp_sat::OptionalBoolean.T use_generalized_cp_sat::OptionalBoolean.T sat_parameters::Union{Nothing,operations_research.sat.SatParameters} + report_intermediate_cp_sat_solutions::Bool fallback_to_cp_sat_size_threshold::Int32 continuous_scheduling_solver::var"RoutingSearchParameters.SchedulingSolver".T mixed_integer_scheduling_solver::var"RoutingSearchParameters.SchedulingSolver".T @@ -364,15 +399,19 @@ struct RoutingSearchParameters solution_limit::Int64 time_limit::Union{Nothing,google.protobuf.Duration} lns_time_limit::Union{Nothing,google.protobuf.Duration} + secondary_ls_time_limit_ratio::Float64 improvement_limit_parameters::Union{Nothing,var"RoutingSearchParameters.ImprovementSearchLimitParameters"} use_full_propagation::Bool log_search::Bool log_cost_scaling_factor::Float64 log_cost_offset::Float64 log_tag::String + use_iterated_local_search::Bool + iterated_local_search_parameters::Union{Nothing,IteratedLocalSearchParameters} end -PB.default_values(::Type{RoutingSearchParameters}) = (;first_solution_strategy = var"FirstSolutionStrategy.Value".UNSET, use_unfiltered_first_solution_strategy = false, savings_neighbors_ratio = zero(Float64), savings_max_memory_usage_bytes = zero(Float64), savings_add_reverse_arcs = false, savings_arc_coefficient = zero(Float64), savings_parallel_routes = false, cheapest_insertion_farthest_seeds_ratio = zero(Float64), cheapest_insertion_first_solution_neighbors_ratio = zero(Float64), cheapest_insertion_first_solution_min_neighbors = zero(Int32), cheapest_insertion_ls_operator_neighbors_ratio = zero(Float64), cheapest_insertion_ls_operator_min_neighbors = zero(Int32), cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization = false, cheapest_insertion_add_unperformed_entries = false, local_cheapest_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC, local_cheapest_cost_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC, christofides_use_minimum_matching = false, local_search_operators = nothing, ls_operator_neighbors_ratio = zero(Float64), ls_operator_min_neighbors = zero(Int32), use_multi_armed_bandit_concatenate_operators = false, multi_armed_bandit_compound_operator_memory_coefficient = zero(Float64), multi_armed_bandit_compound_operator_exploration_coefficient = zero(Float64), relocate_expensive_chain_num_arcs_to_consider = zero(Int32), heuristic_expensive_chain_lns_num_arcs_to_consider = zero(Int32), heuristic_close_nodes_lns_num_nodes = zero(Int32), local_search_metaheuristic = var"LocalSearchMetaheuristic.Value".UNSET, guided_local_search_lambda_coefficient = zero(Float64), guided_local_search_reset_penalties_on_new_best_solution = false, use_depth_first_search = false, use_cp = OptionalBoolean.BOOL_UNSPECIFIED, use_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED, use_generalized_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED, sat_parameters = nothing, fallback_to_cp_sat_size_threshold = zero(Int32), continuous_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET, mixed_integer_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET, disable_scheduling_beware_this_may_degrade_performance = false, optimization_step = zero(Float64), number_of_solutions_to_collect = zero(Int32), solution_limit = zero(Int64), time_limit = nothing, lns_time_limit = nothing, improvement_limit_parameters = nothing, use_full_propagation = false, log_search = false, log_cost_scaling_factor = zero(Float64), log_cost_offset = zero(Float64), log_tag = "") -PB.field_numbers(::Type{RoutingSearchParameters}) = (;first_solution_strategy = 1, use_unfiltered_first_solution_strategy = 2, savings_neighbors_ratio = 14, savings_max_memory_usage_bytes = 23, savings_add_reverse_arcs = 15, savings_arc_coefficient = 18, savings_parallel_routes = 19, cheapest_insertion_farthest_seeds_ratio = 16, cheapest_insertion_first_solution_neighbors_ratio = 21, cheapest_insertion_first_solution_min_neighbors = 44, cheapest_insertion_ls_operator_neighbors_ratio = 31, cheapest_insertion_ls_operator_min_neighbors = 45, cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization = 46, cheapest_insertion_add_unperformed_entries = 40, local_cheapest_insertion_pickup_delivery_strategy = 49, local_cheapest_cost_insertion_pickup_delivery_strategy = 55, christofides_use_minimum_matching = 30, local_search_operators = 3, ls_operator_neighbors_ratio = 53, ls_operator_min_neighbors = 54, use_multi_armed_bandit_concatenate_operators = 41, multi_armed_bandit_compound_operator_memory_coefficient = 42, multi_armed_bandit_compound_operator_exploration_coefficient = 43, relocate_expensive_chain_num_arcs_to_consider = 20, heuristic_expensive_chain_lns_num_arcs_to_consider = 32, heuristic_close_nodes_lns_num_nodes = 35, local_search_metaheuristic = 4, guided_local_search_lambda_coefficient = 5, guided_local_search_reset_penalties_on_new_best_solution = 51, use_depth_first_search = 6, use_cp = 28, use_cp_sat = 27, use_generalized_cp_sat = 47, sat_parameters = 48, fallback_to_cp_sat_size_threshold = 52, continuous_scheduling_solver = 33, mixed_integer_scheduling_solver = 34, disable_scheduling_beware_this_may_degrade_performance = 50, optimization_step = 7, number_of_solutions_to_collect = 17, solution_limit = 8, time_limit = 9, lns_time_limit = 10, improvement_limit_parameters = 37, use_full_propagation = 11, log_search = 13, log_cost_scaling_factor = 22, log_cost_offset = 29, log_tag = 36) +PB.reserved_fields(::Type{RoutingSearchParameters}) = (names = String[], numbers = Union{Int,UnitRange{Int}}[19, 65]) +PB.default_values(::Type{RoutingSearchParameters}) = (;first_solution_strategy = var"FirstSolutionStrategy.Value".UNSET, use_unfiltered_first_solution_strategy = false, savings_neighbors_ratio = zero(Float64), savings_max_memory_usage_bytes = zero(Float64), savings_add_reverse_arcs = false, savings_arc_coefficient = zero(Float64), cheapest_insertion_farthest_seeds_ratio = zero(Float64), cheapest_insertion_first_solution_neighbors_ratio = zero(Float64), cheapest_insertion_first_solution_min_neighbors = zero(Int32), cheapest_insertion_ls_operator_neighbors_ratio = zero(Float64), cheapest_insertion_ls_operator_min_neighbors = zero(Int32), cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization = false, cheapest_insertion_add_unperformed_entries = false, local_cheapest_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC, local_cheapest_cost_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC, local_cheapest_insertion_sorting_properties = Vector{var"RoutingSearchParameters.InsertionSortingProperty".T}(), christofides_use_minimum_matching = false, first_solution_optimization_period = zero(Int32), local_search_operators = nothing, ls_operator_neighbors_ratio = zero(Float64), ls_operator_min_neighbors = zero(Int32), use_multi_armed_bandit_concatenate_operators = false, multi_armed_bandit_compound_operator_memory_coefficient = zero(Float64), multi_armed_bandit_compound_operator_exploration_coefficient = zero(Float64), max_swap_active_chain_size = zero(Int32), relocate_expensive_chain_num_arcs_to_consider = zero(Int32), heuristic_expensive_chain_lns_num_arcs_to_consider = zero(Int32), heuristic_close_nodes_lns_num_nodes = zero(Int32), local_search_metaheuristic = var"LocalSearchMetaheuristic.Value".UNSET, local_search_metaheuristics = Vector{var"LocalSearchMetaheuristic.Value".T}(), num_max_local_optima_before_metaheuristic_switch = zero(Int32), guided_local_search_lambda_coefficient = zero(Float64), guided_local_search_reset_penalties_on_new_best_solution = false, guided_local_search_penalize_with_vehicle_classes = false, use_guided_local_search_penalties_in_local_search_operators = false, use_depth_first_search = false, use_cp = OptionalBoolean.BOOL_UNSPECIFIED, use_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED, use_generalized_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED, sat_parameters = nothing, report_intermediate_cp_sat_solutions = false, fallback_to_cp_sat_size_threshold = zero(Int32), continuous_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET, mixed_integer_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET, disable_scheduling_beware_this_may_degrade_performance = false, optimization_step = zero(Float64), number_of_solutions_to_collect = zero(Int32), solution_limit = zero(Int64), time_limit = nothing, lns_time_limit = nothing, secondary_ls_time_limit_ratio = zero(Float64), improvement_limit_parameters = nothing, use_full_propagation = false, log_search = false, log_cost_scaling_factor = zero(Float64), log_cost_offset = zero(Float64), log_tag = "", use_iterated_local_search = false, iterated_local_search_parameters = nothing) +PB.field_numbers(::Type{RoutingSearchParameters}) = (;first_solution_strategy = 1, use_unfiltered_first_solution_strategy = 2, savings_neighbors_ratio = 14, savings_max_memory_usage_bytes = 23, savings_add_reverse_arcs = 15, savings_arc_coefficient = 18, cheapest_insertion_farthest_seeds_ratio = 16, cheapest_insertion_first_solution_neighbors_ratio = 21, cheapest_insertion_first_solution_min_neighbors = 44, cheapest_insertion_ls_operator_neighbors_ratio = 31, cheapest_insertion_ls_operator_min_neighbors = 45, cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization = 46, cheapest_insertion_add_unperformed_entries = 40, local_cheapest_insertion_pickup_delivery_strategy = 49, local_cheapest_cost_insertion_pickup_delivery_strategy = 55, local_cheapest_insertion_sorting_properties = 67, christofides_use_minimum_matching = 30, first_solution_optimization_period = 59, local_search_operators = 3, ls_operator_neighbors_ratio = 53, ls_operator_min_neighbors = 54, use_multi_armed_bandit_concatenate_operators = 41, multi_armed_bandit_compound_operator_memory_coefficient = 42, multi_armed_bandit_compound_operator_exploration_coefficient = 43, max_swap_active_chain_size = 66, relocate_expensive_chain_num_arcs_to_consider = 20, heuristic_expensive_chain_lns_num_arcs_to_consider = 32, heuristic_close_nodes_lns_num_nodes = 35, local_search_metaheuristic = 4, local_search_metaheuristics = 63, num_max_local_optima_before_metaheuristic_switch = 64, guided_local_search_lambda_coefficient = 5, guided_local_search_reset_penalties_on_new_best_solution = 51, guided_local_search_penalize_with_vehicle_classes = 61, use_guided_local_search_penalties_in_local_search_operators = 62, use_depth_first_search = 6, use_cp = 28, use_cp_sat = 27, use_generalized_cp_sat = 47, sat_parameters = 48, report_intermediate_cp_sat_solutions = 56, fallback_to_cp_sat_size_threshold = 52, continuous_scheduling_solver = 33, mixed_integer_scheduling_solver = 34, disable_scheduling_beware_this_may_degrade_performance = 50, optimization_step = 7, number_of_solutions_to_collect = 17, solution_limit = 8, time_limit = 9, lns_time_limit = 10, secondary_ls_time_limit_ratio = 57, improvement_limit_parameters = 37, use_full_propagation = 11, log_search = 13, log_cost_scaling_factor = 22, log_cost_offset = 29, log_tag = 36, use_iterated_local_search = 58, iterated_local_search_parameters = 60) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters}) first_solution_strategy = var"FirstSolutionStrategy.Value".UNSET @@ -381,7 +420,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} savings_max_memory_usage_bytes = zero(Float64) savings_add_reverse_arcs = false savings_arc_coefficient = zero(Float64) - savings_parallel_routes = false cheapest_insertion_farthest_seeds_ratio = zero(Float64) cheapest_insertion_first_solution_neighbors_ratio = zero(Float64) cheapest_insertion_first_solution_min_neighbors = zero(Int32) @@ -391,24 +429,32 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} cheapest_insertion_add_unperformed_entries = false local_cheapest_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC local_cheapest_cost_insertion_pickup_delivery_strategy = var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC + local_cheapest_insertion_sorting_properties = PB.BufferedVector{var"RoutingSearchParameters.InsertionSortingProperty".T}() christofides_use_minimum_matching = false + first_solution_optimization_period = zero(Int32) local_search_operators = Ref{Union{Nothing,var"RoutingSearchParameters.LocalSearchNeighborhoodOperators"}}(nothing) ls_operator_neighbors_ratio = zero(Float64) ls_operator_min_neighbors = zero(Int32) use_multi_armed_bandit_concatenate_operators = false multi_armed_bandit_compound_operator_memory_coefficient = zero(Float64) multi_armed_bandit_compound_operator_exploration_coefficient = zero(Float64) + max_swap_active_chain_size = zero(Int32) relocate_expensive_chain_num_arcs_to_consider = zero(Int32) heuristic_expensive_chain_lns_num_arcs_to_consider = zero(Int32) heuristic_close_nodes_lns_num_nodes = zero(Int32) local_search_metaheuristic = var"LocalSearchMetaheuristic.Value".UNSET + local_search_metaheuristics = PB.BufferedVector{var"LocalSearchMetaheuristic.Value".T}() + num_max_local_optima_before_metaheuristic_switch = zero(Int32) guided_local_search_lambda_coefficient = zero(Float64) guided_local_search_reset_penalties_on_new_best_solution = false + guided_local_search_penalize_with_vehicle_classes = false + use_guided_local_search_penalties_in_local_search_operators = false use_depth_first_search = false use_cp = OptionalBoolean.BOOL_UNSPECIFIED use_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED use_generalized_cp_sat = OptionalBoolean.BOOL_UNSPECIFIED sat_parameters = Ref{Union{Nothing,operations_research.sat.SatParameters}}(nothing) + report_intermediate_cp_sat_solutions = false fallback_to_cp_sat_size_threshold = zero(Int32) continuous_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET mixed_integer_scheduling_solver = var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET @@ -418,12 +464,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} solution_limit = zero(Int64) time_limit = Ref{Union{Nothing,google.protobuf.Duration}}(nothing) lns_time_limit = Ref{Union{Nothing,google.protobuf.Duration}}(nothing) + secondary_ls_time_limit_ratio = zero(Float64) improvement_limit_parameters = Ref{Union{Nothing,var"RoutingSearchParameters.ImprovementSearchLimitParameters"}}(nothing) use_full_propagation = false log_search = false log_cost_scaling_factor = zero(Float64) log_cost_offset = zero(Float64) log_tag = "" + use_iterated_local_search = false + iterated_local_search_parameters = Ref{Union{Nothing,IteratedLocalSearchParameters}}(nothing) while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 @@ -438,8 +487,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} savings_add_reverse_arcs = PB.decode(d, Bool) elseif field_number == 18 savings_arc_coefficient = PB.decode(d, Float64) - elseif field_number == 19 - savings_parallel_routes = PB.decode(d, Bool) elseif field_number == 16 cheapest_insertion_farthest_seeds_ratio = PB.decode(d, Float64) elseif field_number == 21 @@ -458,8 +505,12 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} local_cheapest_insertion_pickup_delivery_strategy = PB.decode(d, var"RoutingSearchParameters.PairInsertionStrategy".T) elseif field_number == 55 local_cheapest_cost_insertion_pickup_delivery_strategy = PB.decode(d, var"RoutingSearchParameters.PairInsertionStrategy".T) + elseif field_number == 67 + PB.decode!(d, wire_type, local_cheapest_insertion_sorting_properties) elseif field_number == 30 christofides_use_minimum_matching = PB.decode(d, Bool) + elseif field_number == 59 + first_solution_optimization_period = PB.decode(d, Int32) elseif field_number == 3 PB.decode!(d, local_search_operators) elseif field_number == 53 @@ -472,6 +523,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} multi_armed_bandit_compound_operator_memory_coefficient = PB.decode(d, Float64) elseif field_number == 43 multi_armed_bandit_compound_operator_exploration_coefficient = PB.decode(d, Float64) + elseif field_number == 66 + max_swap_active_chain_size = PB.decode(d, Int32) elseif field_number == 20 relocate_expensive_chain_num_arcs_to_consider = PB.decode(d, Int32) elseif field_number == 32 @@ -480,10 +533,18 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} heuristic_close_nodes_lns_num_nodes = PB.decode(d, Int32) elseif field_number == 4 local_search_metaheuristic = PB.decode(d, var"LocalSearchMetaheuristic.Value".T) + elseif field_number == 63 + PB.decode!(d, wire_type, local_search_metaheuristics) + elseif field_number == 64 + num_max_local_optima_before_metaheuristic_switch = PB.decode(d, Int32) elseif field_number == 5 guided_local_search_lambda_coefficient = PB.decode(d, Float64) elseif field_number == 51 guided_local_search_reset_penalties_on_new_best_solution = PB.decode(d, Bool) + elseif field_number == 61 + guided_local_search_penalize_with_vehicle_classes = PB.decode(d, Bool) + elseif field_number == 62 + use_guided_local_search_penalties_in_local_search_operators = PB.decode(d, Bool) elseif field_number == 6 use_depth_first_search = PB.decode(d, Bool) elseif field_number == 28 @@ -494,6 +555,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} use_generalized_cp_sat = PB.decode(d, OptionalBoolean.T) elseif field_number == 48 PB.decode!(d, sat_parameters) + elseif field_number == 56 + report_intermediate_cp_sat_solutions = PB.decode(d, Bool) elseif field_number == 52 fallback_to_cp_sat_size_threshold = PB.decode(d, Int32) elseif field_number == 33 @@ -512,6 +575,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} PB.decode!(d, time_limit) elseif field_number == 10 PB.decode!(d, lns_time_limit) + elseif field_number == 57 + secondary_ls_time_limit_ratio = PB.decode(d, Float64) elseif field_number == 37 PB.decode!(d, improvement_limit_parameters) elseif field_number == 11 @@ -524,116 +589,140 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutingSearchParameters} log_cost_offset = PB.decode(d, Float64) elseif field_number == 36 log_tag = PB.decode(d, String) + elseif field_number == 58 + use_iterated_local_search = PB.decode(d, Bool) + elseif field_number == 60 + PB.decode!(d, iterated_local_search_parameters) else PB.skip(d, wire_type) end end - return RoutingSearchParameters(first_solution_strategy, use_unfiltered_first_solution_strategy, savings_neighbors_ratio, savings_max_memory_usage_bytes, savings_add_reverse_arcs, savings_arc_coefficient, savings_parallel_routes, cheapest_insertion_farthest_seeds_ratio, cheapest_insertion_first_solution_neighbors_ratio, cheapest_insertion_first_solution_min_neighbors, cheapest_insertion_ls_operator_neighbors_ratio, cheapest_insertion_ls_operator_min_neighbors, cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization, cheapest_insertion_add_unperformed_entries, local_cheapest_insertion_pickup_delivery_strategy, local_cheapest_cost_insertion_pickup_delivery_strategy, christofides_use_minimum_matching, local_search_operators[], ls_operator_neighbors_ratio, ls_operator_min_neighbors, use_multi_armed_bandit_concatenate_operators, multi_armed_bandit_compound_operator_memory_coefficient, multi_armed_bandit_compound_operator_exploration_coefficient, relocate_expensive_chain_num_arcs_to_consider, heuristic_expensive_chain_lns_num_arcs_to_consider, heuristic_close_nodes_lns_num_nodes, local_search_metaheuristic, guided_local_search_lambda_coefficient, guided_local_search_reset_penalties_on_new_best_solution, use_depth_first_search, use_cp, use_cp_sat, use_generalized_cp_sat, sat_parameters[], fallback_to_cp_sat_size_threshold, continuous_scheduling_solver, mixed_integer_scheduling_solver, disable_scheduling_beware_this_may_degrade_performance, optimization_step, number_of_solutions_to_collect, solution_limit, time_limit[], lns_time_limit[], improvement_limit_parameters[], use_full_propagation, log_search, log_cost_scaling_factor, log_cost_offset, log_tag) + return RoutingSearchParameters(first_solution_strategy, use_unfiltered_first_solution_strategy, savings_neighbors_ratio, savings_max_memory_usage_bytes, savings_add_reverse_arcs, savings_arc_coefficient, cheapest_insertion_farthest_seeds_ratio, cheapest_insertion_first_solution_neighbors_ratio, cheapest_insertion_first_solution_min_neighbors, cheapest_insertion_ls_operator_neighbors_ratio, cheapest_insertion_ls_operator_min_neighbors, cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization, cheapest_insertion_add_unperformed_entries, local_cheapest_insertion_pickup_delivery_strategy, local_cheapest_cost_insertion_pickup_delivery_strategy, local_cheapest_insertion_sorting_properties[], christofides_use_minimum_matching, first_solution_optimization_period, local_search_operators[], ls_operator_neighbors_ratio, ls_operator_min_neighbors, use_multi_armed_bandit_concatenate_operators, multi_armed_bandit_compound_operator_memory_coefficient, multi_armed_bandit_compound_operator_exploration_coefficient, max_swap_active_chain_size, relocate_expensive_chain_num_arcs_to_consider, heuristic_expensive_chain_lns_num_arcs_to_consider, heuristic_close_nodes_lns_num_nodes, local_search_metaheuristic, local_search_metaheuristics[], num_max_local_optima_before_metaheuristic_switch, guided_local_search_lambda_coefficient, guided_local_search_reset_penalties_on_new_best_solution, guided_local_search_penalize_with_vehicle_classes, use_guided_local_search_penalties_in_local_search_operators, use_depth_first_search, use_cp, use_cp_sat, use_generalized_cp_sat, sat_parameters[], report_intermediate_cp_sat_solutions, fallback_to_cp_sat_size_threshold, continuous_scheduling_solver, mixed_integer_scheduling_solver, disable_scheduling_beware_this_may_degrade_performance, optimization_step, number_of_solutions_to_collect, solution_limit, time_limit[], lns_time_limit[], secondary_ls_time_limit_ratio, improvement_limit_parameters[], use_full_propagation, log_search, log_cost_scaling_factor, log_cost_offset, log_tag, use_iterated_local_search, iterated_local_search_parameters[]) end function PB.encode(e::PB.AbstractProtoEncoder, x::RoutingSearchParameters) initpos = position(e.io) x.first_solution_strategy != var"FirstSolutionStrategy.Value".UNSET && PB.encode(e, 1, x.first_solution_strategy) x.use_unfiltered_first_solution_strategy != false && PB.encode(e, 2, x.use_unfiltered_first_solution_strategy) - x.savings_neighbors_ratio != zero(Float64) && PB.encode(e, 14, x.savings_neighbors_ratio) - x.savings_max_memory_usage_bytes != zero(Float64) && PB.encode(e, 23, x.savings_max_memory_usage_bytes) + x.savings_neighbors_ratio !== zero(Float64) && PB.encode(e, 14, x.savings_neighbors_ratio) + x.savings_max_memory_usage_bytes !== zero(Float64) && PB.encode(e, 23, x.savings_max_memory_usage_bytes) x.savings_add_reverse_arcs != false && PB.encode(e, 15, x.savings_add_reverse_arcs) - x.savings_arc_coefficient != zero(Float64) && PB.encode(e, 18, x.savings_arc_coefficient) - x.savings_parallel_routes != false && PB.encode(e, 19, x.savings_parallel_routes) - x.cheapest_insertion_farthest_seeds_ratio != zero(Float64) && PB.encode(e, 16, x.cheapest_insertion_farthest_seeds_ratio) - x.cheapest_insertion_first_solution_neighbors_ratio != zero(Float64) && PB.encode(e, 21, x.cheapest_insertion_first_solution_neighbors_ratio) + x.savings_arc_coefficient !== zero(Float64) && PB.encode(e, 18, x.savings_arc_coefficient) + x.cheapest_insertion_farthest_seeds_ratio !== zero(Float64) && PB.encode(e, 16, x.cheapest_insertion_farthest_seeds_ratio) + x.cheapest_insertion_first_solution_neighbors_ratio !== zero(Float64) && PB.encode(e, 21, x.cheapest_insertion_first_solution_neighbors_ratio) x.cheapest_insertion_first_solution_min_neighbors != zero(Int32) && PB.encode(e, 44, x.cheapest_insertion_first_solution_min_neighbors) - x.cheapest_insertion_ls_operator_neighbors_ratio != zero(Float64) && PB.encode(e, 31, x.cheapest_insertion_ls_operator_neighbors_ratio) + x.cheapest_insertion_ls_operator_neighbors_ratio !== zero(Float64) && PB.encode(e, 31, x.cheapest_insertion_ls_operator_neighbors_ratio) x.cheapest_insertion_ls_operator_min_neighbors != zero(Int32) && PB.encode(e, 45, x.cheapest_insertion_ls_operator_min_neighbors) x.cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization != false && PB.encode(e, 46, x.cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization) x.cheapest_insertion_add_unperformed_entries != false && PB.encode(e, 40, x.cheapest_insertion_add_unperformed_entries) x.local_cheapest_insertion_pickup_delivery_strategy != var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC && PB.encode(e, 49, x.local_cheapest_insertion_pickup_delivery_strategy) x.local_cheapest_cost_insertion_pickup_delivery_strategy != var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC && PB.encode(e, 55, x.local_cheapest_cost_insertion_pickup_delivery_strategy) + !isempty(x.local_cheapest_insertion_sorting_properties) && PB.encode(e, 67, x.local_cheapest_insertion_sorting_properties) x.christofides_use_minimum_matching != false && PB.encode(e, 30, x.christofides_use_minimum_matching) + x.first_solution_optimization_period != zero(Int32) && PB.encode(e, 59, x.first_solution_optimization_period) !isnothing(x.local_search_operators) && PB.encode(e, 3, x.local_search_operators) - x.ls_operator_neighbors_ratio != zero(Float64) && PB.encode(e, 53, x.ls_operator_neighbors_ratio) + x.ls_operator_neighbors_ratio !== zero(Float64) && PB.encode(e, 53, x.ls_operator_neighbors_ratio) x.ls_operator_min_neighbors != zero(Int32) && PB.encode(e, 54, x.ls_operator_min_neighbors) x.use_multi_armed_bandit_concatenate_operators != false && PB.encode(e, 41, x.use_multi_armed_bandit_concatenate_operators) - x.multi_armed_bandit_compound_operator_memory_coefficient != zero(Float64) && PB.encode(e, 42, x.multi_armed_bandit_compound_operator_memory_coefficient) - x.multi_armed_bandit_compound_operator_exploration_coefficient != zero(Float64) && PB.encode(e, 43, x.multi_armed_bandit_compound_operator_exploration_coefficient) + x.multi_armed_bandit_compound_operator_memory_coefficient !== zero(Float64) && PB.encode(e, 42, x.multi_armed_bandit_compound_operator_memory_coefficient) + x.multi_armed_bandit_compound_operator_exploration_coefficient !== zero(Float64) && PB.encode(e, 43, x.multi_armed_bandit_compound_operator_exploration_coefficient) + x.max_swap_active_chain_size != zero(Int32) && PB.encode(e, 66, x.max_swap_active_chain_size) x.relocate_expensive_chain_num_arcs_to_consider != zero(Int32) && PB.encode(e, 20, x.relocate_expensive_chain_num_arcs_to_consider) x.heuristic_expensive_chain_lns_num_arcs_to_consider != zero(Int32) && PB.encode(e, 32, x.heuristic_expensive_chain_lns_num_arcs_to_consider) x.heuristic_close_nodes_lns_num_nodes != zero(Int32) && PB.encode(e, 35, x.heuristic_close_nodes_lns_num_nodes) x.local_search_metaheuristic != var"LocalSearchMetaheuristic.Value".UNSET && PB.encode(e, 4, x.local_search_metaheuristic) - x.guided_local_search_lambda_coefficient != zero(Float64) && PB.encode(e, 5, x.guided_local_search_lambda_coefficient) + !isempty(x.local_search_metaheuristics) && PB.encode(e, 63, x.local_search_metaheuristics) + x.num_max_local_optima_before_metaheuristic_switch != zero(Int32) && PB.encode(e, 64, x.num_max_local_optima_before_metaheuristic_switch) + x.guided_local_search_lambda_coefficient !== zero(Float64) && PB.encode(e, 5, x.guided_local_search_lambda_coefficient) x.guided_local_search_reset_penalties_on_new_best_solution != false && PB.encode(e, 51, x.guided_local_search_reset_penalties_on_new_best_solution) + x.guided_local_search_penalize_with_vehicle_classes != false && PB.encode(e, 61, x.guided_local_search_penalize_with_vehicle_classes) + x.use_guided_local_search_penalties_in_local_search_operators != false && PB.encode(e, 62, x.use_guided_local_search_penalties_in_local_search_operators) x.use_depth_first_search != false && PB.encode(e, 6, x.use_depth_first_search) x.use_cp != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 28, x.use_cp) x.use_cp_sat != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 27, x.use_cp_sat) x.use_generalized_cp_sat != OptionalBoolean.BOOL_UNSPECIFIED && PB.encode(e, 47, x.use_generalized_cp_sat) !isnothing(x.sat_parameters) && PB.encode(e, 48, x.sat_parameters) + x.report_intermediate_cp_sat_solutions != false && PB.encode(e, 56, x.report_intermediate_cp_sat_solutions) x.fallback_to_cp_sat_size_threshold != zero(Int32) && PB.encode(e, 52, x.fallback_to_cp_sat_size_threshold) x.continuous_scheduling_solver != var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET && PB.encode(e, 33, x.continuous_scheduling_solver) x.mixed_integer_scheduling_solver != var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET && PB.encode(e, 34, x.mixed_integer_scheduling_solver) x.disable_scheduling_beware_this_may_degrade_performance != false && PB.encode(e, 50, x.disable_scheduling_beware_this_may_degrade_performance) - x.optimization_step != zero(Float64) && PB.encode(e, 7, x.optimization_step) + x.optimization_step !== zero(Float64) && PB.encode(e, 7, x.optimization_step) x.number_of_solutions_to_collect != zero(Int32) && PB.encode(e, 17, x.number_of_solutions_to_collect) x.solution_limit != zero(Int64) && PB.encode(e, 8, x.solution_limit) !isnothing(x.time_limit) && PB.encode(e, 9, x.time_limit) !isnothing(x.lns_time_limit) && PB.encode(e, 10, x.lns_time_limit) + x.secondary_ls_time_limit_ratio !== zero(Float64) && PB.encode(e, 57, x.secondary_ls_time_limit_ratio) !isnothing(x.improvement_limit_parameters) && PB.encode(e, 37, x.improvement_limit_parameters) x.use_full_propagation != false && PB.encode(e, 11, x.use_full_propagation) x.log_search != false && PB.encode(e, 13, x.log_search) - x.log_cost_scaling_factor != zero(Float64) && PB.encode(e, 22, x.log_cost_scaling_factor) - x.log_cost_offset != zero(Float64) && PB.encode(e, 29, x.log_cost_offset) + x.log_cost_scaling_factor !== zero(Float64) && PB.encode(e, 22, x.log_cost_scaling_factor) + x.log_cost_offset !== zero(Float64) && PB.encode(e, 29, x.log_cost_offset) !isempty(x.log_tag) && PB.encode(e, 36, x.log_tag) + x.use_iterated_local_search != false && PB.encode(e, 58, x.use_iterated_local_search) + !isnothing(x.iterated_local_search_parameters) && PB.encode(e, 60, x.iterated_local_search_parameters) return position(e.io) - initpos end function PB._encoded_size(x::RoutingSearchParameters) encoded_size = 0 x.first_solution_strategy != var"FirstSolutionStrategy.Value".UNSET && (encoded_size += PB._encoded_size(x.first_solution_strategy, 1)) x.use_unfiltered_first_solution_strategy != false && (encoded_size += PB._encoded_size(x.use_unfiltered_first_solution_strategy, 2)) - x.savings_neighbors_ratio != zero(Float64) && (encoded_size += PB._encoded_size(x.savings_neighbors_ratio, 14)) - x.savings_max_memory_usage_bytes != zero(Float64) && (encoded_size += PB._encoded_size(x.savings_max_memory_usage_bytes, 23)) + x.savings_neighbors_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.savings_neighbors_ratio, 14)) + x.savings_max_memory_usage_bytes !== zero(Float64) && (encoded_size += PB._encoded_size(x.savings_max_memory_usage_bytes, 23)) x.savings_add_reverse_arcs != false && (encoded_size += PB._encoded_size(x.savings_add_reverse_arcs, 15)) - x.savings_arc_coefficient != zero(Float64) && (encoded_size += PB._encoded_size(x.savings_arc_coefficient, 18)) - x.savings_parallel_routes != false && (encoded_size += PB._encoded_size(x.savings_parallel_routes, 19)) - x.cheapest_insertion_farthest_seeds_ratio != zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_farthest_seeds_ratio, 16)) - x.cheapest_insertion_first_solution_neighbors_ratio != zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_first_solution_neighbors_ratio, 21)) + x.savings_arc_coefficient !== zero(Float64) && (encoded_size += PB._encoded_size(x.savings_arc_coefficient, 18)) + x.cheapest_insertion_farthest_seeds_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_farthest_seeds_ratio, 16)) + x.cheapest_insertion_first_solution_neighbors_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_first_solution_neighbors_ratio, 21)) x.cheapest_insertion_first_solution_min_neighbors != zero(Int32) && (encoded_size += PB._encoded_size(x.cheapest_insertion_first_solution_min_neighbors, 44)) - x.cheapest_insertion_ls_operator_neighbors_ratio != zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_ls_operator_neighbors_ratio, 31)) + x.cheapest_insertion_ls_operator_neighbors_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.cheapest_insertion_ls_operator_neighbors_ratio, 31)) x.cheapest_insertion_ls_operator_min_neighbors != zero(Int32) && (encoded_size += PB._encoded_size(x.cheapest_insertion_ls_operator_min_neighbors, 45)) x.cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization != false && (encoded_size += PB._encoded_size(x.cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization, 46)) x.cheapest_insertion_add_unperformed_entries != false && (encoded_size += PB._encoded_size(x.cheapest_insertion_add_unperformed_entries, 40)) x.local_cheapest_insertion_pickup_delivery_strategy != var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC && (encoded_size += PB._encoded_size(x.local_cheapest_insertion_pickup_delivery_strategy, 49)) x.local_cheapest_cost_insertion_pickup_delivery_strategy != var"RoutingSearchParameters.PairInsertionStrategy".AUTOMATIC && (encoded_size += PB._encoded_size(x.local_cheapest_cost_insertion_pickup_delivery_strategy, 55)) + !isempty(x.local_cheapest_insertion_sorting_properties) && (encoded_size += PB._encoded_size(x.local_cheapest_insertion_sorting_properties, 67)) x.christofides_use_minimum_matching != false && (encoded_size += PB._encoded_size(x.christofides_use_minimum_matching, 30)) + x.first_solution_optimization_period != zero(Int32) && (encoded_size += PB._encoded_size(x.first_solution_optimization_period, 59)) !isnothing(x.local_search_operators) && (encoded_size += PB._encoded_size(x.local_search_operators, 3)) - x.ls_operator_neighbors_ratio != zero(Float64) && (encoded_size += PB._encoded_size(x.ls_operator_neighbors_ratio, 53)) + x.ls_operator_neighbors_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.ls_operator_neighbors_ratio, 53)) x.ls_operator_min_neighbors != zero(Int32) && (encoded_size += PB._encoded_size(x.ls_operator_min_neighbors, 54)) x.use_multi_armed_bandit_concatenate_operators != false && (encoded_size += PB._encoded_size(x.use_multi_armed_bandit_concatenate_operators, 41)) - x.multi_armed_bandit_compound_operator_memory_coefficient != zero(Float64) && (encoded_size += PB._encoded_size(x.multi_armed_bandit_compound_operator_memory_coefficient, 42)) - x.multi_armed_bandit_compound_operator_exploration_coefficient != zero(Float64) && (encoded_size += PB._encoded_size(x.multi_armed_bandit_compound_operator_exploration_coefficient, 43)) + x.multi_armed_bandit_compound_operator_memory_coefficient !== zero(Float64) && (encoded_size += PB._encoded_size(x.multi_armed_bandit_compound_operator_memory_coefficient, 42)) + x.multi_armed_bandit_compound_operator_exploration_coefficient !== zero(Float64) && (encoded_size += PB._encoded_size(x.multi_armed_bandit_compound_operator_exploration_coefficient, 43)) + x.max_swap_active_chain_size != zero(Int32) && (encoded_size += PB._encoded_size(x.max_swap_active_chain_size, 66)) x.relocate_expensive_chain_num_arcs_to_consider != zero(Int32) && (encoded_size += PB._encoded_size(x.relocate_expensive_chain_num_arcs_to_consider, 20)) x.heuristic_expensive_chain_lns_num_arcs_to_consider != zero(Int32) && (encoded_size += PB._encoded_size(x.heuristic_expensive_chain_lns_num_arcs_to_consider, 32)) x.heuristic_close_nodes_lns_num_nodes != zero(Int32) && (encoded_size += PB._encoded_size(x.heuristic_close_nodes_lns_num_nodes, 35)) x.local_search_metaheuristic != var"LocalSearchMetaheuristic.Value".UNSET && (encoded_size += PB._encoded_size(x.local_search_metaheuristic, 4)) - x.guided_local_search_lambda_coefficient != zero(Float64) && (encoded_size += PB._encoded_size(x.guided_local_search_lambda_coefficient, 5)) + !isempty(x.local_search_metaheuristics) && (encoded_size += PB._encoded_size(x.local_search_metaheuristics, 63)) + x.num_max_local_optima_before_metaheuristic_switch != zero(Int32) && (encoded_size += PB._encoded_size(x.num_max_local_optima_before_metaheuristic_switch, 64)) + x.guided_local_search_lambda_coefficient !== zero(Float64) && (encoded_size += PB._encoded_size(x.guided_local_search_lambda_coefficient, 5)) x.guided_local_search_reset_penalties_on_new_best_solution != false && (encoded_size += PB._encoded_size(x.guided_local_search_reset_penalties_on_new_best_solution, 51)) + x.guided_local_search_penalize_with_vehicle_classes != false && (encoded_size += PB._encoded_size(x.guided_local_search_penalize_with_vehicle_classes, 61)) + x.use_guided_local_search_penalties_in_local_search_operators != false && (encoded_size += PB._encoded_size(x.use_guided_local_search_penalties_in_local_search_operators, 62)) x.use_depth_first_search != false && (encoded_size += PB._encoded_size(x.use_depth_first_search, 6)) x.use_cp != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_cp, 28)) x.use_cp_sat != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_cp_sat, 27)) x.use_generalized_cp_sat != OptionalBoolean.BOOL_UNSPECIFIED && (encoded_size += PB._encoded_size(x.use_generalized_cp_sat, 47)) !isnothing(x.sat_parameters) && (encoded_size += PB._encoded_size(x.sat_parameters, 48)) + x.report_intermediate_cp_sat_solutions != false && (encoded_size += PB._encoded_size(x.report_intermediate_cp_sat_solutions, 56)) x.fallback_to_cp_sat_size_threshold != zero(Int32) && (encoded_size += PB._encoded_size(x.fallback_to_cp_sat_size_threshold, 52)) x.continuous_scheduling_solver != var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET && (encoded_size += PB._encoded_size(x.continuous_scheduling_solver, 33)) x.mixed_integer_scheduling_solver != var"RoutingSearchParameters.SchedulingSolver".SCHEDULING_UNSET && (encoded_size += PB._encoded_size(x.mixed_integer_scheduling_solver, 34)) x.disable_scheduling_beware_this_may_degrade_performance != false && (encoded_size += PB._encoded_size(x.disable_scheduling_beware_this_may_degrade_performance, 50)) - x.optimization_step != zero(Float64) && (encoded_size += PB._encoded_size(x.optimization_step, 7)) + x.optimization_step !== zero(Float64) && (encoded_size += PB._encoded_size(x.optimization_step, 7)) x.number_of_solutions_to_collect != zero(Int32) && (encoded_size += PB._encoded_size(x.number_of_solutions_to_collect, 17)) x.solution_limit != zero(Int64) && (encoded_size += PB._encoded_size(x.solution_limit, 8)) !isnothing(x.time_limit) && (encoded_size += PB._encoded_size(x.time_limit, 9)) !isnothing(x.lns_time_limit) && (encoded_size += PB._encoded_size(x.lns_time_limit, 10)) + x.secondary_ls_time_limit_ratio !== zero(Float64) && (encoded_size += PB._encoded_size(x.secondary_ls_time_limit_ratio, 57)) !isnothing(x.improvement_limit_parameters) && (encoded_size += PB._encoded_size(x.improvement_limit_parameters, 37)) x.use_full_propagation != false && (encoded_size += PB._encoded_size(x.use_full_propagation, 11)) x.log_search != false && (encoded_size += PB._encoded_size(x.log_search, 13)) - x.log_cost_scaling_factor != zero(Float64) && (encoded_size += PB._encoded_size(x.log_cost_scaling_factor, 22)) - x.log_cost_offset != zero(Float64) && (encoded_size += PB._encoded_size(x.log_cost_offset, 29)) + x.log_cost_scaling_factor !== zero(Float64) && (encoded_size += PB._encoded_size(x.log_cost_scaling_factor, 22)) + x.log_cost_offset !== zero(Float64) && (encoded_size += PB._encoded_size(x.log_cost_offset, 29)) !isempty(x.log_tag) && (encoded_size += PB._encoded_size(x.log_tag, 36)) + x.use_iterated_local_search != false && (encoded_size += PB._encoded_size(x.use_iterated_local_search, 58)) + !isnothing(x.iterated_local_search_parameters) && (encoded_size += PB._encoded_size(x.iterated_local_search_parameters, 60)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/boolean_problem_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/boolean_problem_pb.jl index 1386635195a..d1b4d2ab224 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/boolean_problem_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/boolean_problem_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.245 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/sat/boolean_problem.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.134 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/sat/boolean_problem.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export BooleanAssignment, LinearObjective, LinearBooleanConstraint, LinearBooleanProblem + struct BooleanAssignment literals::Vector{Int32} end @@ -72,16 +73,16 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::LinearObjective) initpos = position(e.io) !isempty(x.literals) && PB.encode(e, 1, x.literals) !isempty(x.coefficients) && PB.encode(e, 2, x.coefficients) - x.offset != Float64(0.0) && PB.encode(e, 3, x.offset) - x.scaling_factor != Float64(1.0) && PB.encode(e, 4, x.scaling_factor) + x.offset !== Float64(0.0) && PB.encode(e, 3, x.offset) + x.scaling_factor !== Float64(1.0) && PB.encode(e, 4, x.scaling_factor) return position(e.io) - initpos end function PB._encoded_size(x::LinearObjective) encoded_size = 0 !isempty(x.literals) && (encoded_size += PB._encoded_size(x.literals, 1)) !isempty(x.coefficients) && (encoded_size += PB._encoded_size(x.coefficients, 2)) - x.offset != Float64(0.0) && (encoded_size += PB._encoded_size(x.offset, 3)) - x.scaling_factor != Float64(1.0) && (encoded_size += PB._encoded_size(x.scaling_factor, 4)) + x.offset !== Float64(0.0) && (encoded_size += PB._encoded_size(x.offset, 3)) + x.scaling_factor !== Float64(1.0) && (encoded_size += PB._encoded_size(x.scaling_factor, 4)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/cp_model_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/cp_model_pb.jl index 42aac89f807..c3641e72f7b 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/cp_model_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/cp_model_pb.jl @@ -1,22 +1,23 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.251 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/sat/cp_model.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.137 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/sat/cp_model.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf using ProtoBuf.EnumX: @enumx export LinearExpressionProto, CpSolverSolution, SparsePermutationProto -export AutomatonConstraintProto, IntegerVariableProto, BoolArgumentProto -export TableConstraintProto, FloatObjectiveProto, ElementConstraintProto +export IntegerVariableProto, BoolArgumentProto, FloatObjectiveProto export PartialVariableAssignment, LinearConstraintProto, CpObjectiveProto export InverseConstraintProto, var"DecisionStrategyProto.VariableSelectionStrategy" export DenseMatrixProto, ListOfVariablesProto, CpSolverStatus, NoOverlap2DConstraintProto -export CircuitConstraintProto, RoutesConstraintProto, NoOverlapConstraintProto -export var"DecisionStrategyProto.AffineTransformation" +export CircuitConstraintProto, NoOverlapConstraintProto export var"DecisionStrategyProto.DomainReductionStrategy", LinearArgumentProto export IntervalConstraintProto, AllDifferentConstraintProto, ReservoirConstraintProto -export CumulativeConstraintProto, SymmetryProto, CpSolverResponse, DecisionStrategyProto -export ConstraintProto, CpModelProto +export ElementConstraintProto, TableConstraintProto, AutomatonConstraintProto +export CumulativeConstraintProto, var"RoutesConstraintProto.NodeExpressions", SymmetryProto +export CpSolverResponse, DecisionStrategyProto, RoutesConstraintProto, ConstraintProto +export CpModelProto + struct LinearExpressionProto vars::Vector{Int32} @@ -126,66 +127,6 @@ function PB._encoded_size(x::SparsePermutationProto) return encoded_size end -struct AutomatonConstraintProto - starting_state::Int64 - final_states::Vector{Int64} - transition_tail::Vector{Int64} - transition_head::Vector{Int64} - transition_label::Vector{Int64} - vars::Vector{Int32} -end -PB.default_values(::Type{AutomatonConstraintProto}) = (;starting_state = zero(Int64), final_states = Vector{Int64}(), transition_tail = Vector{Int64}(), transition_head = Vector{Int64}(), transition_label = Vector{Int64}(), vars = Vector{Int32}()) -PB.field_numbers(::Type{AutomatonConstraintProto}) = (;starting_state = 2, final_states = 3, transition_tail = 4, transition_head = 5, transition_label = 6, vars = 7) - -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:AutomatonConstraintProto}) - starting_state = zero(Int64) - final_states = PB.BufferedVector{Int64}() - transition_tail = PB.BufferedVector{Int64}() - transition_head = PB.BufferedVector{Int64}() - transition_label = PB.BufferedVector{Int64}() - vars = PB.BufferedVector{Int32}() - while !PB.message_done(d) - field_number, wire_type = PB.decode_tag(d) - if field_number == 2 - starting_state = PB.decode(d, Int64) - elseif field_number == 3 - PB.decode!(d, wire_type, final_states) - elseif field_number == 4 - PB.decode!(d, wire_type, transition_tail) - elseif field_number == 5 - PB.decode!(d, wire_type, transition_head) - elseif field_number == 6 - PB.decode!(d, wire_type, transition_label) - elseif field_number == 7 - PB.decode!(d, wire_type, vars) - else - PB.skip(d, wire_type) - end - end - return AutomatonConstraintProto(starting_state, final_states[], transition_tail[], transition_head[], transition_label[], vars[]) -end - -function PB.encode(e::PB.AbstractProtoEncoder, x::AutomatonConstraintProto) - initpos = position(e.io) - x.starting_state != zero(Int64) && PB.encode(e, 2, x.starting_state) - !isempty(x.final_states) && PB.encode(e, 3, x.final_states) - !isempty(x.transition_tail) && PB.encode(e, 4, x.transition_tail) - !isempty(x.transition_head) && PB.encode(e, 5, x.transition_head) - !isempty(x.transition_label) && PB.encode(e, 6, x.transition_label) - !isempty(x.vars) && PB.encode(e, 7, x.vars) - return position(e.io) - initpos -end -function PB._encoded_size(x::AutomatonConstraintProto) - encoded_size = 0 - x.starting_state != zero(Int64) && (encoded_size += PB._encoded_size(x.starting_state, 2)) - !isempty(x.final_states) && (encoded_size += PB._encoded_size(x.final_states, 3)) - !isempty(x.transition_tail) && (encoded_size += PB._encoded_size(x.transition_tail, 4)) - !isempty(x.transition_head) && (encoded_size += PB._encoded_size(x.transition_head, 5)) - !isempty(x.transition_label) && (encoded_size += PB._encoded_size(x.transition_label, 6)) - !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 7)) - return encoded_size -end - struct IntegerVariableProto name::String domain::Vector{Int64} @@ -252,48 +193,6 @@ function PB._encoded_size(x::BoolArgumentProto) return encoded_size end -struct TableConstraintProto - vars::Vector{Int32} - values::Vector{Int64} - negated::Bool -end -PB.default_values(::Type{TableConstraintProto}) = (;vars = Vector{Int32}(), values = Vector{Int64}(), negated = false) -PB.field_numbers(::Type{TableConstraintProto}) = (;vars = 1, values = 2, negated = 3) - -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:TableConstraintProto}) - vars = PB.BufferedVector{Int32}() - values = PB.BufferedVector{Int64}() - negated = false - while !PB.message_done(d) - field_number, wire_type = PB.decode_tag(d) - if field_number == 1 - PB.decode!(d, wire_type, vars) - elseif field_number == 2 - PB.decode!(d, wire_type, values) - elseif field_number == 3 - negated = PB.decode(d, Bool) - else - PB.skip(d, wire_type) - end - end - return TableConstraintProto(vars[], values[], negated) -end - -function PB.encode(e::PB.AbstractProtoEncoder, x::TableConstraintProto) - initpos = position(e.io) - !isempty(x.vars) && PB.encode(e, 1, x.vars) - !isempty(x.values) && PB.encode(e, 2, x.values) - x.negated != false && PB.encode(e, 3, x.negated) - return position(e.io) - initpos -end -function PB._encoded_size(x::TableConstraintProto) - encoded_size = 0 - !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 1)) - !isempty(x.values) && (encoded_size += PB._encoded_size(x.values, 2)) - x.negated != false && (encoded_size += PB._encoded_size(x.negated, 3)) - return encoded_size -end - struct FloatObjectiveProto vars::Vector{Int32} coeffs::Vector{Float64} @@ -329,7 +228,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::FloatObjectiveProto) initpos = position(e.io) !isempty(x.vars) && PB.encode(e, 1, x.vars) !isempty(x.coeffs) && PB.encode(e, 2, x.coeffs) - x.offset != zero(Float64) && PB.encode(e, 3, x.offset) + x.offset !== zero(Float64) && PB.encode(e, 3, x.offset) x.maximize != false && PB.encode(e, 4, x.maximize) return position(e.io) - initpos end @@ -337,53 +236,11 @@ function PB._encoded_size(x::FloatObjectiveProto) encoded_size = 0 !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 1)) !isempty(x.coeffs) && (encoded_size += PB._encoded_size(x.coeffs, 2)) - x.offset != zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 3)) + x.offset !== zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 3)) x.maximize != false && (encoded_size += PB._encoded_size(x.maximize, 4)) return encoded_size end -struct ElementConstraintProto - index::Int32 - target::Int32 - vars::Vector{Int32} -end -PB.default_values(::Type{ElementConstraintProto}) = (;index = zero(Int32), target = zero(Int32), vars = Vector{Int32}()) -PB.field_numbers(::Type{ElementConstraintProto}) = (;index = 1, target = 2, vars = 3) - -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:ElementConstraintProto}) - index = zero(Int32) - target = zero(Int32) - vars = PB.BufferedVector{Int32}() - while !PB.message_done(d) - field_number, wire_type = PB.decode_tag(d) - if field_number == 1 - index = PB.decode(d, Int32) - elseif field_number == 2 - target = PB.decode(d, Int32) - elseif field_number == 3 - PB.decode!(d, wire_type, vars) - else - PB.skip(d, wire_type) - end - end - return ElementConstraintProto(index, target, vars[]) -end - -function PB.encode(e::PB.AbstractProtoEncoder, x::ElementConstraintProto) - initpos = position(e.io) - x.index != zero(Int32) && PB.encode(e, 1, x.index) - x.target != zero(Int32) && PB.encode(e, 2, x.target) - !isempty(x.vars) && PB.encode(e, 3, x.vars) - return position(e.io) - initpos -end -function PB._encoded_size(x::ElementConstraintProto) - encoded_size = 0 - x.index != zero(Int32) && (encoded_size += PB._encoded_size(x.index, 1)) - x.target != zero(Int32) && (encoded_size += PB._encoded_size(x.target, 2)) - !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 3)) - return encoded_size -end - struct PartialVariableAssignment vars::Vector{Int32} values::Vector{Int64} @@ -517,8 +374,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::CpObjectiveProto) initpos = position(e.io) !isempty(x.vars) && PB.encode(e, 1, x.vars) !isempty(x.coeffs) && PB.encode(e, 4, x.coeffs) - x.offset != zero(Float64) && PB.encode(e, 2, x.offset) - x.scaling_factor != zero(Float64) && PB.encode(e, 3, x.scaling_factor) + x.offset !== zero(Float64) && PB.encode(e, 2, x.offset) + x.scaling_factor !== zero(Float64) && PB.encode(e, 3, x.scaling_factor) !isempty(x.domain) && PB.encode(e, 5, x.domain) x.scaling_was_exact != false && PB.encode(e, 6, x.scaling_was_exact) x.integer_before_offset != zero(Int64) && PB.encode(e, 7, x.integer_before_offset) @@ -530,8 +387,8 @@ function PB._encoded_size(x::CpObjectiveProto) encoded_size = 0 !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 1)) !isempty(x.coeffs) && (encoded_size += PB._encoded_size(x.coeffs, 4)) - x.offset != zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 2)) - x.scaling_factor != zero(Float64) && (encoded_size += PB._encoded_size(x.scaling_factor, 3)) + x.offset !== zero(Float64) && (encoded_size += PB._encoded_size(x.offset, 2)) + x.scaling_factor !== zero(Float64) && (encoded_size += PB._encoded_size(x.scaling_factor, 3)) !isempty(x.domain) && (encoded_size += PB._encoded_size(x.domain, 5)) x.scaling_was_exact != false && (encoded_size += PB._encoded_size(x.scaling_was_exact, 6)) x.integer_before_offset != zero(Int64) && (encoded_size += PB._encoded_size(x.integer_before_offset, 7)) @@ -730,60 +587,6 @@ function PB._encoded_size(x::CircuitConstraintProto) return encoded_size end -struct RoutesConstraintProto - tails::Vector{Int32} - heads::Vector{Int32} - literals::Vector{Int32} - demands::Vector{Int32} - capacity::Int64 -end -PB.default_values(::Type{RoutesConstraintProto}) = (;tails = Vector{Int32}(), heads = Vector{Int32}(), literals = Vector{Int32}(), demands = Vector{Int32}(), capacity = zero(Int64)) -PB.field_numbers(::Type{RoutesConstraintProto}) = (;tails = 1, heads = 2, literals = 3, demands = 4, capacity = 5) - -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutesConstraintProto}) - tails = PB.BufferedVector{Int32}() - heads = PB.BufferedVector{Int32}() - literals = PB.BufferedVector{Int32}() - demands = PB.BufferedVector{Int32}() - capacity = zero(Int64) - while !PB.message_done(d) - field_number, wire_type = PB.decode_tag(d) - if field_number == 1 - PB.decode!(d, wire_type, tails) - elseif field_number == 2 - PB.decode!(d, wire_type, heads) - elseif field_number == 3 - PB.decode!(d, wire_type, literals) - elseif field_number == 4 - PB.decode!(d, wire_type, demands) - elseif field_number == 5 - capacity = PB.decode(d, Int64) - else - PB.skip(d, wire_type) - end - end - return RoutesConstraintProto(tails[], heads[], literals[], demands[], capacity) -end - -function PB.encode(e::PB.AbstractProtoEncoder, x::RoutesConstraintProto) - initpos = position(e.io) - !isempty(x.tails) && PB.encode(e, 1, x.tails) - !isempty(x.heads) && PB.encode(e, 2, x.heads) - !isempty(x.literals) && PB.encode(e, 3, x.literals) - !isempty(x.demands) && PB.encode(e, 4, x.demands) - x.capacity != zero(Int64) && PB.encode(e, 5, x.capacity) - return position(e.io) - initpos -end -function PB._encoded_size(x::RoutesConstraintProto) - encoded_size = 0 - !isempty(x.tails) && (encoded_size += PB._encoded_size(x.tails, 1)) - !isempty(x.heads) && (encoded_size += PB._encoded_size(x.heads, 2)) - !isempty(x.literals) && (encoded_size += PB._encoded_size(x.literals, 3)) - !isempty(x.demands) && (encoded_size += PB._encoded_size(x.demands, 4)) - x.capacity != zero(Int64) && (encoded_size += PB._encoded_size(x.capacity, 5)) - return encoded_size -end - struct NoOverlapConstraintProto intervals::Vector{Int32} end @@ -814,49 +617,7 @@ function PB._encoded_size(x::NoOverlapConstraintProto) return encoded_size end -struct var"DecisionStrategyProto.AffineTransformation" - index::Int32 - offset::Int64 - positive_coeff::Int64 -end -PB.default_values(::Type{var"DecisionStrategyProto.AffineTransformation"}) = (;index = zero(Int32), offset = zero(Int64), positive_coeff = zero(Int64)) -PB.field_numbers(::Type{var"DecisionStrategyProto.AffineTransformation"}) = (;index = 1, offset = 2, positive_coeff = 3) - -function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"DecisionStrategyProto.AffineTransformation"}) - index = zero(Int32) - offset = zero(Int64) - positive_coeff = zero(Int64) - while !PB.message_done(d) - field_number, wire_type = PB.decode_tag(d) - if field_number == 1 - index = PB.decode(d, Int32) - elseif field_number == 2 - offset = PB.decode(d, Int64) - elseif field_number == 3 - positive_coeff = PB.decode(d, Int64) - else - PB.skip(d, wire_type) - end - end - return var"DecisionStrategyProto.AffineTransformation"(index, offset, positive_coeff) -end - -function PB.encode(e::PB.AbstractProtoEncoder, x::var"DecisionStrategyProto.AffineTransformation") - initpos = position(e.io) - x.index != zero(Int32) && PB.encode(e, 1, x.index) - x.offset != zero(Int64) && PB.encode(e, 2, x.offset) - x.positive_coeff != zero(Int64) && PB.encode(e, 3, x.positive_coeff) - return position(e.io) - initpos -end -function PB._encoded_size(x::var"DecisionStrategyProto.AffineTransformation") - encoded_size = 0 - x.index != zero(Int32) && (encoded_size += PB._encoded_size(x.index, 1)) - x.offset != zero(Int64) && (encoded_size += PB._encoded_size(x.offset, 2)) - x.positive_coeff != zero(Int64) && (encoded_size += PB._encoded_size(x.positive_coeff, 3)) - return encoded_size -end - -@enumx var"DecisionStrategyProto.DomainReductionStrategy" SELECT_MIN_VALUE=0 SELECT_MAX_VALUE=1 SELECT_LOWER_HALF=2 SELECT_UPPER_HALF=3 SELECT_MEDIAN_VALUE=4 +@enumx var"DecisionStrategyProto.DomainReductionStrategy" SELECT_MIN_VALUE=0 SELECT_MAX_VALUE=1 SELECT_LOWER_HALF=2 SELECT_UPPER_HALF=3 SELECT_MEDIAN_VALUE=4 SELECT_RANDOM_HALF=5 struct LinearArgumentProto target::Union{Nothing,LinearExpressionProto} @@ -1021,6 +782,180 @@ function PB._encoded_size(x::ReservoirConstraintProto) return encoded_size end +struct ElementConstraintProto + index::Int32 + target::Int32 + vars::Vector{Int32} + linear_index::Union{Nothing,LinearExpressionProto} + linear_target::Union{Nothing,LinearExpressionProto} + exprs::Vector{LinearExpressionProto} +end +PB.default_values(::Type{ElementConstraintProto}) = (;index = zero(Int32), target = zero(Int32), vars = Vector{Int32}(), linear_index = nothing, linear_target = nothing, exprs = Vector{LinearExpressionProto}()) +PB.field_numbers(::Type{ElementConstraintProto}) = (;index = 1, target = 2, vars = 3, linear_index = 4, linear_target = 5, exprs = 6) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:ElementConstraintProto}) + index = zero(Int32) + target = zero(Int32) + vars = PB.BufferedVector{Int32}() + linear_index = Ref{Union{Nothing,LinearExpressionProto}}(nothing) + linear_target = Ref{Union{Nothing,LinearExpressionProto}}(nothing) + exprs = PB.BufferedVector{LinearExpressionProto}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + index = PB.decode(d, Int32) + elseif field_number == 2 + target = PB.decode(d, Int32) + elseif field_number == 3 + PB.decode!(d, wire_type, vars) + elseif field_number == 4 + PB.decode!(d, linear_index) + elseif field_number == 5 + PB.decode!(d, linear_target) + elseif field_number == 6 + PB.decode!(d, exprs) + else + PB.skip(d, wire_type) + end + end + return ElementConstraintProto(index, target, vars[], linear_index[], linear_target[], exprs[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::ElementConstraintProto) + initpos = position(e.io) + x.index != zero(Int32) && PB.encode(e, 1, x.index) + x.target != zero(Int32) && PB.encode(e, 2, x.target) + !isempty(x.vars) && PB.encode(e, 3, x.vars) + !isnothing(x.linear_index) && PB.encode(e, 4, x.linear_index) + !isnothing(x.linear_target) && PB.encode(e, 5, x.linear_target) + !isempty(x.exprs) && PB.encode(e, 6, x.exprs) + return position(e.io) - initpos +end +function PB._encoded_size(x::ElementConstraintProto) + encoded_size = 0 + x.index != zero(Int32) && (encoded_size += PB._encoded_size(x.index, 1)) + x.target != zero(Int32) && (encoded_size += PB._encoded_size(x.target, 2)) + !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 3)) + !isnothing(x.linear_index) && (encoded_size += PB._encoded_size(x.linear_index, 4)) + !isnothing(x.linear_target) && (encoded_size += PB._encoded_size(x.linear_target, 5)) + !isempty(x.exprs) && (encoded_size += PB._encoded_size(x.exprs, 6)) + return encoded_size +end + +struct TableConstraintProto + vars::Vector{Int32} + values::Vector{Int64} + exprs::Vector{LinearExpressionProto} + negated::Bool +end +PB.default_values(::Type{TableConstraintProto}) = (;vars = Vector{Int32}(), values = Vector{Int64}(), exprs = Vector{LinearExpressionProto}(), negated = false) +PB.field_numbers(::Type{TableConstraintProto}) = (;vars = 1, values = 2, exprs = 4, negated = 3) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:TableConstraintProto}) + vars = PB.BufferedVector{Int32}() + values = PB.BufferedVector{Int64}() + exprs = PB.BufferedVector{LinearExpressionProto}() + negated = false + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + PB.decode!(d, wire_type, vars) + elseif field_number == 2 + PB.decode!(d, wire_type, values) + elseif field_number == 4 + PB.decode!(d, exprs) + elseif field_number == 3 + negated = PB.decode(d, Bool) + else + PB.skip(d, wire_type) + end + end + return TableConstraintProto(vars[], values[], exprs[], negated) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::TableConstraintProto) + initpos = position(e.io) + !isempty(x.vars) && PB.encode(e, 1, x.vars) + !isempty(x.values) && PB.encode(e, 2, x.values) + !isempty(x.exprs) && PB.encode(e, 4, x.exprs) + x.negated != false && PB.encode(e, 3, x.negated) + return position(e.io) - initpos +end +function PB._encoded_size(x::TableConstraintProto) + encoded_size = 0 + !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 1)) + !isempty(x.values) && (encoded_size += PB._encoded_size(x.values, 2)) + !isempty(x.exprs) && (encoded_size += PB._encoded_size(x.exprs, 4)) + x.negated != false && (encoded_size += PB._encoded_size(x.negated, 3)) + return encoded_size +end + +struct AutomatonConstraintProto + starting_state::Int64 + final_states::Vector{Int64} + transition_tail::Vector{Int64} + transition_head::Vector{Int64} + transition_label::Vector{Int64} + vars::Vector{Int32} + exprs::Vector{LinearExpressionProto} +end +PB.default_values(::Type{AutomatonConstraintProto}) = (;starting_state = zero(Int64), final_states = Vector{Int64}(), transition_tail = Vector{Int64}(), transition_head = Vector{Int64}(), transition_label = Vector{Int64}(), vars = Vector{Int32}(), exprs = Vector{LinearExpressionProto}()) +PB.field_numbers(::Type{AutomatonConstraintProto}) = (;starting_state = 2, final_states = 3, transition_tail = 4, transition_head = 5, transition_label = 6, vars = 7, exprs = 8) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:AutomatonConstraintProto}) + starting_state = zero(Int64) + final_states = PB.BufferedVector{Int64}() + transition_tail = PB.BufferedVector{Int64}() + transition_head = PB.BufferedVector{Int64}() + transition_label = PB.BufferedVector{Int64}() + vars = PB.BufferedVector{Int32}() + exprs = PB.BufferedVector{LinearExpressionProto}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 2 + starting_state = PB.decode(d, Int64) + elseif field_number == 3 + PB.decode!(d, wire_type, final_states) + elseif field_number == 4 + PB.decode!(d, wire_type, transition_tail) + elseif field_number == 5 + PB.decode!(d, wire_type, transition_head) + elseif field_number == 6 + PB.decode!(d, wire_type, transition_label) + elseif field_number == 7 + PB.decode!(d, wire_type, vars) + elseif field_number == 8 + PB.decode!(d, exprs) + else + PB.skip(d, wire_type) + end + end + return AutomatonConstraintProto(starting_state, final_states[], transition_tail[], transition_head[], transition_label[], vars[], exprs[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::AutomatonConstraintProto) + initpos = position(e.io) + x.starting_state != zero(Int64) && PB.encode(e, 2, x.starting_state) + !isempty(x.final_states) && PB.encode(e, 3, x.final_states) + !isempty(x.transition_tail) && PB.encode(e, 4, x.transition_tail) + !isempty(x.transition_head) && PB.encode(e, 5, x.transition_head) + !isempty(x.transition_label) && PB.encode(e, 6, x.transition_label) + !isempty(x.vars) && PB.encode(e, 7, x.vars) + !isempty(x.exprs) && PB.encode(e, 8, x.exprs) + return position(e.io) - initpos +end +function PB._encoded_size(x::AutomatonConstraintProto) + encoded_size = 0 + x.starting_state != zero(Int64) && (encoded_size += PB._encoded_size(x.starting_state, 2)) + !isempty(x.final_states) && (encoded_size += PB._encoded_size(x.final_states, 3)) + !isempty(x.transition_tail) && (encoded_size += PB._encoded_size(x.transition_tail, 4)) + !isempty(x.transition_head) && (encoded_size += PB._encoded_size(x.transition_head, 5)) + !isempty(x.transition_label) && (encoded_size += PB._encoded_size(x.transition_label, 6)) + !isempty(x.vars) && (encoded_size += PB._encoded_size(x.vars, 7)) + !isempty(x.exprs) && (encoded_size += PB._encoded_size(x.exprs, 8)) + return encoded_size +end + struct CumulativeConstraintProto capacity::Union{Nothing,LinearExpressionProto} intervals::Vector{Int32} @@ -1063,6 +998,36 @@ function PB._encoded_size(x::CumulativeConstraintProto) return encoded_size end +struct var"RoutesConstraintProto.NodeExpressions" + exprs::Vector{LinearExpressionProto} +end +PB.default_values(::Type{var"RoutesConstraintProto.NodeExpressions"}) = (;exprs = Vector{LinearExpressionProto}()) +PB.field_numbers(::Type{var"RoutesConstraintProto.NodeExpressions"}) = (;exprs = 1) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"RoutesConstraintProto.NodeExpressions"}) + exprs = PB.BufferedVector{LinearExpressionProto}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + PB.decode!(d, exprs) + else + PB.skip(d, wire_type) + end + end + return var"RoutesConstraintProto.NodeExpressions"(exprs[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::var"RoutesConstraintProto.NodeExpressions") + initpos = position(e.io) + !isempty(x.exprs) && PB.encode(e, 1, x.exprs) + return position(e.io) - initpos +end +function PB._encoded_size(x::var"RoutesConstraintProto.NodeExpressions") + encoded_size = 0 + !isempty(x.exprs) && (encoded_size += PB._encoded_size(x.exprs, 1)) + return encoded_size +end + struct SymmetryProto permutations::Vector{SparsePermutationProto} orbitopes::Vector{DenseMatrixProto} @@ -1111,6 +1076,7 @@ struct CpSolverResponse inner_objective_lower_bound::Int64 num_integers::Int64 num_booleans::Int64 + num_fixed_booleans::Int64 num_conflicts::Int64 num_branches::Int64 num_binary_propagations::Int64 @@ -1124,8 +1090,8 @@ struct CpSolverResponse solution_info::String solve_log::String end -PB.default_values(::Type{CpSolverResponse}) = (;status = CpSolverStatus.UNKNOWN, solution = Vector{Int64}(), objective_value = zero(Float64), best_objective_bound = zero(Float64), additional_solutions = Vector{CpSolverSolution}(), tightened_variables = Vector{IntegerVariableProto}(), sufficient_assumptions_for_infeasibility = Vector{Int32}(), integer_objective = nothing, inner_objective_lower_bound = zero(Int64), num_integers = zero(Int64), num_booleans = zero(Int64), num_conflicts = zero(Int64), num_branches = zero(Int64), num_binary_propagations = zero(Int64), num_integer_propagations = zero(Int64), num_restarts = zero(Int64), num_lp_iterations = zero(Int64), wall_time = zero(Float64), user_time = zero(Float64), deterministic_time = zero(Float64), gap_integral = zero(Float64), solution_info = "", solve_log = "") -PB.field_numbers(::Type{CpSolverResponse}) = (;status = 1, solution = 2, objective_value = 3, best_objective_bound = 4, additional_solutions = 27, tightened_variables = 21, sufficient_assumptions_for_infeasibility = 23, integer_objective = 28, inner_objective_lower_bound = 29, num_integers = 30, num_booleans = 10, num_conflicts = 11, num_branches = 12, num_binary_propagations = 13, num_integer_propagations = 14, num_restarts = 24, num_lp_iterations = 25, wall_time = 15, user_time = 16, deterministic_time = 17, gap_integral = 22, solution_info = 20, solve_log = 26) +PB.default_values(::Type{CpSolverResponse}) = (;status = CpSolverStatus.UNKNOWN, solution = Vector{Int64}(), objective_value = zero(Float64), best_objective_bound = zero(Float64), additional_solutions = Vector{CpSolverSolution}(), tightened_variables = Vector{IntegerVariableProto}(), sufficient_assumptions_for_infeasibility = Vector{Int32}(), integer_objective = nothing, inner_objective_lower_bound = zero(Int64), num_integers = zero(Int64), num_booleans = zero(Int64), num_fixed_booleans = zero(Int64), num_conflicts = zero(Int64), num_branches = zero(Int64), num_binary_propagations = zero(Int64), num_integer_propagations = zero(Int64), num_restarts = zero(Int64), num_lp_iterations = zero(Int64), wall_time = zero(Float64), user_time = zero(Float64), deterministic_time = zero(Float64), gap_integral = zero(Float64), solution_info = "", solve_log = "") +PB.field_numbers(::Type{CpSolverResponse}) = (;status = 1, solution = 2, objective_value = 3, best_objective_bound = 4, additional_solutions = 27, tightened_variables = 21, sufficient_assumptions_for_infeasibility = 23, integer_objective = 28, inner_objective_lower_bound = 29, num_integers = 30, num_booleans = 10, num_fixed_booleans = 31, num_conflicts = 11, num_branches = 12, num_binary_propagations = 13, num_integer_propagations = 14, num_restarts = 24, num_lp_iterations = 25, wall_time = 15, user_time = 16, deterministic_time = 17, gap_integral = 22, solution_info = 20, solve_log = 26) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:CpSolverResponse}) status = CpSolverStatus.UNKNOWN @@ -1139,6 +1105,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:CpSolverResponse}) inner_objective_lower_bound = zero(Int64) num_integers = zero(Int64) num_booleans = zero(Int64) + num_fixed_booleans = zero(Int64) num_conflicts = zero(Int64) num_branches = zero(Int64) num_binary_propagations = zero(Int64) @@ -1175,6 +1142,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:CpSolverResponse}) num_integers = PB.decode(d, Int64) elseif field_number == 10 num_booleans = PB.decode(d, Int64) + elseif field_number == 31 + num_fixed_booleans = PB.decode(d, Int64) elseif field_number == 11 num_conflicts = PB.decode(d, Int64) elseif field_number == 12 @@ -1203,15 +1172,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:CpSolverResponse}) PB.skip(d, wire_type) end end - return CpSolverResponse(status, solution[], objective_value, best_objective_bound, additional_solutions[], tightened_variables[], sufficient_assumptions_for_infeasibility[], integer_objective[], inner_objective_lower_bound, num_integers, num_booleans, num_conflicts, num_branches, num_binary_propagations, num_integer_propagations, num_restarts, num_lp_iterations, wall_time, user_time, deterministic_time, gap_integral, solution_info, solve_log) + return CpSolverResponse(status, solution[], objective_value, best_objective_bound, additional_solutions[], tightened_variables[], sufficient_assumptions_for_infeasibility[], integer_objective[], inner_objective_lower_bound, num_integers, num_booleans, num_fixed_booleans, num_conflicts, num_branches, num_binary_propagations, num_integer_propagations, num_restarts, num_lp_iterations, wall_time, user_time, deterministic_time, gap_integral, solution_info, solve_log) end function PB.encode(e::PB.AbstractProtoEncoder, x::CpSolverResponse) initpos = position(e.io) x.status != CpSolverStatus.UNKNOWN && PB.encode(e, 1, x.status) !isempty(x.solution) && PB.encode(e, 2, x.solution) - x.objective_value != zero(Float64) && PB.encode(e, 3, x.objective_value) - x.best_objective_bound != zero(Float64) && PB.encode(e, 4, x.best_objective_bound) + x.objective_value !== zero(Float64) && PB.encode(e, 3, x.objective_value) + x.best_objective_bound !== zero(Float64) && PB.encode(e, 4, x.best_objective_bound) !isempty(x.additional_solutions) && PB.encode(e, 27, x.additional_solutions) !isempty(x.tightened_variables) && PB.encode(e, 21, x.tightened_variables) !isempty(x.sufficient_assumptions_for_infeasibility) && PB.encode(e, 23, x.sufficient_assumptions_for_infeasibility) @@ -1219,16 +1188,17 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::CpSolverResponse) x.inner_objective_lower_bound != zero(Int64) && PB.encode(e, 29, x.inner_objective_lower_bound) x.num_integers != zero(Int64) && PB.encode(e, 30, x.num_integers) x.num_booleans != zero(Int64) && PB.encode(e, 10, x.num_booleans) + x.num_fixed_booleans != zero(Int64) && PB.encode(e, 31, x.num_fixed_booleans) x.num_conflicts != zero(Int64) && PB.encode(e, 11, x.num_conflicts) x.num_branches != zero(Int64) && PB.encode(e, 12, x.num_branches) x.num_binary_propagations != zero(Int64) && PB.encode(e, 13, x.num_binary_propagations) x.num_integer_propagations != zero(Int64) && PB.encode(e, 14, x.num_integer_propagations) x.num_restarts != zero(Int64) && PB.encode(e, 24, x.num_restarts) x.num_lp_iterations != zero(Int64) && PB.encode(e, 25, x.num_lp_iterations) - x.wall_time != zero(Float64) && PB.encode(e, 15, x.wall_time) - x.user_time != zero(Float64) && PB.encode(e, 16, x.user_time) - x.deterministic_time != zero(Float64) && PB.encode(e, 17, x.deterministic_time) - x.gap_integral != zero(Float64) && PB.encode(e, 22, x.gap_integral) + x.wall_time !== zero(Float64) && PB.encode(e, 15, x.wall_time) + x.user_time !== zero(Float64) && PB.encode(e, 16, x.user_time) + x.deterministic_time !== zero(Float64) && PB.encode(e, 17, x.deterministic_time) + x.gap_integral !== zero(Float64) && PB.encode(e, 22, x.gap_integral) !isempty(x.solution_info) && PB.encode(e, 20, x.solution_info) !isempty(x.solve_log) && PB.encode(e, 26, x.solve_log) return position(e.io) - initpos @@ -1237,8 +1207,8 @@ function PB._encoded_size(x::CpSolverResponse) encoded_size = 0 x.status != CpSolverStatus.UNKNOWN && (encoded_size += PB._encoded_size(x.status, 1)) !isempty(x.solution) && (encoded_size += PB._encoded_size(x.solution, 2)) - x.objective_value != zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 3)) - x.best_objective_bound != zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective_bound, 4)) + x.objective_value !== zero(Float64) && (encoded_size += PB._encoded_size(x.objective_value, 3)) + x.best_objective_bound !== zero(Float64) && (encoded_size += PB._encoded_size(x.best_objective_bound, 4)) !isempty(x.additional_solutions) && (encoded_size += PB._encoded_size(x.additional_solutions, 27)) !isempty(x.tightened_variables) && (encoded_size += PB._encoded_size(x.tightened_variables, 21)) !isempty(x.sufficient_assumptions_for_infeasibility) && (encoded_size += PB._encoded_size(x.sufficient_assumptions_for_infeasibility, 23)) @@ -1246,16 +1216,17 @@ function PB._encoded_size(x::CpSolverResponse) x.inner_objective_lower_bound != zero(Int64) && (encoded_size += PB._encoded_size(x.inner_objective_lower_bound, 29)) x.num_integers != zero(Int64) && (encoded_size += PB._encoded_size(x.num_integers, 30)) x.num_booleans != zero(Int64) && (encoded_size += PB._encoded_size(x.num_booleans, 10)) + x.num_fixed_booleans != zero(Int64) && (encoded_size += PB._encoded_size(x.num_fixed_booleans, 31)) x.num_conflicts != zero(Int64) && (encoded_size += PB._encoded_size(x.num_conflicts, 11)) x.num_branches != zero(Int64) && (encoded_size += PB._encoded_size(x.num_branches, 12)) x.num_binary_propagations != zero(Int64) && (encoded_size += PB._encoded_size(x.num_binary_propagations, 13)) x.num_integer_propagations != zero(Int64) && (encoded_size += PB._encoded_size(x.num_integer_propagations, 14)) x.num_restarts != zero(Int64) && (encoded_size += PB._encoded_size(x.num_restarts, 24)) x.num_lp_iterations != zero(Int64) && (encoded_size += PB._encoded_size(x.num_lp_iterations, 25)) - x.wall_time != zero(Float64) && (encoded_size += PB._encoded_size(x.wall_time, 15)) - x.user_time != zero(Float64) && (encoded_size += PB._encoded_size(x.user_time, 16)) - x.deterministic_time != zero(Float64) && (encoded_size += PB._encoded_size(x.deterministic_time, 17)) - x.gap_integral != zero(Float64) && (encoded_size += PB._encoded_size(x.gap_integral, 22)) + x.wall_time !== zero(Float64) && (encoded_size += PB._encoded_size(x.wall_time, 15)) + x.user_time !== zero(Float64) && (encoded_size += PB._encoded_size(x.user_time, 16)) + x.deterministic_time !== zero(Float64) && (encoded_size += PB._encoded_size(x.deterministic_time, 17)) + x.gap_integral !== zero(Float64) && (encoded_size += PB._encoded_size(x.gap_integral, 22)) !isempty(x.solution_info) && (encoded_size += PB._encoded_size(x.solution_info, 20)) !isempty(x.solve_log) && (encoded_size += PB._encoded_size(x.solve_log, 26)) return encoded_size @@ -1263,49 +1234,109 @@ end struct DecisionStrategyProto variables::Vector{Int32} + exprs::Vector{LinearExpressionProto} variable_selection_strategy::var"DecisionStrategyProto.VariableSelectionStrategy".T domain_reduction_strategy::var"DecisionStrategyProto.DomainReductionStrategy".T - transformations::Vector{var"DecisionStrategyProto.AffineTransformation"} end -PB.default_values(::Type{DecisionStrategyProto}) = (;variables = Vector{Int32}(), variable_selection_strategy = var"DecisionStrategyProto.VariableSelectionStrategy".CHOOSE_FIRST, domain_reduction_strategy = var"DecisionStrategyProto.DomainReductionStrategy".SELECT_MIN_VALUE, transformations = Vector{var"DecisionStrategyProto.AffineTransformation"}()) -PB.field_numbers(::Type{DecisionStrategyProto}) = (;variables = 1, variable_selection_strategy = 2, domain_reduction_strategy = 3, transformations = 4) +PB.default_values(::Type{DecisionStrategyProto}) = (;variables = Vector{Int32}(), exprs = Vector{LinearExpressionProto}(), variable_selection_strategy = var"DecisionStrategyProto.VariableSelectionStrategy".CHOOSE_FIRST, domain_reduction_strategy = var"DecisionStrategyProto.DomainReductionStrategy".SELECT_MIN_VALUE) +PB.field_numbers(::Type{DecisionStrategyProto}) = (;variables = 1, exprs = 5, variable_selection_strategy = 2, domain_reduction_strategy = 3) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:DecisionStrategyProto}) variables = PB.BufferedVector{Int32}() + exprs = PB.BufferedVector{LinearExpressionProto}() variable_selection_strategy = var"DecisionStrategyProto.VariableSelectionStrategy".CHOOSE_FIRST domain_reduction_strategy = var"DecisionStrategyProto.DomainReductionStrategy".SELECT_MIN_VALUE - transformations = PB.BufferedVector{var"DecisionStrategyProto.AffineTransformation"}() while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 PB.decode!(d, wire_type, variables) + elseif field_number == 5 + PB.decode!(d, exprs) elseif field_number == 2 variable_selection_strategy = PB.decode(d, var"DecisionStrategyProto.VariableSelectionStrategy".T) elseif field_number == 3 domain_reduction_strategy = PB.decode(d, var"DecisionStrategyProto.DomainReductionStrategy".T) - elseif field_number == 4 - PB.decode!(d, transformations) else PB.skip(d, wire_type) end end - return DecisionStrategyProto(variables[], variable_selection_strategy, domain_reduction_strategy, transformations[]) + return DecisionStrategyProto(variables[], exprs[], variable_selection_strategy, domain_reduction_strategy) end function PB.encode(e::PB.AbstractProtoEncoder, x::DecisionStrategyProto) initpos = position(e.io) !isempty(x.variables) && PB.encode(e, 1, x.variables) + !isempty(x.exprs) && PB.encode(e, 5, x.exprs) x.variable_selection_strategy != var"DecisionStrategyProto.VariableSelectionStrategy".CHOOSE_FIRST && PB.encode(e, 2, x.variable_selection_strategy) x.domain_reduction_strategy != var"DecisionStrategyProto.DomainReductionStrategy".SELECT_MIN_VALUE && PB.encode(e, 3, x.domain_reduction_strategy) - !isempty(x.transformations) && PB.encode(e, 4, x.transformations) return position(e.io) - initpos end function PB._encoded_size(x::DecisionStrategyProto) encoded_size = 0 !isempty(x.variables) && (encoded_size += PB._encoded_size(x.variables, 1)) + !isempty(x.exprs) && (encoded_size += PB._encoded_size(x.exprs, 5)) x.variable_selection_strategy != var"DecisionStrategyProto.VariableSelectionStrategy".CHOOSE_FIRST && (encoded_size += PB._encoded_size(x.variable_selection_strategy, 2)) x.domain_reduction_strategy != var"DecisionStrategyProto.DomainReductionStrategy".SELECT_MIN_VALUE && (encoded_size += PB._encoded_size(x.domain_reduction_strategy, 3)) - !isempty(x.transformations) && (encoded_size += PB._encoded_size(x.transformations, 4)) + return encoded_size +end + +struct RoutesConstraintProto + tails::Vector{Int32} + heads::Vector{Int32} + literals::Vector{Int32} + demands::Vector{Int32} + capacity::Int64 + dimensions::Vector{var"RoutesConstraintProto.NodeExpressions"} +end +PB.default_values(::Type{RoutesConstraintProto}) = (;tails = Vector{Int32}(), heads = Vector{Int32}(), literals = Vector{Int32}(), demands = Vector{Int32}(), capacity = zero(Int64), dimensions = Vector{var"RoutesConstraintProto.NodeExpressions"}()) +PB.field_numbers(::Type{RoutesConstraintProto}) = (;tails = 1, heads = 2, literals = 3, demands = 4, capacity = 5, dimensions = 6) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:RoutesConstraintProto}) + tails = PB.BufferedVector{Int32}() + heads = PB.BufferedVector{Int32}() + literals = PB.BufferedVector{Int32}() + demands = PB.BufferedVector{Int32}() + capacity = zero(Int64) + dimensions = PB.BufferedVector{var"RoutesConstraintProto.NodeExpressions"}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + PB.decode!(d, wire_type, tails) + elseif field_number == 2 + PB.decode!(d, wire_type, heads) + elseif field_number == 3 + PB.decode!(d, wire_type, literals) + elseif field_number == 4 + PB.decode!(d, wire_type, demands) + elseif field_number == 5 + capacity = PB.decode(d, Int64) + elseif field_number == 6 + PB.decode!(d, dimensions) + else + PB.skip(d, wire_type) + end + end + return RoutesConstraintProto(tails[], heads[], literals[], demands[], capacity, dimensions[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::RoutesConstraintProto) + initpos = position(e.io) + !isempty(x.tails) && PB.encode(e, 1, x.tails) + !isempty(x.heads) && PB.encode(e, 2, x.heads) + !isempty(x.literals) && PB.encode(e, 3, x.literals) + !isempty(x.demands) && PB.encode(e, 4, x.demands) + x.capacity != zero(Int64) && PB.encode(e, 5, x.capacity) + !isempty(x.dimensions) && PB.encode(e, 6, x.dimensions) + return position(e.io) - initpos +end +function PB._encoded_size(x::RoutesConstraintProto) + encoded_size = 0 + !isempty(x.tails) && (encoded_size += PB._encoded_size(x.tails, 1)) + !isempty(x.heads) && (encoded_size += PB._encoded_size(x.heads, 2)) + !isempty(x.literals) && (encoded_size += PB._encoded_size(x.literals, 3)) + !isempty(x.demands) && (encoded_size += PB._encoded_size(x.demands, 4)) + x.capacity != zero(Int64) && (encoded_size += PB._encoded_size(x.capacity, 5)) + !isempty(x.dimensions) && (encoded_size += PB._encoded_size(x.dimensions, 6)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/sat_parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/sat_parameters_pb.jl index 295154ce01a..cf669b36e04 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/sat_parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/sat/sat_parameters_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.247 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/sat/sat_parameters.proto (proto2 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.135 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/sat/sat_parameters.proto (proto2 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -14,17 +14,14 @@ export var"SatParameters.MaxSatStratificationAlgorithm" export var"SatParameters.RestartAlgorithm", var"SatParameters.BinaryMinizationAlgorithm" export var"SatParameters.ClauseOrdering", SatParameters -# Abstract types to help resolve mutually recursive definitions -abstract type var"##AbstractSatParameters" end - @enumx var"SatParameters.ConflictMinimizationAlgorithm" NONE=0 SIMPLE=1 RECURSIVE=2 EXPERIMENTAL=3 @enumx var"SatParameters.FPRoundingMethod" NEAREST_INTEGER=0 LOCK_BASED=1 ACTIVE_LOCK_BASED=3 PROPAGATION_ASSISTED=2 -@enumx var"SatParameters.Polarity" POLARITY_TRUE=0 POLARITY_FALSE=1 POLARITY_RANDOM=2 POLARITY_WEIGHTED_SIGN=3 POLARITY_REVERSE_WEIGHTED_SIGN=4 +@enumx var"SatParameters.Polarity" POLARITY_TRUE=0 POLARITY_FALSE=1 POLARITY_RANDOM=2 -@enumx var"SatParameters.SearchBranching" AUTOMATIC_SEARCH=0 FIXED_SEARCH=1 PORTFOLIO_SEARCH=2 LP_SEARCH=3 PSEUDO_COST_SEARCH=4 PORTFOLIO_WITH_QUICK_RESTART_SEARCH=5 HINT_SEARCH=6 PARTIAL_FIXED_SEARCH=7 +@enumx var"SatParameters.SearchBranching" AUTOMATIC_SEARCH=0 FIXED_SEARCH=1 PORTFOLIO_SEARCH=2 LP_SEARCH=3 PSEUDO_COST_SEARCH=4 PORTFOLIO_WITH_QUICK_RESTART_SEARCH=5 HINT_SEARCH=6 PARTIAL_FIXED_SEARCH=7 RANDOMIZED_SEARCH=8 @enumx var"SatParameters.SharedTreeSplitStrategy" SPLIT_STRATEGY_AUTO=0 SPLIT_STRATEGY_DISCREPANCY=1 SPLIT_STRATEGY_OBJECTIVE_LB=2 SPLIT_STRATEGY_BALANCED_TREE=3 SPLIT_STRATEGY_FIRST_PROPOSAL=4 @@ -42,12 +39,13 @@ abstract type var"##AbstractSatParameters" end @enumx var"SatParameters.ClauseOrdering" CLAUSE_ACTIVITY=0 CLAUSE_LBD=1 -struct SatParameters <: var"##AbstractSatParameters" +struct SatParameters name::String preferred_variable_order::var"SatParameters.VariableOrder".T initial_polarity::var"SatParameters.Polarity".T use_phase_saving::Bool polarity_rephase_increment::Int32 + polarity_exploit_ls_hints::Bool random_polarity_ratio::Float64 random_branches_ratio::Float64 use_erwa_heuristic::Bool @@ -64,8 +62,6 @@ struct SatParameters <: var"##AbstractSatParameters" clause_cleanup_ordering::var"SatParameters.ClauseOrdering".T pb_cleanup_increment::Int32 pb_cleanup_ratio::Float64 - minimize_with_propagation_restart_period::Int32 - minimize_with_propagation_num_decisions::Int32 variable_activity_decay::Float64 max_variable_activity_value::Float64 glucose_max_decay::Float64 @@ -86,6 +82,7 @@ struct SatParameters <: var"##AbstractSatParameters" strategy_change_increase_ratio::Float64 max_time_in_seconds::Float64 max_deterministic_time::Float64 + max_num_deterministic_batches::Int32 max_number_of_conflicts::Int64 max_memory_in_mb::Int64 absolute_gap_limit::Float64 @@ -95,8 +92,6 @@ struct SatParameters <: var"##AbstractSatParameters" permute_presolve_constraint_order::Bool use_absl_random::Bool log_search_progress::Bool - log_frequency_in_seconds::Float64 - model_reduction_log_frequency_in_seconds::Float64 log_subsolver_statistics::Bool log_prefix::String log_to_stdout::Bool @@ -105,6 +100,7 @@ struct SatParameters <: var"##AbstractSatParameters" minimize_reduction_during_pb_resolution::Bool count_assumption_levels_in_lbd::Bool presolve_bve_threshold::Int32 + filter_sat_postsolve_clauses::Bool presolve_bve_clause_weight::Int32 probing_deterministic_time_limit::Float64 presolve_probing_deterministic_time_limit::Float64 @@ -115,11 +111,15 @@ struct SatParameters <: var"##AbstractSatParameters" cp_model_presolve::Bool cp_model_probing_level::Int32 cp_model_use_sat_presolve::Bool - use_sat_inprocessing::Bool + remove_fixed_variables_early::Bool detect_table_with_cost::Bool table_compression_level::Int32 expand_alldiff_constraints::Bool + max_alldiff_domain_size::Int32 expand_reservoir_constraints::Bool + expand_reservoir_using_circuit::Bool + encode_cumulative_as_reservoir::Bool + max_lin_max_size_for_expansion::Int32 disable_constraint_expansion::Bool encode_complex_linear_constraint_with_integer::Bool merge_no_overlap_work_limit::Float64 @@ -130,23 +130,34 @@ struct SatParameters <: var"##AbstractSatParameters" ignore_names::Bool infer_all_diffs::Bool find_big_linear_overlap::Bool + use_sat_inprocessing::Bool + inprocessing_dtime_ratio::Float64 + inprocessing_probing_dtime::Float64 + inprocessing_minimization_dtime::Float64 + inprocessing_minimization_use_conflict_analysis::Bool + inprocessing_minimization_use_all_orderings::Bool num_workers::Int32 num_search_workers::Int32 - min_num_lns_workers::Int32 + num_full_subsolvers::Int32 subsolvers::Vector{String} extra_subsolvers::Vector{String} ignore_subsolvers::Vector{String} - subsolver_params::Vector{<:SatParameters} + filter_subsolvers::Vector{String} + subsolver_params::Vector{SatParameters} interleave_search::Bool interleave_batch_size::Int32 share_objective_bounds::Bool share_level_zero_bounds::Bool share_binary_clauses::Bool + share_glue_clauses::Bool + minimize_shared_clauses::Bool + share_glue_clauses_dtime::Float64 debug_postsolve_with_full_solver::Bool debug_max_num_presolve_operations::Int32 debug_crash_on_bad_hint::Bool + debug_crash_if_presolve_breaks_hint::Bool use_optimization_hints::Bool - minimize_core::Bool + core_minimization_level::Int32 find_multiple_cores::Bool cover_optimization::Bool max_sat_assumption_order::var"SatParameters.MaxSatAssumptionOrder".T @@ -156,72 +167,72 @@ struct SatParameters <: var"##AbstractSatParameters" use_precedences_in_disjunctive_constraint::Bool max_size_to_create_precedence_literals_in_disjunctive::Int32 use_strong_propagation_in_disjunctive::Bool + use_dynamic_precedence_in_disjunctive::Bool + use_dynamic_precedence_in_cumulative::Bool use_overload_checker_in_cumulative::Bool + use_conservative_scale_overload_checker::Bool use_timetable_edge_finding_in_cumulative::Bool + max_num_intervals_for_timetable_edge_finding::Int32 use_hard_precedences_in_cumulative::Bool exploit_all_precedences::Bool use_disjunctive_constraint_in_cumulative::Bool + no_overlap_2d_boolean_relations_limit::Int32 use_timetabling_in_no_overlap_2d::Bool use_energetic_reasoning_in_no_overlap_2d::Bool - use_pairwise_reasoning_in_no_overlap_2d::Bool + use_area_energetic_reasoning_in_no_overlap_2d::Bool + use_try_edge_reasoning_in_no_overlap_2d::Bool + max_pairs_pairwise_reasoning_in_no_overlap_2d::Int32 + maximum_regions_to_split_in_disconnected_no_overlap_2d::Int32 + use_linear3_for_no_overlap_2d_precedences::Bool use_dual_scheduling_heuristics::Bool - linearization_level::Int32 - boolean_encoding_level::Int32 - max_domain_size_when_encoding_eq_neq_constraints::Int32 - max_num_cuts::Int32 - cut_level::Int32 - only_add_cuts_at_level_zero::Bool - add_objective_cut::Bool - add_cg_cuts::Bool - add_mir_cuts::Bool - add_zero_half_cuts::Bool - add_clique_cuts::Bool - max_all_diff_cut_size::Int32 - add_lin_max_cuts::Bool - max_integer_rounding_scaling::Int32 - add_lp_constraints_lazily::Bool - root_lp_iterations::Int32 - min_orthogonality_for_lp_constraints::Float64 - max_cut_rounds_at_level_zero::Int32 - max_consecutive_inactive_count::Int32 - cut_max_active_count_value::Float64 - cut_active_count_decay::Float64 - cut_cleanup_target::Int32 - new_constraints_batch_size::Int32 + use_all_different_for_circuit::Bool + routing_cut_subset_size_for_binary_relation_bound::Int32 + routing_cut_subset_size_for_tight_binary_relation_bound::Int32 + routing_cut_subset_size_for_exact_binary_relation_bound::Int32 + routing_cut_subset_size_for_shortest_paths_bound::Int32 + routing_cut_dp_effort::Float64 + routing_cut_max_infeasible_path_length::Int32 search_branching::var"SatParameters.SearchBranching".T hint_conflict_limit::Int32 repair_hint::Bool fix_variables_to_their_hinted_value::Bool - exploit_integer_lp_solution::Bool - exploit_all_lp_solution::Bool - exploit_best_solution::Bool - exploit_relaxation_solution::Bool - exploit_objective::Bool - probing_period_at_root::Int64 use_probing_search::Bool + use_extended_probing::Bool + probing_num_combinations_limit::Int32 shaving_deterministic_time_in_probing_search::Float64 shaving_search_deterministic_time::Float64 + shaving_search_threshold::Int64 use_objective_lb_search::Bool use_objective_shaving_search::Bool + variables_shaving_level::Int32 pseudo_cost_reliability_threshold::Int64 optimize_with_core::Bool optimize_with_lb_tree_search::Bool + save_lp_basis_in_lb_tree_search::Bool binary_search_num_conflicts::Int32 optimize_with_max_hs::Bool - test_feasibility_jump::Bool - feasibility_jump_max_num_values_scanned::Int64 - feasibility_jump_protect_linear_feasibility::Bool + use_feasibility_jump::Bool + use_ls_only::Bool feasibility_jump_decay::Float64 + feasibility_jump_linearization_level::Int32 + feasibility_jump_restart_factor::Int32 + feasibility_jump_batch_dtime::Float64 feasibility_jump_var_randomization_probability::Float64 feasibility_jump_var_perburbation_range_ratio::Float64 feasibility_jump_enable_restarts::Bool + feasibility_jump_max_expanded_constraint_size::Int32 num_violation_ls::Int32 violation_ls_perturbation_period::Int32 + violation_ls_compound_move_probability::Float64 shared_tree_num_workers::Int32 use_shared_tree_search::Bool - shared_tree_worker_objective_split_probability::Float64 + shared_tree_worker_min_restarts_per_subtree::Int32 + shared_tree_worker_enable_trail_sharing::Bool + shared_tree_worker_enable_phase_sharing::Bool + shared_tree_open_leaves_per_worker::Float64 shared_tree_max_nodes_per_worker::Int32 shared_tree_split_strategy::var"SatParameters.SharedTreeSplitStrategy".T + shared_tree_balance_tolerance::Int32 enumerate_all_solutions::Bool keep_all_feasible_solutions_in_presolve::Bool fill_tightened_domains_in_response::Bool @@ -231,26 +242,66 @@ struct SatParameters <: var"##AbstractSatParameters" stop_after_first_solution::Bool stop_after_presolve::Bool stop_after_root_propagation::Bool + lns_initial_difficulty::Float64 + lns_initial_deterministic_limit::Float64 + use_lns::Bool use_lns_only::Bool solution_pool_size::Int32 use_rins_lns::Bool use_feasibility_pump::Bool use_lb_relax_lns::Bool + lb_relax_num_workers_threshold::Int32 fp_rounding::var"SatParameters.FPRoundingMethod".T diversify_lns_params::Bool randomize_search::Bool - search_randomization_tolerance::Int64 + search_random_variable_pool_size::Int64 + push_all_tasks_toward_start::Bool use_optional_variables::Bool use_exact_lp_reason::Bool - use_branching_in_lp::Bool use_combined_no_overlap::Bool + at_most_one_max_expansion_size::Int32 catch_sigint_signal::Bool use_implied_bounds::Bool polish_lp_solution::Bool + lp_primal_tolerance::Float64 + lp_dual_tolerance::Float64 convert_intervals::Bool symmetry_level::Int32 + use_symmetry_in_lp::Bool + keep_symmetry_in_presolve::Bool + symmetry_detection_deterministic_time_limit::Float64 new_linear_propagation::Bool linear_split_size::Int32 + linearization_level::Int32 + boolean_encoding_level::Int32 + max_domain_size_when_encoding_eq_neq_constraints::Int32 + max_num_cuts::Int32 + cut_level::Int32 + only_add_cuts_at_level_zero::Bool + add_objective_cut::Bool + add_cg_cuts::Bool + add_mir_cuts::Bool + add_zero_half_cuts::Bool + add_clique_cuts::Bool + add_rlt_cuts::Bool + max_all_diff_cut_size::Int32 + add_lin_max_cuts::Bool + max_integer_rounding_scaling::Int32 + add_lp_constraints_lazily::Bool + root_lp_iterations::Int32 + min_orthogonality_for_lp_constraints::Float64 + max_cut_rounds_at_level_zero::Int32 + max_consecutive_inactive_count::Int32 + cut_max_active_count_value::Float64 + cut_active_count_decay::Float64 + cut_cleanup_target::Int32 + new_constraints_batch_size::Int32 + exploit_integer_lp_solution::Bool + exploit_all_lp_solution::Bool + exploit_best_solution::Bool + exploit_relaxation_solution::Bool + exploit_objective::Bool + detect_linearized_product::Bool mip_max_bound::Float64 mip_var_scaling::Float64 mip_scale_large_domain::Bool @@ -261,10 +312,12 @@ struct SatParameters <: var"##AbstractSatParameters" mip_check_precision::Float64 mip_compute_true_objective_bound::Bool mip_max_valid_magnitude::Float64 + mip_treat_high_magnitude_bounds_as_infinity::Bool mip_drop_tolerance::Float64 + mip_presolve_level::Int32 end -PB.default_values(::Type{SatParameters}) = (;name = "", preferred_variable_order = var"SatParameters.VariableOrder".IN_ORDER, initial_polarity = var"SatParameters.Polarity".POLARITY_FALSE, use_phase_saving = true, polarity_rephase_increment = Int32(1000), random_polarity_ratio = Float64(0.0), random_branches_ratio = Float64(0.0), use_erwa_heuristic = false, initial_variables_activity = Float64(0.0), also_bump_variables_in_conflict_reasons = false, minimization_algorithm = var"SatParameters.ConflictMinimizationAlgorithm".RECURSIVE, binary_minimization_algorithm = var"SatParameters.BinaryMinizationAlgorithm".BINARY_MINIMIZATION_FIRST, subsumption_during_conflict_analysis = true, clause_cleanup_period = Int32(10000), clause_cleanup_target = Int32(0), clause_cleanup_ratio = Float64(0.5), clause_cleanup_protection = var"SatParameters.ClauseProtection".PROTECTION_NONE, clause_cleanup_lbd_bound = Int32(5), clause_cleanup_ordering = var"SatParameters.ClauseOrdering".CLAUSE_ACTIVITY, pb_cleanup_increment = Int32(200), pb_cleanup_ratio = Float64(0.5), minimize_with_propagation_restart_period = Int32(10), minimize_with_propagation_num_decisions = Int32(1000), variable_activity_decay = Float64(0.8), max_variable_activity_value = Float64(1e100), glucose_max_decay = Float64(0.95), glucose_decay_increment = Float64(0.01), glucose_decay_increment_period = Int32(5000), clause_activity_decay = Float64(0.999), max_clause_activity_value = Float64(1e20), restart_algorithms = Vector{var"SatParameters.RestartAlgorithm".T}(), default_restart_algorithms = "LUBY_RESTART,LBD_MOVING_AVERAGE_RESTART,DL_MOVING_AVERAGE_RESTART", restart_period = Int32(50), restart_running_window_size = Int32(50), restart_dl_average_ratio = Float64(1.0), restart_lbd_average_ratio = Float64(1.0), use_blocking_restart = false, blocking_restart_window_size = Int32(5000), blocking_restart_multiplier = Float64(1.4), num_conflicts_before_strategy_changes = Int32(0), strategy_change_increase_ratio = Float64(0.0), max_time_in_seconds = Float64(Inf), max_deterministic_time = Float64(Inf), max_number_of_conflicts = Int64(9223372036854775807), max_memory_in_mb = Int64(10000), absolute_gap_limit = Float64(1e-4), relative_gap_limit = Float64(0.0), random_seed = Int32(1), permute_variable_randomly = false, permute_presolve_constraint_order = false, use_absl_random = false, log_search_progress = false, log_frequency_in_seconds = Float64(-1), model_reduction_log_frequency_in_seconds = Float64(5), log_subsolver_statistics = false, log_prefix = "", log_to_stdout = true, log_to_response = false, use_pb_resolution = false, minimize_reduction_during_pb_resolution = false, count_assumption_levels_in_lbd = true, presolve_bve_threshold = Int32(500), presolve_bve_clause_weight = Int32(3), probing_deterministic_time_limit = Float64(1), presolve_probing_deterministic_time_limit = Float64(30.0), presolve_blocked_clause = true, presolve_use_bva = true, presolve_bva_threshold = Int32(1), max_presolve_iterations = Int32(3), cp_model_presolve = true, cp_model_probing_level = Int32(2), cp_model_use_sat_presolve = true, use_sat_inprocessing = false, detect_table_with_cost = false, table_compression_level = Int32(2), expand_alldiff_constraints = false, expand_reservoir_constraints = true, disable_constraint_expansion = false, encode_complex_linear_constraint_with_integer = false, merge_no_overlap_work_limit = Float64(1e12), merge_at_most_one_work_limit = Float64(1e8), presolve_substitution_level = Int32(1), presolve_extract_integer_enforcement = false, presolve_inclusion_work_limit = Int64(100000000), ignore_names = true, infer_all_diffs = true, find_big_linear_overlap = true, num_workers = Int32(0), num_search_workers = Int32(0), min_num_lns_workers = Int32(2), subsolvers = Vector{String}(), extra_subsolvers = Vector{String}(), ignore_subsolvers = Vector{String}(), subsolver_params = Vector{SatParameters}(), interleave_search = false, interleave_batch_size = Int32(0), share_objective_bounds = true, share_level_zero_bounds = true, share_binary_clauses = true, debug_postsolve_with_full_solver = false, debug_max_num_presolve_operations = Int32(0), debug_crash_on_bad_hint = false, use_optimization_hints = true, minimize_core = true, find_multiple_cores = true, cover_optimization = true, max_sat_assumption_order = var"SatParameters.MaxSatAssumptionOrder".DEFAULT_ASSUMPTION_ORDER, max_sat_reverse_assumption_order = false, max_sat_stratification = var"SatParameters.MaxSatStratificationAlgorithm".STRATIFICATION_DESCENT, propagation_loop_detection_factor = Float64(10.0), use_precedences_in_disjunctive_constraint = true, max_size_to_create_precedence_literals_in_disjunctive = Int32(60), use_strong_propagation_in_disjunctive = false, use_overload_checker_in_cumulative = false, use_timetable_edge_finding_in_cumulative = false, use_hard_precedences_in_cumulative = false, exploit_all_precedences = false, use_disjunctive_constraint_in_cumulative = true, use_timetabling_in_no_overlap_2d = false, use_energetic_reasoning_in_no_overlap_2d = false, use_pairwise_reasoning_in_no_overlap_2d = false, use_dual_scheduling_heuristics = true, linearization_level = Int32(1), boolean_encoding_level = Int32(1), max_domain_size_when_encoding_eq_neq_constraints = Int32(16), max_num_cuts = Int32(10000), cut_level = Int32(1), only_add_cuts_at_level_zero = false, add_objective_cut = false, add_cg_cuts = true, add_mir_cuts = true, add_zero_half_cuts = true, add_clique_cuts = true, max_all_diff_cut_size = Int32(64), add_lin_max_cuts = true, max_integer_rounding_scaling = Int32(600), add_lp_constraints_lazily = true, root_lp_iterations = Int32(2000), min_orthogonality_for_lp_constraints = Float64(0.05), max_cut_rounds_at_level_zero = Int32(1), max_consecutive_inactive_count = Int32(100), cut_max_active_count_value = Float64(1e10), cut_active_count_decay = Float64(0.8), cut_cleanup_target = Int32(1000), new_constraints_batch_size = Int32(50), search_branching = var"SatParameters.SearchBranching".AUTOMATIC_SEARCH, hint_conflict_limit = Int32(10), repair_hint = false, fix_variables_to_their_hinted_value = false, exploit_integer_lp_solution = true, exploit_all_lp_solution = true, exploit_best_solution = false, exploit_relaxation_solution = false, exploit_objective = true, probing_period_at_root = Int64(0), use_probing_search = false, shaving_deterministic_time_in_probing_search = true, shaving_search_deterministic_time = Float64(0.001), use_objective_lb_search = false, use_objective_shaving_search = false, pseudo_cost_reliability_threshold = Int64(100), optimize_with_core = false, optimize_with_lb_tree_search = false, binary_search_num_conflicts = Int32(-1), optimize_with_max_hs = false, test_feasibility_jump = false, feasibility_jump_max_num_values_scanned = Int64(4096), feasibility_jump_protect_linear_feasibility = true, feasibility_jump_decay = Float64(1.0), feasibility_jump_var_randomization_probability = Float64(0.0), feasibility_jump_var_perburbation_range_ratio = Float64(0.2), feasibility_jump_enable_restarts = true, num_violation_ls = Int32(0), violation_ls_perturbation_period = Int32(100), shared_tree_num_workers = Int32(0), use_shared_tree_search = false, shared_tree_worker_objective_split_probability = Float64(0.5), shared_tree_max_nodes_per_worker = Int32(128), shared_tree_split_strategy = var"SatParameters.SharedTreeSplitStrategy".SPLIT_STRATEGY_AUTO, enumerate_all_solutions = false, keep_all_feasible_solutions_in_presolve = false, fill_tightened_domains_in_response = false, fill_additional_solutions_in_response = false, instantiate_all_variables = true, auto_detect_greater_than_at_least_one_of = true, stop_after_first_solution = false, stop_after_presolve = false, stop_after_root_propagation = false, use_lns_only = false, solution_pool_size = Int32(3), use_rins_lns = true, use_feasibility_pump = true, use_lb_relax_lns = false, fp_rounding = var"SatParameters.FPRoundingMethod".PROPAGATION_ASSISTED, diversify_lns_params = false, randomize_search = false, search_randomization_tolerance = Int64(0), use_optional_variables = false, use_exact_lp_reason = true, use_branching_in_lp = false, use_combined_no_overlap = false, catch_sigint_signal = true, use_implied_bounds = true, polish_lp_solution = false, convert_intervals = true, symmetry_level = Int32(2), new_linear_propagation = false, linear_split_size = Int32(100), mip_max_bound = Float64(1e7), mip_var_scaling = Float64(1.0), mip_scale_large_domain = false, mip_automatically_scale_variables = true, only_solve_ip = false, mip_wanted_precision = Float64(1e-6), mip_max_activity_exponent = Int32(53), mip_check_precision = Float64(1e-4), mip_compute_true_objective_bound = true, mip_max_valid_magnitude = Float64(1e30), mip_drop_tolerance = Float64(1e-16)) -PB.field_numbers(::Type{SatParameters}) = (;name = 171, preferred_variable_order = 1, initial_polarity = 2, use_phase_saving = 44, polarity_rephase_increment = 168, random_polarity_ratio = 45, random_branches_ratio = 32, use_erwa_heuristic = 75, initial_variables_activity = 76, also_bump_variables_in_conflict_reasons = 77, minimization_algorithm = 4, binary_minimization_algorithm = 34, subsumption_during_conflict_analysis = 56, clause_cleanup_period = 11, clause_cleanup_target = 13, clause_cleanup_ratio = 190, clause_cleanup_protection = 58, clause_cleanup_lbd_bound = 59, clause_cleanup_ordering = 60, pb_cleanup_increment = 46, pb_cleanup_ratio = 47, minimize_with_propagation_restart_period = 96, minimize_with_propagation_num_decisions = 97, variable_activity_decay = 15, max_variable_activity_value = 16, glucose_max_decay = 22, glucose_decay_increment = 23, glucose_decay_increment_period = 24, clause_activity_decay = 17, max_clause_activity_value = 18, restart_algorithms = 61, default_restart_algorithms = 70, restart_period = 30, restart_running_window_size = 62, restart_dl_average_ratio = 63, restart_lbd_average_ratio = 71, use_blocking_restart = 64, blocking_restart_window_size = 65, blocking_restart_multiplier = 66, num_conflicts_before_strategy_changes = 68, strategy_change_increase_ratio = 69, max_time_in_seconds = 36, max_deterministic_time = 67, max_number_of_conflicts = 37, max_memory_in_mb = 40, absolute_gap_limit = 159, relative_gap_limit = 160, random_seed = 31, permute_variable_randomly = 178, permute_presolve_constraint_order = 179, use_absl_random = 180, log_search_progress = 41, log_frequency_in_seconds = 212, model_reduction_log_frequency_in_seconds = 218, log_subsolver_statistics = 189, log_prefix = 185, log_to_stdout = 186, log_to_response = 187, use_pb_resolution = 43, minimize_reduction_during_pb_resolution = 48, count_assumption_levels_in_lbd = 49, presolve_bve_threshold = 54, presolve_bve_clause_weight = 55, probing_deterministic_time_limit = 226, presolve_probing_deterministic_time_limit = 57, presolve_blocked_clause = 88, presolve_use_bva = 72, presolve_bva_threshold = 73, max_presolve_iterations = 138, cp_model_presolve = 86, cp_model_probing_level = 110, cp_model_use_sat_presolve = 93, use_sat_inprocessing = 163, detect_table_with_cost = 216, table_compression_level = 217, expand_alldiff_constraints = 170, expand_reservoir_constraints = 182, disable_constraint_expansion = 181, encode_complex_linear_constraint_with_integer = 223, merge_no_overlap_work_limit = 145, merge_at_most_one_work_limit = 146, presolve_substitution_level = 147, presolve_extract_integer_enforcement = 174, presolve_inclusion_work_limit = 201, ignore_names = 202, infer_all_diffs = 233, find_big_linear_overlap = 234, num_workers = 206, num_search_workers = 100, min_num_lns_workers = 211, subsolvers = 207, extra_subsolvers = 219, ignore_subsolvers = 209, subsolver_params = 210, interleave_search = 136, interleave_batch_size = 134, share_objective_bounds = 113, share_level_zero_bounds = 114, share_binary_clauses = 203, debug_postsolve_with_full_solver = 162, debug_max_num_presolve_operations = 151, debug_crash_on_bad_hint = 195, use_optimization_hints = 35, minimize_core = 50, find_multiple_cores = 84, cover_optimization = 89, max_sat_assumption_order = 51, max_sat_reverse_assumption_order = 52, max_sat_stratification = 53, propagation_loop_detection_factor = 221, use_precedences_in_disjunctive_constraint = 74, max_size_to_create_precedence_literals_in_disjunctive = 229, use_strong_propagation_in_disjunctive = 230, use_overload_checker_in_cumulative = 78, use_timetable_edge_finding_in_cumulative = 79, use_hard_precedences_in_cumulative = 215, exploit_all_precedences = 220, use_disjunctive_constraint_in_cumulative = 80, use_timetabling_in_no_overlap_2d = 200, use_energetic_reasoning_in_no_overlap_2d = 213, use_pairwise_reasoning_in_no_overlap_2d = 251, use_dual_scheduling_heuristics = 214, linearization_level = 90, boolean_encoding_level = 107, max_domain_size_when_encoding_eq_neq_constraints = 191, max_num_cuts = 91, cut_level = 196, only_add_cuts_at_level_zero = 92, add_objective_cut = 197, add_cg_cuts = 117, add_mir_cuts = 120, add_zero_half_cuts = 169, add_clique_cuts = 172, max_all_diff_cut_size = 148, add_lin_max_cuts = 152, max_integer_rounding_scaling = 119, add_lp_constraints_lazily = 112, root_lp_iterations = 227, min_orthogonality_for_lp_constraints = 115, max_cut_rounds_at_level_zero = 154, max_consecutive_inactive_count = 121, cut_max_active_count_value = 155, cut_active_count_decay = 156, cut_cleanup_target = 157, new_constraints_batch_size = 122, search_branching = 82, hint_conflict_limit = 153, repair_hint = 167, fix_variables_to_their_hinted_value = 192, exploit_integer_lp_solution = 94, exploit_all_lp_solution = 116, exploit_best_solution = 130, exploit_relaxation_solution = 161, exploit_objective = 131, probing_period_at_root = 142, use_probing_search = 176, shaving_deterministic_time_in_probing_search = 204, shaving_search_deterministic_time = 205, use_objective_lb_search = 228, use_objective_shaving_search = 253, pseudo_cost_reliability_threshold = 123, optimize_with_core = 83, optimize_with_lb_tree_search = 188, binary_search_num_conflicts = 99, optimize_with_max_hs = 85, test_feasibility_jump = 240, feasibility_jump_max_num_values_scanned = 245, feasibility_jump_protect_linear_feasibility = 246, feasibility_jump_decay = 242, feasibility_jump_var_randomization_probability = 247, feasibility_jump_var_perburbation_range_ratio = 248, feasibility_jump_enable_restarts = 250, num_violation_ls = 244, violation_ls_perturbation_period = 249, shared_tree_num_workers = 235, use_shared_tree_search = 236, shared_tree_worker_objective_split_probability = 237, shared_tree_max_nodes_per_worker = 238, shared_tree_split_strategy = 239, enumerate_all_solutions = 87, keep_all_feasible_solutions_in_presolve = 173, fill_tightened_domains_in_response = 132, fill_additional_solutions_in_response = 194, instantiate_all_variables = 106, auto_detect_greater_than_at_least_one_of = 95, stop_after_first_solution = 98, stop_after_presolve = 149, stop_after_root_propagation = 252, use_lns_only = 101, solution_pool_size = 193, use_rins_lns = 129, use_feasibility_pump = 164, use_lb_relax_lns = 255, fp_rounding = 165, diversify_lns_params = 137, randomize_search = 103, search_randomization_tolerance = 104, use_optional_variables = 108, use_exact_lp_reason = 109, use_branching_in_lp = 139, use_combined_no_overlap = 133, catch_sigint_signal = 135, use_implied_bounds = 144, polish_lp_solution = 175, convert_intervals = 177, symmetry_level = 183, new_linear_propagation = 224, linear_split_size = 256, mip_max_bound = 124, mip_var_scaling = 125, mip_scale_large_domain = 225, mip_automatically_scale_variables = 166, only_solve_ip = 222, mip_wanted_precision = 126, mip_max_activity_exponent = 127, mip_check_precision = 128, mip_compute_true_objective_bound = 198, mip_max_valid_magnitude = 199, mip_drop_tolerance = 232) +PB.default_values(::Type{SatParameters}) = (;name = "", preferred_variable_order = var"SatParameters.VariableOrder".IN_ORDER, initial_polarity = var"SatParameters.Polarity".POLARITY_FALSE, use_phase_saving = true, polarity_rephase_increment = Int32(1000), polarity_exploit_ls_hints = false, random_polarity_ratio = Float64(0.0), random_branches_ratio = Float64(0.0), use_erwa_heuristic = false, initial_variables_activity = Float64(0.0), also_bump_variables_in_conflict_reasons = false, minimization_algorithm = var"SatParameters.ConflictMinimizationAlgorithm".RECURSIVE, binary_minimization_algorithm = var"SatParameters.BinaryMinizationAlgorithm".BINARY_MINIMIZATION_FIRST, subsumption_during_conflict_analysis = true, clause_cleanup_period = Int32(10000), clause_cleanup_target = Int32(0), clause_cleanup_ratio = Float64(0.5), clause_cleanup_protection = var"SatParameters.ClauseProtection".PROTECTION_NONE, clause_cleanup_lbd_bound = Int32(5), clause_cleanup_ordering = var"SatParameters.ClauseOrdering".CLAUSE_ACTIVITY, pb_cleanup_increment = Int32(200), pb_cleanup_ratio = Float64(0.5), variable_activity_decay = Float64(0.8), max_variable_activity_value = Float64(1e100), glucose_max_decay = Float64(0.95), glucose_decay_increment = Float64(0.01), glucose_decay_increment_period = Int32(5000), clause_activity_decay = Float64(0.999), max_clause_activity_value = Float64(1e20), restart_algorithms = Vector{var"SatParameters.RestartAlgorithm".T}(), default_restart_algorithms = "LUBY_RESTART,LBD_MOVING_AVERAGE_RESTART,DL_MOVING_AVERAGE_RESTART", restart_period = Int32(50), restart_running_window_size = Int32(50), restart_dl_average_ratio = Float64(1.0), restart_lbd_average_ratio = Float64(1.0), use_blocking_restart = false, blocking_restart_window_size = Int32(5000), blocking_restart_multiplier = Float64(1.4), num_conflicts_before_strategy_changes = Int32(0), strategy_change_increase_ratio = Float64(0.0), max_time_in_seconds = Float64(Inf), max_deterministic_time = Float64(Inf), max_num_deterministic_batches = Int32(0), max_number_of_conflicts = Int64(9223372036854775807), max_memory_in_mb = Int64(10000), absolute_gap_limit = Float64(1e-4), relative_gap_limit = Float64(0.0), random_seed = Int32(1), permute_variable_randomly = false, permute_presolve_constraint_order = false, use_absl_random = false, log_search_progress = false, log_subsolver_statistics = false, log_prefix = "", log_to_stdout = true, log_to_response = false, use_pb_resolution = false, minimize_reduction_during_pb_resolution = false, count_assumption_levels_in_lbd = true, presolve_bve_threshold = Int32(500), filter_sat_postsolve_clauses = false, presolve_bve_clause_weight = Int32(3), probing_deterministic_time_limit = Float64(1.0), presolve_probing_deterministic_time_limit = Float64(30.0), presolve_blocked_clause = true, presolve_use_bva = true, presolve_bva_threshold = Int32(1), max_presolve_iterations = Int32(3), cp_model_presolve = true, cp_model_probing_level = Int32(2), cp_model_use_sat_presolve = true, remove_fixed_variables_early = true, detect_table_with_cost = false, table_compression_level = Int32(2), expand_alldiff_constraints = false, max_alldiff_domain_size = Int32(256), expand_reservoir_constraints = true, expand_reservoir_using_circuit = false, encode_cumulative_as_reservoir = false, max_lin_max_size_for_expansion = Int32(0), disable_constraint_expansion = false, encode_complex_linear_constraint_with_integer = false, merge_no_overlap_work_limit = Float64(1e12), merge_at_most_one_work_limit = Float64(1e8), presolve_substitution_level = Int32(1), presolve_extract_integer_enforcement = false, presolve_inclusion_work_limit = Int64(100000000), ignore_names = true, infer_all_diffs = true, find_big_linear_overlap = true, use_sat_inprocessing = true, inprocessing_dtime_ratio = Float64(0.2), inprocessing_probing_dtime = Float64(1.0), inprocessing_minimization_dtime = Float64(1.0), inprocessing_minimization_use_conflict_analysis = true, inprocessing_minimization_use_all_orderings = false, num_workers = Int32(0), num_search_workers = Int32(0), num_full_subsolvers = Int32(0), subsolvers = Vector{String}(), extra_subsolvers = Vector{String}(), ignore_subsolvers = Vector{String}(), filter_subsolvers = Vector{String}(), subsolver_params = Vector{SatParameters}(), interleave_search = false, interleave_batch_size = Int32(0), share_objective_bounds = true, share_level_zero_bounds = true, share_binary_clauses = true, share_glue_clauses = false, minimize_shared_clauses = true, share_glue_clauses_dtime = Float64(1.0), debug_postsolve_with_full_solver = false, debug_max_num_presolve_operations = Int32(0), debug_crash_on_bad_hint = false, debug_crash_if_presolve_breaks_hint = false, use_optimization_hints = true, core_minimization_level = Int32(2), find_multiple_cores = true, cover_optimization = true, max_sat_assumption_order = var"SatParameters.MaxSatAssumptionOrder".DEFAULT_ASSUMPTION_ORDER, max_sat_reverse_assumption_order = false, max_sat_stratification = var"SatParameters.MaxSatStratificationAlgorithm".STRATIFICATION_DESCENT, propagation_loop_detection_factor = Float64(10.0), use_precedences_in_disjunctive_constraint = true, max_size_to_create_precedence_literals_in_disjunctive = Int32(60), use_strong_propagation_in_disjunctive = false, use_dynamic_precedence_in_disjunctive = false, use_dynamic_precedence_in_cumulative = false, use_overload_checker_in_cumulative = false, use_conservative_scale_overload_checker = false, use_timetable_edge_finding_in_cumulative = false, max_num_intervals_for_timetable_edge_finding = Int32(100), use_hard_precedences_in_cumulative = false, exploit_all_precedences = false, use_disjunctive_constraint_in_cumulative = true, no_overlap_2d_boolean_relations_limit = Int32(10), use_timetabling_in_no_overlap_2d = false, use_energetic_reasoning_in_no_overlap_2d = false, use_area_energetic_reasoning_in_no_overlap_2d = false, use_try_edge_reasoning_in_no_overlap_2d = false, max_pairs_pairwise_reasoning_in_no_overlap_2d = Int32(1250), maximum_regions_to_split_in_disconnected_no_overlap_2d = Int32(0), use_linear3_for_no_overlap_2d_precedences = true, use_dual_scheduling_heuristics = true, use_all_different_for_circuit = false, routing_cut_subset_size_for_binary_relation_bound = Int32(0), routing_cut_subset_size_for_tight_binary_relation_bound = Int32(0), routing_cut_subset_size_for_exact_binary_relation_bound = Int32(8), routing_cut_subset_size_for_shortest_paths_bound = Int32(8), routing_cut_dp_effort = Float64(1e7), routing_cut_max_infeasible_path_length = Int32(6), search_branching = var"SatParameters.SearchBranching".AUTOMATIC_SEARCH, hint_conflict_limit = Int32(10), repair_hint = false, fix_variables_to_their_hinted_value = false, use_probing_search = false, use_extended_probing = true, probing_num_combinations_limit = Int32(20000), shaving_deterministic_time_in_probing_search = Float64(0.001), shaving_search_deterministic_time = Float64(0.1), shaving_search_threshold = Int64(64), use_objective_lb_search = false, use_objective_shaving_search = false, variables_shaving_level = Int32(-1), pseudo_cost_reliability_threshold = Int64(100), optimize_with_core = false, optimize_with_lb_tree_search = false, save_lp_basis_in_lb_tree_search = false, binary_search_num_conflicts = Int32(-1), optimize_with_max_hs = false, use_feasibility_jump = true, use_ls_only = false, feasibility_jump_decay = Float64(0.95), feasibility_jump_linearization_level = Int32(2), feasibility_jump_restart_factor = Int32(1), feasibility_jump_batch_dtime = Float64(0.1), feasibility_jump_var_randomization_probability = Float64(0.05), feasibility_jump_var_perburbation_range_ratio = Float64(0.2), feasibility_jump_enable_restarts = true, feasibility_jump_max_expanded_constraint_size = Int32(500), num_violation_ls = Int32(0), violation_ls_perturbation_period = Int32(100), violation_ls_compound_move_probability = Float64(0.5), shared_tree_num_workers = Int32(0), use_shared_tree_search = false, shared_tree_worker_min_restarts_per_subtree = Int32(1), shared_tree_worker_enable_trail_sharing = true, shared_tree_worker_enable_phase_sharing = true, shared_tree_open_leaves_per_worker = Float64(2.0), shared_tree_max_nodes_per_worker = Int32(10000), shared_tree_split_strategy = var"SatParameters.SharedTreeSplitStrategy".SPLIT_STRATEGY_AUTO, shared_tree_balance_tolerance = Int32(1), enumerate_all_solutions = false, keep_all_feasible_solutions_in_presolve = false, fill_tightened_domains_in_response = false, fill_additional_solutions_in_response = false, instantiate_all_variables = true, auto_detect_greater_than_at_least_one_of = true, stop_after_first_solution = false, stop_after_presolve = false, stop_after_root_propagation = false, lns_initial_difficulty = Float64(0.5), lns_initial_deterministic_limit = Float64(0.1), use_lns = true, use_lns_only = false, solution_pool_size = Int32(3), use_rins_lns = true, use_feasibility_pump = true, use_lb_relax_lns = true, lb_relax_num_workers_threshold = Int32(16), fp_rounding = var"SatParameters.FPRoundingMethod".PROPAGATION_ASSISTED, diversify_lns_params = false, randomize_search = false, search_random_variable_pool_size = Int64(0), push_all_tasks_toward_start = false, use_optional_variables = false, use_exact_lp_reason = true, use_combined_no_overlap = false, at_most_one_max_expansion_size = Int32(3), catch_sigint_signal = true, use_implied_bounds = true, polish_lp_solution = false, lp_primal_tolerance = Float64(1e-7), lp_dual_tolerance = Float64(1e-7), convert_intervals = true, symmetry_level = Int32(2), use_symmetry_in_lp = false, keep_symmetry_in_presolve = false, symmetry_detection_deterministic_time_limit = Float64(1.0), new_linear_propagation = true, linear_split_size = Int32(100), linearization_level = Int32(1), boolean_encoding_level = Int32(1), max_domain_size_when_encoding_eq_neq_constraints = Int32(16), max_num_cuts = Int32(10000), cut_level = Int32(1), only_add_cuts_at_level_zero = false, add_objective_cut = false, add_cg_cuts = true, add_mir_cuts = true, add_zero_half_cuts = true, add_clique_cuts = true, add_rlt_cuts = true, max_all_diff_cut_size = Int32(64), add_lin_max_cuts = true, max_integer_rounding_scaling = Int32(600), add_lp_constraints_lazily = true, root_lp_iterations = Int32(2000), min_orthogonality_for_lp_constraints = Float64(0.05), max_cut_rounds_at_level_zero = Int32(1), max_consecutive_inactive_count = Int32(100), cut_max_active_count_value = Float64(1e10), cut_active_count_decay = Float64(0.8), cut_cleanup_target = Int32(1000), new_constraints_batch_size = Int32(50), exploit_integer_lp_solution = true, exploit_all_lp_solution = true, exploit_best_solution = false, exploit_relaxation_solution = false, exploit_objective = true, detect_linearized_product = false, mip_max_bound = Float64(1e7), mip_var_scaling = Float64(1.0), mip_scale_large_domain = false, mip_automatically_scale_variables = true, only_solve_ip = false, mip_wanted_precision = Float64(1e-6), mip_max_activity_exponent = Int32(53), mip_check_precision = Float64(1e-4), mip_compute_true_objective_bound = true, mip_max_valid_magnitude = Float64(1e20), mip_treat_high_magnitude_bounds_as_infinity = false, mip_drop_tolerance = Float64(1e-16), mip_presolve_level = Int32(2)) +PB.field_numbers(::Type{SatParameters}) = (;name = 171, preferred_variable_order = 1, initial_polarity = 2, use_phase_saving = 44, polarity_rephase_increment = 168, polarity_exploit_ls_hints = 309, random_polarity_ratio = 45, random_branches_ratio = 32, use_erwa_heuristic = 75, initial_variables_activity = 76, also_bump_variables_in_conflict_reasons = 77, minimization_algorithm = 4, binary_minimization_algorithm = 34, subsumption_during_conflict_analysis = 56, clause_cleanup_period = 11, clause_cleanup_target = 13, clause_cleanup_ratio = 190, clause_cleanup_protection = 58, clause_cleanup_lbd_bound = 59, clause_cleanup_ordering = 60, pb_cleanup_increment = 46, pb_cleanup_ratio = 47, variable_activity_decay = 15, max_variable_activity_value = 16, glucose_max_decay = 22, glucose_decay_increment = 23, glucose_decay_increment_period = 24, clause_activity_decay = 17, max_clause_activity_value = 18, restart_algorithms = 61, default_restart_algorithms = 70, restart_period = 30, restart_running_window_size = 62, restart_dl_average_ratio = 63, restart_lbd_average_ratio = 71, use_blocking_restart = 64, blocking_restart_window_size = 65, blocking_restart_multiplier = 66, num_conflicts_before_strategy_changes = 68, strategy_change_increase_ratio = 69, max_time_in_seconds = 36, max_deterministic_time = 67, max_num_deterministic_batches = 291, max_number_of_conflicts = 37, max_memory_in_mb = 40, absolute_gap_limit = 159, relative_gap_limit = 160, random_seed = 31, permute_variable_randomly = 178, permute_presolve_constraint_order = 179, use_absl_random = 180, log_search_progress = 41, log_subsolver_statistics = 189, log_prefix = 185, log_to_stdout = 186, log_to_response = 187, use_pb_resolution = 43, minimize_reduction_during_pb_resolution = 48, count_assumption_levels_in_lbd = 49, presolve_bve_threshold = 54, filter_sat_postsolve_clauses = 324, presolve_bve_clause_weight = 55, probing_deterministic_time_limit = 226, presolve_probing_deterministic_time_limit = 57, presolve_blocked_clause = 88, presolve_use_bva = 72, presolve_bva_threshold = 73, max_presolve_iterations = 138, cp_model_presolve = 86, cp_model_probing_level = 110, cp_model_use_sat_presolve = 93, remove_fixed_variables_early = 310, detect_table_with_cost = 216, table_compression_level = 217, expand_alldiff_constraints = 170, max_alldiff_domain_size = 320, expand_reservoir_constraints = 182, expand_reservoir_using_circuit = 288, encode_cumulative_as_reservoir = 287, max_lin_max_size_for_expansion = 280, disable_constraint_expansion = 181, encode_complex_linear_constraint_with_integer = 223, merge_no_overlap_work_limit = 145, merge_at_most_one_work_limit = 146, presolve_substitution_level = 147, presolve_extract_integer_enforcement = 174, presolve_inclusion_work_limit = 201, ignore_names = 202, infer_all_diffs = 233, find_big_linear_overlap = 234, use_sat_inprocessing = 163, inprocessing_dtime_ratio = 273, inprocessing_probing_dtime = 274, inprocessing_minimization_dtime = 275, inprocessing_minimization_use_conflict_analysis = 297, inprocessing_minimization_use_all_orderings = 298, num_workers = 206, num_search_workers = 100, num_full_subsolvers = 294, subsolvers = 207, extra_subsolvers = 219, ignore_subsolvers = 209, filter_subsolvers = 293, subsolver_params = 210, interleave_search = 136, interleave_batch_size = 134, share_objective_bounds = 113, share_level_zero_bounds = 114, share_binary_clauses = 203, share_glue_clauses = 285, minimize_shared_clauses = 300, share_glue_clauses_dtime = 322, debug_postsolve_with_full_solver = 162, debug_max_num_presolve_operations = 151, debug_crash_on_bad_hint = 195, debug_crash_if_presolve_breaks_hint = 306, use_optimization_hints = 35, core_minimization_level = 50, find_multiple_cores = 84, cover_optimization = 89, max_sat_assumption_order = 51, max_sat_reverse_assumption_order = 52, max_sat_stratification = 53, propagation_loop_detection_factor = 221, use_precedences_in_disjunctive_constraint = 74, max_size_to_create_precedence_literals_in_disjunctive = 229, use_strong_propagation_in_disjunctive = 230, use_dynamic_precedence_in_disjunctive = 263, use_dynamic_precedence_in_cumulative = 268, use_overload_checker_in_cumulative = 78, use_conservative_scale_overload_checker = 286, use_timetable_edge_finding_in_cumulative = 79, max_num_intervals_for_timetable_edge_finding = 260, use_hard_precedences_in_cumulative = 215, exploit_all_precedences = 220, use_disjunctive_constraint_in_cumulative = 80, no_overlap_2d_boolean_relations_limit = 321, use_timetabling_in_no_overlap_2d = 200, use_energetic_reasoning_in_no_overlap_2d = 213, use_area_energetic_reasoning_in_no_overlap_2d = 271, use_try_edge_reasoning_in_no_overlap_2d = 299, max_pairs_pairwise_reasoning_in_no_overlap_2d = 276, maximum_regions_to_split_in_disconnected_no_overlap_2d = 315, use_linear3_for_no_overlap_2d_precedences = 323, use_dual_scheduling_heuristics = 214, use_all_different_for_circuit = 311, routing_cut_subset_size_for_binary_relation_bound = 312, routing_cut_subset_size_for_tight_binary_relation_bound = 313, routing_cut_subset_size_for_exact_binary_relation_bound = 316, routing_cut_subset_size_for_shortest_paths_bound = 318, routing_cut_dp_effort = 314, routing_cut_max_infeasible_path_length = 317, search_branching = 82, hint_conflict_limit = 153, repair_hint = 167, fix_variables_to_their_hinted_value = 192, use_probing_search = 176, use_extended_probing = 269, probing_num_combinations_limit = 272, shaving_deterministic_time_in_probing_search = 204, shaving_search_deterministic_time = 205, shaving_search_threshold = 290, use_objective_lb_search = 228, use_objective_shaving_search = 253, variables_shaving_level = 289, pseudo_cost_reliability_threshold = 123, optimize_with_core = 83, optimize_with_lb_tree_search = 188, save_lp_basis_in_lb_tree_search = 284, binary_search_num_conflicts = 99, optimize_with_max_hs = 85, use_feasibility_jump = 265, use_ls_only = 240, feasibility_jump_decay = 242, feasibility_jump_linearization_level = 257, feasibility_jump_restart_factor = 258, feasibility_jump_batch_dtime = 292, feasibility_jump_var_randomization_probability = 247, feasibility_jump_var_perburbation_range_ratio = 248, feasibility_jump_enable_restarts = 250, feasibility_jump_max_expanded_constraint_size = 264, num_violation_ls = 244, violation_ls_perturbation_period = 249, violation_ls_compound_move_probability = 259, shared_tree_num_workers = 235, use_shared_tree_search = 236, shared_tree_worker_min_restarts_per_subtree = 282, shared_tree_worker_enable_trail_sharing = 295, shared_tree_worker_enable_phase_sharing = 304, shared_tree_open_leaves_per_worker = 281, shared_tree_max_nodes_per_worker = 238, shared_tree_split_strategy = 239, shared_tree_balance_tolerance = 305, enumerate_all_solutions = 87, keep_all_feasible_solutions_in_presolve = 173, fill_tightened_domains_in_response = 132, fill_additional_solutions_in_response = 194, instantiate_all_variables = 106, auto_detect_greater_than_at_least_one_of = 95, stop_after_first_solution = 98, stop_after_presolve = 149, stop_after_root_propagation = 252, lns_initial_difficulty = 307, lns_initial_deterministic_limit = 308, use_lns = 283, use_lns_only = 101, solution_pool_size = 193, use_rins_lns = 129, use_feasibility_pump = 164, use_lb_relax_lns = 255, lb_relax_num_workers_threshold = 296, fp_rounding = 165, diversify_lns_params = 137, randomize_search = 103, search_random_variable_pool_size = 104, push_all_tasks_toward_start = 262, use_optional_variables = 108, use_exact_lp_reason = 109, use_combined_no_overlap = 133, at_most_one_max_expansion_size = 270, catch_sigint_signal = 135, use_implied_bounds = 144, polish_lp_solution = 175, lp_primal_tolerance = 266, lp_dual_tolerance = 267, convert_intervals = 177, symmetry_level = 183, use_symmetry_in_lp = 301, keep_symmetry_in_presolve = 303, symmetry_detection_deterministic_time_limit = 302, new_linear_propagation = 224, linear_split_size = 256, linearization_level = 90, boolean_encoding_level = 107, max_domain_size_when_encoding_eq_neq_constraints = 191, max_num_cuts = 91, cut_level = 196, only_add_cuts_at_level_zero = 92, add_objective_cut = 197, add_cg_cuts = 117, add_mir_cuts = 120, add_zero_half_cuts = 169, add_clique_cuts = 172, add_rlt_cuts = 279, max_all_diff_cut_size = 148, add_lin_max_cuts = 152, max_integer_rounding_scaling = 119, add_lp_constraints_lazily = 112, root_lp_iterations = 227, min_orthogonality_for_lp_constraints = 115, max_cut_rounds_at_level_zero = 154, max_consecutive_inactive_count = 121, cut_max_active_count_value = 155, cut_active_count_decay = 156, cut_cleanup_target = 157, new_constraints_batch_size = 122, exploit_integer_lp_solution = 94, exploit_all_lp_solution = 116, exploit_best_solution = 130, exploit_relaxation_solution = 161, exploit_objective = 131, detect_linearized_product = 277, mip_max_bound = 124, mip_var_scaling = 125, mip_scale_large_domain = 225, mip_automatically_scale_variables = 166, only_solve_ip = 222, mip_wanted_precision = 126, mip_max_activity_exponent = 127, mip_check_precision = 128, mip_compute_true_objective_bound = 198, mip_max_valid_magnitude = 199, mip_treat_high_magnitude_bounds_as_infinity = 278, mip_drop_tolerance = 232, mip_presolve_level = 261) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) name = "" @@ -272,6 +325,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) initial_polarity = var"SatParameters.Polarity".POLARITY_FALSE use_phase_saving = true polarity_rephase_increment = Int32(1000) + polarity_exploit_ls_hints = false random_polarity_ratio = Float64(0.0) random_branches_ratio = Float64(0.0) use_erwa_heuristic = false @@ -288,8 +342,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) clause_cleanup_ordering = var"SatParameters.ClauseOrdering".CLAUSE_ACTIVITY pb_cleanup_increment = Int32(200) pb_cleanup_ratio = Float64(0.5) - minimize_with_propagation_restart_period = Int32(10) - minimize_with_propagation_num_decisions = Int32(1000) variable_activity_decay = Float64(0.8) max_variable_activity_value = Float64(1e100) glucose_max_decay = Float64(0.95) @@ -310,6 +362,7 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) strategy_change_increase_ratio = Float64(0.0) max_time_in_seconds = Float64(Inf) max_deterministic_time = Float64(Inf) + max_num_deterministic_batches = Int32(0) max_number_of_conflicts = Int64(9223372036854775807) max_memory_in_mb = Int64(10000) absolute_gap_limit = Float64(1e-4) @@ -319,8 +372,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) permute_presolve_constraint_order = false use_absl_random = false log_search_progress = false - log_frequency_in_seconds = Float64(-1) - model_reduction_log_frequency_in_seconds = Float64(5) log_subsolver_statistics = false log_prefix = "" log_to_stdout = true @@ -329,8 +380,9 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) minimize_reduction_during_pb_resolution = false count_assumption_levels_in_lbd = true presolve_bve_threshold = Int32(500) + filter_sat_postsolve_clauses = false presolve_bve_clause_weight = Int32(3) - probing_deterministic_time_limit = Float64(1) + probing_deterministic_time_limit = Float64(1.0) presolve_probing_deterministic_time_limit = Float64(30.0) presolve_blocked_clause = true presolve_use_bva = true @@ -339,11 +391,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) cp_model_presolve = true cp_model_probing_level = Int32(2) cp_model_use_sat_presolve = true - use_sat_inprocessing = false + remove_fixed_variables_early = true detect_table_with_cost = false table_compression_level = Int32(2) expand_alldiff_constraints = false + max_alldiff_domain_size = Int32(256) expand_reservoir_constraints = true + expand_reservoir_using_circuit = false + encode_cumulative_as_reservoir = false + max_lin_max_size_for_expansion = Int32(0) disable_constraint_expansion = false encode_complex_linear_constraint_with_integer = false merge_no_overlap_work_limit = Float64(1e12) @@ -354,23 +410,34 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) ignore_names = true infer_all_diffs = true find_big_linear_overlap = true + use_sat_inprocessing = true + inprocessing_dtime_ratio = Float64(0.2) + inprocessing_probing_dtime = Float64(1.0) + inprocessing_minimization_dtime = Float64(1.0) + inprocessing_minimization_use_conflict_analysis = true + inprocessing_minimization_use_all_orderings = false num_workers = Int32(0) num_search_workers = Int32(0) - min_num_lns_workers = Int32(2) + num_full_subsolvers = Int32(0) subsolvers = PB.BufferedVector{String}() extra_subsolvers = PB.BufferedVector{String}() ignore_subsolvers = PB.BufferedVector{String}() + filter_subsolvers = PB.BufferedVector{String}() subsolver_params = PB.BufferedVector{SatParameters}() interleave_search = false interleave_batch_size = Int32(0) share_objective_bounds = true share_level_zero_bounds = true share_binary_clauses = true + share_glue_clauses = false + minimize_shared_clauses = true + share_glue_clauses_dtime = Float64(1.0) debug_postsolve_with_full_solver = false debug_max_num_presolve_operations = Int32(0) debug_crash_on_bad_hint = false + debug_crash_if_presolve_breaks_hint = false use_optimization_hints = true - minimize_core = true + core_minimization_level = Int32(2) find_multiple_cores = true cover_optimization = true max_sat_assumption_order = var"SatParameters.MaxSatAssumptionOrder".DEFAULT_ASSUMPTION_ORDER @@ -380,72 +447,72 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) use_precedences_in_disjunctive_constraint = true max_size_to_create_precedence_literals_in_disjunctive = Int32(60) use_strong_propagation_in_disjunctive = false + use_dynamic_precedence_in_disjunctive = false + use_dynamic_precedence_in_cumulative = false use_overload_checker_in_cumulative = false + use_conservative_scale_overload_checker = false use_timetable_edge_finding_in_cumulative = false + max_num_intervals_for_timetable_edge_finding = Int32(100) use_hard_precedences_in_cumulative = false exploit_all_precedences = false use_disjunctive_constraint_in_cumulative = true + no_overlap_2d_boolean_relations_limit = Int32(10) use_timetabling_in_no_overlap_2d = false use_energetic_reasoning_in_no_overlap_2d = false - use_pairwise_reasoning_in_no_overlap_2d = false + use_area_energetic_reasoning_in_no_overlap_2d = false + use_try_edge_reasoning_in_no_overlap_2d = false + max_pairs_pairwise_reasoning_in_no_overlap_2d = Int32(1250) + maximum_regions_to_split_in_disconnected_no_overlap_2d = Int32(0) + use_linear3_for_no_overlap_2d_precedences = true use_dual_scheduling_heuristics = true - linearization_level = Int32(1) - boolean_encoding_level = Int32(1) - max_domain_size_when_encoding_eq_neq_constraints = Int32(16) - max_num_cuts = Int32(10000) - cut_level = Int32(1) - only_add_cuts_at_level_zero = false - add_objective_cut = false - add_cg_cuts = true - add_mir_cuts = true - add_zero_half_cuts = true - add_clique_cuts = true - max_all_diff_cut_size = Int32(64) - add_lin_max_cuts = true - max_integer_rounding_scaling = Int32(600) - add_lp_constraints_lazily = true - root_lp_iterations = Int32(2000) - min_orthogonality_for_lp_constraints = Float64(0.05) - max_cut_rounds_at_level_zero = Int32(1) - max_consecutive_inactive_count = Int32(100) - cut_max_active_count_value = Float64(1e10) - cut_active_count_decay = Float64(0.8) - cut_cleanup_target = Int32(1000) - new_constraints_batch_size = Int32(50) + use_all_different_for_circuit = false + routing_cut_subset_size_for_binary_relation_bound = Int32(0) + routing_cut_subset_size_for_tight_binary_relation_bound = Int32(0) + routing_cut_subset_size_for_exact_binary_relation_bound = Int32(8) + routing_cut_subset_size_for_shortest_paths_bound = Int32(8) + routing_cut_dp_effort = Float64(1e7) + routing_cut_max_infeasible_path_length = Int32(6) search_branching = var"SatParameters.SearchBranching".AUTOMATIC_SEARCH hint_conflict_limit = Int32(10) repair_hint = false fix_variables_to_their_hinted_value = false - exploit_integer_lp_solution = true - exploit_all_lp_solution = true - exploit_best_solution = false - exploit_relaxation_solution = false - exploit_objective = true - probing_period_at_root = Int64(0) use_probing_search = false + use_extended_probing = true + probing_num_combinations_limit = Int32(20000) shaving_deterministic_time_in_probing_search = Float64(0.001) - shaving_search_deterministic_time = Float64(0.001) + shaving_search_deterministic_time = Float64(0.1) + shaving_search_threshold = Int64(64) use_objective_lb_search = false use_objective_shaving_search = false + variables_shaving_level = Int32(-1) pseudo_cost_reliability_threshold = Int64(100) optimize_with_core = false optimize_with_lb_tree_search = false + save_lp_basis_in_lb_tree_search = false binary_search_num_conflicts = Int32(-1) optimize_with_max_hs = false - test_feasibility_jump = false - feasibility_jump_max_num_values_scanned = Int64(4096) - feasibility_jump_protect_linear_feasibility = true - feasibility_jump_decay = Float64(1.0) - feasibility_jump_var_randomization_probability = Float64(0.0) + use_feasibility_jump = true + use_ls_only = false + feasibility_jump_decay = Float64(0.95) + feasibility_jump_linearization_level = Int32(2) + feasibility_jump_restart_factor = Int32(1) + feasibility_jump_batch_dtime = Float64(0.1) + feasibility_jump_var_randomization_probability = Float64(0.05) feasibility_jump_var_perburbation_range_ratio = Float64(0.2) feasibility_jump_enable_restarts = true + feasibility_jump_max_expanded_constraint_size = Int32(500) num_violation_ls = Int32(0) violation_ls_perturbation_period = Int32(100) + violation_ls_compound_move_probability = Float64(0.5) shared_tree_num_workers = Int32(0) use_shared_tree_search = false - shared_tree_worker_objective_split_probability = Float64(0.5) - shared_tree_max_nodes_per_worker = Int32(128) + shared_tree_worker_min_restarts_per_subtree = Int32(1) + shared_tree_worker_enable_trail_sharing = true + shared_tree_worker_enable_phase_sharing = true + shared_tree_open_leaves_per_worker = Float64(2.0) + shared_tree_max_nodes_per_worker = Int32(10000) shared_tree_split_strategy = var"SatParameters.SharedTreeSplitStrategy".SPLIT_STRATEGY_AUTO + shared_tree_balance_tolerance = Int32(1) enumerate_all_solutions = false keep_all_feasible_solutions_in_presolve = false fill_tightened_domains_in_response = false @@ -455,26 +522,66 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) stop_after_first_solution = false stop_after_presolve = false stop_after_root_propagation = false + lns_initial_difficulty = Float64(0.5) + lns_initial_deterministic_limit = Float64(0.1) + use_lns = true use_lns_only = false solution_pool_size = Int32(3) use_rins_lns = true use_feasibility_pump = true - use_lb_relax_lns = false + use_lb_relax_lns = true + lb_relax_num_workers_threshold = Int32(16) fp_rounding = var"SatParameters.FPRoundingMethod".PROPAGATION_ASSISTED diversify_lns_params = false randomize_search = false - search_randomization_tolerance = Int64(0) + search_random_variable_pool_size = Int64(0) + push_all_tasks_toward_start = false use_optional_variables = false use_exact_lp_reason = true - use_branching_in_lp = false use_combined_no_overlap = false + at_most_one_max_expansion_size = Int32(3) catch_sigint_signal = true use_implied_bounds = true polish_lp_solution = false + lp_primal_tolerance = Float64(1e-7) + lp_dual_tolerance = Float64(1e-7) convert_intervals = true symmetry_level = Int32(2) - new_linear_propagation = false + use_symmetry_in_lp = false + keep_symmetry_in_presolve = false + symmetry_detection_deterministic_time_limit = Float64(1.0) + new_linear_propagation = true linear_split_size = Int32(100) + linearization_level = Int32(1) + boolean_encoding_level = Int32(1) + max_domain_size_when_encoding_eq_neq_constraints = Int32(16) + max_num_cuts = Int32(10000) + cut_level = Int32(1) + only_add_cuts_at_level_zero = false + add_objective_cut = false + add_cg_cuts = true + add_mir_cuts = true + add_zero_half_cuts = true + add_clique_cuts = true + add_rlt_cuts = true + max_all_diff_cut_size = Int32(64) + add_lin_max_cuts = true + max_integer_rounding_scaling = Int32(600) + add_lp_constraints_lazily = true + root_lp_iterations = Int32(2000) + min_orthogonality_for_lp_constraints = Float64(0.05) + max_cut_rounds_at_level_zero = Int32(1) + max_consecutive_inactive_count = Int32(100) + cut_max_active_count_value = Float64(1e10) + cut_active_count_decay = Float64(0.8) + cut_cleanup_target = Int32(1000) + new_constraints_batch_size = Int32(50) + exploit_integer_lp_solution = true + exploit_all_lp_solution = true + exploit_best_solution = false + exploit_relaxation_solution = false + exploit_objective = true + detect_linearized_product = false mip_max_bound = Float64(1e7) mip_var_scaling = Float64(1.0) mip_scale_large_domain = false @@ -484,8 +591,10 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) mip_max_activity_exponent = Int32(53) mip_check_precision = Float64(1e-4) mip_compute_true_objective_bound = true - mip_max_valid_magnitude = Float64(1e30) + mip_max_valid_magnitude = Float64(1e20) + mip_treat_high_magnitude_bounds_as_infinity = false mip_drop_tolerance = Float64(1e-16) + mip_presolve_level = Int32(2) while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 171 @@ -498,6 +607,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) use_phase_saving = PB.decode(d, Bool) elseif field_number == 168 polarity_rephase_increment = PB.decode(d, Int32) + elseif field_number == 309 + polarity_exploit_ls_hints = PB.decode(d, Bool) elseif field_number == 45 random_polarity_ratio = PB.decode(d, Float64) elseif field_number == 32 @@ -530,10 +641,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) pb_cleanup_increment = PB.decode(d, Int32) elseif field_number == 47 pb_cleanup_ratio = PB.decode(d, Float64) - elseif field_number == 96 - minimize_with_propagation_restart_period = PB.decode(d, Int32) - elseif field_number == 97 - minimize_with_propagation_num_decisions = PB.decode(d, Int32) elseif field_number == 15 variable_activity_decay = PB.decode(d, Float64) elseif field_number == 16 @@ -574,6 +681,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) max_time_in_seconds = PB.decode(d, Float64) elseif field_number == 67 max_deterministic_time = PB.decode(d, Float64) + elseif field_number == 291 + max_num_deterministic_batches = PB.decode(d, Int32) elseif field_number == 37 max_number_of_conflicts = PB.decode(d, Int64) elseif field_number == 40 @@ -592,10 +701,6 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) use_absl_random = PB.decode(d, Bool) elseif field_number == 41 log_search_progress = PB.decode(d, Bool) - elseif field_number == 212 - log_frequency_in_seconds = PB.decode(d, Float64) - elseif field_number == 218 - model_reduction_log_frequency_in_seconds = PB.decode(d, Float64) elseif field_number == 189 log_subsolver_statistics = PB.decode(d, Bool) elseif field_number == 185 @@ -612,6 +717,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) count_assumption_levels_in_lbd = PB.decode(d, Bool) elseif field_number == 54 presolve_bve_threshold = PB.decode(d, Int32) + elseif field_number == 324 + filter_sat_postsolve_clauses = PB.decode(d, Bool) elseif field_number == 55 presolve_bve_clause_weight = PB.decode(d, Int32) elseif field_number == 226 @@ -632,16 +739,24 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) cp_model_probing_level = PB.decode(d, Int32) elseif field_number == 93 cp_model_use_sat_presolve = PB.decode(d, Bool) - elseif field_number == 163 - use_sat_inprocessing = PB.decode(d, Bool) + elseif field_number == 310 + remove_fixed_variables_early = PB.decode(d, Bool) elseif field_number == 216 detect_table_with_cost = PB.decode(d, Bool) elseif field_number == 217 table_compression_level = PB.decode(d, Int32) elseif field_number == 170 expand_alldiff_constraints = PB.decode(d, Bool) + elseif field_number == 320 + max_alldiff_domain_size = PB.decode(d, Int32) elseif field_number == 182 expand_reservoir_constraints = PB.decode(d, Bool) + elseif field_number == 288 + expand_reservoir_using_circuit = PB.decode(d, Bool) + elseif field_number == 287 + encode_cumulative_as_reservoir = PB.decode(d, Bool) + elseif field_number == 280 + max_lin_max_size_for_expansion = PB.decode(d, Int32) elseif field_number == 181 disable_constraint_expansion = PB.decode(d, Bool) elseif field_number == 223 @@ -662,18 +777,32 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) infer_all_diffs = PB.decode(d, Bool) elseif field_number == 234 find_big_linear_overlap = PB.decode(d, Bool) + elseif field_number == 163 + use_sat_inprocessing = PB.decode(d, Bool) + elseif field_number == 273 + inprocessing_dtime_ratio = PB.decode(d, Float64) + elseif field_number == 274 + inprocessing_probing_dtime = PB.decode(d, Float64) + elseif field_number == 275 + inprocessing_minimization_dtime = PB.decode(d, Float64) + elseif field_number == 297 + inprocessing_minimization_use_conflict_analysis = PB.decode(d, Bool) + elseif field_number == 298 + inprocessing_minimization_use_all_orderings = PB.decode(d, Bool) elseif field_number == 206 num_workers = PB.decode(d, Int32) elseif field_number == 100 num_search_workers = PB.decode(d, Int32) - elseif field_number == 211 - min_num_lns_workers = PB.decode(d, Int32) + elseif field_number == 294 + num_full_subsolvers = PB.decode(d, Int32) elseif field_number == 207 PB.decode!(d, subsolvers) elseif field_number == 219 PB.decode!(d, extra_subsolvers) elseif field_number == 209 PB.decode!(d, ignore_subsolvers) + elseif field_number == 293 + PB.decode!(d, filter_subsolvers) elseif field_number == 210 PB.decode!(d, subsolver_params) elseif field_number == 136 @@ -686,16 +815,24 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) share_level_zero_bounds = PB.decode(d, Bool) elseif field_number == 203 share_binary_clauses = PB.decode(d, Bool) + elseif field_number == 285 + share_glue_clauses = PB.decode(d, Bool) + elseif field_number == 300 + minimize_shared_clauses = PB.decode(d, Bool) + elseif field_number == 322 + share_glue_clauses_dtime = PB.decode(d, Float64) elseif field_number == 162 debug_postsolve_with_full_solver = PB.decode(d, Bool) elseif field_number == 151 debug_max_num_presolve_operations = PB.decode(d, Int32) elseif field_number == 195 debug_crash_on_bad_hint = PB.decode(d, Bool) + elseif field_number == 306 + debug_crash_if_presolve_breaks_hint = PB.decode(d, Bool) elseif field_number == 35 use_optimization_hints = PB.decode(d, Bool) elseif field_number == 50 - minimize_core = PB.decode(d, Bool) + core_minimization_level = PB.decode(d, Int32) elseif field_number == 84 find_multiple_cores = PB.decode(d, Bool) elseif field_number == 89 @@ -714,70 +851,56 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) max_size_to_create_precedence_literals_in_disjunctive = PB.decode(d, Int32) elseif field_number == 230 use_strong_propagation_in_disjunctive = PB.decode(d, Bool) + elseif field_number == 263 + use_dynamic_precedence_in_disjunctive = PB.decode(d, Bool) + elseif field_number == 268 + use_dynamic_precedence_in_cumulative = PB.decode(d, Bool) elseif field_number == 78 use_overload_checker_in_cumulative = PB.decode(d, Bool) + elseif field_number == 286 + use_conservative_scale_overload_checker = PB.decode(d, Bool) elseif field_number == 79 use_timetable_edge_finding_in_cumulative = PB.decode(d, Bool) + elseif field_number == 260 + max_num_intervals_for_timetable_edge_finding = PB.decode(d, Int32) elseif field_number == 215 use_hard_precedences_in_cumulative = PB.decode(d, Bool) elseif field_number == 220 exploit_all_precedences = PB.decode(d, Bool) elseif field_number == 80 use_disjunctive_constraint_in_cumulative = PB.decode(d, Bool) + elseif field_number == 321 + no_overlap_2d_boolean_relations_limit = PB.decode(d, Int32) elseif field_number == 200 use_timetabling_in_no_overlap_2d = PB.decode(d, Bool) elseif field_number == 213 use_energetic_reasoning_in_no_overlap_2d = PB.decode(d, Bool) - elseif field_number == 251 - use_pairwise_reasoning_in_no_overlap_2d = PB.decode(d, Bool) + elseif field_number == 271 + use_area_energetic_reasoning_in_no_overlap_2d = PB.decode(d, Bool) + elseif field_number == 299 + use_try_edge_reasoning_in_no_overlap_2d = PB.decode(d, Bool) + elseif field_number == 276 + max_pairs_pairwise_reasoning_in_no_overlap_2d = PB.decode(d, Int32) + elseif field_number == 315 + maximum_regions_to_split_in_disconnected_no_overlap_2d = PB.decode(d, Int32) + elseif field_number == 323 + use_linear3_for_no_overlap_2d_precedences = PB.decode(d, Bool) elseif field_number == 214 use_dual_scheduling_heuristics = PB.decode(d, Bool) - elseif field_number == 90 - linearization_level = PB.decode(d, Int32) - elseif field_number == 107 - boolean_encoding_level = PB.decode(d, Int32) - elseif field_number == 191 - max_domain_size_when_encoding_eq_neq_constraints = PB.decode(d, Int32) - elseif field_number == 91 - max_num_cuts = PB.decode(d, Int32) - elseif field_number == 196 - cut_level = PB.decode(d, Int32) - elseif field_number == 92 - only_add_cuts_at_level_zero = PB.decode(d, Bool) - elseif field_number == 197 - add_objective_cut = PB.decode(d, Bool) - elseif field_number == 117 - add_cg_cuts = PB.decode(d, Bool) - elseif field_number == 120 - add_mir_cuts = PB.decode(d, Bool) - elseif field_number == 169 - add_zero_half_cuts = PB.decode(d, Bool) - elseif field_number == 172 - add_clique_cuts = PB.decode(d, Bool) - elseif field_number == 148 - max_all_diff_cut_size = PB.decode(d, Int32) - elseif field_number == 152 - add_lin_max_cuts = PB.decode(d, Bool) - elseif field_number == 119 - max_integer_rounding_scaling = PB.decode(d, Int32) - elseif field_number == 112 - add_lp_constraints_lazily = PB.decode(d, Bool) - elseif field_number == 227 - root_lp_iterations = PB.decode(d, Int32) - elseif field_number == 115 - min_orthogonality_for_lp_constraints = PB.decode(d, Float64) - elseif field_number == 154 - max_cut_rounds_at_level_zero = PB.decode(d, Int32) - elseif field_number == 121 - max_consecutive_inactive_count = PB.decode(d, Int32) - elseif field_number == 155 - cut_max_active_count_value = PB.decode(d, Float64) - elseif field_number == 156 - cut_active_count_decay = PB.decode(d, Float64) - elseif field_number == 157 - cut_cleanup_target = PB.decode(d, Int32) - elseif field_number == 122 - new_constraints_batch_size = PB.decode(d, Int32) + elseif field_number == 311 + use_all_different_for_circuit = PB.decode(d, Bool) + elseif field_number == 312 + routing_cut_subset_size_for_binary_relation_bound = PB.decode(d, Int32) + elseif field_number == 313 + routing_cut_subset_size_for_tight_binary_relation_bound = PB.decode(d, Int32) + elseif field_number == 316 + routing_cut_subset_size_for_exact_binary_relation_bound = PB.decode(d, Int32) + elseif field_number == 318 + routing_cut_subset_size_for_shortest_paths_bound = PB.decode(d, Int32) + elseif field_number == 314 + routing_cut_dp_effort = PB.decode(d, Float64) + elseif field_number == 317 + routing_cut_max_infeasible_path_length = PB.decode(d, Int32) elseif field_number == 82 search_branching = PB.decode(d, var"SatParameters.SearchBranching".T) elseif field_number == 153 @@ -786,66 +909,80 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) repair_hint = PB.decode(d, Bool) elseif field_number == 192 fix_variables_to_their_hinted_value = PB.decode(d, Bool) - elseif field_number == 94 - exploit_integer_lp_solution = PB.decode(d, Bool) - elseif field_number == 116 - exploit_all_lp_solution = PB.decode(d, Bool) - elseif field_number == 130 - exploit_best_solution = PB.decode(d, Bool) - elseif field_number == 161 - exploit_relaxation_solution = PB.decode(d, Bool) - elseif field_number == 131 - exploit_objective = PB.decode(d, Bool) - elseif field_number == 142 - probing_period_at_root = PB.decode(d, Int64) elseif field_number == 176 use_probing_search = PB.decode(d, Bool) + elseif field_number == 269 + use_extended_probing = PB.decode(d, Bool) + elseif field_number == 272 + probing_num_combinations_limit = PB.decode(d, Int32) elseif field_number == 204 shaving_deterministic_time_in_probing_search = PB.decode(d, Float64) elseif field_number == 205 shaving_search_deterministic_time = PB.decode(d, Float64) + elseif field_number == 290 + shaving_search_threshold = PB.decode(d, Int64) elseif field_number == 228 use_objective_lb_search = PB.decode(d, Bool) elseif field_number == 253 use_objective_shaving_search = PB.decode(d, Bool) + elseif field_number == 289 + variables_shaving_level = PB.decode(d, Int32) elseif field_number == 123 pseudo_cost_reliability_threshold = PB.decode(d, Int64) elseif field_number == 83 optimize_with_core = PB.decode(d, Bool) elseif field_number == 188 optimize_with_lb_tree_search = PB.decode(d, Bool) + elseif field_number == 284 + save_lp_basis_in_lb_tree_search = PB.decode(d, Bool) elseif field_number == 99 binary_search_num_conflicts = PB.decode(d, Int32) elseif field_number == 85 optimize_with_max_hs = PB.decode(d, Bool) + elseif field_number == 265 + use_feasibility_jump = PB.decode(d, Bool) elseif field_number == 240 - test_feasibility_jump = PB.decode(d, Bool) - elseif field_number == 245 - feasibility_jump_max_num_values_scanned = PB.decode(d, Int64) - elseif field_number == 246 - feasibility_jump_protect_linear_feasibility = PB.decode(d, Bool) + use_ls_only = PB.decode(d, Bool) elseif field_number == 242 feasibility_jump_decay = PB.decode(d, Float64) + elseif field_number == 257 + feasibility_jump_linearization_level = PB.decode(d, Int32) + elseif field_number == 258 + feasibility_jump_restart_factor = PB.decode(d, Int32) + elseif field_number == 292 + feasibility_jump_batch_dtime = PB.decode(d, Float64) elseif field_number == 247 feasibility_jump_var_randomization_probability = PB.decode(d, Float64) elseif field_number == 248 feasibility_jump_var_perburbation_range_ratio = PB.decode(d, Float64) elseif field_number == 250 feasibility_jump_enable_restarts = PB.decode(d, Bool) + elseif field_number == 264 + feasibility_jump_max_expanded_constraint_size = PB.decode(d, Int32) elseif field_number == 244 num_violation_ls = PB.decode(d, Int32) elseif field_number == 249 violation_ls_perturbation_period = PB.decode(d, Int32) + elseif field_number == 259 + violation_ls_compound_move_probability = PB.decode(d, Float64) elseif field_number == 235 shared_tree_num_workers = PB.decode(d, Int32) elseif field_number == 236 use_shared_tree_search = PB.decode(d, Bool) - elseif field_number == 237 - shared_tree_worker_objective_split_probability = PB.decode(d, Float64) + elseif field_number == 282 + shared_tree_worker_min_restarts_per_subtree = PB.decode(d, Int32) + elseif field_number == 295 + shared_tree_worker_enable_trail_sharing = PB.decode(d, Bool) + elseif field_number == 304 + shared_tree_worker_enable_phase_sharing = PB.decode(d, Bool) + elseif field_number == 281 + shared_tree_open_leaves_per_worker = PB.decode(d, Float64) elseif field_number == 238 shared_tree_max_nodes_per_worker = PB.decode(d, Int32) elseif field_number == 239 shared_tree_split_strategy = PB.decode(d, var"SatParameters.SharedTreeSplitStrategy".T) + elseif field_number == 305 + shared_tree_balance_tolerance = PB.decode(d, Int32) elseif field_number == 87 enumerate_all_solutions = PB.decode(d, Bool) elseif field_number == 173 @@ -864,6 +1001,12 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) stop_after_presolve = PB.decode(d, Bool) elseif field_number == 252 stop_after_root_propagation = PB.decode(d, Bool) + elseif field_number == 307 + lns_initial_difficulty = PB.decode(d, Float64) + elseif field_number == 308 + lns_initial_deterministic_limit = PB.decode(d, Float64) + elseif field_number == 283 + use_lns = PB.decode(d, Bool) elseif field_number == 101 use_lns_only = PB.decode(d, Bool) elseif field_number == 193 @@ -874,6 +1017,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) use_feasibility_pump = PB.decode(d, Bool) elseif field_number == 255 use_lb_relax_lns = PB.decode(d, Bool) + elseif field_number == 296 + lb_relax_num_workers_threshold = PB.decode(d, Int32) elseif field_number == 165 fp_rounding = PB.decode(d, var"SatParameters.FPRoundingMethod".T) elseif field_number == 137 @@ -881,29 +1026,101 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) elseif field_number == 103 randomize_search = PB.decode(d, Bool) elseif field_number == 104 - search_randomization_tolerance = PB.decode(d, Int64) + search_random_variable_pool_size = PB.decode(d, Int64) + elseif field_number == 262 + push_all_tasks_toward_start = PB.decode(d, Bool) elseif field_number == 108 use_optional_variables = PB.decode(d, Bool) elseif field_number == 109 use_exact_lp_reason = PB.decode(d, Bool) - elseif field_number == 139 - use_branching_in_lp = PB.decode(d, Bool) elseif field_number == 133 use_combined_no_overlap = PB.decode(d, Bool) + elseif field_number == 270 + at_most_one_max_expansion_size = PB.decode(d, Int32) elseif field_number == 135 catch_sigint_signal = PB.decode(d, Bool) elseif field_number == 144 use_implied_bounds = PB.decode(d, Bool) elseif field_number == 175 polish_lp_solution = PB.decode(d, Bool) + elseif field_number == 266 + lp_primal_tolerance = PB.decode(d, Float64) + elseif field_number == 267 + lp_dual_tolerance = PB.decode(d, Float64) elseif field_number == 177 convert_intervals = PB.decode(d, Bool) elseif field_number == 183 symmetry_level = PB.decode(d, Int32) + elseif field_number == 301 + use_symmetry_in_lp = PB.decode(d, Bool) + elseif field_number == 303 + keep_symmetry_in_presolve = PB.decode(d, Bool) + elseif field_number == 302 + symmetry_detection_deterministic_time_limit = PB.decode(d, Float64) elseif field_number == 224 new_linear_propagation = PB.decode(d, Bool) elseif field_number == 256 linear_split_size = PB.decode(d, Int32) + elseif field_number == 90 + linearization_level = PB.decode(d, Int32) + elseif field_number == 107 + boolean_encoding_level = PB.decode(d, Int32) + elseif field_number == 191 + max_domain_size_when_encoding_eq_neq_constraints = PB.decode(d, Int32) + elseif field_number == 91 + max_num_cuts = PB.decode(d, Int32) + elseif field_number == 196 + cut_level = PB.decode(d, Int32) + elseif field_number == 92 + only_add_cuts_at_level_zero = PB.decode(d, Bool) + elseif field_number == 197 + add_objective_cut = PB.decode(d, Bool) + elseif field_number == 117 + add_cg_cuts = PB.decode(d, Bool) + elseif field_number == 120 + add_mir_cuts = PB.decode(d, Bool) + elseif field_number == 169 + add_zero_half_cuts = PB.decode(d, Bool) + elseif field_number == 172 + add_clique_cuts = PB.decode(d, Bool) + elseif field_number == 279 + add_rlt_cuts = PB.decode(d, Bool) + elseif field_number == 148 + max_all_diff_cut_size = PB.decode(d, Int32) + elseif field_number == 152 + add_lin_max_cuts = PB.decode(d, Bool) + elseif field_number == 119 + max_integer_rounding_scaling = PB.decode(d, Int32) + elseif field_number == 112 + add_lp_constraints_lazily = PB.decode(d, Bool) + elseif field_number == 227 + root_lp_iterations = PB.decode(d, Int32) + elseif field_number == 115 + min_orthogonality_for_lp_constraints = PB.decode(d, Float64) + elseif field_number == 154 + max_cut_rounds_at_level_zero = PB.decode(d, Int32) + elseif field_number == 121 + max_consecutive_inactive_count = PB.decode(d, Int32) + elseif field_number == 155 + cut_max_active_count_value = PB.decode(d, Float64) + elseif field_number == 156 + cut_active_count_decay = PB.decode(d, Float64) + elseif field_number == 157 + cut_cleanup_target = PB.decode(d, Int32) + elseif field_number == 122 + new_constraints_batch_size = PB.decode(d, Int32) + elseif field_number == 94 + exploit_integer_lp_solution = PB.decode(d, Bool) + elseif field_number == 116 + exploit_all_lp_solution = PB.decode(d, Bool) + elseif field_number == 130 + exploit_best_solution = PB.decode(d, Bool) + elseif field_number == 161 + exploit_relaxation_solution = PB.decode(d, Bool) + elseif field_number == 131 + exploit_objective = PB.decode(d, Bool) + elseif field_number == 277 + detect_linearized_product = PB.decode(d, Bool) elseif field_number == 124 mip_max_bound = PB.decode(d, Float64) elseif field_number == 125 @@ -924,13 +1141,17 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SatParameters}) mip_compute_true_objective_bound = PB.decode(d, Bool) elseif field_number == 199 mip_max_valid_magnitude = PB.decode(d, Float64) + elseif field_number == 278 + mip_treat_high_magnitude_bounds_as_infinity = PB.decode(d, Bool) elseif field_number == 232 mip_drop_tolerance = PB.decode(d, Float64) + elseif field_number == 261 + mip_presolve_level = PB.decode(d, Int32) else PB.skip(d, wire_type) end end - return SatParameters(name, preferred_variable_order, initial_polarity, use_phase_saving, polarity_rephase_increment, random_polarity_ratio, random_branches_ratio, use_erwa_heuristic, initial_variables_activity, also_bump_variables_in_conflict_reasons, minimization_algorithm, binary_minimization_algorithm, subsumption_during_conflict_analysis, clause_cleanup_period, clause_cleanup_target, clause_cleanup_ratio, clause_cleanup_protection, clause_cleanup_lbd_bound, clause_cleanup_ordering, pb_cleanup_increment, pb_cleanup_ratio, minimize_with_propagation_restart_period, minimize_with_propagation_num_decisions, variable_activity_decay, max_variable_activity_value, glucose_max_decay, glucose_decay_increment, glucose_decay_increment_period, clause_activity_decay, max_clause_activity_value, restart_algorithms[], default_restart_algorithms, restart_period, restart_running_window_size, restart_dl_average_ratio, restart_lbd_average_ratio, use_blocking_restart, blocking_restart_window_size, blocking_restart_multiplier, num_conflicts_before_strategy_changes, strategy_change_increase_ratio, max_time_in_seconds, max_deterministic_time, max_number_of_conflicts, max_memory_in_mb, absolute_gap_limit, relative_gap_limit, random_seed, permute_variable_randomly, permute_presolve_constraint_order, use_absl_random, log_search_progress, log_frequency_in_seconds, model_reduction_log_frequency_in_seconds, log_subsolver_statistics, log_prefix, log_to_stdout, log_to_response, use_pb_resolution, minimize_reduction_during_pb_resolution, count_assumption_levels_in_lbd, presolve_bve_threshold, presolve_bve_clause_weight, probing_deterministic_time_limit, presolve_probing_deterministic_time_limit, presolve_blocked_clause, presolve_use_bva, presolve_bva_threshold, max_presolve_iterations, cp_model_presolve, cp_model_probing_level, cp_model_use_sat_presolve, use_sat_inprocessing, detect_table_with_cost, table_compression_level, expand_alldiff_constraints, expand_reservoir_constraints, disable_constraint_expansion, encode_complex_linear_constraint_with_integer, merge_no_overlap_work_limit, merge_at_most_one_work_limit, presolve_substitution_level, presolve_extract_integer_enforcement, presolve_inclusion_work_limit, ignore_names, infer_all_diffs, find_big_linear_overlap, num_workers, num_search_workers, min_num_lns_workers, subsolvers[], extra_subsolvers[], ignore_subsolvers[], subsolver_params[], interleave_search, interleave_batch_size, share_objective_bounds, share_level_zero_bounds, share_binary_clauses, debug_postsolve_with_full_solver, debug_max_num_presolve_operations, debug_crash_on_bad_hint, use_optimization_hints, minimize_core, find_multiple_cores, cover_optimization, max_sat_assumption_order, max_sat_reverse_assumption_order, max_sat_stratification, propagation_loop_detection_factor, use_precedences_in_disjunctive_constraint, max_size_to_create_precedence_literals_in_disjunctive, use_strong_propagation_in_disjunctive, use_overload_checker_in_cumulative, use_timetable_edge_finding_in_cumulative, use_hard_precedences_in_cumulative, exploit_all_precedences, use_disjunctive_constraint_in_cumulative, use_timetabling_in_no_overlap_2d, use_energetic_reasoning_in_no_overlap_2d, use_pairwise_reasoning_in_no_overlap_2d, use_dual_scheduling_heuristics, linearization_level, boolean_encoding_level, max_domain_size_when_encoding_eq_neq_constraints, max_num_cuts, cut_level, only_add_cuts_at_level_zero, add_objective_cut, add_cg_cuts, add_mir_cuts, add_zero_half_cuts, add_clique_cuts, max_all_diff_cut_size, add_lin_max_cuts, max_integer_rounding_scaling, add_lp_constraints_lazily, root_lp_iterations, min_orthogonality_for_lp_constraints, max_cut_rounds_at_level_zero, max_consecutive_inactive_count, cut_max_active_count_value, cut_active_count_decay, cut_cleanup_target, new_constraints_batch_size, search_branching, hint_conflict_limit, repair_hint, fix_variables_to_their_hinted_value, exploit_integer_lp_solution, exploit_all_lp_solution, exploit_best_solution, exploit_relaxation_solution, exploit_objective, probing_period_at_root, use_probing_search, shaving_deterministic_time_in_probing_search, shaving_search_deterministic_time, use_objective_lb_search, use_objective_shaving_search, pseudo_cost_reliability_threshold, optimize_with_core, optimize_with_lb_tree_search, binary_search_num_conflicts, optimize_with_max_hs, test_feasibility_jump, feasibility_jump_max_num_values_scanned, feasibility_jump_protect_linear_feasibility, feasibility_jump_decay, feasibility_jump_var_randomization_probability, feasibility_jump_var_perburbation_range_ratio, feasibility_jump_enable_restarts, num_violation_ls, violation_ls_perturbation_period, shared_tree_num_workers, use_shared_tree_search, shared_tree_worker_objective_split_probability, shared_tree_max_nodes_per_worker, shared_tree_split_strategy, enumerate_all_solutions, keep_all_feasible_solutions_in_presolve, fill_tightened_domains_in_response, fill_additional_solutions_in_response, instantiate_all_variables, auto_detect_greater_than_at_least_one_of, stop_after_first_solution, stop_after_presolve, stop_after_root_propagation, use_lns_only, solution_pool_size, use_rins_lns, use_feasibility_pump, use_lb_relax_lns, fp_rounding, diversify_lns_params, randomize_search, search_randomization_tolerance, use_optional_variables, use_exact_lp_reason, use_branching_in_lp, use_combined_no_overlap, catch_sigint_signal, use_implied_bounds, polish_lp_solution, convert_intervals, symmetry_level, new_linear_propagation, linear_split_size, mip_max_bound, mip_var_scaling, mip_scale_large_domain, mip_automatically_scale_variables, only_solve_ip, mip_wanted_precision, mip_max_activity_exponent, mip_check_precision, mip_compute_true_objective_bound, mip_max_valid_magnitude, mip_drop_tolerance) + return SatParameters(name, preferred_variable_order, initial_polarity, use_phase_saving, polarity_rephase_increment, polarity_exploit_ls_hints, random_polarity_ratio, random_branches_ratio, use_erwa_heuristic, initial_variables_activity, also_bump_variables_in_conflict_reasons, minimization_algorithm, binary_minimization_algorithm, subsumption_during_conflict_analysis, clause_cleanup_period, clause_cleanup_target, clause_cleanup_ratio, clause_cleanup_protection, clause_cleanup_lbd_bound, clause_cleanup_ordering, pb_cleanup_increment, pb_cleanup_ratio, variable_activity_decay, max_variable_activity_value, glucose_max_decay, glucose_decay_increment, glucose_decay_increment_period, clause_activity_decay, max_clause_activity_value, restart_algorithms[], default_restart_algorithms, restart_period, restart_running_window_size, restart_dl_average_ratio, restart_lbd_average_ratio, use_blocking_restart, blocking_restart_window_size, blocking_restart_multiplier, num_conflicts_before_strategy_changes, strategy_change_increase_ratio, max_time_in_seconds, max_deterministic_time, max_num_deterministic_batches, max_number_of_conflicts, max_memory_in_mb, absolute_gap_limit, relative_gap_limit, random_seed, permute_variable_randomly, permute_presolve_constraint_order, use_absl_random, log_search_progress, log_subsolver_statistics, log_prefix, log_to_stdout, log_to_response, use_pb_resolution, minimize_reduction_during_pb_resolution, count_assumption_levels_in_lbd, presolve_bve_threshold, filter_sat_postsolve_clauses, presolve_bve_clause_weight, probing_deterministic_time_limit, presolve_probing_deterministic_time_limit, presolve_blocked_clause, presolve_use_bva, presolve_bva_threshold, max_presolve_iterations, cp_model_presolve, cp_model_probing_level, cp_model_use_sat_presolve, remove_fixed_variables_early, detect_table_with_cost, table_compression_level, expand_alldiff_constraints, max_alldiff_domain_size, expand_reservoir_constraints, expand_reservoir_using_circuit, encode_cumulative_as_reservoir, max_lin_max_size_for_expansion, disable_constraint_expansion, encode_complex_linear_constraint_with_integer, merge_no_overlap_work_limit, merge_at_most_one_work_limit, presolve_substitution_level, presolve_extract_integer_enforcement, presolve_inclusion_work_limit, ignore_names, infer_all_diffs, find_big_linear_overlap, use_sat_inprocessing, inprocessing_dtime_ratio, inprocessing_probing_dtime, inprocessing_minimization_dtime, inprocessing_minimization_use_conflict_analysis, inprocessing_minimization_use_all_orderings, num_workers, num_search_workers, num_full_subsolvers, subsolvers[], extra_subsolvers[], ignore_subsolvers[], filter_subsolvers[], subsolver_params[], interleave_search, interleave_batch_size, share_objective_bounds, share_level_zero_bounds, share_binary_clauses, share_glue_clauses, minimize_shared_clauses, share_glue_clauses_dtime, debug_postsolve_with_full_solver, debug_max_num_presolve_operations, debug_crash_on_bad_hint, debug_crash_if_presolve_breaks_hint, use_optimization_hints, core_minimization_level, find_multiple_cores, cover_optimization, max_sat_assumption_order, max_sat_reverse_assumption_order, max_sat_stratification, propagation_loop_detection_factor, use_precedences_in_disjunctive_constraint, max_size_to_create_precedence_literals_in_disjunctive, use_strong_propagation_in_disjunctive, use_dynamic_precedence_in_disjunctive, use_dynamic_precedence_in_cumulative, use_overload_checker_in_cumulative, use_conservative_scale_overload_checker, use_timetable_edge_finding_in_cumulative, max_num_intervals_for_timetable_edge_finding, use_hard_precedences_in_cumulative, exploit_all_precedences, use_disjunctive_constraint_in_cumulative, no_overlap_2d_boolean_relations_limit, use_timetabling_in_no_overlap_2d, use_energetic_reasoning_in_no_overlap_2d, use_area_energetic_reasoning_in_no_overlap_2d, use_try_edge_reasoning_in_no_overlap_2d, max_pairs_pairwise_reasoning_in_no_overlap_2d, maximum_regions_to_split_in_disconnected_no_overlap_2d, use_linear3_for_no_overlap_2d_precedences, use_dual_scheduling_heuristics, use_all_different_for_circuit, routing_cut_subset_size_for_binary_relation_bound, routing_cut_subset_size_for_tight_binary_relation_bound, routing_cut_subset_size_for_exact_binary_relation_bound, routing_cut_subset_size_for_shortest_paths_bound, routing_cut_dp_effort, routing_cut_max_infeasible_path_length, search_branching, hint_conflict_limit, repair_hint, fix_variables_to_their_hinted_value, use_probing_search, use_extended_probing, probing_num_combinations_limit, shaving_deterministic_time_in_probing_search, shaving_search_deterministic_time, shaving_search_threshold, use_objective_lb_search, use_objective_shaving_search, variables_shaving_level, pseudo_cost_reliability_threshold, optimize_with_core, optimize_with_lb_tree_search, save_lp_basis_in_lb_tree_search, binary_search_num_conflicts, optimize_with_max_hs, use_feasibility_jump, use_ls_only, feasibility_jump_decay, feasibility_jump_linearization_level, feasibility_jump_restart_factor, feasibility_jump_batch_dtime, feasibility_jump_var_randomization_probability, feasibility_jump_var_perburbation_range_ratio, feasibility_jump_enable_restarts, feasibility_jump_max_expanded_constraint_size, num_violation_ls, violation_ls_perturbation_period, violation_ls_compound_move_probability, shared_tree_num_workers, use_shared_tree_search, shared_tree_worker_min_restarts_per_subtree, shared_tree_worker_enable_trail_sharing, shared_tree_worker_enable_phase_sharing, shared_tree_open_leaves_per_worker, shared_tree_max_nodes_per_worker, shared_tree_split_strategy, shared_tree_balance_tolerance, enumerate_all_solutions, keep_all_feasible_solutions_in_presolve, fill_tightened_domains_in_response, fill_additional_solutions_in_response, instantiate_all_variables, auto_detect_greater_than_at_least_one_of, stop_after_first_solution, stop_after_presolve, stop_after_root_propagation, lns_initial_difficulty, lns_initial_deterministic_limit, use_lns, use_lns_only, solution_pool_size, use_rins_lns, use_feasibility_pump, use_lb_relax_lns, lb_relax_num_workers_threshold, fp_rounding, diversify_lns_params, randomize_search, search_random_variable_pool_size, push_all_tasks_toward_start, use_optional_variables, use_exact_lp_reason, use_combined_no_overlap, at_most_one_max_expansion_size, catch_sigint_signal, use_implied_bounds, polish_lp_solution, lp_primal_tolerance, lp_dual_tolerance, convert_intervals, symmetry_level, use_symmetry_in_lp, keep_symmetry_in_presolve, symmetry_detection_deterministic_time_limit, new_linear_propagation, linear_split_size, linearization_level, boolean_encoding_level, max_domain_size_when_encoding_eq_neq_constraints, max_num_cuts, cut_level, only_add_cuts_at_level_zero, add_objective_cut, add_cg_cuts, add_mir_cuts, add_zero_half_cuts, add_clique_cuts, add_rlt_cuts, max_all_diff_cut_size, add_lin_max_cuts, max_integer_rounding_scaling, add_lp_constraints_lazily, root_lp_iterations, min_orthogonality_for_lp_constraints, max_cut_rounds_at_level_zero, max_consecutive_inactive_count, cut_max_active_count_value, cut_active_count_decay, cut_cleanup_target, new_constraints_batch_size, exploit_integer_lp_solution, exploit_all_lp_solution, exploit_best_solution, exploit_relaxation_solution, exploit_objective, detect_linearized_product, mip_max_bound, mip_var_scaling, mip_scale_large_domain, mip_automatically_scale_variables, only_solve_ip, mip_wanted_precision, mip_max_activity_exponent, mip_check_precision, mip_compute_true_objective_bound, mip_max_valid_magnitude, mip_treat_high_magnitude_bounds_as_infinity, mip_drop_tolerance, mip_presolve_level) end function PB.encode(e::PB.AbstractProtoEncoder, x::SatParameters) @@ -940,55 +1161,53 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SatParameters) x.initial_polarity != var"SatParameters.Polarity".POLARITY_FALSE && PB.encode(e, 2, x.initial_polarity) x.use_phase_saving != true && PB.encode(e, 44, x.use_phase_saving) x.polarity_rephase_increment != Int32(1000) && PB.encode(e, 168, x.polarity_rephase_increment) - x.random_polarity_ratio != Float64(0.0) && PB.encode(e, 45, x.random_polarity_ratio) - x.random_branches_ratio != Float64(0.0) && PB.encode(e, 32, x.random_branches_ratio) + x.polarity_exploit_ls_hints != false && PB.encode(e, 309, x.polarity_exploit_ls_hints) + x.random_polarity_ratio !== Float64(0.0) && PB.encode(e, 45, x.random_polarity_ratio) + x.random_branches_ratio !== Float64(0.0) && PB.encode(e, 32, x.random_branches_ratio) x.use_erwa_heuristic != false && PB.encode(e, 75, x.use_erwa_heuristic) - x.initial_variables_activity != Float64(0.0) && PB.encode(e, 76, x.initial_variables_activity) + x.initial_variables_activity !== Float64(0.0) && PB.encode(e, 76, x.initial_variables_activity) x.also_bump_variables_in_conflict_reasons != false && PB.encode(e, 77, x.also_bump_variables_in_conflict_reasons) x.minimization_algorithm != var"SatParameters.ConflictMinimizationAlgorithm".RECURSIVE && PB.encode(e, 4, x.minimization_algorithm) x.binary_minimization_algorithm != var"SatParameters.BinaryMinizationAlgorithm".BINARY_MINIMIZATION_FIRST && PB.encode(e, 34, x.binary_minimization_algorithm) x.subsumption_during_conflict_analysis != true && PB.encode(e, 56, x.subsumption_during_conflict_analysis) x.clause_cleanup_period != Int32(10000) && PB.encode(e, 11, x.clause_cleanup_period) x.clause_cleanup_target != Int32(0) && PB.encode(e, 13, x.clause_cleanup_target) - x.clause_cleanup_ratio != Float64(0.5) && PB.encode(e, 190, x.clause_cleanup_ratio) + x.clause_cleanup_ratio !== Float64(0.5) && PB.encode(e, 190, x.clause_cleanup_ratio) x.clause_cleanup_protection != var"SatParameters.ClauseProtection".PROTECTION_NONE && PB.encode(e, 58, x.clause_cleanup_protection) x.clause_cleanup_lbd_bound != Int32(5) && PB.encode(e, 59, x.clause_cleanup_lbd_bound) x.clause_cleanup_ordering != var"SatParameters.ClauseOrdering".CLAUSE_ACTIVITY && PB.encode(e, 60, x.clause_cleanup_ordering) x.pb_cleanup_increment != Int32(200) && PB.encode(e, 46, x.pb_cleanup_increment) - x.pb_cleanup_ratio != Float64(0.5) && PB.encode(e, 47, x.pb_cleanup_ratio) - x.minimize_with_propagation_restart_period != Int32(10) && PB.encode(e, 96, x.minimize_with_propagation_restart_period) - x.minimize_with_propagation_num_decisions != Int32(1000) && PB.encode(e, 97, x.minimize_with_propagation_num_decisions) - x.variable_activity_decay != Float64(0.8) && PB.encode(e, 15, x.variable_activity_decay) - x.max_variable_activity_value != Float64(1e100) && PB.encode(e, 16, x.max_variable_activity_value) - x.glucose_max_decay != Float64(0.95) && PB.encode(e, 22, x.glucose_max_decay) - x.glucose_decay_increment != Float64(0.01) && PB.encode(e, 23, x.glucose_decay_increment) + x.pb_cleanup_ratio !== Float64(0.5) && PB.encode(e, 47, x.pb_cleanup_ratio) + x.variable_activity_decay !== Float64(0.8) && PB.encode(e, 15, x.variable_activity_decay) + x.max_variable_activity_value !== Float64(1e100) && PB.encode(e, 16, x.max_variable_activity_value) + x.glucose_max_decay !== Float64(0.95) && PB.encode(e, 22, x.glucose_max_decay) + x.glucose_decay_increment !== Float64(0.01) && PB.encode(e, 23, x.glucose_decay_increment) x.glucose_decay_increment_period != Int32(5000) && PB.encode(e, 24, x.glucose_decay_increment_period) - x.clause_activity_decay != Float64(0.999) && PB.encode(e, 17, x.clause_activity_decay) - x.max_clause_activity_value != Float64(1e20) && PB.encode(e, 18, x.max_clause_activity_value) + x.clause_activity_decay !== Float64(0.999) && PB.encode(e, 17, x.clause_activity_decay) + x.max_clause_activity_value !== Float64(1e20) && PB.encode(e, 18, x.max_clause_activity_value) !isempty(x.restart_algorithms) && PB.encode(e, 61, x.restart_algorithms) x.default_restart_algorithms != "LUBY_RESTART,LBD_MOVING_AVERAGE_RESTART,DL_MOVING_AVERAGE_RESTART" && PB.encode(e, 70, x.default_restart_algorithms) x.restart_period != Int32(50) && PB.encode(e, 30, x.restart_period) x.restart_running_window_size != Int32(50) && PB.encode(e, 62, x.restart_running_window_size) - x.restart_dl_average_ratio != Float64(1.0) && PB.encode(e, 63, x.restart_dl_average_ratio) - x.restart_lbd_average_ratio != Float64(1.0) && PB.encode(e, 71, x.restart_lbd_average_ratio) + x.restart_dl_average_ratio !== Float64(1.0) && PB.encode(e, 63, x.restart_dl_average_ratio) + x.restart_lbd_average_ratio !== Float64(1.0) && PB.encode(e, 71, x.restart_lbd_average_ratio) x.use_blocking_restart != false && PB.encode(e, 64, x.use_blocking_restart) x.blocking_restart_window_size != Int32(5000) && PB.encode(e, 65, x.blocking_restart_window_size) - x.blocking_restart_multiplier != Float64(1.4) && PB.encode(e, 66, x.blocking_restart_multiplier) + x.blocking_restart_multiplier !== Float64(1.4) && PB.encode(e, 66, x.blocking_restart_multiplier) x.num_conflicts_before_strategy_changes != Int32(0) && PB.encode(e, 68, x.num_conflicts_before_strategy_changes) - x.strategy_change_increase_ratio != Float64(0.0) && PB.encode(e, 69, x.strategy_change_increase_ratio) - x.max_time_in_seconds != Float64(Inf) && PB.encode(e, 36, x.max_time_in_seconds) - x.max_deterministic_time != Float64(Inf) && PB.encode(e, 67, x.max_deterministic_time) + x.strategy_change_increase_ratio !== Float64(0.0) && PB.encode(e, 69, x.strategy_change_increase_ratio) + x.max_time_in_seconds !== Float64(Inf) && PB.encode(e, 36, x.max_time_in_seconds) + x.max_deterministic_time !== Float64(Inf) && PB.encode(e, 67, x.max_deterministic_time) + x.max_num_deterministic_batches != Int32(0) && PB.encode(e, 291, x.max_num_deterministic_batches) x.max_number_of_conflicts != Int64(9223372036854775807) && PB.encode(e, 37, x.max_number_of_conflicts) x.max_memory_in_mb != Int64(10000) && PB.encode(e, 40, x.max_memory_in_mb) - x.absolute_gap_limit != Float64(1e-4) && PB.encode(e, 159, x.absolute_gap_limit) - x.relative_gap_limit != Float64(0.0) && PB.encode(e, 160, x.relative_gap_limit) + x.absolute_gap_limit !== Float64(1e-4) && PB.encode(e, 159, x.absolute_gap_limit) + x.relative_gap_limit !== Float64(0.0) && PB.encode(e, 160, x.relative_gap_limit) x.random_seed != Int32(1) && PB.encode(e, 31, x.random_seed) x.permute_variable_randomly != false && PB.encode(e, 178, x.permute_variable_randomly) x.permute_presolve_constraint_order != false && PB.encode(e, 179, x.permute_presolve_constraint_order) x.use_absl_random != false && PB.encode(e, 180, x.use_absl_random) x.log_search_progress != false && PB.encode(e, 41, x.log_search_progress) - x.log_frequency_in_seconds != Float64(-1) && PB.encode(e, 212, x.log_frequency_in_seconds) - x.model_reduction_log_frequency_in_seconds != Float64(5) && PB.encode(e, 218, x.model_reduction_log_frequency_in_seconds) x.log_subsolver_statistics != false && PB.encode(e, 189, x.log_subsolver_statistics) x.log_prefix != "" && PB.encode(e, 185, x.log_prefix) x.log_to_stdout != true && PB.encode(e, 186, x.log_to_stdout) @@ -997,9 +1216,10 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SatParameters) x.minimize_reduction_during_pb_resolution != false && PB.encode(e, 48, x.minimize_reduction_during_pb_resolution) x.count_assumption_levels_in_lbd != true && PB.encode(e, 49, x.count_assumption_levels_in_lbd) x.presolve_bve_threshold != Int32(500) && PB.encode(e, 54, x.presolve_bve_threshold) + x.filter_sat_postsolve_clauses != false && PB.encode(e, 324, x.filter_sat_postsolve_clauses) x.presolve_bve_clause_weight != Int32(3) && PB.encode(e, 55, x.presolve_bve_clause_weight) - x.probing_deterministic_time_limit != Float64(1) && PB.encode(e, 226, x.probing_deterministic_time_limit) - x.presolve_probing_deterministic_time_limit != Float64(30.0) && PB.encode(e, 57, x.presolve_probing_deterministic_time_limit) + x.probing_deterministic_time_limit !== Float64(1.0) && PB.encode(e, 226, x.probing_deterministic_time_limit) + x.presolve_probing_deterministic_time_limit !== Float64(30.0) && PB.encode(e, 57, x.presolve_probing_deterministic_time_limit) x.presolve_blocked_clause != true && PB.encode(e, 88, x.presolve_blocked_clause) x.presolve_use_bva != true && PB.encode(e, 72, x.presolve_use_bva) x.presolve_bva_threshold != Int32(1) && PB.encode(e, 73, x.presolve_bva_threshold) @@ -1007,113 +1227,128 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SatParameters) x.cp_model_presolve != true && PB.encode(e, 86, x.cp_model_presolve) x.cp_model_probing_level != Int32(2) && PB.encode(e, 110, x.cp_model_probing_level) x.cp_model_use_sat_presolve != true && PB.encode(e, 93, x.cp_model_use_sat_presolve) - x.use_sat_inprocessing != false && PB.encode(e, 163, x.use_sat_inprocessing) + x.remove_fixed_variables_early != true && PB.encode(e, 310, x.remove_fixed_variables_early) x.detect_table_with_cost != false && PB.encode(e, 216, x.detect_table_with_cost) x.table_compression_level != Int32(2) && PB.encode(e, 217, x.table_compression_level) x.expand_alldiff_constraints != false && PB.encode(e, 170, x.expand_alldiff_constraints) + x.max_alldiff_domain_size != Int32(256) && PB.encode(e, 320, x.max_alldiff_domain_size) x.expand_reservoir_constraints != true && PB.encode(e, 182, x.expand_reservoir_constraints) + x.expand_reservoir_using_circuit != false && PB.encode(e, 288, x.expand_reservoir_using_circuit) + x.encode_cumulative_as_reservoir != false && PB.encode(e, 287, x.encode_cumulative_as_reservoir) + x.max_lin_max_size_for_expansion != Int32(0) && PB.encode(e, 280, x.max_lin_max_size_for_expansion) x.disable_constraint_expansion != false && PB.encode(e, 181, x.disable_constraint_expansion) x.encode_complex_linear_constraint_with_integer != false && PB.encode(e, 223, x.encode_complex_linear_constraint_with_integer) - x.merge_no_overlap_work_limit != Float64(1e12) && PB.encode(e, 145, x.merge_no_overlap_work_limit) - x.merge_at_most_one_work_limit != Float64(1e8) && PB.encode(e, 146, x.merge_at_most_one_work_limit) + x.merge_no_overlap_work_limit !== Float64(1e12) && PB.encode(e, 145, x.merge_no_overlap_work_limit) + x.merge_at_most_one_work_limit !== Float64(1e8) && PB.encode(e, 146, x.merge_at_most_one_work_limit) x.presolve_substitution_level != Int32(1) && PB.encode(e, 147, x.presolve_substitution_level) x.presolve_extract_integer_enforcement != false && PB.encode(e, 174, x.presolve_extract_integer_enforcement) x.presolve_inclusion_work_limit != Int64(100000000) && PB.encode(e, 201, x.presolve_inclusion_work_limit) x.ignore_names != true && PB.encode(e, 202, x.ignore_names) x.infer_all_diffs != true && PB.encode(e, 233, x.infer_all_diffs) x.find_big_linear_overlap != true && PB.encode(e, 234, x.find_big_linear_overlap) + x.use_sat_inprocessing != true && PB.encode(e, 163, x.use_sat_inprocessing) + x.inprocessing_dtime_ratio !== Float64(0.2) && PB.encode(e, 273, x.inprocessing_dtime_ratio) + x.inprocessing_probing_dtime !== Float64(1.0) && PB.encode(e, 274, x.inprocessing_probing_dtime) + x.inprocessing_minimization_dtime !== Float64(1.0) && PB.encode(e, 275, x.inprocessing_minimization_dtime) + x.inprocessing_minimization_use_conflict_analysis != true && PB.encode(e, 297, x.inprocessing_minimization_use_conflict_analysis) + x.inprocessing_minimization_use_all_orderings != false && PB.encode(e, 298, x.inprocessing_minimization_use_all_orderings) x.num_workers != Int32(0) && PB.encode(e, 206, x.num_workers) x.num_search_workers != Int32(0) && PB.encode(e, 100, x.num_search_workers) - x.min_num_lns_workers != Int32(2) && PB.encode(e, 211, x.min_num_lns_workers) + x.num_full_subsolvers != Int32(0) && PB.encode(e, 294, x.num_full_subsolvers) !isempty(x.subsolvers) && PB.encode(e, 207, x.subsolvers) !isempty(x.extra_subsolvers) && PB.encode(e, 219, x.extra_subsolvers) !isempty(x.ignore_subsolvers) && PB.encode(e, 209, x.ignore_subsolvers) + !isempty(x.filter_subsolvers) && PB.encode(e, 293, x.filter_subsolvers) !isempty(x.subsolver_params) && PB.encode(e, 210, x.subsolver_params) x.interleave_search != false && PB.encode(e, 136, x.interleave_search) x.interleave_batch_size != Int32(0) && PB.encode(e, 134, x.interleave_batch_size) x.share_objective_bounds != true && PB.encode(e, 113, x.share_objective_bounds) x.share_level_zero_bounds != true && PB.encode(e, 114, x.share_level_zero_bounds) x.share_binary_clauses != true && PB.encode(e, 203, x.share_binary_clauses) + x.share_glue_clauses != false && PB.encode(e, 285, x.share_glue_clauses) + x.minimize_shared_clauses != true && PB.encode(e, 300, x.minimize_shared_clauses) + x.share_glue_clauses_dtime !== Float64(1.0) && PB.encode(e, 322, x.share_glue_clauses_dtime) x.debug_postsolve_with_full_solver != false && PB.encode(e, 162, x.debug_postsolve_with_full_solver) x.debug_max_num_presolve_operations != Int32(0) && PB.encode(e, 151, x.debug_max_num_presolve_operations) x.debug_crash_on_bad_hint != false && PB.encode(e, 195, x.debug_crash_on_bad_hint) + x.debug_crash_if_presolve_breaks_hint != false && PB.encode(e, 306, x.debug_crash_if_presolve_breaks_hint) x.use_optimization_hints != true && PB.encode(e, 35, x.use_optimization_hints) - x.minimize_core != true && PB.encode(e, 50, x.minimize_core) + x.core_minimization_level != Int32(2) && PB.encode(e, 50, x.core_minimization_level) x.find_multiple_cores != true && PB.encode(e, 84, x.find_multiple_cores) x.cover_optimization != true && PB.encode(e, 89, x.cover_optimization) x.max_sat_assumption_order != var"SatParameters.MaxSatAssumptionOrder".DEFAULT_ASSUMPTION_ORDER && PB.encode(e, 51, x.max_sat_assumption_order) x.max_sat_reverse_assumption_order != false && PB.encode(e, 52, x.max_sat_reverse_assumption_order) x.max_sat_stratification != var"SatParameters.MaxSatStratificationAlgorithm".STRATIFICATION_DESCENT && PB.encode(e, 53, x.max_sat_stratification) - x.propagation_loop_detection_factor != Float64(10.0) && PB.encode(e, 221, x.propagation_loop_detection_factor) + x.propagation_loop_detection_factor !== Float64(10.0) && PB.encode(e, 221, x.propagation_loop_detection_factor) x.use_precedences_in_disjunctive_constraint != true && PB.encode(e, 74, x.use_precedences_in_disjunctive_constraint) x.max_size_to_create_precedence_literals_in_disjunctive != Int32(60) && PB.encode(e, 229, x.max_size_to_create_precedence_literals_in_disjunctive) x.use_strong_propagation_in_disjunctive != false && PB.encode(e, 230, x.use_strong_propagation_in_disjunctive) + x.use_dynamic_precedence_in_disjunctive != false && PB.encode(e, 263, x.use_dynamic_precedence_in_disjunctive) + x.use_dynamic_precedence_in_cumulative != false && PB.encode(e, 268, x.use_dynamic_precedence_in_cumulative) x.use_overload_checker_in_cumulative != false && PB.encode(e, 78, x.use_overload_checker_in_cumulative) + x.use_conservative_scale_overload_checker != false && PB.encode(e, 286, x.use_conservative_scale_overload_checker) x.use_timetable_edge_finding_in_cumulative != false && PB.encode(e, 79, x.use_timetable_edge_finding_in_cumulative) + x.max_num_intervals_for_timetable_edge_finding != Int32(100) && PB.encode(e, 260, x.max_num_intervals_for_timetable_edge_finding) x.use_hard_precedences_in_cumulative != false && PB.encode(e, 215, x.use_hard_precedences_in_cumulative) x.exploit_all_precedences != false && PB.encode(e, 220, x.exploit_all_precedences) x.use_disjunctive_constraint_in_cumulative != true && PB.encode(e, 80, x.use_disjunctive_constraint_in_cumulative) + x.no_overlap_2d_boolean_relations_limit != Int32(10) && PB.encode(e, 321, x.no_overlap_2d_boolean_relations_limit) x.use_timetabling_in_no_overlap_2d != false && PB.encode(e, 200, x.use_timetabling_in_no_overlap_2d) x.use_energetic_reasoning_in_no_overlap_2d != false && PB.encode(e, 213, x.use_energetic_reasoning_in_no_overlap_2d) - x.use_pairwise_reasoning_in_no_overlap_2d != false && PB.encode(e, 251, x.use_pairwise_reasoning_in_no_overlap_2d) + x.use_area_energetic_reasoning_in_no_overlap_2d != false && PB.encode(e, 271, x.use_area_energetic_reasoning_in_no_overlap_2d) + x.use_try_edge_reasoning_in_no_overlap_2d != false && PB.encode(e, 299, x.use_try_edge_reasoning_in_no_overlap_2d) + x.max_pairs_pairwise_reasoning_in_no_overlap_2d != Int32(1250) && PB.encode(e, 276, x.max_pairs_pairwise_reasoning_in_no_overlap_2d) + x.maximum_regions_to_split_in_disconnected_no_overlap_2d != Int32(0) && PB.encode(e, 315, x.maximum_regions_to_split_in_disconnected_no_overlap_2d) + x.use_linear3_for_no_overlap_2d_precedences != true && PB.encode(e, 323, x.use_linear3_for_no_overlap_2d_precedences) x.use_dual_scheduling_heuristics != true && PB.encode(e, 214, x.use_dual_scheduling_heuristics) - x.linearization_level != Int32(1) && PB.encode(e, 90, x.linearization_level) - x.boolean_encoding_level != Int32(1) && PB.encode(e, 107, x.boolean_encoding_level) - x.max_domain_size_when_encoding_eq_neq_constraints != Int32(16) && PB.encode(e, 191, x.max_domain_size_when_encoding_eq_neq_constraints) - x.max_num_cuts != Int32(10000) && PB.encode(e, 91, x.max_num_cuts) - x.cut_level != Int32(1) && PB.encode(e, 196, x.cut_level) - x.only_add_cuts_at_level_zero != false && PB.encode(e, 92, x.only_add_cuts_at_level_zero) - x.add_objective_cut != false && PB.encode(e, 197, x.add_objective_cut) - x.add_cg_cuts != true && PB.encode(e, 117, x.add_cg_cuts) - x.add_mir_cuts != true && PB.encode(e, 120, x.add_mir_cuts) - x.add_zero_half_cuts != true && PB.encode(e, 169, x.add_zero_half_cuts) - x.add_clique_cuts != true && PB.encode(e, 172, x.add_clique_cuts) - x.max_all_diff_cut_size != Int32(64) && PB.encode(e, 148, x.max_all_diff_cut_size) - x.add_lin_max_cuts != true && PB.encode(e, 152, x.add_lin_max_cuts) - x.max_integer_rounding_scaling != Int32(600) && PB.encode(e, 119, x.max_integer_rounding_scaling) - x.add_lp_constraints_lazily != true && PB.encode(e, 112, x.add_lp_constraints_lazily) - x.root_lp_iterations != Int32(2000) && PB.encode(e, 227, x.root_lp_iterations) - x.min_orthogonality_for_lp_constraints != Float64(0.05) && PB.encode(e, 115, x.min_orthogonality_for_lp_constraints) - x.max_cut_rounds_at_level_zero != Int32(1) && PB.encode(e, 154, x.max_cut_rounds_at_level_zero) - x.max_consecutive_inactive_count != Int32(100) && PB.encode(e, 121, x.max_consecutive_inactive_count) - x.cut_max_active_count_value != Float64(1e10) && PB.encode(e, 155, x.cut_max_active_count_value) - x.cut_active_count_decay != Float64(0.8) && PB.encode(e, 156, x.cut_active_count_decay) - x.cut_cleanup_target != Int32(1000) && PB.encode(e, 157, x.cut_cleanup_target) - x.new_constraints_batch_size != Int32(50) && PB.encode(e, 122, x.new_constraints_batch_size) + x.use_all_different_for_circuit != false && PB.encode(e, 311, x.use_all_different_for_circuit) + x.routing_cut_subset_size_for_binary_relation_bound != Int32(0) && PB.encode(e, 312, x.routing_cut_subset_size_for_binary_relation_bound) + x.routing_cut_subset_size_for_tight_binary_relation_bound != Int32(0) && PB.encode(e, 313, x.routing_cut_subset_size_for_tight_binary_relation_bound) + x.routing_cut_subset_size_for_exact_binary_relation_bound != Int32(8) && PB.encode(e, 316, x.routing_cut_subset_size_for_exact_binary_relation_bound) + x.routing_cut_subset_size_for_shortest_paths_bound != Int32(8) && PB.encode(e, 318, x.routing_cut_subset_size_for_shortest_paths_bound) + x.routing_cut_dp_effort !== Float64(1e7) && PB.encode(e, 314, x.routing_cut_dp_effort) + x.routing_cut_max_infeasible_path_length != Int32(6) && PB.encode(e, 317, x.routing_cut_max_infeasible_path_length) x.search_branching != var"SatParameters.SearchBranching".AUTOMATIC_SEARCH && PB.encode(e, 82, x.search_branching) x.hint_conflict_limit != Int32(10) && PB.encode(e, 153, x.hint_conflict_limit) x.repair_hint != false && PB.encode(e, 167, x.repair_hint) x.fix_variables_to_their_hinted_value != false && PB.encode(e, 192, x.fix_variables_to_their_hinted_value) - x.exploit_integer_lp_solution != true && PB.encode(e, 94, x.exploit_integer_lp_solution) - x.exploit_all_lp_solution != true && PB.encode(e, 116, x.exploit_all_lp_solution) - x.exploit_best_solution != false && PB.encode(e, 130, x.exploit_best_solution) - x.exploit_relaxation_solution != false && PB.encode(e, 161, x.exploit_relaxation_solution) - x.exploit_objective != true && PB.encode(e, 131, x.exploit_objective) - x.probing_period_at_root != Int64(0) && PB.encode(e, 142, x.probing_period_at_root) x.use_probing_search != false && PB.encode(e, 176, x.use_probing_search) - x.shaving_deterministic_time_in_probing_search != Float64(0.001) && PB.encode(e, 204, x.shaving_deterministic_time_in_probing_search) - x.shaving_search_deterministic_time != Float64(0.1) && PB.encode(e, 205, x.shaving_search_deterministic_time) + x.use_extended_probing != true && PB.encode(e, 269, x.use_extended_probing) + x.probing_num_combinations_limit != Int32(20000) && PB.encode(e, 272, x.probing_num_combinations_limit) + x.shaving_deterministic_time_in_probing_search !== Float64(0.001) && PB.encode(e, 204, x.shaving_deterministic_time_in_probing_search) + x.shaving_search_deterministic_time !== Float64(0.1) && PB.encode(e, 205, x.shaving_search_deterministic_time) + x.shaving_search_threshold != Int64(64) && PB.encode(e, 290, x.shaving_search_threshold) x.use_objective_lb_search != false && PB.encode(e, 228, x.use_objective_lb_search) x.use_objective_shaving_search != false && PB.encode(e, 253, x.use_objective_shaving_search) + x.variables_shaving_level != Int32(-1) && PB.encode(e, 289, x.variables_shaving_level) x.pseudo_cost_reliability_threshold != Int64(100) && PB.encode(e, 123, x.pseudo_cost_reliability_threshold) x.optimize_with_core != false && PB.encode(e, 83, x.optimize_with_core) x.optimize_with_lb_tree_search != false && PB.encode(e, 188, x.optimize_with_lb_tree_search) + x.save_lp_basis_in_lb_tree_search != false && PB.encode(e, 284, x.save_lp_basis_in_lb_tree_search) x.binary_search_num_conflicts != Int32(-1) && PB.encode(e, 99, x.binary_search_num_conflicts) x.optimize_with_max_hs != false && PB.encode(e, 85, x.optimize_with_max_hs) - x.test_feasibility_jump != false && PB.encode(e, 240, x.test_feasibility_jump) - x.feasibility_jump_max_num_values_scanned != Int64(4096) && PB.encode(e, 245, x.feasibility_jump_max_num_values_scanned) - x.feasibility_jump_protect_linear_feasibility != true && PB.encode(e, 246, x.feasibility_jump_protect_linear_feasibility) - x.feasibility_jump_decay != Float64(1.0) && PB.encode(e, 242, x.feasibility_jump_decay) - x.feasibility_jump_var_randomization_probability != Float64(0.0) && PB.encode(e, 247, x.feasibility_jump_var_randomization_probability) - x.feasibility_jump_var_perburbation_range_ratio != Float64(0.2) && PB.encode(e, 248, x.feasibility_jump_var_perburbation_range_ratio) + x.use_feasibility_jump != true && PB.encode(e, 265, x.use_feasibility_jump) + x.use_ls_only != false && PB.encode(e, 240, x.use_ls_only) + x.feasibility_jump_decay !== Float64(0.95) && PB.encode(e, 242, x.feasibility_jump_decay) + x.feasibility_jump_linearization_level != Int32(2) && PB.encode(e, 257, x.feasibility_jump_linearization_level) + x.feasibility_jump_restart_factor != Int32(1) && PB.encode(e, 258, x.feasibility_jump_restart_factor) + x.feasibility_jump_batch_dtime !== Float64(0.1) && PB.encode(e, 292, x.feasibility_jump_batch_dtime) + x.feasibility_jump_var_randomization_probability !== Float64(0.05) && PB.encode(e, 247, x.feasibility_jump_var_randomization_probability) + x.feasibility_jump_var_perburbation_range_ratio !== Float64(0.2) && PB.encode(e, 248, x.feasibility_jump_var_perburbation_range_ratio) x.feasibility_jump_enable_restarts != true && PB.encode(e, 250, x.feasibility_jump_enable_restarts) + x.feasibility_jump_max_expanded_constraint_size != Int32(500) && PB.encode(e, 264, x.feasibility_jump_max_expanded_constraint_size) x.num_violation_ls != Int32(0) && PB.encode(e, 244, x.num_violation_ls) x.violation_ls_perturbation_period != Int32(100) && PB.encode(e, 249, x.violation_ls_perturbation_period) + x.violation_ls_compound_move_probability !== Float64(0.5) && PB.encode(e, 259, x.violation_ls_compound_move_probability) x.shared_tree_num_workers != Int32(0) && PB.encode(e, 235, x.shared_tree_num_workers) x.use_shared_tree_search != false && PB.encode(e, 236, x.use_shared_tree_search) - x.shared_tree_worker_objective_split_probability != Float64(0.5) && PB.encode(e, 237, x.shared_tree_worker_objective_split_probability) - x.shared_tree_max_nodes_per_worker != Int32(128) && PB.encode(e, 238, x.shared_tree_max_nodes_per_worker) + x.shared_tree_worker_min_restarts_per_subtree != Int32(1) && PB.encode(e, 282, x.shared_tree_worker_min_restarts_per_subtree) + x.shared_tree_worker_enable_trail_sharing != true && PB.encode(e, 295, x.shared_tree_worker_enable_trail_sharing) + x.shared_tree_worker_enable_phase_sharing != true && PB.encode(e, 304, x.shared_tree_worker_enable_phase_sharing) + x.shared_tree_open_leaves_per_worker !== Float64(2.0) && PB.encode(e, 281, x.shared_tree_open_leaves_per_worker) + x.shared_tree_max_nodes_per_worker != Int32(10000) && PB.encode(e, 238, x.shared_tree_max_nodes_per_worker) x.shared_tree_split_strategy != var"SatParameters.SharedTreeSplitStrategy".SPLIT_STRATEGY_AUTO && PB.encode(e, 239, x.shared_tree_split_strategy) + x.shared_tree_balance_tolerance != Int32(1) && PB.encode(e, 305, x.shared_tree_balance_tolerance) x.enumerate_all_solutions != false && PB.encode(e, 87, x.enumerate_all_solutions) x.keep_all_feasible_solutions_in_presolve != false && PB.encode(e, 173, x.keep_all_feasible_solutions_in_presolve) x.fill_tightened_domains_in_response != false && PB.encode(e, 132, x.fill_tightened_domains_in_response) @@ -1123,37 +1358,79 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::SatParameters) x.stop_after_first_solution != false && PB.encode(e, 98, x.stop_after_first_solution) x.stop_after_presolve != false && PB.encode(e, 149, x.stop_after_presolve) x.stop_after_root_propagation != false && PB.encode(e, 252, x.stop_after_root_propagation) + x.lns_initial_difficulty !== Float64(0.5) && PB.encode(e, 307, x.lns_initial_difficulty) + x.lns_initial_deterministic_limit !== Float64(0.1) && PB.encode(e, 308, x.lns_initial_deterministic_limit) + x.use_lns != true && PB.encode(e, 283, x.use_lns) x.use_lns_only != false && PB.encode(e, 101, x.use_lns_only) x.solution_pool_size != Int32(3) && PB.encode(e, 193, x.solution_pool_size) x.use_rins_lns != true && PB.encode(e, 129, x.use_rins_lns) x.use_feasibility_pump != true && PB.encode(e, 164, x.use_feasibility_pump) - x.use_lb_relax_lns != false && PB.encode(e, 255, x.use_lb_relax_lns) + x.use_lb_relax_lns != true && PB.encode(e, 255, x.use_lb_relax_lns) + x.lb_relax_num_workers_threshold != Int32(16) && PB.encode(e, 296, x.lb_relax_num_workers_threshold) x.fp_rounding != var"SatParameters.FPRoundingMethod".PROPAGATION_ASSISTED && PB.encode(e, 165, x.fp_rounding) x.diversify_lns_params != false && PB.encode(e, 137, x.diversify_lns_params) x.randomize_search != false && PB.encode(e, 103, x.randomize_search) - x.search_randomization_tolerance != Int64(0) && PB.encode(e, 104, x.search_randomization_tolerance) + x.search_random_variable_pool_size != Int64(0) && PB.encode(e, 104, x.search_random_variable_pool_size) + x.push_all_tasks_toward_start != false && PB.encode(e, 262, x.push_all_tasks_toward_start) x.use_optional_variables != false && PB.encode(e, 108, x.use_optional_variables) x.use_exact_lp_reason != true && PB.encode(e, 109, x.use_exact_lp_reason) - x.use_branching_in_lp != false && PB.encode(e, 139, x.use_branching_in_lp) x.use_combined_no_overlap != false && PB.encode(e, 133, x.use_combined_no_overlap) + x.at_most_one_max_expansion_size != Int32(3) && PB.encode(e, 270, x.at_most_one_max_expansion_size) x.catch_sigint_signal != true && PB.encode(e, 135, x.catch_sigint_signal) x.use_implied_bounds != true && PB.encode(e, 144, x.use_implied_bounds) x.polish_lp_solution != false && PB.encode(e, 175, x.polish_lp_solution) + x.lp_primal_tolerance !== Float64(1e-7) && PB.encode(e, 266, x.lp_primal_tolerance) + x.lp_dual_tolerance !== Float64(1e-7) && PB.encode(e, 267, x.lp_dual_tolerance) x.convert_intervals != true && PB.encode(e, 177, x.convert_intervals) x.symmetry_level != Int32(2) && PB.encode(e, 183, x.symmetry_level) - x.new_linear_propagation != false && PB.encode(e, 224, x.new_linear_propagation) + x.use_symmetry_in_lp != false && PB.encode(e, 301, x.use_symmetry_in_lp) + x.keep_symmetry_in_presolve != false && PB.encode(e, 303, x.keep_symmetry_in_presolve) + x.symmetry_detection_deterministic_time_limit !== Float64(1.0) && PB.encode(e, 302, x.symmetry_detection_deterministic_time_limit) + x.new_linear_propagation != true && PB.encode(e, 224, x.new_linear_propagation) x.linear_split_size != Int32(100) && PB.encode(e, 256, x.linear_split_size) - x.mip_max_bound != Float64(1e7) && PB.encode(e, 124, x.mip_max_bound) - x.mip_var_scaling != Float64(1.0) && PB.encode(e, 125, x.mip_var_scaling) + x.linearization_level != Int32(1) && PB.encode(e, 90, x.linearization_level) + x.boolean_encoding_level != Int32(1) && PB.encode(e, 107, x.boolean_encoding_level) + x.max_domain_size_when_encoding_eq_neq_constraints != Int32(16) && PB.encode(e, 191, x.max_domain_size_when_encoding_eq_neq_constraints) + x.max_num_cuts != Int32(10000) && PB.encode(e, 91, x.max_num_cuts) + x.cut_level != Int32(1) && PB.encode(e, 196, x.cut_level) + x.only_add_cuts_at_level_zero != false && PB.encode(e, 92, x.only_add_cuts_at_level_zero) + x.add_objective_cut != false && PB.encode(e, 197, x.add_objective_cut) + x.add_cg_cuts != true && PB.encode(e, 117, x.add_cg_cuts) + x.add_mir_cuts != true && PB.encode(e, 120, x.add_mir_cuts) + x.add_zero_half_cuts != true && PB.encode(e, 169, x.add_zero_half_cuts) + x.add_clique_cuts != true && PB.encode(e, 172, x.add_clique_cuts) + x.add_rlt_cuts != true && PB.encode(e, 279, x.add_rlt_cuts) + x.max_all_diff_cut_size != Int32(64) && PB.encode(e, 148, x.max_all_diff_cut_size) + x.add_lin_max_cuts != true && PB.encode(e, 152, x.add_lin_max_cuts) + x.max_integer_rounding_scaling != Int32(600) && PB.encode(e, 119, x.max_integer_rounding_scaling) + x.add_lp_constraints_lazily != true && PB.encode(e, 112, x.add_lp_constraints_lazily) + x.root_lp_iterations != Int32(2000) && PB.encode(e, 227, x.root_lp_iterations) + x.min_orthogonality_for_lp_constraints !== Float64(0.05) && PB.encode(e, 115, x.min_orthogonality_for_lp_constraints) + x.max_cut_rounds_at_level_zero != Int32(1) && PB.encode(e, 154, x.max_cut_rounds_at_level_zero) + x.max_consecutive_inactive_count != Int32(100) && PB.encode(e, 121, x.max_consecutive_inactive_count) + x.cut_max_active_count_value !== Float64(1e10) && PB.encode(e, 155, x.cut_max_active_count_value) + x.cut_active_count_decay !== Float64(0.8) && PB.encode(e, 156, x.cut_active_count_decay) + x.cut_cleanup_target != Int32(1000) && PB.encode(e, 157, x.cut_cleanup_target) + x.new_constraints_batch_size != Int32(50) && PB.encode(e, 122, x.new_constraints_batch_size) + x.exploit_integer_lp_solution != true && PB.encode(e, 94, x.exploit_integer_lp_solution) + x.exploit_all_lp_solution != true && PB.encode(e, 116, x.exploit_all_lp_solution) + x.exploit_best_solution != false && PB.encode(e, 130, x.exploit_best_solution) + x.exploit_relaxation_solution != false && PB.encode(e, 161, x.exploit_relaxation_solution) + x.exploit_objective != true && PB.encode(e, 131, x.exploit_objective) + x.detect_linearized_product != false && PB.encode(e, 277, x.detect_linearized_product) + x.mip_max_bound !== Float64(1e7) && PB.encode(e, 124, x.mip_max_bound) + x.mip_var_scaling !== Float64(1.0) && PB.encode(e, 125, x.mip_var_scaling) x.mip_scale_large_domain != false && PB.encode(e, 225, x.mip_scale_large_domain) x.mip_automatically_scale_variables != true && PB.encode(e, 166, x.mip_automatically_scale_variables) x.only_solve_ip != false && PB.encode(e, 222, x.only_solve_ip) - x.mip_wanted_precision != Float64(1e-6) && PB.encode(e, 126, x.mip_wanted_precision) + x.mip_wanted_precision !== Float64(1e-6) && PB.encode(e, 126, x.mip_wanted_precision) x.mip_max_activity_exponent != Int32(53) && PB.encode(e, 127, x.mip_max_activity_exponent) - x.mip_check_precision != Float64(1e-4) && PB.encode(e, 128, x.mip_check_precision) + x.mip_check_precision !== Float64(1e-4) && PB.encode(e, 128, x.mip_check_precision) x.mip_compute_true_objective_bound != true && PB.encode(e, 198, x.mip_compute_true_objective_bound) - x.mip_max_valid_magnitude != Float64(1e30) && PB.encode(e, 199, x.mip_max_valid_magnitude) - x.mip_drop_tolerance != Float64(1e-16) && PB.encode(e, 232, x.mip_drop_tolerance) + x.mip_max_valid_magnitude !== Float64(1e20) && PB.encode(e, 199, x.mip_max_valid_magnitude) + x.mip_treat_high_magnitude_bounds_as_infinity != false && PB.encode(e, 278, x.mip_treat_high_magnitude_bounds_as_infinity) + x.mip_drop_tolerance !== Float64(1e-16) && PB.encode(e, 232, x.mip_drop_tolerance) + x.mip_presolve_level != Int32(2) && PB.encode(e, 261, x.mip_presolve_level) return position(e.io) - initpos end function PB._encoded_size(x::SatParameters) @@ -1163,55 +1440,53 @@ function PB._encoded_size(x::SatParameters) x.initial_polarity != var"SatParameters.Polarity".POLARITY_FALSE && (encoded_size += PB._encoded_size(x.initial_polarity, 2)) x.use_phase_saving != true && (encoded_size += PB._encoded_size(x.use_phase_saving, 44)) x.polarity_rephase_increment != Int32(1000) && (encoded_size += PB._encoded_size(x.polarity_rephase_increment, 168)) - x.random_polarity_ratio != Float64(0.0) && (encoded_size += PB._encoded_size(x.random_polarity_ratio, 45)) - x.random_branches_ratio != Float64(0.0) && (encoded_size += PB._encoded_size(x.random_branches_ratio, 32)) + x.polarity_exploit_ls_hints != false && (encoded_size += PB._encoded_size(x.polarity_exploit_ls_hints, 309)) + x.random_polarity_ratio !== Float64(0.0) && (encoded_size += PB._encoded_size(x.random_polarity_ratio, 45)) + x.random_branches_ratio !== Float64(0.0) && (encoded_size += PB._encoded_size(x.random_branches_ratio, 32)) x.use_erwa_heuristic != false && (encoded_size += PB._encoded_size(x.use_erwa_heuristic, 75)) - x.initial_variables_activity != Float64(0.0) && (encoded_size += PB._encoded_size(x.initial_variables_activity, 76)) + x.initial_variables_activity !== Float64(0.0) && (encoded_size += PB._encoded_size(x.initial_variables_activity, 76)) x.also_bump_variables_in_conflict_reasons != false && (encoded_size += PB._encoded_size(x.also_bump_variables_in_conflict_reasons, 77)) x.minimization_algorithm != var"SatParameters.ConflictMinimizationAlgorithm".RECURSIVE && (encoded_size += PB._encoded_size(x.minimization_algorithm, 4)) x.binary_minimization_algorithm != var"SatParameters.BinaryMinizationAlgorithm".BINARY_MINIMIZATION_FIRST && (encoded_size += PB._encoded_size(x.binary_minimization_algorithm, 34)) x.subsumption_during_conflict_analysis != true && (encoded_size += PB._encoded_size(x.subsumption_during_conflict_analysis, 56)) x.clause_cleanup_period != Int32(10000) && (encoded_size += PB._encoded_size(x.clause_cleanup_period, 11)) x.clause_cleanup_target != Int32(0) && (encoded_size += PB._encoded_size(x.clause_cleanup_target, 13)) - x.clause_cleanup_ratio != Float64(0.5) && (encoded_size += PB._encoded_size(x.clause_cleanup_ratio, 190)) + x.clause_cleanup_ratio !== Float64(0.5) && (encoded_size += PB._encoded_size(x.clause_cleanup_ratio, 190)) x.clause_cleanup_protection != var"SatParameters.ClauseProtection".PROTECTION_NONE && (encoded_size += PB._encoded_size(x.clause_cleanup_protection, 58)) x.clause_cleanup_lbd_bound != Int32(5) && (encoded_size += PB._encoded_size(x.clause_cleanup_lbd_bound, 59)) x.clause_cleanup_ordering != var"SatParameters.ClauseOrdering".CLAUSE_ACTIVITY && (encoded_size += PB._encoded_size(x.clause_cleanup_ordering, 60)) x.pb_cleanup_increment != Int32(200) && (encoded_size += PB._encoded_size(x.pb_cleanup_increment, 46)) - x.pb_cleanup_ratio != Float64(0.5) && (encoded_size += PB._encoded_size(x.pb_cleanup_ratio, 47)) - x.minimize_with_propagation_restart_period != Int32(10) && (encoded_size += PB._encoded_size(x.minimize_with_propagation_restart_period, 96)) - x.minimize_with_propagation_num_decisions != Int32(1000) && (encoded_size += PB._encoded_size(x.minimize_with_propagation_num_decisions, 97)) - x.variable_activity_decay != Float64(0.8) && (encoded_size += PB._encoded_size(x.variable_activity_decay, 15)) - x.max_variable_activity_value != Float64(1e100) && (encoded_size += PB._encoded_size(x.max_variable_activity_value, 16)) - x.glucose_max_decay != Float64(0.95) && (encoded_size += PB._encoded_size(x.glucose_max_decay, 22)) - x.glucose_decay_increment != Float64(0.01) && (encoded_size += PB._encoded_size(x.glucose_decay_increment, 23)) + x.pb_cleanup_ratio !== Float64(0.5) && (encoded_size += PB._encoded_size(x.pb_cleanup_ratio, 47)) + x.variable_activity_decay !== Float64(0.8) && (encoded_size += PB._encoded_size(x.variable_activity_decay, 15)) + x.max_variable_activity_value !== Float64(1e100) && (encoded_size += PB._encoded_size(x.max_variable_activity_value, 16)) + x.glucose_max_decay !== Float64(0.95) && (encoded_size += PB._encoded_size(x.glucose_max_decay, 22)) + x.glucose_decay_increment !== Float64(0.01) && (encoded_size += PB._encoded_size(x.glucose_decay_increment, 23)) x.glucose_decay_increment_period != Int32(5000) && (encoded_size += PB._encoded_size(x.glucose_decay_increment_period, 24)) - x.clause_activity_decay != Float64(0.999) && (encoded_size += PB._encoded_size(x.clause_activity_decay, 17)) - x.max_clause_activity_value != Float64(1e20) && (encoded_size += PB._encoded_size(x.max_clause_activity_value, 18)) + x.clause_activity_decay !== Float64(0.999) && (encoded_size += PB._encoded_size(x.clause_activity_decay, 17)) + x.max_clause_activity_value !== Float64(1e20) && (encoded_size += PB._encoded_size(x.max_clause_activity_value, 18)) !isempty(x.restart_algorithms) && (encoded_size += PB._encoded_size(x.restart_algorithms, 61)) x.default_restart_algorithms != "LUBY_RESTART,LBD_MOVING_AVERAGE_RESTART,DL_MOVING_AVERAGE_RESTART" && (encoded_size += PB._encoded_size(x.default_restart_algorithms, 70)) x.restart_period != Int32(50) && (encoded_size += PB._encoded_size(x.restart_period, 30)) x.restart_running_window_size != Int32(50) && (encoded_size += PB._encoded_size(x.restart_running_window_size, 62)) - x.restart_dl_average_ratio != Float64(1.0) && (encoded_size += PB._encoded_size(x.restart_dl_average_ratio, 63)) - x.restart_lbd_average_ratio != Float64(1.0) && (encoded_size += PB._encoded_size(x.restart_lbd_average_ratio, 71)) + x.restart_dl_average_ratio !== Float64(1.0) && (encoded_size += PB._encoded_size(x.restart_dl_average_ratio, 63)) + x.restart_lbd_average_ratio !== Float64(1.0) && (encoded_size += PB._encoded_size(x.restart_lbd_average_ratio, 71)) x.use_blocking_restart != false && (encoded_size += PB._encoded_size(x.use_blocking_restart, 64)) x.blocking_restart_window_size != Int32(5000) && (encoded_size += PB._encoded_size(x.blocking_restart_window_size, 65)) - x.blocking_restart_multiplier != Float64(1.4) && (encoded_size += PB._encoded_size(x.blocking_restart_multiplier, 66)) + x.blocking_restart_multiplier !== Float64(1.4) && (encoded_size += PB._encoded_size(x.blocking_restart_multiplier, 66)) x.num_conflicts_before_strategy_changes != Int32(0) && (encoded_size += PB._encoded_size(x.num_conflicts_before_strategy_changes, 68)) - x.strategy_change_increase_ratio != Float64(0.0) && (encoded_size += PB._encoded_size(x.strategy_change_increase_ratio, 69)) - x.max_time_in_seconds != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 36)) - x.max_deterministic_time != Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 67)) + x.strategy_change_increase_ratio !== Float64(0.0) && (encoded_size += PB._encoded_size(x.strategy_change_increase_ratio, 69)) + x.max_time_in_seconds !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_time_in_seconds, 36)) + x.max_deterministic_time !== Float64(Inf) && (encoded_size += PB._encoded_size(x.max_deterministic_time, 67)) + x.max_num_deterministic_batches != Int32(0) && (encoded_size += PB._encoded_size(x.max_num_deterministic_batches, 291)) x.max_number_of_conflicts != Int64(9223372036854775807) && (encoded_size += PB._encoded_size(x.max_number_of_conflicts, 37)) x.max_memory_in_mb != Int64(10000) && (encoded_size += PB._encoded_size(x.max_memory_in_mb, 40)) - x.absolute_gap_limit != Float64(1e-4) && (encoded_size += PB._encoded_size(x.absolute_gap_limit, 159)) - x.relative_gap_limit != Float64(0.0) && (encoded_size += PB._encoded_size(x.relative_gap_limit, 160)) + x.absolute_gap_limit !== Float64(1e-4) && (encoded_size += PB._encoded_size(x.absolute_gap_limit, 159)) + x.relative_gap_limit !== Float64(0.0) && (encoded_size += PB._encoded_size(x.relative_gap_limit, 160)) x.random_seed != Int32(1) && (encoded_size += PB._encoded_size(x.random_seed, 31)) x.permute_variable_randomly != false && (encoded_size += PB._encoded_size(x.permute_variable_randomly, 178)) x.permute_presolve_constraint_order != false && (encoded_size += PB._encoded_size(x.permute_presolve_constraint_order, 179)) x.use_absl_random != false && (encoded_size += PB._encoded_size(x.use_absl_random, 180)) x.log_search_progress != false && (encoded_size += PB._encoded_size(x.log_search_progress, 41)) - x.log_frequency_in_seconds != Float64(-1) && (encoded_size += PB._encoded_size(x.log_frequency_in_seconds, 212)) - x.model_reduction_log_frequency_in_seconds != Float64(5) && (encoded_size += PB._encoded_size(x.model_reduction_log_frequency_in_seconds, 218)) x.log_subsolver_statistics != false && (encoded_size += PB._encoded_size(x.log_subsolver_statistics, 189)) x.log_prefix != "" && (encoded_size += PB._encoded_size(x.log_prefix, 185)) x.log_to_stdout != true && (encoded_size += PB._encoded_size(x.log_to_stdout, 186)) @@ -1220,9 +1495,10 @@ function PB._encoded_size(x::SatParameters) x.minimize_reduction_during_pb_resolution != false && (encoded_size += PB._encoded_size(x.minimize_reduction_during_pb_resolution, 48)) x.count_assumption_levels_in_lbd != true && (encoded_size += PB._encoded_size(x.count_assumption_levels_in_lbd, 49)) x.presolve_bve_threshold != Int32(500) && (encoded_size += PB._encoded_size(x.presolve_bve_threshold, 54)) + x.filter_sat_postsolve_clauses != false && (encoded_size += PB._encoded_size(x.filter_sat_postsolve_clauses, 324)) x.presolve_bve_clause_weight != Int32(3) && (encoded_size += PB._encoded_size(x.presolve_bve_clause_weight, 55)) - x.probing_deterministic_time_limit != Float64(1) && (encoded_size += PB._encoded_size(x.probing_deterministic_time_limit, 226)) - x.presolve_probing_deterministic_time_limit != Float64(30.0) && (encoded_size += PB._encoded_size(x.presolve_probing_deterministic_time_limit, 57)) + x.probing_deterministic_time_limit !== Float64(1.0) && (encoded_size += PB._encoded_size(x.probing_deterministic_time_limit, 226)) + x.presolve_probing_deterministic_time_limit !== Float64(30.0) && (encoded_size += PB._encoded_size(x.presolve_probing_deterministic_time_limit, 57)) x.presolve_blocked_clause != true && (encoded_size += PB._encoded_size(x.presolve_blocked_clause, 88)) x.presolve_use_bva != true && (encoded_size += PB._encoded_size(x.presolve_use_bva, 72)) x.presolve_bva_threshold != Int32(1) && (encoded_size += PB._encoded_size(x.presolve_bva_threshold, 73)) @@ -1230,113 +1506,128 @@ function PB._encoded_size(x::SatParameters) x.cp_model_presolve != true && (encoded_size += PB._encoded_size(x.cp_model_presolve, 86)) x.cp_model_probing_level != Int32(2) && (encoded_size += PB._encoded_size(x.cp_model_probing_level, 110)) x.cp_model_use_sat_presolve != true && (encoded_size += PB._encoded_size(x.cp_model_use_sat_presolve, 93)) - x.use_sat_inprocessing != false && (encoded_size += PB._encoded_size(x.use_sat_inprocessing, 163)) + x.remove_fixed_variables_early != true && (encoded_size += PB._encoded_size(x.remove_fixed_variables_early, 310)) x.detect_table_with_cost != false && (encoded_size += PB._encoded_size(x.detect_table_with_cost, 216)) x.table_compression_level != Int32(2) && (encoded_size += PB._encoded_size(x.table_compression_level, 217)) x.expand_alldiff_constraints != false && (encoded_size += PB._encoded_size(x.expand_alldiff_constraints, 170)) + x.max_alldiff_domain_size != Int32(256) && (encoded_size += PB._encoded_size(x.max_alldiff_domain_size, 320)) x.expand_reservoir_constraints != true && (encoded_size += PB._encoded_size(x.expand_reservoir_constraints, 182)) + x.expand_reservoir_using_circuit != false && (encoded_size += PB._encoded_size(x.expand_reservoir_using_circuit, 288)) + x.encode_cumulative_as_reservoir != false && (encoded_size += PB._encoded_size(x.encode_cumulative_as_reservoir, 287)) + x.max_lin_max_size_for_expansion != Int32(0) && (encoded_size += PB._encoded_size(x.max_lin_max_size_for_expansion, 280)) x.disable_constraint_expansion != false && (encoded_size += PB._encoded_size(x.disable_constraint_expansion, 181)) x.encode_complex_linear_constraint_with_integer != false && (encoded_size += PB._encoded_size(x.encode_complex_linear_constraint_with_integer, 223)) - x.merge_no_overlap_work_limit != Float64(1e12) && (encoded_size += PB._encoded_size(x.merge_no_overlap_work_limit, 145)) - x.merge_at_most_one_work_limit != Float64(1e8) && (encoded_size += PB._encoded_size(x.merge_at_most_one_work_limit, 146)) + x.merge_no_overlap_work_limit !== Float64(1e12) && (encoded_size += PB._encoded_size(x.merge_no_overlap_work_limit, 145)) + x.merge_at_most_one_work_limit !== Float64(1e8) && (encoded_size += PB._encoded_size(x.merge_at_most_one_work_limit, 146)) x.presolve_substitution_level != Int32(1) && (encoded_size += PB._encoded_size(x.presolve_substitution_level, 147)) x.presolve_extract_integer_enforcement != false && (encoded_size += PB._encoded_size(x.presolve_extract_integer_enforcement, 174)) x.presolve_inclusion_work_limit != Int64(100000000) && (encoded_size += PB._encoded_size(x.presolve_inclusion_work_limit, 201)) x.ignore_names != true && (encoded_size += PB._encoded_size(x.ignore_names, 202)) x.infer_all_diffs != true && (encoded_size += PB._encoded_size(x.infer_all_diffs, 233)) x.find_big_linear_overlap != true && (encoded_size += PB._encoded_size(x.find_big_linear_overlap, 234)) + x.use_sat_inprocessing != true && (encoded_size += PB._encoded_size(x.use_sat_inprocessing, 163)) + x.inprocessing_dtime_ratio !== Float64(0.2) && (encoded_size += PB._encoded_size(x.inprocessing_dtime_ratio, 273)) + x.inprocessing_probing_dtime !== Float64(1.0) && (encoded_size += PB._encoded_size(x.inprocessing_probing_dtime, 274)) + x.inprocessing_minimization_dtime !== Float64(1.0) && (encoded_size += PB._encoded_size(x.inprocessing_minimization_dtime, 275)) + x.inprocessing_minimization_use_conflict_analysis != true && (encoded_size += PB._encoded_size(x.inprocessing_minimization_use_conflict_analysis, 297)) + x.inprocessing_minimization_use_all_orderings != false && (encoded_size += PB._encoded_size(x.inprocessing_minimization_use_all_orderings, 298)) x.num_workers != Int32(0) && (encoded_size += PB._encoded_size(x.num_workers, 206)) x.num_search_workers != Int32(0) && (encoded_size += PB._encoded_size(x.num_search_workers, 100)) - x.min_num_lns_workers != Int32(2) && (encoded_size += PB._encoded_size(x.min_num_lns_workers, 211)) + x.num_full_subsolvers != Int32(0) && (encoded_size += PB._encoded_size(x.num_full_subsolvers, 294)) !isempty(x.subsolvers) && (encoded_size += PB._encoded_size(x.subsolvers, 207)) !isempty(x.extra_subsolvers) && (encoded_size += PB._encoded_size(x.extra_subsolvers, 219)) !isempty(x.ignore_subsolvers) && (encoded_size += PB._encoded_size(x.ignore_subsolvers, 209)) + !isempty(x.filter_subsolvers) && (encoded_size += PB._encoded_size(x.filter_subsolvers, 293)) !isempty(x.subsolver_params) && (encoded_size += PB._encoded_size(x.subsolver_params, 210)) x.interleave_search != false && (encoded_size += PB._encoded_size(x.interleave_search, 136)) x.interleave_batch_size != Int32(0) && (encoded_size += PB._encoded_size(x.interleave_batch_size, 134)) x.share_objective_bounds != true && (encoded_size += PB._encoded_size(x.share_objective_bounds, 113)) x.share_level_zero_bounds != true && (encoded_size += PB._encoded_size(x.share_level_zero_bounds, 114)) x.share_binary_clauses != true && (encoded_size += PB._encoded_size(x.share_binary_clauses, 203)) + x.share_glue_clauses != false && (encoded_size += PB._encoded_size(x.share_glue_clauses, 285)) + x.minimize_shared_clauses != true && (encoded_size += PB._encoded_size(x.minimize_shared_clauses, 300)) + x.share_glue_clauses_dtime !== Float64(1.0) && (encoded_size += PB._encoded_size(x.share_glue_clauses_dtime, 322)) x.debug_postsolve_with_full_solver != false && (encoded_size += PB._encoded_size(x.debug_postsolve_with_full_solver, 162)) x.debug_max_num_presolve_operations != Int32(0) && (encoded_size += PB._encoded_size(x.debug_max_num_presolve_operations, 151)) x.debug_crash_on_bad_hint != false && (encoded_size += PB._encoded_size(x.debug_crash_on_bad_hint, 195)) + x.debug_crash_if_presolve_breaks_hint != false && (encoded_size += PB._encoded_size(x.debug_crash_if_presolve_breaks_hint, 306)) x.use_optimization_hints != true && (encoded_size += PB._encoded_size(x.use_optimization_hints, 35)) - x.minimize_core != true && (encoded_size += PB._encoded_size(x.minimize_core, 50)) + x.core_minimization_level != Int32(2) && (encoded_size += PB._encoded_size(x.core_minimization_level, 50)) x.find_multiple_cores != true && (encoded_size += PB._encoded_size(x.find_multiple_cores, 84)) x.cover_optimization != true && (encoded_size += PB._encoded_size(x.cover_optimization, 89)) x.max_sat_assumption_order != var"SatParameters.MaxSatAssumptionOrder".DEFAULT_ASSUMPTION_ORDER && (encoded_size += PB._encoded_size(x.max_sat_assumption_order, 51)) x.max_sat_reverse_assumption_order != false && (encoded_size += PB._encoded_size(x.max_sat_reverse_assumption_order, 52)) x.max_sat_stratification != var"SatParameters.MaxSatStratificationAlgorithm".STRATIFICATION_DESCENT && (encoded_size += PB._encoded_size(x.max_sat_stratification, 53)) - x.propagation_loop_detection_factor != Float64(10.0) && (encoded_size += PB._encoded_size(x.propagation_loop_detection_factor, 221)) + x.propagation_loop_detection_factor !== Float64(10.0) && (encoded_size += PB._encoded_size(x.propagation_loop_detection_factor, 221)) x.use_precedences_in_disjunctive_constraint != true && (encoded_size += PB._encoded_size(x.use_precedences_in_disjunctive_constraint, 74)) x.max_size_to_create_precedence_literals_in_disjunctive != Int32(60) && (encoded_size += PB._encoded_size(x.max_size_to_create_precedence_literals_in_disjunctive, 229)) x.use_strong_propagation_in_disjunctive != false && (encoded_size += PB._encoded_size(x.use_strong_propagation_in_disjunctive, 230)) + x.use_dynamic_precedence_in_disjunctive != false && (encoded_size += PB._encoded_size(x.use_dynamic_precedence_in_disjunctive, 263)) + x.use_dynamic_precedence_in_cumulative != false && (encoded_size += PB._encoded_size(x.use_dynamic_precedence_in_cumulative, 268)) x.use_overload_checker_in_cumulative != false && (encoded_size += PB._encoded_size(x.use_overload_checker_in_cumulative, 78)) + x.use_conservative_scale_overload_checker != false && (encoded_size += PB._encoded_size(x.use_conservative_scale_overload_checker, 286)) x.use_timetable_edge_finding_in_cumulative != false && (encoded_size += PB._encoded_size(x.use_timetable_edge_finding_in_cumulative, 79)) + x.max_num_intervals_for_timetable_edge_finding != Int32(100) && (encoded_size += PB._encoded_size(x.max_num_intervals_for_timetable_edge_finding, 260)) x.use_hard_precedences_in_cumulative != false && (encoded_size += PB._encoded_size(x.use_hard_precedences_in_cumulative, 215)) x.exploit_all_precedences != false && (encoded_size += PB._encoded_size(x.exploit_all_precedences, 220)) x.use_disjunctive_constraint_in_cumulative != true && (encoded_size += PB._encoded_size(x.use_disjunctive_constraint_in_cumulative, 80)) + x.no_overlap_2d_boolean_relations_limit != Int32(10) && (encoded_size += PB._encoded_size(x.no_overlap_2d_boolean_relations_limit, 321)) x.use_timetabling_in_no_overlap_2d != false && (encoded_size += PB._encoded_size(x.use_timetabling_in_no_overlap_2d, 200)) x.use_energetic_reasoning_in_no_overlap_2d != false && (encoded_size += PB._encoded_size(x.use_energetic_reasoning_in_no_overlap_2d, 213)) - x.use_pairwise_reasoning_in_no_overlap_2d != false && (encoded_size += PB._encoded_size(x.use_pairwise_reasoning_in_no_overlap_2d, 251)) + x.use_area_energetic_reasoning_in_no_overlap_2d != false && (encoded_size += PB._encoded_size(x.use_area_energetic_reasoning_in_no_overlap_2d, 271)) + x.use_try_edge_reasoning_in_no_overlap_2d != false && (encoded_size += PB._encoded_size(x.use_try_edge_reasoning_in_no_overlap_2d, 299)) + x.max_pairs_pairwise_reasoning_in_no_overlap_2d != Int32(1250) && (encoded_size += PB._encoded_size(x.max_pairs_pairwise_reasoning_in_no_overlap_2d, 276)) + x.maximum_regions_to_split_in_disconnected_no_overlap_2d != Int32(0) && (encoded_size += PB._encoded_size(x.maximum_regions_to_split_in_disconnected_no_overlap_2d, 315)) + x.use_linear3_for_no_overlap_2d_precedences != true && (encoded_size += PB._encoded_size(x.use_linear3_for_no_overlap_2d_precedences, 323)) x.use_dual_scheduling_heuristics != true && (encoded_size += PB._encoded_size(x.use_dual_scheduling_heuristics, 214)) - x.linearization_level != Int32(1) && (encoded_size += PB._encoded_size(x.linearization_level, 90)) - x.boolean_encoding_level != Int32(1) && (encoded_size += PB._encoded_size(x.boolean_encoding_level, 107)) - x.max_domain_size_when_encoding_eq_neq_constraints != Int32(16) && (encoded_size += PB._encoded_size(x.max_domain_size_when_encoding_eq_neq_constraints, 191)) - x.max_num_cuts != Int32(10000) && (encoded_size += PB._encoded_size(x.max_num_cuts, 91)) - x.cut_level != Int32(1) && (encoded_size += PB._encoded_size(x.cut_level, 196)) - x.only_add_cuts_at_level_zero != false && (encoded_size += PB._encoded_size(x.only_add_cuts_at_level_zero, 92)) - x.add_objective_cut != false && (encoded_size += PB._encoded_size(x.add_objective_cut, 197)) - x.add_cg_cuts != true && (encoded_size += PB._encoded_size(x.add_cg_cuts, 117)) - x.add_mir_cuts != true && (encoded_size += PB._encoded_size(x.add_mir_cuts, 120)) - x.add_zero_half_cuts != true && (encoded_size += PB._encoded_size(x.add_zero_half_cuts, 169)) - x.add_clique_cuts != true && (encoded_size += PB._encoded_size(x.add_clique_cuts, 172)) - x.max_all_diff_cut_size != Int32(64) && (encoded_size += PB._encoded_size(x.max_all_diff_cut_size, 148)) - x.add_lin_max_cuts != true && (encoded_size += PB._encoded_size(x.add_lin_max_cuts, 152)) - x.max_integer_rounding_scaling != Int32(600) && (encoded_size += PB._encoded_size(x.max_integer_rounding_scaling, 119)) - x.add_lp_constraints_lazily != true && (encoded_size += PB._encoded_size(x.add_lp_constraints_lazily, 112)) - x.root_lp_iterations != Int32(2000) && (encoded_size += PB._encoded_size(x.root_lp_iterations, 227)) - x.min_orthogonality_for_lp_constraints != Float64(0.05) && (encoded_size += PB._encoded_size(x.min_orthogonality_for_lp_constraints, 115)) - x.max_cut_rounds_at_level_zero != Int32(1) && (encoded_size += PB._encoded_size(x.max_cut_rounds_at_level_zero, 154)) - x.max_consecutive_inactive_count != Int32(100) && (encoded_size += PB._encoded_size(x.max_consecutive_inactive_count, 121)) - x.cut_max_active_count_value != Float64(1e10) && (encoded_size += PB._encoded_size(x.cut_max_active_count_value, 155)) - x.cut_active_count_decay != Float64(0.8) && (encoded_size += PB._encoded_size(x.cut_active_count_decay, 156)) - x.cut_cleanup_target != Int32(1000) && (encoded_size += PB._encoded_size(x.cut_cleanup_target, 157)) - x.new_constraints_batch_size != Int32(50) && (encoded_size += PB._encoded_size(x.new_constraints_batch_size, 122)) + x.use_all_different_for_circuit != false && (encoded_size += PB._encoded_size(x.use_all_different_for_circuit, 311)) + x.routing_cut_subset_size_for_binary_relation_bound != Int32(0) && (encoded_size += PB._encoded_size(x.routing_cut_subset_size_for_binary_relation_bound, 312)) + x.routing_cut_subset_size_for_tight_binary_relation_bound != Int32(0) && (encoded_size += PB._encoded_size(x.routing_cut_subset_size_for_tight_binary_relation_bound, 313)) + x.routing_cut_subset_size_for_exact_binary_relation_bound != Int32(8) && (encoded_size += PB._encoded_size(x.routing_cut_subset_size_for_exact_binary_relation_bound, 316)) + x.routing_cut_subset_size_for_shortest_paths_bound != Int32(8) && (encoded_size += PB._encoded_size(x.routing_cut_subset_size_for_shortest_paths_bound, 318)) + x.routing_cut_dp_effort !== Float64(1e7) && (encoded_size += PB._encoded_size(x.routing_cut_dp_effort, 314)) + x.routing_cut_max_infeasible_path_length != Int32(6) && (encoded_size += PB._encoded_size(x.routing_cut_max_infeasible_path_length, 317)) x.search_branching != var"SatParameters.SearchBranching".AUTOMATIC_SEARCH && (encoded_size += PB._encoded_size(x.search_branching, 82)) x.hint_conflict_limit != Int32(10) && (encoded_size += PB._encoded_size(x.hint_conflict_limit, 153)) x.repair_hint != false && (encoded_size += PB._encoded_size(x.repair_hint, 167)) x.fix_variables_to_their_hinted_value != false && (encoded_size += PB._encoded_size(x.fix_variables_to_their_hinted_value, 192)) - x.exploit_integer_lp_solution != true && (encoded_size += PB._encoded_size(x.exploit_integer_lp_solution, 94)) - x.exploit_all_lp_solution != true && (encoded_size += PB._encoded_size(x.exploit_all_lp_solution, 116)) - x.exploit_best_solution != false && (encoded_size += PB._encoded_size(x.exploit_best_solution, 130)) - x.exploit_relaxation_solution != false && (encoded_size += PB._encoded_size(x.exploit_relaxation_solution, 161)) - x.exploit_objective != true && (encoded_size += PB._encoded_size(x.exploit_objective, 131)) - x.probing_period_at_root != Int64(0) && (encoded_size += PB._encoded_size(x.probing_period_at_root, 142)) x.use_probing_search != false && (encoded_size += PB._encoded_size(x.use_probing_search, 176)) - x.shaving_deterministic_time_in_probing_search != Float64(0.001) && (encoded_size += PB._encoded_size(x.shaving_deterministic_time_in_probing_search, 204)) - x.shaving_search_deterministic_time != Float64(0.001) && (encoded_size += PB._encoded_size(x.shaving_search_deterministic_time, 205)) + x.use_extended_probing != true && (encoded_size += PB._encoded_size(x.use_extended_probing, 269)) + x.probing_num_combinations_limit != Int32(20000) && (encoded_size += PB._encoded_size(x.probing_num_combinations_limit, 272)) + x.shaving_deterministic_time_in_probing_search !== Float64(0.001) && (encoded_size += PB._encoded_size(x.shaving_deterministic_time_in_probing_search, 204)) + x.shaving_search_deterministic_time !== Float64(0.1) && (encoded_size += PB._encoded_size(x.shaving_search_deterministic_time, 205)) + x.shaving_search_threshold != Int64(64) && (encoded_size += PB._encoded_size(x.shaving_search_threshold, 290)) x.use_objective_lb_search != false && (encoded_size += PB._encoded_size(x.use_objective_lb_search, 228)) x.use_objective_shaving_search != false && (encoded_size += PB._encoded_size(x.use_objective_shaving_search, 253)) + x.variables_shaving_level != Int32(-1) && (encoded_size += PB._encoded_size(x.variables_shaving_level, 289)) x.pseudo_cost_reliability_threshold != Int64(100) && (encoded_size += PB._encoded_size(x.pseudo_cost_reliability_threshold, 123)) x.optimize_with_core != false && (encoded_size += PB._encoded_size(x.optimize_with_core, 83)) x.optimize_with_lb_tree_search != false && (encoded_size += PB._encoded_size(x.optimize_with_lb_tree_search, 188)) + x.save_lp_basis_in_lb_tree_search != false && (encoded_size += PB._encoded_size(x.save_lp_basis_in_lb_tree_search, 284)) x.binary_search_num_conflicts != Int32(-1) && (encoded_size += PB._encoded_size(x.binary_search_num_conflicts, 99)) x.optimize_with_max_hs != false && (encoded_size += PB._encoded_size(x.optimize_with_max_hs, 85)) - x.test_feasibility_jump != false && (encoded_size += PB._encoded_size(x.test_feasibility_jump, 240)) - x.feasibility_jump_max_num_values_scanned != Int64(4096) && (encoded_size += PB._encoded_size(x.feasibility_jump_max_num_values_scanned, 245)) - x.feasibility_jump_protect_linear_feasibility != true && (encoded_size += PB._encoded_size(x.feasibility_jump_protect_linear_feasibility, 246)) - x.feasibility_jump_decay != Float64(1.0) && (encoded_size += PB._encoded_size(x.feasibility_jump_decay, 242)) - x.feasibility_jump_var_randomization_probability != Float64(0.0) && (encoded_size += PB._encoded_size(x.feasibility_jump_var_randomization_probability, 247)) - x.feasibility_jump_var_perburbation_range_ratio != Float64(0.2) && (encoded_size += PB._encoded_size(x.feasibility_jump_var_perburbation_range_ratio, 248)) + x.use_feasibility_jump != true && (encoded_size += PB._encoded_size(x.use_feasibility_jump, 265)) + x.use_ls_only != false && (encoded_size += PB._encoded_size(x.use_ls_only, 240)) + x.feasibility_jump_decay !== Float64(0.95) && (encoded_size += PB._encoded_size(x.feasibility_jump_decay, 242)) + x.feasibility_jump_linearization_level != Int32(2) && (encoded_size += PB._encoded_size(x.feasibility_jump_linearization_level, 257)) + x.feasibility_jump_restart_factor != Int32(1) && (encoded_size += PB._encoded_size(x.feasibility_jump_restart_factor, 258)) + x.feasibility_jump_batch_dtime !== Float64(0.1) && (encoded_size += PB._encoded_size(x.feasibility_jump_batch_dtime, 292)) + x.feasibility_jump_var_randomization_probability !== Float64(0.05) && (encoded_size += PB._encoded_size(x.feasibility_jump_var_randomization_probability, 247)) + x.feasibility_jump_var_perburbation_range_ratio !== Float64(0.2) && (encoded_size += PB._encoded_size(x.feasibility_jump_var_perburbation_range_ratio, 248)) x.feasibility_jump_enable_restarts != true && (encoded_size += PB._encoded_size(x.feasibility_jump_enable_restarts, 250)) + x.feasibility_jump_max_expanded_constraint_size != Int32(500) && (encoded_size += PB._encoded_size(x.feasibility_jump_max_expanded_constraint_size, 264)) x.num_violation_ls != Int32(0) && (encoded_size += PB._encoded_size(x.num_violation_ls, 244)) x.violation_ls_perturbation_period != Int32(100) && (encoded_size += PB._encoded_size(x.violation_ls_perturbation_period, 249)) + x.violation_ls_compound_move_probability !== Float64(0.5) && (encoded_size += PB._encoded_size(x.violation_ls_compound_move_probability, 259)) x.shared_tree_num_workers != Int32(0) && (encoded_size += PB._encoded_size(x.shared_tree_num_workers, 235)) x.use_shared_tree_search != false && (encoded_size += PB._encoded_size(x.use_shared_tree_search, 236)) - x.shared_tree_worker_objective_split_probability != Float64(0.5) && (encoded_size += PB._encoded_size(x.shared_tree_worker_objective_split_probability, 237)) - x.shared_tree_max_nodes_per_worker != Int32(128) && (encoded_size += PB._encoded_size(x.shared_tree_max_nodes_per_worker, 238)) + x.shared_tree_worker_min_restarts_per_subtree != Int32(1) && (encoded_size += PB._encoded_size(x.shared_tree_worker_min_restarts_per_subtree, 282)) + x.shared_tree_worker_enable_trail_sharing != true && (encoded_size += PB._encoded_size(x.shared_tree_worker_enable_trail_sharing, 295)) + x.shared_tree_worker_enable_phase_sharing != true && (encoded_size += PB._encoded_size(x.shared_tree_worker_enable_phase_sharing, 304)) + x.shared_tree_open_leaves_per_worker !== Float64(2.0) && (encoded_size += PB._encoded_size(x.shared_tree_open_leaves_per_worker, 281)) + x.shared_tree_max_nodes_per_worker != Int32(10000) && (encoded_size += PB._encoded_size(x.shared_tree_max_nodes_per_worker, 238)) x.shared_tree_split_strategy != var"SatParameters.SharedTreeSplitStrategy".SPLIT_STRATEGY_AUTO && (encoded_size += PB._encoded_size(x.shared_tree_split_strategy, 239)) + x.shared_tree_balance_tolerance != Int32(1) && (encoded_size += PB._encoded_size(x.shared_tree_balance_tolerance, 305)) x.enumerate_all_solutions != false && (encoded_size += PB._encoded_size(x.enumerate_all_solutions, 87)) x.keep_all_feasible_solutions_in_presolve != false && (encoded_size += PB._encoded_size(x.keep_all_feasible_solutions_in_presolve, 173)) x.fill_tightened_domains_in_response != false && (encoded_size += PB._encoded_size(x.fill_tightened_domains_in_response, 132)) @@ -1346,36 +1637,78 @@ function PB._encoded_size(x::SatParameters) x.stop_after_first_solution != false && (encoded_size += PB._encoded_size(x.stop_after_first_solution, 98)) x.stop_after_presolve != false && (encoded_size += PB._encoded_size(x.stop_after_presolve, 149)) x.stop_after_root_propagation != false && (encoded_size += PB._encoded_size(x.stop_after_root_propagation, 252)) + x.lns_initial_difficulty !== Float64(0.5) && (encoded_size += PB._encoded_size(x.lns_initial_difficulty, 307)) + x.lns_initial_deterministic_limit !== Float64(0.1) && (encoded_size += PB._encoded_size(x.lns_initial_deterministic_limit, 308)) + x.use_lns != true && (encoded_size += PB._encoded_size(x.use_lns, 283)) x.use_lns_only != false && (encoded_size += PB._encoded_size(x.use_lns_only, 101)) x.solution_pool_size != Int32(3) && (encoded_size += PB._encoded_size(x.solution_pool_size, 193)) x.use_rins_lns != true && (encoded_size += PB._encoded_size(x.use_rins_lns, 129)) x.use_feasibility_pump != true && (encoded_size += PB._encoded_size(x.use_feasibility_pump, 164)) - x.use_lb_relax_lns != false && (encoded_size += PB._encoded_size(x.use_lb_relax_lns, 255)) + x.use_lb_relax_lns != true && (encoded_size += PB._encoded_size(x.use_lb_relax_lns, 255)) + x.lb_relax_num_workers_threshold != Int32(16) && (encoded_size += PB._encoded_size(x.lb_relax_num_workers_threshold, 296)) x.fp_rounding != var"SatParameters.FPRoundingMethod".PROPAGATION_ASSISTED && (encoded_size += PB._encoded_size(x.fp_rounding, 165)) x.diversify_lns_params != false && (encoded_size += PB._encoded_size(x.diversify_lns_params, 137)) x.randomize_search != false && (encoded_size += PB._encoded_size(x.randomize_search, 103)) - x.search_randomization_tolerance != Int64(0) && (encoded_size += PB._encoded_size(x.search_randomization_tolerance, 104)) + x.search_random_variable_pool_size != Int64(0) && (encoded_size += PB._encoded_size(x.search_random_variable_pool_size, 104)) + x.push_all_tasks_toward_start != false && (encoded_size += PB._encoded_size(x.push_all_tasks_toward_start, 262)) x.use_optional_variables != false && (encoded_size += PB._encoded_size(x.use_optional_variables, 108)) x.use_exact_lp_reason != true && (encoded_size += PB._encoded_size(x.use_exact_lp_reason, 109)) - x.use_branching_in_lp != false && (encoded_size += PB._encoded_size(x.use_branching_in_lp, 139)) x.use_combined_no_overlap != false && (encoded_size += PB._encoded_size(x.use_combined_no_overlap, 133)) + x.at_most_one_max_expansion_size != Int32(3) && (encoded_size += PB._encoded_size(x.at_most_one_max_expansion_size, 270)) x.catch_sigint_signal != true && (encoded_size += PB._encoded_size(x.catch_sigint_signal, 135)) x.use_implied_bounds != true && (encoded_size += PB._encoded_size(x.use_implied_bounds, 144)) x.polish_lp_solution != false && (encoded_size += PB._encoded_size(x.polish_lp_solution, 175)) + x.lp_primal_tolerance !== Float64(1e-7) && (encoded_size += PB._encoded_size(x.lp_primal_tolerance, 266)) + x.lp_dual_tolerance !== Float64(1e-7) && (encoded_size += PB._encoded_size(x.lp_dual_tolerance, 267)) x.convert_intervals != true && (encoded_size += PB._encoded_size(x.convert_intervals, 177)) x.symmetry_level != Int32(2) && (encoded_size += PB._encoded_size(x.symmetry_level, 183)) - x.new_linear_propagation != false && (encoded_size += PB._encoded_size(x.new_linear_propagation, 224)) + x.use_symmetry_in_lp != false && (encoded_size += PB._encoded_size(x.use_symmetry_in_lp, 301)) + x.keep_symmetry_in_presolve != false && (encoded_size += PB._encoded_size(x.keep_symmetry_in_presolve, 303)) + x.symmetry_detection_deterministic_time_limit !== Float64(1.0) && (encoded_size += PB._encoded_size(x.symmetry_detection_deterministic_time_limit, 302)) + x.new_linear_propagation != true && (encoded_size += PB._encoded_size(x.new_linear_propagation, 224)) x.linear_split_size != Int32(100) && (encoded_size += PB._encoded_size(x.linear_split_size, 256)) - x.mip_max_bound != Float64(1e7) && (encoded_size += PB._encoded_size(x.mip_max_bound, 124)) - x.mip_var_scaling != Float64(1.0) && (encoded_size += PB._encoded_size(x.mip_var_scaling, 125)) + x.linearization_level != Int32(1) && (encoded_size += PB._encoded_size(x.linearization_level, 90)) + x.boolean_encoding_level != Int32(1) && (encoded_size += PB._encoded_size(x.boolean_encoding_level, 107)) + x.max_domain_size_when_encoding_eq_neq_constraints != Int32(16) && (encoded_size += PB._encoded_size(x.max_domain_size_when_encoding_eq_neq_constraints, 191)) + x.max_num_cuts != Int32(10000) && (encoded_size += PB._encoded_size(x.max_num_cuts, 91)) + x.cut_level != Int32(1) && (encoded_size += PB._encoded_size(x.cut_level, 196)) + x.only_add_cuts_at_level_zero != false && (encoded_size += PB._encoded_size(x.only_add_cuts_at_level_zero, 92)) + x.add_objective_cut != false && (encoded_size += PB._encoded_size(x.add_objective_cut, 197)) + x.add_cg_cuts != true && (encoded_size += PB._encoded_size(x.add_cg_cuts, 117)) + x.add_mir_cuts != true && (encoded_size += PB._encoded_size(x.add_mir_cuts, 120)) + x.add_zero_half_cuts != true && (encoded_size += PB._encoded_size(x.add_zero_half_cuts, 169)) + x.add_clique_cuts != true && (encoded_size += PB._encoded_size(x.add_clique_cuts, 172)) + x.add_rlt_cuts != true && (encoded_size += PB._encoded_size(x.add_rlt_cuts, 279)) + x.max_all_diff_cut_size != Int32(64) && (encoded_size += PB._encoded_size(x.max_all_diff_cut_size, 148)) + x.add_lin_max_cuts != true && (encoded_size += PB._encoded_size(x.add_lin_max_cuts, 152)) + x.max_integer_rounding_scaling != Int32(600) && (encoded_size += PB._encoded_size(x.max_integer_rounding_scaling, 119)) + x.add_lp_constraints_lazily != true && (encoded_size += PB._encoded_size(x.add_lp_constraints_lazily, 112)) + x.root_lp_iterations != Int32(2000) && (encoded_size += PB._encoded_size(x.root_lp_iterations, 227)) + x.min_orthogonality_for_lp_constraints !== Float64(0.05) && (encoded_size += PB._encoded_size(x.min_orthogonality_for_lp_constraints, 115)) + x.max_cut_rounds_at_level_zero != Int32(1) && (encoded_size += PB._encoded_size(x.max_cut_rounds_at_level_zero, 154)) + x.max_consecutive_inactive_count != Int32(100) && (encoded_size += PB._encoded_size(x.max_consecutive_inactive_count, 121)) + x.cut_max_active_count_value !== Float64(1e10) && (encoded_size += PB._encoded_size(x.cut_max_active_count_value, 155)) + x.cut_active_count_decay !== Float64(0.8) && (encoded_size += PB._encoded_size(x.cut_active_count_decay, 156)) + x.cut_cleanup_target != Int32(1000) && (encoded_size += PB._encoded_size(x.cut_cleanup_target, 157)) + x.new_constraints_batch_size != Int32(50) && (encoded_size += PB._encoded_size(x.new_constraints_batch_size, 122)) + x.exploit_integer_lp_solution != true && (encoded_size += PB._encoded_size(x.exploit_integer_lp_solution, 94)) + x.exploit_all_lp_solution != true && (encoded_size += PB._encoded_size(x.exploit_all_lp_solution, 116)) + x.exploit_best_solution != false && (encoded_size += PB._encoded_size(x.exploit_best_solution, 130)) + x.exploit_relaxation_solution != false && (encoded_size += PB._encoded_size(x.exploit_relaxation_solution, 161)) + x.exploit_objective != true && (encoded_size += PB._encoded_size(x.exploit_objective, 131)) + x.detect_linearized_product != false && (encoded_size += PB._encoded_size(x.detect_linearized_product, 277)) + x.mip_max_bound !== Float64(1e7) && (encoded_size += PB._encoded_size(x.mip_max_bound, 124)) + x.mip_var_scaling !== Float64(1.0) && (encoded_size += PB._encoded_size(x.mip_var_scaling, 125)) x.mip_scale_large_domain != false && (encoded_size += PB._encoded_size(x.mip_scale_large_domain, 225)) x.mip_automatically_scale_variables != true && (encoded_size += PB._encoded_size(x.mip_automatically_scale_variables, 166)) x.only_solve_ip != false && (encoded_size += PB._encoded_size(x.only_solve_ip, 222)) - x.mip_wanted_precision != Float64(1e-6) && (encoded_size += PB._encoded_size(x.mip_wanted_precision, 126)) + x.mip_wanted_precision !== Float64(1e-6) && (encoded_size += PB._encoded_size(x.mip_wanted_precision, 126)) x.mip_max_activity_exponent != Int32(53) && (encoded_size += PB._encoded_size(x.mip_max_activity_exponent, 127)) - x.mip_check_precision != Float64(1e-4) && (encoded_size += PB._encoded_size(x.mip_check_precision, 128)) + x.mip_check_precision !== Float64(1e-4) && (encoded_size += PB._encoded_size(x.mip_check_precision, 128)) x.mip_compute_true_objective_bound != true && (encoded_size += PB._encoded_size(x.mip_compute_true_objective_bound, 198)) - x.mip_max_valid_magnitude != Float64(1e30) && (encoded_size += PB._encoded_size(x.mip_max_valid_magnitude, 199)) - x.mip_drop_tolerance != Float64(1e-16) && (encoded_size += PB._encoded_size(x.mip_drop_tolerance, 232)) + x.mip_max_valid_magnitude !== Float64(1e20) && (encoded_size += PB._encoded_size(x.mip_max_valid_magnitude, 199)) + x.mip_treat_high_magnitude_bounds_as_infinity != false && (encoded_size += PB._encoded_size(x.mip_treat_high_magnitude_bounds_as_infinity, 278)) + x.mip_drop_tolerance !== Float64(1e-16) && (encoded_size += PB._encoded_size(x.mip_drop_tolerance, 232)) + x.mip_presolve_level != Int32(2) && (encoded_size += PB._encoded_size(x.mip_presolve_level, 261)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/jssp/jobshop_scheduling_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/jssp/jobshop_scheduling_pb.jl index cef4052e44b..d077111f712 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/jssp/jobshop_scheduling_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/jssp/jobshop_scheduling_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.157 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/scheduling/jssp/jobshop_scheduling.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.111 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/scheduling/jssp/jobshop_scheduling.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export JobPrecedence, Task, AssignedTask, TransitionTimeMatrix, Job, AssignedJob, Machine export JsspOutputSolution, JsspInputProblem + struct JobPrecedence first_job_index::Int32 second_job_index::Int32 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/rcpsp/rcpsp_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/rcpsp/rcpsp_pb.jl index 0aa9c29f7df..5dc0820fa15 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/rcpsp/rcpsp_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/scheduling/rcpsp/rcpsp_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:47.161 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/scheduling/rcpsp/rcpsp.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:02.111 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/scheduling/rcpsp/rcpsp.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -8,6 +8,7 @@ using ProtoBuf.EnumX: @enumx export PerRecipeDelays, Recipe, Resource, RcpspAssignment, PerSuccessorDelays, Task export RcpspProblem + struct PerRecipeDelays min_delays::Vector{Int32} end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_limit_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_limit_pb.jl index debfb8105d6..3a7d96ebc65 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_limit_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_limit_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.917 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/search_limit.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.862 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/search_limit.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export RegularLimitParameters + struct RegularLimitParameters time::Int64 branches::Int64 diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_stats_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_stats_pb.jl index 8e55432f9ca..825c8c35170 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_stats_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/search_stats_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.670 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/search_stats.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.674 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/search_stats.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -10,6 +10,7 @@ export var"LocalSearchStatistics.LocalSearchFilterStatistics", ConstraintSolverS export var"LocalSearchStatistics.LocalSearchOperatorStatistics", LocalSearchStatistics export SearchStatistics + struct var"LocalSearchStatistics.FirstSolutionStatistics" strategy::String duration_seconds::Float64 @@ -36,13 +37,13 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::var"LocalSearchStatistics.FirstSolutionStatistics") initpos = position(e.io) !isempty(x.strategy) && PB.encode(e, 1, x.strategy) - x.duration_seconds != zero(Float64) && PB.encode(e, 2, x.duration_seconds) + x.duration_seconds !== zero(Float64) && PB.encode(e, 2, x.duration_seconds) return position(e.io) - initpos end function PB._encoded_size(x::var"LocalSearchStatistics.FirstSolutionStatistics") encoded_size = 0 !isempty(x.strategy) && (encoded_size += PB._encoded_size(x.strategy, 1)) - x.duration_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 2)) + x.duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 2)) return encoded_size end @@ -90,8 +91,8 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::var"LocalSearchStatistics.Loca !isempty(x.local_search_filter) && PB.encode(e, 1, x.local_search_filter) x.num_calls != zero(Int64) && PB.encode(e, 2, x.num_calls) x.num_rejects != zero(Int64) && PB.encode(e, 3, x.num_rejects) - x.duration_seconds != zero(Float64) && PB.encode(e, 4, x.duration_seconds) - x.num_rejects_per_second != zero(Float64) && PB.encode(e, 5, x.num_rejects_per_second) + x.duration_seconds !== zero(Float64) && PB.encode(e, 4, x.duration_seconds) + x.num_rejects_per_second !== zero(Float64) && PB.encode(e, 5, x.num_rejects_per_second) !isempty(x.context) && PB.encode(e, 6, x.context) return position(e.io) - initpos end @@ -100,8 +101,8 @@ function PB._encoded_size(x::var"LocalSearchStatistics.LocalSearchFilterStatisti !isempty(x.local_search_filter) && (encoded_size += PB._encoded_size(x.local_search_filter, 1)) x.num_calls != zero(Int64) && (encoded_size += PB._encoded_size(x.num_calls, 2)) x.num_rejects != zero(Int64) && (encoded_size += PB._encoded_size(x.num_rejects, 3)) - x.duration_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 4)) - x.num_rejects_per_second != zero(Float64) && (encoded_size += PB._encoded_size(x.num_rejects_per_second, 5)) + x.duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 4)) + x.num_rejects_per_second !== zero(Float64) && (encoded_size += PB._encoded_size(x.num_rejects_per_second, 5)) !isempty(x.context) && (encoded_size += PB._encoded_size(x.context, 6)) return encoded_size end @@ -147,7 +148,7 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::ConstraintSolverStatistics) x.num_failures != zero(Int64) && PB.encode(e, 2, x.num_failures) x.num_solutions != zero(Int64) && PB.encode(e, 3, x.num_solutions) x.bytes_used != zero(Int64) && PB.encode(e, 4, x.bytes_used) - x.duration_seconds != zero(Float64) && PB.encode(e, 5, x.duration_seconds) + x.duration_seconds !== zero(Float64) && PB.encode(e, 5, x.duration_seconds) return position(e.io) - initpos end function PB._encoded_size(x::ConstraintSolverStatistics) @@ -156,7 +157,7 @@ function PB._encoded_size(x::ConstraintSolverStatistics) x.num_failures != zero(Int64) && (encoded_size += PB._encoded_size(x.num_failures, 2)) x.num_solutions != zero(Int64) && (encoded_size += PB._encoded_size(x.num_solutions, 3)) x.bytes_used != zero(Int64) && (encoded_size += PB._encoded_size(x.bytes_used, 4)) - x.duration_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 5)) + x.duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 5)) return encoded_size end @@ -166,9 +167,11 @@ struct var"LocalSearchStatistics.LocalSearchOperatorStatistics" num_filtered_neighbors::Int64 num_accepted_neighbors::Int64 duration_seconds::Float64 + make_next_neighbor_duration_seconds::Float64 + accept_neighbor_duration_seconds::Float64 end -PB.default_values(::Type{var"LocalSearchStatistics.LocalSearchOperatorStatistics"}) = (;local_search_operator = "", num_neighbors = zero(Int64), num_filtered_neighbors = zero(Int64), num_accepted_neighbors = zero(Int64), duration_seconds = zero(Float64)) -PB.field_numbers(::Type{var"LocalSearchStatistics.LocalSearchOperatorStatistics"}) = (;local_search_operator = 1, num_neighbors = 2, num_filtered_neighbors = 3, num_accepted_neighbors = 4, duration_seconds = 5) +PB.default_values(::Type{var"LocalSearchStatistics.LocalSearchOperatorStatistics"}) = (;local_search_operator = "", num_neighbors = zero(Int64), num_filtered_neighbors = zero(Int64), num_accepted_neighbors = zero(Int64), duration_seconds = zero(Float64), make_next_neighbor_duration_seconds = zero(Float64), accept_neighbor_duration_seconds = zero(Float64)) +PB.field_numbers(::Type{var"LocalSearchStatistics.LocalSearchOperatorStatistics"}) = (;local_search_operator = 1, num_neighbors = 2, num_filtered_neighbors = 3, num_accepted_neighbors = 4, duration_seconds = 5, make_next_neighbor_duration_seconds = 6, accept_neighbor_duration_seconds = 7) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"LocalSearchStatistics.LocalSearchOperatorStatistics"}) local_search_operator = "" @@ -176,6 +179,8 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"LocalSearchStatistic num_filtered_neighbors = zero(Int64) num_accepted_neighbors = zero(Int64) duration_seconds = zero(Float64) + make_next_neighbor_duration_seconds = zero(Float64) + accept_neighbor_duration_seconds = zero(Float64) while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 @@ -188,11 +193,15 @@ function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:var"LocalSearchStatistic num_accepted_neighbors = PB.decode(d, Int64) elseif field_number == 5 duration_seconds = PB.decode(d, Float64) + elseif field_number == 6 + make_next_neighbor_duration_seconds = PB.decode(d, Float64) + elseif field_number == 7 + accept_neighbor_duration_seconds = PB.decode(d, Float64) else PB.skip(d, wire_type) end end - return var"LocalSearchStatistics.LocalSearchOperatorStatistics"(local_search_operator, num_neighbors, num_filtered_neighbors, num_accepted_neighbors, duration_seconds) + return var"LocalSearchStatistics.LocalSearchOperatorStatistics"(local_search_operator, num_neighbors, num_filtered_neighbors, num_accepted_neighbors, duration_seconds, make_next_neighbor_duration_seconds, accept_neighbor_duration_seconds) end function PB.encode(e::PB.AbstractProtoEncoder, x::var"LocalSearchStatistics.LocalSearchOperatorStatistics") @@ -201,7 +210,9 @@ function PB.encode(e::PB.AbstractProtoEncoder, x::var"LocalSearchStatistics.Loca x.num_neighbors != zero(Int64) && PB.encode(e, 2, x.num_neighbors) x.num_filtered_neighbors != zero(Int64) && PB.encode(e, 3, x.num_filtered_neighbors) x.num_accepted_neighbors != zero(Int64) && PB.encode(e, 4, x.num_accepted_neighbors) - x.duration_seconds != zero(Float64) && PB.encode(e, 5, x.duration_seconds) + x.duration_seconds !== zero(Float64) && PB.encode(e, 5, x.duration_seconds) + x.make_next_neighbor_duration_seconds !== zero(Float64) && PB.encode(e, 6, x.make_next_neighbor_duration_seconds) + x.accept_neighbor_duration_seconds !== zero(Float64) && PB.encode(e, 7, x.accept_neighbor_duration_seconds) return position(e.io) - initpos end function PB._encoded_size(x::var"LocalSearchStatistics.LocalSearchOperatorStatistics") @@ -210,7 +221,9 @@ function PB._encoded_size(x::var"LocalSearchStatistics.LocalSearchOperatorStatis x.num_neighbors != zero(Int64) && (encoded_size += PB._encoded_size(x.num_neighbors, 2)) x.num_filtered_neighbors != zero(Int64) && (encoded_size += PB._encoded_size(x.num_filtered_neighbors, 3)) x.num_accepted_neighbors != zero(Int64) && (encoded_size += PB._encoded_size(x.num_accepted_neighbors, 4)) - x.duration_seconds != zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 5)) + x.duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.duration_seconds, 5)) + x.make_next_neighbor_duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.make_next_neighbor_duration_seconds, 6)) + x.accept_neighbor_duration_seconds !== zero(Float64) && (encoded_size += PB._encoded_size(x.accept_neighbor_duration_seconds, 7)) return encoded_size end @@ -275,15 +288,15 @@ function PB._encoded_size(x::LocalSearchStatistics) end struct SearchStatistics - local_search_statistics::Union{Nothing,LocalSearchStatistics} - constraint_solver_statistics::Union{Nothing,ConstraintSolverStatistics} + local_search_statistics::Vector{LocalSearchStatistics} + constraint_solver_statistics::Vector{ConstraintSolverStatistics} end -PB.default_values(::Type{SearchStatistics}) = (;local_search_statistics = nothing, constraint_solver_statistics = nothing) +PB.default_values(::Type{SearchStatistics}) = (;local_search_statistics = Vector{LocalSearchStatistics}(), constraint_solver_statistics = Vector{ConstraintSolverStatistics}()) PB.field_numbers(::Type{SearchStatistics}) = (;local_search_statistics = 1, constraint_solver_statistics = 2) function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:SearchStatistics}) - local_search_statistics = Ref{Union{Nothing,LocalSearchStatistics}}(nothing) - constraint_solver_statistics = Ref{Union{Nothing,ConstraintSolverStatistics}}(nothing) + local_search_statistics = PB.BufferedVector{LocalSearchStatistics}() + constraint_solver_statistics = PB.BufferedVector{ConstraintSolverStatistics}() while !PB.message_done(d) field_number, wire_type = PB.decode_tag(d) if field_number == 1 @@ -299,13 +312,13 @@ end function PB.encode(e::PB.AbstractProtoEncoder, x::SearchStatistics) initpos = position(e.io) - !isnothing(x.local_search_statistics) && PB.encode(e, 1, x.local_search_statistics) - !isnothing(x.constraint_solver_statistics) && PB.encode(e, 2, x.constraint_solver_statistics) + !isempty(x.local_search_statistics) && PB.encode(e, 1, x.local_search_statistics) + !isempty(x.constraint_solver_statistics) && PB.encode(e, 2, x.constraint_solver_statistics) return position(e.io) - initpos end function PB._encoded_size(x::SearchStatistics) encoded_size = 0 - !isnothing(x.local_search_statistics) && (encoded_size += PB._encoded_size(x.local_search_statistics, 1)) - !isnothing(x.constraint_solver_statistics) && (encoded_size += PB._encoded_size(x.constraint_solver_statistics, 2)) + !isempty(x.local_search_statistics) && (encoded_size += PB._encoded_size(x.local_search_statistics, 1)) + !isempty(x.constraint_solver_statistics) && (encoded_size += PB._encoded_size(x.constraint_solver_statistics, 2)) return encoded_size end diff --git a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/solver_parameters_pb.jl b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/solver_parameters_pb.jl index 90a98b3f005..f7e91a00e14 100644 --- a/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/solver_parameters_pb.jl +++ b/ortools/julia/ORToolsGenerated.jl/src/genproto/operations_research/solver_parameters_pb.jl @@ -1,5 +1,5 @@ -# Autogenerated using ProtoBuf.jl v1.0.14 on 2024-01-02T18:44:46.402 -# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/cc3d5aa28fb2158ce4ff5aed9899545a37503a6b/include/ortools/constraint_solver/solver_parameters.proto (proto3 syntax) +# Autogenerated using ProtoBuf.jl v1.1.1 on 2025-07-02T15:45:01.455 +# original file: /usr/local/google/home/tcuvelier/.julia/artifacts/502992654d3e610bc079dfc8ac9e663bff6f3a24/include/ortools/constraint_solver/solver_parameters.proto (proto3 syntax) import ProtoBuf as PB using ProtoBuf: OneOf @@ -7,6 +7,7 @@ using ProtoBuf.EnumX: @enumx export var"ConstraintSolverParameters.TrailCompression", ConstraintSolverParameters + @enumx var"ConstraintSolverParameters.TrailCompression" NO_COMPRESSION=0 COMPRESS_WITH_ZLIB=1 struct ConstraintSolverParameters diff --git a/ortools/julia/docs/index.md b/ortools/julia/docs/index.md index 2b813287429..42d0c214f45 100644 --- a/ortools/julia/docs/index.md +++ b/ortools/julia/docs/index.md @@ -68,6 +68,12 @@ When releasing a new version of OR-Tools: @JuliaRegistrator register subdir=ortools/julia/ORToolsGenerated.jl ``` + If registering from a nondefault branch like `julia/dev`: + + ``` + @JuliaRegistrator register subdir=ortools/julia/ORToolsGenerated.jl branch=julia/dev + ``` + This operation starts an automated process that will publish a new version of the `ORToolsGenerated.jl` package to the [official Julia package repository](https://juliapackages.com/). The version @@ -75,7 +81,11 @@ When releasing a new version of OR-Tools: 5. In case the C API changes, you must update `ORTools.jl` accordingly: after the manual update, follow the steps 3 and 4, replacing `ORToolsGenerated.jl` - by `ORTools.jl`. + by `ORTools.jl`. To register the new version, the command thus looks like: + + ``` + @JuliaRegistrator register subdir=ortools/julia/ORTools.jl branch=julia/dev + ``` There is only one set-up action to perform: install Julia's Registrator bot into OR-Tools' repository (which only needs read access) using @@ -124,7 +134,6 @@ to update the generated Protocol Buffers code. This script is automatically called by [`update_package.jl`](../ORToolsGenerated.jl/scripts/update_package.jl). - ## Design decisions ### Storing generated code From eb3bc66dd2f345095605357240e23a707301b336 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 27 Aug 2025 07:52:16 +0200 Subject: [PATCH 015/491] base: rework message-matchers to have math_opt tests pass --- ortools/base/BUILD.bazel | 30 +- ortools/base/CMakeLists.txt | 6 +- ortools/base/gmock.h | 4 +- ortools/base/message_matchers.h | 177 ---- ortools/base/parse_text_proto.h | 33 +- ortools/base/protocol-buffer-matchers.h | 805 ++++++++++++++++++ .../{status_matchers.h => status-matchers.h} | 21 +- 7 files changed, 862 insertions(+), 214 deletions(-) delete mode 100644 ortools/base/message_matchers.h create mode 100644 ortools/base/protocol-buffer-matchers.h rename ortools/base/{status_matchers.h => status-matchers.h} (94%) diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 1eb380feaff..86720ddd936 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -266,8 +266,8 @@ cc_library( name = "gmock", hdrs = ["gmock.h"], deps = [ - ":message_matchers", - ":status_matchers", + ":protocol-buffer-matchers", + ":status-matchers", "@googletest//:gtest", ], ) @@ -408,16 +408,6 @@ cc_library( ], ) -cc_library( - name = "message_matchers", - hdrs = ["message_matchers.h"], - deps = [ - "@abseil-cpp//absl/strings", - "@googletest//:gtest", - "@protobuf", - ], -) - cc_library( name = "murmur", hdrs = ["murmur.h"], @@ -447,6 +437,7 @@ cc_library( name = "parse_text_proto", hdrs = ["parse_text_proto.h"], deps = [ + "//ortools/base:status_macros", "@abseil-cpp//absl/log:check", "@protobuf", ], @@ -477,6 +468,17 @@ cc_library( hdrs = ["protobuf_util.h"], ) +cc_library( + name = "protocol-buffer-matchers", + hdrs = ["protocol-buffer-matchers.h"], + deps = [ + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + "@googletest//:gtest", + "@protobuf", + ], +) + cc_library( name = "protoutil", hdrs = ["protoutil.h"], @@ -540,8 +542,8 @@ cc_library( ) cc_library( - name = "status_matchers", - hdrs = ["status_matchers.h"], + name = "status-matchers", + hdrs = ["status-matchers.h"], deps = [ ":base", "@abseil-cpp//absl/status", diff --git a/ortools/base/CMakeLists.txt b/ortools/base/CMakeLists.txt index ffddb85b86e..61c3524d4fb 100644 --- a/ortools/base/CMakeLists.txt +++ b/ortools/base/CMakeLists.txt @@ -12,8 +12,10 @@ # limitations under the License. file(GLOB _SRCS "*.h" "*.cc") -list(FILTER _SRCS EXCLUDE REGEX ".*/.*_test.cc") -list(FILTER _SRCS EXCLUDE REGEX "/status_matchers\\.h") +list(FILTER _SRCS EXCLUDE REGEX "/.*_test.cc") +list(FILTER _SRCS EXCLUDE REGEX "/status-matchers\.h") +list(FILTER _SRCS EXCLUDE REGEX "/protocol-buffer-matchers\.h") + set(NAME ${PROJECT_NAME}_base) # Will be merge in libortools.so diff --git a/ortools/base/gmock.h b/ortools/base/gmock.h index e3ab7195c35..b941d1b83f0 100644 --- a/ortools/base/gmock.h +++ b/ortools/base/gmock.h @@ -15,7 +15,7 @@ #define OR_TOOLS_BASE_GMOCK_H_ #include "gmock/gmock.h" -#include "ortools/base/message_matchers.h" -#include "ortools/base/status_matchers.h" +#include "ortools/base/protocol-buffer-matchers.h" // IWYU pragma: export +#include "ortools/base/status-matchers.h" // IWYU pragma: export #endif // OR_TOOLS_BASE_GMOCK_H_ diff --git a/ortools/base/message_matchers.h b/ortools/base/message_matchers.h deleted file mode 100644 index a16433f0f76..00000000000 --- a/ortools/base/message_matchers.h +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2010-2025 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_BASE_MESSAGE_MATCHERS_H_ -#define OR_TOOLS_BASE_MESSAGE_MATCHERS_H_ - -#include - -#include "absl/strings/string_view.h" -#include "gmock/gmock-matchers.h" -#include "gmock/gmock.h" -#include "google/protobuf/message.h" -#include "google/protobuf/util/message_differencer.h" - -namespace testing { -namespace internal { -// Utilities. -// How to compare two fields (equal vs. equivalent). -typedef ::google::protobuf::util::MessageDifferencer::MessageFieldComparison - ProtoFieldComparison; - -// How to compare two floating-points (exact vs. approximate). -typedef ::google::protobuf::util::DefaultFieldComparator::FloatComparison - ProtoFloatComparison; - -// How to compare repeated fields (whether the order of elements matters). -typedef ::google::protobuf::util::MessageDifferencer::RepeatedFieldComparison - RepeatedFieldComparison; - -// Whether to compare all fields (full) or only fields present in the -// expected protobuf (partial). -typedef ::google::protobuf::util::MessageDifferencer::Scope - ProtoComparisonScope; - -const ProtoFieldComparison kProtoEqual = - ::google::protobuf::util::MessageDifferencer::EQUAL; -const ProtoFieldComparison kProtoEquiv = - ::google::protobuf::util::MessageDifferencer::EQUIVALENT; -const ProtoFloatComparison kProtoExact = - ::google::protobuf::util::DefaultFieldComparator::EXACT; -const ProtoFloatComparison kProtoApproximate = - ::google::protobuf::util::DefaultFieldComparator::APPROXIMATE; -const RepeatedFieldComparison kProtoCompareRepeatedFieldsRespectOrdering = - ::google::protobuf::util::MessageDifferencer::AS_LIST; -const RepeatedFieldComparison kProtoCompareRepeatedFieldsIgnoringOrdering = - ::google::protobuf::util::MessageDifferencer::AS_SET; -const ProtoComparisonScope kProtoFull = - ::google::protobuf::util::MessageDifferencer::FULL; -const ProtoComparisonScope kProtoPartial = - ::google::protobuf::util::MessageDifferencer::PARTIAL; - -// Options for comparing two protobufs. -struct ProtoComparison { - ProtoComparison() - : field_comp(kProtoEqual), - float_comp(kProtoExact), - treating_nan_as_equal(false), - has_custom_margin(false), - has_custom_fraction(false), - repeated_field_comp(kProtoCompareRepeatedFieldsRespectOrdering), - scope(kProtoFull), - float_margin(0.0), - float_fraction(0.0), - ignore_debug_string_format(false), - fail_on_no_presence_default_values(false), - verified_presence_in_string(false) {} - - ProtoFieldComparison field_comp; - ProtoFloatComparison float_comp; - bool treating_nan_as_equal; - bool has_custom_margin; // only used when float_comp = APPROXIMATE - bool has_custom_fraction; // only used when float_comp = APPROXIMATE - RepeatedFieldComparison repeated_field_comp; - ProtoComparisonScope scope; - double float_margin; // only used when has_custom_margin is set. - double float_fraction; // only used when has_custom_fraction is set. - std::vector ignore_fields; - std::vector ignore_field_paths; - std::vector unordered_fields; - bool ignore_debug_string_format; - bool fail_on_no_presence_default_values; - bool verified_presence_in_string; -}; - -// Whether the protobuf must be initialized. -const bool kMustBeInitialized = true; -const bool kMayBeUninitialized = false; - -class ProtoMatcher { - public: - using is_gtest_matcher = void; - using MessageType = ::google::protobuf::Message; - - explicit ProtoMatcher(const MessageType& message) - : message_(CloneMessage(message)) {} - - ProtoMatcher(const MessageType& message, bool, ProtoComparison&) - : message_(CloneMessage(message)) {} - - bool MatchAndExplain(const MessageType& m, - testing::MatchResultListener*) const { - return EqualsMessage(*message_, m); - } - bool MatchAndExplain(const MessageType* m, - testing::MatchResultListener*) const { - return EqualsMessage(*message_, *m); - } - - void DescribeTo(std::ostream* os) const { - *os << "has the same serialization as " << ExpectedMessageDescription(); - } - - void DescribeNegationTo(std::ostream* os) const { - *os << "does not have the same serialization as " - << ExpectedMessageDescription(); - } - - private: - std::unique_ptr CloneMessage(const MessageType& message) { - std::unique_ptr clone(message.New()); - clone->CheckTypeAndMergeFrom(message); - return clone; - } - - bool EqualsMessage(const ::google::protobuf::Message& m_1, - const ::google::protobuf::Message& m_2) const { - std::string s_1, s_2; - m_1.SerializeToString(&s_1); - m_2.SerializeToString(&s_2); - return s_1 == s_2; - } - - std::string ExpectedMessageDescription() const { - return message_->DebugString(); - } - - const std::shared_ptr message_; -}; - -using PolymorphicProtoMatcher = PolymorphicMatcher; -} // namespace internal - -inline internal::ProtoMatcher EqualsProto( - const ::google::protobuf::Message& message) { - return internal::ProtoMatcher(message); -} - -// for Pointwise -MATCHER(EqualsProto, "") { - const auto& a = ::testing::get<0>(arg); - const auto& b = ::testing::get<1>(arg); - return ::testing::ExplainMatchResult(EqualsProto(b), a, result_listener); -} - -// Constructs a matcher that matches the argument if -// argument.Equivalent(x) or argument->Equivalent(x) returns true. -inline internal::PolymorphicProtoMatcher EquivToProto( - const ::google::protobuf::Message& x) { - internal::ProtoComparison comp; - comp.field_comp = internal::kProtoEquiv; - return MakePolymorphicMatcher( - internal::ProtoMatcher(x, internal::kMayBeUninitialized, comp)); -} - -} // namespace testing - -#endif // OR_TOOLS_BASE_MESSAGE_MATCHERS_H_ diff --git a/ortools/base/parse_text_proto.h b/ortools/base/parse_text_proto.h index cc3c0411e32..292cbf419ec 100644 --- a/ortools/base/parse_text_proto.h +++ b/ortools/base/parse_text_proto.h @@ -11,27 +11,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ -#define OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ +// emulates g3/net/proto2/contrib/parse_proto/parse_text_proto.h +#ifndef ORTOOLS_BASE_PARSE_TEXT_PROTO_H_ +#define ORTOOLS_BASE_PARSE_TEXT_PROTO_H_ #include -#include #include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" #include "google/protobuf/message.h" #include "google/protobuf/text_format.h" +#include "ortools/base/status_macros.h" namespace google::protobuf::contrib::parse_proto { template -bool ParseTextProto(const std::string& input, T* proto) { - return google::protobuf::TextFormat::ParseFromString(input, proto); +absl::Status ParseTextProtoInto(absl::string_view input, T* proto) { + if (google::protobuf::TextFormat::ParseFromString(input, proto)) + return absl::OkStatus(); + return absl::Status(absl::StatusCode::kInvalidArgument, + "Could not parse the text proto\n"); } template -T ParseTextOrDie(const std::string& input) { +absl::StatusOr ParseTextProto(absl::string_view asciipb) { + T msg; + RETURN_IF_ERROR(ParseTextProtoInto(asciipb, &msg)); + return msg; +} + +template +T ParseTextOrDie(absl::string_view input) { T result; - CHECK(ParseTextProto(input, &result)); + CHECK(google::protobuf::TextFormat::ParseFromString(input, &result)); return result; } @@ -39,7 +52,7 @@ namespace text_proto_internal { class ParseProtoHelper { public: - explicit ParseProtoHelper(std::string_view asciipb) : asciipb_(asciipb) {} + explicit ParseProtoHelper(absl::string_view asciipb) : asciipb_(asciipb) {} template operator T() { // NOLINT(runtime/explicit) T result; @@ -56,10 +69,10 @@ class ParseProtoHelper { } // namespace text_proto_internal inline text_proto_internal::ParseProtoHelper ParseTextProtoOrDie( - std::string_view input) { + absl::string_view input) { return text_proto_internal::ParseProtoHelper(input); } } // namespace google::protobuf::contrib::parse_proto -#endif // OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ +#endif // ORTOOLS_BASE_PARSE_TEXT_PROTO_H_ diff --git a/ortools/base/protocol-buffer-matchers.h b/ortools/base/protocol-buffer-matchers.h new file mode 100644 index 00000000000..943c9f5c3c7 --- /dev/null +++ b/ortools/base/protocol-buffer-matchers.h @@ -0,0 +1,805 @@ +// Copyright 2010-2025 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. + +// emulates g3/testing/base/public/gmock_utils/protocol-buffer-matchers.h +#ifndef ORTOOLS_BASE_PROTOCOL_BUFFER_MATCHERS_H_ +#define ORTOOLS_BASE_PROTOCOL_BUFFER_MATCHERS_H_ + +// gMock matchers used to validate protocol buffer arguments. + +// WHAT THIS IS +// ============ +// +// This library defines the following matchers in the ::protobuf_matchers +// namespace: +// +// EqualsProto(pb) The argument equals pb. +// EquivToProto(pb) The argument is equivalent to pb. +// +// where: +// +// - pb can be either a protobuf value or a human-readable string +// representation of it. +// - When pb is a string, the matcher can optionally accept a +// template argument for the type of the protobuf, +// e.g. EqualsProto("foo: 1"). +// - "equals" is defined as the argument's Equals(pb) method returns true. +// - "equivalent to" is defined as the argument's Equivalent(pb) method +// returns true. +// +// These matchers can match either a protobuf value or a pointer to +// it. They make a copy of pb, and thus can out-live pb. When the +// match fails, the matchers print a detailed message (the value of +// the actual protobuf, the value of the expected protobuf, and which +// fields are different). +// +// EXAMPLES +// ======== +// +// using ::protobuf_matchers::EqualsProto; +// using ::protobuf_matchers::EquivToProto; +// +// // my_pb.Equals(expected_pb). +// EXPECT_THAT(my_pb, EqualsProto(expected_pb)); +// +// // my_pb is equivalent to a protobuf whose foo field is 1 and +// // whose bar field is "x". +// EXPECT_THAT(my_pb, EquivToProto("foo: 1 " +// "bar: 'x'")); + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/strings/string_view.h" +#include "gmock/gmock-matchers.h" +#include "gmock/gmock.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/io/tokenizer.h" +#include "google/protobuf/message.h" +#include "google/protobuf/util/message_differencer.h" + +namespace testing { +using DifferencerConfigFunction = + std::function; +namespace internal { +// Utilities. +// How to compare two fields (equal vs. equivalent). +typedef ::google::protobuf::util::MessageDifferencer::MessageFieldComparison + ProtoFieldComparison; + +// How to compare two floating-points (exact vs. approximate). +typedef ::google::protobuf::util::DefaultFieldComparator::FloatComparison + ProtoFloatComparison; + +// How to compare repeated fields (whether the order of elements matters). +typedef ::google::protobuf::util::MessageDifferencer::RepeatedFieldComparison + RepeatedFieldComparison; + +// Whether to compare all fields (full) or only fields present in the +// expected protobuf (partial). +typedef ::google::protobuf::util::MessageDifferencer::Scope + ProtoComparisonScope; + +const ProtoFieldComparison kProtoEqual = + ::google::protobuf::util::MessageDifferencer::EQUAL; +const ProtoFieldComparison kProtoEquiv = + ::google::protobuf::util::MessageDifferencer::EQUIVALENT; +const ProtoFloatComparison kProtoExact = + ::google::protobuf::util::DefaultFieldComparator::EXACT; +const ProtoFloatComparison kProtoApproximate = + ::google::protobuf::util::DefaultFieldComparator::APPROXIMATE; +const RepeatedFieldComparison kProtoCompareRepeatedFieldsRespectOrdering = + ::google::protobuf::util::MessageDifferencer::AS_LIST; +const RepeatedFieldComparison kProtoCompareRepeatedFieldsIgnoringOrdering = + ::google::protobuf::util::MessageDifferencer::AS_SET; +const ProtoComparisonScope kProtoFull = + ::google::protobuf::util::MessageDifferencer::FULL; +const ProtoComparisonScope kProtoPartial = + ::google::protobuf::util::MessageDifferencer::PARTIAL; + +// Options for comparing two protobufs. +struct ProtoComparison { + ProtoComparison() + : field_comp(kProtoEqual), + float_comp(kProtoExact), + treating_nan_as_equal(false), + has_custom_margin(false), + has_custom_fraction(false), + repeated_field_comp(kProtoCompareRepeatedFieldsRespectOrdering), + scope(kProtoFull), + float_margin(0.0), + float_fraction(0.0) {} + + ProtoFieldComparison field_comp; + ProtoFloatComparison float_comp; + bool treating_nan_as_equal; + bool has_custom_margin; // only used when float_comp = APPROXIMATE + bool has_custom_fraction; // only used when float_comp = APPROXIMATE + RepeatedFieldComparison repeated_field_comp; + ProtoComparisonScope scope; + double float_margin; // only used when has_custom_margin is set. + double float_fraction; // only used when has_custom_fraction is set. + std::vector ignore_fields; + std::vector ignore_field_paths; + DifferencerConfigFunction differencer_config_function; +}; + +// Whether the protobuf must be initialized. +const bool kMustBeInitialized = true; +const bool kMayBeUninitialized = false; + +class StringErrorCollector : public ::google::protobuf::io::ErrorCollector { + public: + explicit StringErrorCollector(std::string* error_text) + : error_text_(error_text) {} + + void RecordError(int line, int column, absl::string_view message) override { + std::ostringstream stream; + stream << line << '(' << column << "): " << message << std::endl; + *error_text_ += stream.str(); + } + + void RecordWarning(int line, int column, absl::string_view message) override { + std::ostringstream stream; + stream << line << '(' << column << "): " << message << std::endl; + *error_text_ += stream.str(); + } + + private: + std::string* error_text_; + StringErrorCollector(const StringErrorCollector&) = delete; + StringErrorCollector& operator=(const StringErrorCollector&) = delete; +}; + +// Parses the TextFormat representation of a protobuf, allowing required fields +// to be missing. Returns true if successful. +bool ParsePartialFromAscii(const std::string& pb_ascii, + ::google::protobuf::Message* proto, + std::string* error_text) { + ::google::protobuf::TextFormat::Parser parser; + StringErrorCollector collector(error_text); + parser.RecordErrorsTo(&collector); + parser.AllowPartialMessage(true); + return parser.ParseFromString(pb_ascii, proto); +} + +// Returns true iff p and q can be compared (i.e. have the same descriptor). +bool ProtoComparable(const ::google::protobuf::Message& p, + const ::google::protobuf::Message& q) { + return p.GetDescriptor() == q.GetDescriptor(); +} + +template +std::string JoinStringPieces(const Container& strings, + absl::string_view separator) { + std::stringstream stream; + absl::string_view sep = ""; + for (const absl::string_view str : strings) { + stream << sep << str; + sep = separator; + } + return stream.str(); +} + +// Find all the descriptors for the ingore_fields. +std::vector GetFieldDescriptors( + const ::google::protobuf::Descriptor* proto_descriptor, + const std::vector& ignore_fields) { + std::vector ignore_descriptors; + std::vector remaining_descriptors; + + const ::google::protobuf::DescriptorPool* pool = + proto_descriptor->file()->pool(); + for (const std::string& name : ignore_fields) { + if (const ::google::protobuf::FieldDescriptor* field = + pool->FindFieldByName(name)) { + ignore_descriptors.push_back(field); + } else { + remaining_descriptors.push_back(name); + } + } + + CHECK(remaining_descriptors.empty()) + << "Could not find fields for proto " << proto_descriptor->full_name() + << " with fully qualified names: " + << JoinStringPieces(remaining_descriptors, ","); + return ignore_descriptors; +} + +// Sets the ignored fields corresponding to ignore_fields in differencer. Dies +// if any is invalid. +void SetIgnoredFieldsOrDie( + const ::google::protobuf::Descriptor& root_descriptor, + const std::vector& ignore_fields, + ::google::protobuf::util::MessageDifferencer* differencer) { + if (!ignore_fields.empty()) { + std::vector ignore_descriptors = + GetFieldDescriptors(&root_descriptor, ignore_fields); + for (std::vector::iterator it = + ignore_descriptors.begin(); + it != ignore_descriptors.end(); ++it) { + differencer->IgnoreField(*it); + } + } +} + +// A criterion that ignores a field path. +class IgnoreFieldPathCriteria + : public ::google::protobuf::util::MessageDifferencer::IgnoreCriteria { + public: + explicit IgnoreFieldPathCriteria( + const std::vector< + ::google::protobuf::util::MessageDifferencer::SpecificField>& + field_path) + : ignored_field_path_(field_path) {} + + bool IsIgnored( + const ::google::protobuf::Message& message1, + const ::google::protobuf::Message& message2, + const ::google::protobuf::FieldDescriptor* field, + const std::vector< + ::google::protobuf::util::MessageDifferencer::SpecificField>& + parent_fields) override { + // The off by one is for the current field. + if (parent_fields.size() + 1 != ignored_field_path_.size()) { + return false; + } + for (size_t i = 0; i < parent_fields.size(); ++i) { + const auto& cur_field = parent_fields[i]; + const auto& ignored_field = ignored_field_path_[i]; + // We could compare pointers but it's not guaranteed that descriptors come + // from the same pool. + if (cur_field.field->full_name() != ignored_field.field->full_name()) { + return false; + } + + // repeated_field[i] is ignored if repeated_field is ignored. To put it + // differently: if ignored_field specifies an index, we ignore only a + // field with the same index. + if (ignored_field.index != -1 && ignored_field.index != cur_field.index) { + return false; + } + } + return field->full_name() == ignored_field_path_.back().field->full_name(); + } + + private: + const std::vector + ignored_field_path_; +}; + +// Parses a field path and returns individual components. +std::vector +ParseFieldPathOrDie(const std::string& relative_field_path, + const ::google::protobuf::Descriptor& root_descriptor) { + std::vector + field_path; + + // We're parsing a dot-separated list of elements that can be either: + // - field names + // - extension names + // - indexed field names + // The parser is very permissive as to what is a field name, then we check + // the field name against the descriptor. + + // Regular parsers. Consume() does not handle optional captures so we split it + // in two regexps. + const std::regex field_regex(R"(([^.()[\]]+))"); + const std::regex field_subscript_regex(R"(([^.()[\]]+)\[(\d+)\])"); + const std::regex extension_regex(R"(\(([^)]+)\))"); + + const auto begin = std::begin(relative_field_path); + auto it = begin; + const auto end = std::end(relative_field_path); + while (it != end) { + // Consume a dot, except on the first iteration. + if (it != std::begin(relative_field_path) && *(it++) != '.') { + LOG(FATAL) << "Cannot parse field path '" << relative_field_path + << "' at offset " << std::distance(begin, it) + << ": expected '.'"; + } + // Try to consume a field name. If that fails, consume an extension name. + ::google::protobuf::util::MessageDifferencer::SpecificField field; + std::smatch match_results; + if (std::regex_search(it, end, match_results, field_subscript_regex) || + std::regex_search(it, end, match_results, field_regex)) { + std::string name = match_results[1].str(); + if (field_path.empty()) { + field.field = root_descriptor.FindFieldByName(name); + CHECK(field.field) << "No such field '" << name << "' in message '" + << root_descriptor.full_name() << "'"; + } else { + const ::google::protobuf::util::MessageDifferencer::SpecificField& + parent = field_path.back(); + field.field = parent.field->message_type()->FindFieldByName(name); + CHECK(field.field) << "No such field '" << name << "' in '" + << parent.field->full_name() << "'"; + } + if (match_results.size() > 2 && match_results[2].matched) { + std::string number = match_results[2].str(); + field.index = std::stoi(number); + } + + } else if (std::regex_search(it, end, match_results, extension_regex)) { + std::string name = match_results[1].str(); + field.field = ::google::protobuf::DescriptorPool::generated_pool() + ->FindExtensionByName(name); + CHECK(field.field) << "No such extension '" << name << "'"; + if (field_path.empty()) { + CHECK(root_descriptor.IsExtensionNumber(field.field->number())) + << "Extension '" << name << "' does not extend message '" + << root_descriptor.full_name() << "'"; + } else { + const ::google::protobuf::util::MessageDifferencer::SpecificField& + parent = field_path.back(); + CHECK(parent.field->message_type()->IsExtensionNumber( + field.field->number())) + << "Extension '" << name << "' does not extend '" + << parent.field->full_name() << "'"; + } + } else { + LOG(FATAL) << "Cannot parse field path '" << relative_field_path + << "' at offset " << std::distance(begin, it) + << ": expected field or extension"; + } + auto consume = match_results[0].length(); + it += consume; + field_path.push_back(field); + } + + CHECK(!field_path.empty()); + CHECK(field_path.back().index == -1) + << "Terminally ignoring fields by index is currently not supported ('" + << relative_field_path << "')"; + + return field_path; +} + +// Sets the ignored field paths corresponding to field_paths in differencer. +// Dies if any path is invalid. +void SetIgnoredFieldPathsOrDie( + const ::google::protobuf::Descriptor& root_descriptor, + const std::vector& field_paths, + ::google::protobuf::util::MessageDifferencer* differencer) { + for (const std::string& field_path : field_paths) { + differencer->AddIgnoreCriteria(new IgnoreFieldPathCriteria( + ParseFieldPathOrDie(field_path, root_descriptor))); + } +} + +// Configures a MessageDifferencer and DefaultFieldComparator to use the logic +// described in comp. The configured differencer is the output of this function, +// but a FieldComparator must be provided to keep ownership clear. +void ConfigureDifferencer( + const ProtoComparison& comp, + ::google::protobuf::util::DefaultFieldComparator* comparator, + ::google::protobuf::util::MessageDifferencer* differencer, + const ::google::protobuf::Descriptor* descriptor) { + differencer->set_message_field_comparison(comp.field_comp); + differencer->set_scope(comp.scope); + comparator->set_float_comparison(comp.float_comp); + comparator->set_treat_nan_as_equal(comp.treating_nan_as_equal); + differencer->set_repeated_field_comparison(comp.repeated_field_comp); + SetIgnoredFieldsOrDie(*descriptor, comp.ignore_fields, differencer); + SetIgnoredFieldPathsOrDie(*descriptor, comp.ignore_field_paths, differencer); + if (comp.float_comp == kProtoApproximate && + (comp.has_custom_margin || comp.has_custom_fraction)) { + // Two fields will be considered equal if they're within the fraction _or_ + // within the margin. So setting the fraction to 0.0 makes this effectively + // a "SetMargin". Similarly, setting the margin to 0.0 makes this + // effectively a "SetFraction". + comparator->SetDefaultFractionAndMargin(comp.float_fraction, + comp.float_margin); + } + differencer->set_field_comparator(comparator); + if (comp.differencer_config_function) { + comp.differencer_config_function(comparator, differencer); + } +} + +// Returns true iff actual and expected are comparable and match. The +// comp argument specifies how the two are compared. +bool ProtoCompare(const ProtoComparison& comp, + const ::google::protobuf::Message& actual, + const ::google::protobuf::Message& expected) { + if (!ProtoComparable(actual, expected)) return false; + + ::google::protobuf::util::MessageDifferencer differencer; + ::google::protobuf::util::DefaultFieldComparator field_comparator; + ConfigureDifferencer(comp, &field_comparator, &differencer, + actual.GetDescriptor()); + + // It's important for 'expected' to be the first argument here, as + // Compare() is not symmetric. When we do a partial comparison, + // only fields present in the first argument of Compare() are + // considered. + return differencer.Compare(expected, actual); +} + +// Describes the types of the expected and the actual protocol buffer. +std::string DescribeTypes(const ::google::protobuf::Message& expected, + const ::google::protobuf::Message& actual) { + std::ostringstream s; + s << "whose type should be " << expected.GetDescriptor()->full_name() + << " but actually is " << actual.GetDescriptor()->full_name(); + return s.str(); +} + +// Prints the protocol buffer pointed to by proto. +std::string PrintProtoPointee(const ::google::protobuf::Message* proto) { + if (proto == NULL) return ""; + return "which points to " + ::testing::PrintToString(*proto); +} + +// Describes the differences between the two protocol buffers. +std::string DescribeDiff(const ProtoComparison& comp, + const ::google::protobuf::Message& actual, + const ::google::protobuf::Message& expected) { + ::google::protobuf::util::MessageDifferencer differencer; + ::google::protobuf::util::DefaultFieldComparator field_comparator; + ConfigureDifferencer(comp, &field_comparator, &differencer, + actual.GetDescriptor()); + + std::string diff; + differencer.ReportDifferencesToString(&diff); + + // We must put 'expected' as the first argument here, as Compare() + // reports the diff in terms of how the protobuf changes from the + // first argument to the second argument. + differencer.Compare(expected, actual); + + // Removes the trailing '\n' in the diff to make the output look nicer. + if (diff.length() > 0 && *(diff.end() - 1) == '\n') { + diff.erase(diff.end() - 1); + } + + return "with the difference:\n" + diff; +} + +// Common code for implementing EqualsProto and EquivToProto. +class ProtoMatcherBase { + public: + ProtoMatcherBase( + bool must_be_initialized, // Must the argument be fully initialized? + const ProtoComparison& comp) // How to compare the two protobufs. + : must_be_initialized_(must_be_initialized), comp_(new auto(comp)) {} + + ProtoMatcherBase(const ProtoMatcherBase& other) + : must_be_initialized_(other.must_be_initialized_), + comp_(new auto(*other.comp_)) {} + + ProtoMatcherBase(ProtoMatcherBase&& other) = default; + + virtual ~ProtoMatcherBase() {} + + // Prints the expected protocol buffer. + virtual void PrintExpectedTo(::std::ostream* os) const = 0; + + // Returns the expected value as a protobuf object; if the object + // cannot be created (e.g. in ProtoStringMatcher), explains why to + // 'listener' and returns NULL. The caller must call + // DeleteExpectedProto() on the returned value later. + virtual const ::google::protobuf::Message* CreateExpectedProto( + const ::google::protobuf::Message& arg, // For determining the type of + // the expected protobuf. + ::testing::MatchResultListener* listener) const = 0; + + // Deletes the given expected protobuf, which must be obtained from + // a call to CreateExpectedProto() earlier. + virtual void DeleteExpectedProto( + const ::google::protobuf::Message* expected) const = 0; + + bool MatchAndExplain(const ::google::protobuf::Message& arg, + ::testing::MatchResultListener* listener) const { + return MatchAndExplain(arg, false, listener); + } + + bool MatchAndExplain(const ::google::protobuf::Message* arg, + ::testing::MatchResultListener* listener) const { + return (arg != NULL) && MatchAndExplain(*arg, true, listener); + } + + // Describes the expected relation between the actual protobuf and + // the expected one. + void DescribeRelationToExpectedProto(::std::ostream* os) const { + if (comp_->repeated_field_comp == + kProtoCompareRepeatedFieldsIgnoringOrdering) { + *os << "(ignoring repeated field ordering) "; + } + if (!comp_->ignore_fields.empty()) { + *os << "(ignoring fields: "; + const char* sep = ""; + for (size_t i = 0; i < comp_->ignore_fields.size(); ++i, sep = ", ") + *os << sep << comp_->ignore_fields[i]; + *os << ") "; + } + if (comp_->float_comp == kProtoApproximate) { + *os << "approximately "; + if (comp_->has_custom_margin || comp_->has_custom_fraction) { + *os << "("; + if (comp_->has_custom_margin) { + std::stringstream ss; + ss << std::setprecision(std::numeric_limits::digits10 + 2) + << comp_->float_margin; + *os << "absolute error of float or double fields <= " << ss.str(); + } + if (comp_->has_custom_margin && comp_->has_custom_fraction) { + *os << " or "; + } + if (comp_->has_custom_fraction) { + std::stringstream ss; + ss << std::setprecision(std::numeric_limits::digits10 + 2) + << comp_->float_fraction; + *os << "relative error of float or double fields <= " << ss.str(); + } + *os << ") "; + } + } + + if (comp_->differencer_config_function) { + *os << "(with custom differencer config) "; + } + + *os << (comp_->scope == kProtoPartial ? "partially " : "") + << (comp_->field_comp == kProtoEqual ? "equal" : "equivalent") + << (comp_->treating_nan_as_equal ? " (treating NaNs as equal)" : "") + << " to "; + PrintExpectedTo(os); + } + + void DescribeTo(::std::ostream* os) const { + *os << "is " << (must_be_initialized_ ? "fully initialized and " : ""); + DescribeRelationToExpectedProto(os); + } + + void DescribeNegationTo(::std::ostream* os) const { + *os << "is " << (must_be_initialized_ ? "not fully initialized or " : "") + << "not "; + DescribeRelationToExpectedProto(os); + } + + bool must_be_initialized() const { return must_be_initialized_; } + + const ProtoComparison& comp() const { return *comp_; } + + private: + bool MatchAndExplain( + const ::google::protobuf::Message& arg, + bool is_matcher_for_pointer, // true iff this matcher is used to match a protobuf pointer. + ::testing::MatchResultListener* listener) const { + if (must_be_initialized_ && !arg.IsInitialized()) { + *listener << "which isn't fully initialized"; + return false; + } + + const ::google::protobuf::Message* const expected = + CreateExpectedProto(arg, listener); + if (expected == NULL) return false; + + // Protobufs of different types cannot be compared. + const bool comparable = ProtoComparable(arg, *expected); + const bool match = comparable && ProtoCompare(comp(), arg, *expected); + + // Explaining the match result is expensive. We don't want to waste + // time calculating an explanation if the listener isn't interested. + if (listener->IsInterested()) { + const char* sep = ""; + if (is_matcher_for_pointer) { + *listener << PrintProtoPointee(&arg); + sep = ",\n"; + } + + if (!comparable) { + *listener << sep << DescribeTypes(*expected, arg); + } else if (!match) { + *listener << sep << DescribeDiff(comp(), arg, *expected); + } + } + + DeleteExpectedProto(expected); + return match; + } + + const bool must_be_initialized_; + std::unique_ptr comp_; +}; + +// Returns a copy of the given proto2 message. +inline ::google::protobuf::Message* CloneProto2( + const ::google::protobuf::Message& src) { + ::google::protobuf::Message* clone = src.New(); + clone->CopyFrom(src); + return clone; +} + +// Implements EqualsProto and EquivToProto where the matcher parameter is a +// protobuf. +class ProtoMatcher : public ProtoMatcherBase { + public: + using MessageType = ::google::protobuf::Message; + + ProtoMatcher( + const MessageType& expected, // The expected protobuf. + bool must_be_initialized, // Must the argument be fully initialized? + const ProtoComparison& comp) // How to compare the two protobufs. + : ProtoMatcherBase(must_be_initialized, comp), + expected_(CloneProto2(expected)) { + if (must_be_initialized) { + CHECK(expected.IsInitialized()) + << "The protocol buffer given to *InitializedProto() " + << "must itself be initialized, but the following required fields " + << "are missing: " << expected.InitializationErrorString() << "."; + } + } + + virtual void PrintExpectedTo(::std::ostream* os) const { + *os << expected_->GetDescriptor()->full_name() << " "; + ::testing::internal::UniversalPrint(*expected_, os); + } + + virtual const ::google::protobuf::Message* CreateExpectedProto( + const ::google::protobuf::Message& /* arg */, + ::testing::MatchResultListener* /* listener */) const { + return expected_.get(); + } + + virtual void DeleteExpectedProto( + const ::google::protobuf::Message* /* expected */) const {} + + private: + const std::shared_ptr expected_; +}; + +using PolymorphicProtoMatcher = ::testing::PolymorphicMatcher; + +// Implements EqualsProto and EquivToProto where the matcher parameter is a +// string. +class ProtoStringMatcher : public ProtoMatcherBase { + public: + using MessageType = ::google::protobuf::Message; + + ProtoStringMatcher( + absl::string_view + expected, // The text representing the expected protobuf. + bool must_be_initialized, // Must the argument be fully initialized? + const ProtoComparison comp) // How to compare the two protobufs. + : ProtoMatcherBase(must_be_initialized, comp), expected_(std::string(expected)) {} + + // Parses the expected string as a protobuf of the same type as arg, + // and returns the parsed protobuf (or NULL when the parse fails). + // The caller must call DeleteExpectedProto() on the return value + // later. + virtual const MessageType* CreateExpectedProto( + const MessageType& arg, + ::testing::MatchResultListener* listener) const { + ::google::protobuf::Message* expected_proto = arg.New(); + // We don't insist that the expected string parses as an + // *initialized* protobuf. Otherwise EqualsProto("...") may + // wrongfully fail when the actual protobuf is not fully + // initialized. + std::string error_text; + if (ParsePartialFromAscii(expected_, expected_proto, &error_text)) { + return expected_proto; + } else { + delete expected_proto; + if (listener->IsInterested()) { + *listener << "where "; + PrintExpectedTo(listener->stream()); + *listener << " doesn't parse as a " << arg.GetDescriptor()->full_name() + << ":\n" + << error_text; + } + return NULL; + } + } + + virtual void DeleteExpectedProto( + const ::google::protobuf::Message* expected) const { + delete expected; + } + + virtual void PrintExpectedTo(::std::ostream* os) const { + *os << "<" << expected_ << ">"; + } + + private: + const std::string expected_; +}; + +} // namespace internal + +// Constructs a matcher that matches the argument if +// argument.Equals(m) or argument->Equals(m) returns true. +inline internal::PolymorphicProtoMatcher EqualsProto( + const ::google::protobuf::Message& m) { + internal::ProtoComparison comp; + comp.field_comp = internal::kProtoEqual; + return ::testing::MakePolymorphicMatcher( + internal::ProtoMatcher(m, internal::kMayBeUninitialized, comp)); +} + +inline PolymorphicMatcher EqualsProto( + absl::string_view m) { + internal::ProtoComparison comp; + comp.field_comp = internal::kProtoEqual; + return MakePolymorphicMatcher( + internal::ProtoStringMatcher(m, internal::kMayBeUninitialized, comp)); +} + +// for Pointwise +MATCHER(EqualsProto, "") { + const auto& a = ::testing::get<0>(arg); + const auto& b = ::testing::get<1>(arg); + return ::testing::ExplainMatchResult(EqualsProto(b), a, result_listener); +} + +// Constructs a matcher that matches the argument if +// argument.Equivalent(m) or argument->Equivalent(m) returns true. +inline internal::PolymorphicProtoMatcher EquivToProto( + const ::google::protobuf::Message& m) { + internal::ProtoComparison comp; + comp.field_comp = internal::kProtoEquiv; + return MakePolymorphicMatcher( + internal::ProtoMatcher(m, internal::kMayBeUninitialized, comp)); +} + +inline PolymorphicMatcher EquivToProto( + absl::string_view m) { + internal::ProtoComparison comp; + comp.field_comp = internal::kProtoEquiv; + return MakePolymorphicMatcher( + internal::ProtoStringMatcher(m, internal::kMayBeUninitialized, comp)); +} + +// Returns a matcher that is the same as a given inner matcher, but applies a +// given function to the message differencer before using it for the +// comparison between the expected and actual protobufs. +// +// Prefer more specific transformer functions if possible; they result in +// better error messages and more readable test code. +// +// By default, the differencer is configured to use the field comparator which +// is also passed to the config function. It's possible to modify that +// comparator, although it's preferable to customize it through other +// transformers, e.g. Approximately. +// +// It's also possible to replace the comparator entirely, by passing it to +// set_field_comparator() method of the provided differencer. The user retains +// the ownership over the comparator and must guarantee that its lifetime +// exceeds the lifetime of the matcher. +// +// The config function will be applied after any configuration settings +// specified by other transformers. Overwriting these settings may result in +// misleading test failure messages; in particular, a config function that +// provides its own field comparator should not be used with transformers that +// rely on the default comparator, i.e. Approximately and TreatingNaNsAsEqual. +template +inline InnerProtoMatcher WithDifferencerConfig( + DifferencerConfigFunction differencer_config_function, + InnerProtoMatcher inner_proto_matcher) { + inner_proto_matcher.mutable_impl().SetDifferencerConfigFunction( + differencer_config_function); + return inner_proto_matcher; +} + +} // namespace testing + +#endif // ORTOOLS_BASE_PROTOCOL_BUFFER_MATCHERS_H_ diff --git a/ortools/base/status_matchers.h b/ortools/base/status-matchers.h similarity index 94% rename from ortools/base/status_matchers.h rename to ortools/base/status-matchers.h index 10b4aa1a735..f1524d014ac 100644 --- a/ortools/base/status_matchers.h +++ b/ortools/base/status-matchers.h @@ -11,17 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_BASE_STATUS_MATCHERS_H_ -#define OR_TOOLS_BASE_STATUS_MATCHERS_H_ +// emulates g3/testing/base/public/gmock_utils/status-matchers.h +#ifndef ORTOOLS_BASE_STATUS_MATCHERS_H_ +#define ORTOOLS_BASE_STATUS_MATCHERS_H_ -#include #include #include +#include +#include #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "gmock/gmock-matchers.h" +#include "gtest/gtest.h" namespace testing::status { @@ -159,7 +162,7 @@ class StatusIsMatcherCommonImpl { public: StatusIsMatcherCommonImpl( ::testing::Matcher code_matcher, - ::testing::Matcher message_matcher) + ::testing::Matcher message_matcher) : code_matcher_(std::move(code_matcher)), message_matcher_(std::move(message_matcher)) {} @@ -200,7 +203,7 @@ class StatusIsMatcherCommonImpl { private: const ::testing::Matcher code_matcher_; - const ::testing::Matcher message_matcher_; + const ::testing::Matcher message_matcher_; }; // Monomorphic implementation of matcher StatusIs() for a given type T. T can @@ -234,10 +237,10 @@ class MonoStatusIsMatcherImpl : public ::testing::MatcherInterface { class StatusIsMatcher { public: StatusIsMatcher(::testing::Matcher code_matcher, - ::testing::Matcher message_matcher) + ::testing::Matcher message_matcher) : common_impl_( ::testing::MatcherCast(code_matcher), - ::testing::MatcherCast(message_matcher)) {} + ::testing::MatcherCast(message_matcher)) {} // Converts this polymorphic matcher to a monomorphic matcher of the given // type. T can be StatusOr<>, Status, or a reference to either of them. @@ -288,4 +291,4 @@ internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher) { ASSERT_TRUE(statusor.ok()) << statusor.status(); \ lhs = std::move(statusor.value()) -#endif // OR_TOOLS_BASE_STATUS_MATCHERS_H_ +#endif // ORTOOLS_BASE_STATUS_MATCHERS_H_ From 4f0d1e9eeb67b8b87fed478fa244ebe019a9e451 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 29 Aug 2025 15:14:17 +0200 Subject: [PATCH 016/491] math_opt: export some cpp/ tests --- ortools/math_opt/cpp/BUILD.bazel | 84 + ortools/math_opt/cpp/CMakeLists.txt | 1 + ortools/math_opt/cpp/basis_status_test.cc | 25 + ortools/math_opt/cpp/enums_testing.h | 108 + ortools/math_opt/cpp/key_types.h | 4 +- ortools/math_opt/cpp/model_test.cc | 2587 +++++++++++++++++ .../cpp/remote_streaming_mode_test.cc | 72 + .../math_opt/cpp/sparse_containers_test.cc | 513 ++++ ortools/math_opt/elemental/element_storage.h | 3 +- ortools/math_opt/elemental/elemental_test.cc | 3 +- ortools/math_opt/python/BUILD.bazel | 6 +- .../math_opt/python/bounded_expressions.py | 1 + .../math_opt/python/linear_expression_test.py | 28 +- .../python/normalized_inequality_test.py | 18 +- ortools/math_opt/python/result_test.py | 14 +- ortools/math_opt/samples/cpp/BUILD.bazel | 5 - .../solver_tests/invalid_input_tests.cc | 1 - ortools/math_opt/solvers/gscip/BUILD.bazel | 2 +- 18 files changed, 3425 insertions(+), 50 deletions(-) create mode 100644 ortools/math_opt/cpp/basis_status_test.cc create mode 100644 ortools/math_opt/cpp/enums_testing.h create mode 100644 ortools/math_opt/cpp/model_test.cc create mode 100644 ortools/math_opt/cpp/remote_streaming_mode_test.cc create mode 100644 ortools/math_opt/cpp/sparse_containers_test.cc diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index 82931c20cfb..99da30313cb 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") @@ -43,6 +45,17 @@ cc_library( ], ) +cc_test( + name = "basis_status_test", + srcs = ["basis_status_test.cc"], + deps = [ + ":basis_status", + ":enums_testing", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + ], +) + cc_library( name = "sparse_containers", srcs = ["sparse_containers.cc"], @@ -70,6 +83,21 @@ cc_library( ], ) +cc_test( + name = "sparse_containers_test", + srcs = ["sparse_containers_test.cc"], + deps = [ + ":matchers", + ":math_opt", + ":sparse_containers", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/math_opt/constraints/quadratic:quadratic_constraint", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/status", + ], +) + cc_library( name = "model", srcs = ["model.cc"], @@ -107,6 +135,35 @@ cc_library( ], ) +cc_test( + name = "model_test", + srcs = ["model_test.cc"], + deps = [ + ":key_types", + ":linear_constraint", + ":math_opt", + ":model", + ":update_tracker", + ":variable_and_expressions", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/base:parse_text_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/constraints/indicator:indicator_constraint", + "//ortools/math_opt/constraints/quadratic:quadratic_constraint", + "//ortools/math_opt/constraints/second_order_cone:second_order_cone_constraint", + "//ortools/math_opt/constraints/sos:sos1_constraint", + "//ortools/math_opt/constraints/sos:sos2_constraint", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_types", + "//ortools/math_opt/testing:stream", + "//ortools/util:fp_roundtrip_conv_testing", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + ], +) + cc_library( name = "variable_and_expressions", srcs = ["variable_and_expressions.cc"], @@ -412,6 +469,7 @@ cc_library( "//ortools/math_opt/solvers:gurobi_cc_proto", "//ortools/math_opt/solvers:highs_cc_proto", "//ortools/math_opt/solvers/gscip:gscip_cc_proto", + "//ortools/pdlp:solvers_cc_proto", "//ortools/port:proto_utils", "//ortools/sat:sat_parameters_cc_proto", "//ortools/util:status_macros", @@ -457,6 +515,21 @@ cc_library( ], ) +cc_library( + name = "enums_testing", + testonly = True, + hdrs = ["enums_testing.h"], + deps = [ + ":enums", + "//ortools/base:gmock", + "//ortools/base:logging", + "@abseil-cpp//absl/numeric:int128", + "@abseil-cpp//absl/strings", + ], + # Make sure the tests are included when using --dynamic_mode=off. + alwayslink = 1, +) + cc_library( name = "statistics", srcs = ["statistics.cc"], @@ -583,3 +656,14 @@ cc_library( hdrs = ["remote_streaming_mode.h"], deps = ["@abseil-cpp//absl/strings:string_view"], ) + +cc_test( + name = "remote_streaming_mode_test", + srcs = ["remote_streaming_mode_test.cc"], + deps = [ + ":remote_streaming_mode", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/flags:marshalling", + "@abseil-cpp//absl/strings", + ], +) diff --git a/ortools/math_opt/cpp/CMakeLists.txt b/ortools/math_opt/cpp/CMakeLists.txt index 323cc6c99bf..a0f57d18086 100644 --- a/ortools/math_opt/cpp/CMakeLists.txt +++ b/ortools/math_opt/cpp/CMakeLists.txt @@ -15,6 +15,7 @@ set(NAME ${PROJECT_NAME}_math_opt_cpp) add_library(${NAME} OBJECT) file(GLOB _SRCS "*.h" "*.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc") list(FILTER _SRCS EXCLUDE REGEX "/matchers\\.") target_sources(${NAME} PRIVATE ${_SRCS}) diff --git a/ortools/math_opt/cpp/basis_status_test.cc b/ortools/math_opt/cpp/basis_status_test.cc new file mode 100644 index 00000000000..ca602c76393 --- /dev/null +++ b/ortools/math_opt/cpp/basis_status_test.cc @@ -0,0 +1,25 @@ +// Copyright 2010-2025 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/basis_status.h" + +#include "gtest/gtest.h" +#include "ortools/math_opt/cpp/enums_testing.h" + +namespace operations_research::math_opt { +namespace { + +INSTANTIATE_TYPED_TEST_SUITE_P(BasisStatus, EnumTest, BasisStatus); + +} +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/enums_testing.h b/ortools/math_opt/cpp/enums_testing.h new file mode 100644 index 00000000000..db9880e2b19 --- /dev/null +++ b/ortools/math_opt/cpp/enums_testing.h @@ -0,0 +1,108 @@ +// Copyright 2010-2025 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. + +// Typed tests for enums that use enums.h. +#ifndef OR_TOOLS_MATH_OPT_CPP_ENUMS_TESTING_H_ +#define OR_TOOLS_MATH_OPT_CPP_ENUMS_TESTING_H_ + +#include +#include +#include +#include + +#include "absl/numeric/int128.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/math_opt/cpp/enums.h" + +namespace operations_research::math_opt { + +// A type parameterized test suite for testing the correct implementation of +// Enum for a given enum. +// +// Usage: +// +// INSTANTIATE_TYPED_TEST_SUITE_P(, EnumTest, ); +// +template +class EnumTest : public testing::Test { + public: +}; + +TYPED_TEST_SUITE_P(EnumTest); + +TYPED_TEST_P(EnumTest, UnderlyingTypeFits) { + // We could static_assert that but using a gUnit test works too. + using underlying_type_limits = + std::numeric_limits>; + using Proto = typename Enum::Proto; + CHECK_GE(EnumProto::kMin, underlying_type_limits::min()); + CHECK_LE(EnumProto::kMax, underlying_type_limits::max()); +} + +TYPED_TEST_P(EnumTest, AllProtoValues) { + using Proto = typename Enum::Proto; + + bool found_unspecified = false; + std::vector found_values; + for (int i = EnumProto::kMin; i <= EnumProto::kMax; ++i) { + if (!EnumProto::kIsValid(i)) { + continue; + } + + const auto proto_value = static_cast(i); + SCOPED_TRACE(proto_value); + + const std::optional cpp_enum = EnumFromProto(proto_value); + if (proto_value == Enum::kProtoUnspecifiedValue) { + found_unspecified = true; + ASSERT_FALSE(cpp_enum.has_value()); + } else { + ASSERT_TRUE(cpp_enum.has_value()); + found_values.push_back(*cpp_enum); + } + + const Proto reconverted_proto_value = EnumToProto(cpp_enum); + EXPECT_EQ(proto_value, reconverted_proto_value); + } + + // We should have found all C++ enum + nullopt when traversing all possible + // Proto enums. And the AllValues() of C++ should not contain any additional + // value. + ASSERT_TRUE(found_unspecified); + ASSERT_THAT(found_values, ::testing::UnorderedElementsAreArray( + Enum::AllValues())); +} + +TYPED_TEST_P(EnumTest, AllCppValuesString) { + for (const TypeParam cpp_value : Enum::AllValues()) { + const auto cpp_underlying_value = + static_cast>(cpp_value); + const std::optional opt_str_value = + EnumToOptString(cpp_value); + ASSERT_TRUE(opt_str_value.has_value()) + << "cpp_value: " << cpp_underlying_value; + EXPECT_THAT(EnumFromString(*opt_str_value), + ::testing::Optional(cpp_value)) + << "cpp_value: " << cpp_underlying_value; + } +} + +REGISTER_TYPED_TEST_SUITE_P(EnumTest, UnderlyingTypeFits, AllProtoValues, + AllCppValuesString); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CPP_ENUMS_TESTING_H_ diff --git a/ortools/math_opt/cpp/key_types.h b/ortools/math_opt/cpp/key_types.h index 5ceec1dd80d..a1c304bfcf0 100644 --- a/ortools/math_opt/cpp/key_types.h +++ b/ortools/math_opt/cpp/key_types.h @@ -149,12 +149,12 @@ std::vector Values(const Map& map, namespace internal { // The CHECK message to use when a KeyType::storage() is nullptr. -inline constexpr absl::string_view kKeyHasNullModelStorage = +inline const std::string kKeyHasNullModelStorage = "The input key has null .storage()."; // The CHECK message to use when two KeyType with different storage() are used // in the same collection. -inline constexpr absl::string_view kObjectsFromOtherModelStorage = +inline const std::string kObjectsFromOtherModelStorage = "The input objects belongs to another model."; // The Status message to use when an input KeyType is from an unexpected diff --git a/ortools/math_opt/cpp/model_test.cc b/ortools/math_opt/cpp/model_test.cc new file mode 100644 index 00000000000..1ae84a23913 --- /dev/null +++ b/ortools/math_opt/cpp/model_test.cc @@ -0,0 +1,2587 @@ +// Copyright 2010-2025 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. + +// Unit tests for math_opt.h on model reading and construction. The underlying +// solver is not invoked. For tests that run Solve(), see +// ortools/math_opt/solver_tests/*. + +#include "ortools/math_opt/cpp/model.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_text_proto.h" +#include "ortools/base/string_view_migration.h" +#include "ortools/math_opt/constraints/indicator/indicator_constraint.h" +#include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" +#include "ortools/math_opt/constraints/sos/sos1_constraint.h" +#include "ortools/math_opt/constraints/sos/sos2_constraint.h" +#include "ortools/math_opt/cpp/key_types.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/update_tracker.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/model_storage_types.h" +#include "ortools/math_opt/testing/stream.h" +#include "ortools/util/fp_roundtrip_conv_testing.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTextProto; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +constexpr double kInf = std::numeric_limits::infinity(); + +// max 2.0 * y + 3.5 +// s.t. x + y - 1 <= 0.5 (c) +// 2.0 * y >= 0.5 (d) +// x unbounded +// y in {0, 1} +class ModelingTest : public testing::Test { + protected: + ModelingTest() + : model_("math_opt_model"), + x_(model_.AddVariable("x")), + y_(model_.AddBinaryVariable("y")), + c_(model_.AddLinearConstraint(x_ + y_ - 1.0 <= 0.5, "c")), + d_(model_.AddLinearConstraint(2.0 * y_ >= 0.5, "d")) { + model_.Maximize(2.0 * y_ + 3.5); + } + + Model model_; + const Variable x_; + const Variable y_; + const LinearConstraint c_; + const LinearConstraint d_; +}; + +TEST(ModelTest, FromValidModelProto) { + // Here we assume Model::FromModelProto() uses ModelStorage::FromModelProto() + // and thus we don't test everything. + ModelProto model_proto; + model_proto.set_name("model"); + const VariableId x_id(1); + model_proto.mutable_variables()->add_ids(x_id.value()); + model_proto.mutable_variables()->add_lower_bounds(0.0); + model_proto.mutable_variables()->add_upper_bounds(1.0); + model_proto.mutable_variables()->add_integers(false); + model_proto.mutable_variables()->add_names("x"); + + ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, + Model::FromModelProto(model_proto)); + EXPECT_THAT(model->ExportModel(), EquivToProto(model_proto)); + ASSERT_EQ(model->num_variables(), 1); + EXPECT_EQ(model->Variables().front().typed_id(), x_id); +} + +TEST(ModelTest, FromInvalidModelProto) { + // Here we assume Model::FromModelProto() uses ValidateModel() via + // ModelStorage::FromModelProto() and thus we don't test all possible errors. + ModelProto model_proto; + model_proto.set_name("model"); + model_proto.mutable_variables()->add_ids(1); + // Missing lower_bounds entry. + model_proto.mutable_variables()->add_upper_bounds(1.0); + model_proto.mutable_variables()->add_integers(false); + model_proto.mutable_variables()->add_names("x"); + + EXPECT_THAT( + Model::FromModelProto(model_proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("lower_bounds"))); +} + +TEST(ModelTest, FromStorage) { + // In this test, we only test adding one variable. We assume here that the + // constructor will move the provided storage in-place. Thus it is not + // necessary to over-test this feature. + auto storage = std::make_unique("test"); + + // Here we directly delete variables since the ModelStorage won't reuse an id + // already returned. We don't bother giving names or bounds to these + // variables. + storage->DeleteVariable(storage->AddVariable()); + const VariableId x_id = storage->AddVariable( + /*lower_bound=*/0.0, /*upper_bound=*/1.0, /*is_integer=*/true, "x"); + + const Model model(std::move(storage)); + + const std::vector variables = model.Variables(); + ASSERT_EQ(variables.size(), 1); + const Variable x = variables[0]; + EXPECT_EQ(x.typed_id(), x_id); + EXPECT_EQ(x.name(), "x"); + EXPECT_EQ(x.lower_bound(), 0.0); + EXPECT_EQ(x.upper_bound(), 1.0); +} + +// We can't use Pointwise(Property(...)) matchers, hence here we have this +// function to extract the typed ids to use UnorderedElementsAre(). +template +std::vector TypedIds(const std::vector& v) { + std::vector ids; + for (const T& e : v) { + ids.push_back(e.typed_id()); + } + return ids; +} + +TEST_F(ModelingTest, Clone) { + const Model& const_ref = model_; + + { + const std::unique_ptr clone = const_ref.Clone(); + + EXPECT_THAT(clone->ExportModel(), EquivToProto(model_.ExportModel())); + + EXPECT_THAT(TypedIds(clone->SortedVariables()), + ElementsAreArray(TypedIds(model_.SortedVariables()))); + EXPECT_THAT(TypedIds(clone->SortedLinearConstraints()), + ElementsAreArray(TypedIds(model_.SortedLinearConstraints()))); + } + + // Redo the test after removing the first variable and a new variable that we + // just added. This should shift the new variables's IDs by one. + { + model_.DeleteVariable(x_); + model_.DeleteVariable(model_.AddVariable()); + + // Same with constraints. + model_.DeleteLinearConstraint(c_); + model_.DeleteLinearConstraint(model_.AddLinearConstraint()); + + const std::unique_ptr clone = const_ref.Clone(); + + EXPECT_THAT(clone->ExportModel(), EquivToProto(model_.ExportModel())); + + EXPECT_THAT(TypedIds(clone->SortedVariables()), + ElementsAreArray(TypedIds(model_.SortedVariables()))); + EXPECT_THAT(TypedIds(clone->SortedLinearConstraints()), + ElementsAreArray(TypedIds(model_.SortedLinearConstraints()))); + + // New variables and constraints should start with the same id. + EXPECT_EQ(clone->AddVariable().typed_id(), model_.AddVariable().typed_id()); + EXPECT_EQ(clone->AddLinearConstraint().typed_id(), + model_.AddLinearConstraint().typed_id()); + } + + // Test renaming. + { + const std::unique_ptr clone = const_ref.Clone("new_name"); + + ModelProto expected_proto = model_.ExportModel(); + expected_proto.set_name("new_name"); + EXPECT_THAT(clone->ExportModel(), EquivToProto(expected_proto)); + + EXPECT_THAT(TypedIds(clone->SortedVariables()), + ElementsAreArray(TypedIds(model_.SortedVariables()))); + EXPECT_THAT(TypedIds(clone->SortedLinearConstraints()), + ElementsAreArray(TypedIds(model_.SortedLinearConstraints()))); + } +} + +TEST(ModelTest, ApplyValidUpdateProto) { + // Here we assume Model::ApplyUpdateProto() uses + // ModelStorage::ApplyUpdateProto() and thus we don't test everything. + ModelProto model_proto; + model_proto.set_name("model"); + const VariableId x_id(1); + model_proto.mutable_variables()->add_ids(x_id.value()); + model_proto.mutable_variables()->add_lower_bounds(0.0); + model_proto.mutable_variables()->add_upper_bounds(1.0); + model_proto.mutable_variables()->add_integers(false); + model_proto.mutable_variables()->add_names("x"); + + ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, + Model::FromModelProto(model_proto)); + EXPECT_THAT(model->ExportModel(), EquivToProto(model_proto)); + + ModelUpdateProto update_proto; + update_proto.mutable_variable_updates()->mutable_lower_bounds()->add_ids( + x_id.value()); + update_proto.mutable_variable_updates()->mutable_lower_bounds()->add_values( + -3.0); + ASSERT_OK(model->ApplyUpdateProto(update_proto)); + + model_proto.mutable_variables()->mutable_lower_bounds()->Set(0, -3.0); + EXPECT_THAT(model->ExportModel(), EquivToProto(model_proto)); +} + +TEST(ModelTest, ApplyInvalidUpdateProto) { + // Here we assume Model::ApplyUpdateProto() uses + // ModelStorage::ApplyUpdateProto() and thus we don't test everything. + ModelProto model_proto; + model_proto.set_name("model"); + const VariableId x_id(1); + model_proto.mutable_variables()->add_ids(x_id.value()); + model_proto.mutable_variables()->add_lower_bounds(0.0); + model_proto.mutable_variables()->add_upper_bounds(1.0); + model_proto.mutable_variables()->add_integers(false); + model_proto.mutable_variables()->add_names("x"); + + ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, + Model::FromModelProto(model_proto)); + EXPECT_THAT(model->ExportModel(), EquivToProto(model_proto)); + + ModelUpdateProto update_proto; + // Id 0 does not exist. + update_proto.mutable_variable_updates()->mutable_lower_bounds()->add_ids(0); + update_proto.mutable_variable_updates()->mutable_lower_bounds()->add_values( + -3.0); + EXPECT_THAT(model->ApplyUpdateProto(update_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid variable id"))); +} + +TEST(ModelTest, VariableGetters) { + Model model; + const Model& const_model = model; + { + const Variable v = + model.AddVariable(/*lower_bound=*/-kInf, /*upper_bound=*/kInf, + /*is_integer=*/false, "continuous"); + EXPECT_EQ(const_model.name(v), "continuous"); + EXPECT_EQ(const_model.lower_bound(v), -kInf); + EXPECT_EQ(const_model.upper_bound(v), kInf); + EXPECT_FALSE(const_model.is_integer(v)); + } + { + const Variable v = + model.AddVariable(/*lower_bound=*/3.0, /*upper_bound=*/5.0, + /*is_integer=*/true, "integer"); + EXPECT_EQ(const_model.name(v), "integer"); + EXPECT_EQ(const_model.lower_bound(v), 3.0); + EXPECT_EQ(const_model.upper_bound(v), 5.0); + EXPECT_TRUE(const_model.is_integer(v)); + } +} + +TEST(ModelTest, VariableSetters) { + Model model; + const Model& const_model = model; + const Variable v = + model.AddVariable(/*lower_bound=*/-kInf, /*upper_bound=*/kInf, + /*is_integer=*/false, "v"); + + model.set_lower_bound(v, 3.0); + model.set_upper_bound(v, 5.0); + model.set_is_integer(v, true); + + EXPECT_EQ(const_model.lower_bound(v), 3.0); + EXPECT_EQ(const_model.upper_bound(v), 5.0); + EXPECT_TRUE(const_model.is_integer(v)); + + model.set_continuous(v); + EXPECT_FALSE(const_model.is_integer(v)); + + model.set_integer(v); + EXPECT_TRUE(const_model.is_integer(v)); +} + +TEST(ModelTest, VariableById) { + Model model; + const Variable x0 = model.AddBinaryVariable("x0"); + const Variable x1 = model.AddBinaryVariable("x1"); + const Variable x2 = model.AddContinuousVariable(-1.0, 2.0, "x2"); + model.DeleteVariable(x1); + EXPECT_TRUE(model.has_variable(x0.id())); + EXPECT_FALSE(model.has_variable(x1.id())); + EXPECT_TRUE(model.has_variable(x2.id())); + EXPECT_EQ(model.variable(x0.id()).name(), "x0"); + EXPECT_EQ(model.variable(x0.id()).lower_bound(), 0.0); + EXPECT_EQ(model.variable(x0.id()).upper_bound(), 1.0); + EXPECT_EQ(model.variable(x2.id()).name(), "x2"); + EXPECT_EQ(model.variable(x2.id()).lower_bound(), -1.0); + EXPECT_EQ(model.variable(x2.id()).upper_bound(), 2.0); + + EXPECT_TRUE(model.has_variable(x0.typed_id())); + EXPECT_FALSE(model.has_variable(x1.typed_id())); + EXPECT_TRUE(model.has_variable(x2.typed_id())); + EXPECT_EQ(model.variable(x0.typed_id()).name(), "x0"); + EXPECT_EQ(model.variable(x2.typed_id()).name(), "x2"); +} + +TEST(ModelTest, ValidateExistingVariableOfThisModel) { + Model model_a; + const Variable x0 = model_a.AddBinaryVariable("x0"); + const Variable x1 = model_a.AddBinaryVariable("x1"); + model_a.DeleteVariable(x0); + + Model model_b("b"); + + EXPECT_OK(model_a.ValidateExistingVariableOfThisModel(x1)); + EXPECT_THAT( + model_a.ValidateExistingVariableOfThisModel(x0), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("not found"))); + EXPECT_THAT(model_b.ValidateExistingVariableOfThisModel(x1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("different model"))); +} + +TEST(ModelDeathTest, VariableByIdOutOfBounds) { + Model model; + model.AddBinaryVariable("x0"); + EXPECT_DEATH(model.variable(-1), + AllOf(HasSubstr("variable"), HasSubstr("-1"))); + EXPECT_DEATH(model.variable(2), AllOf(HasSubstr("variable"), HasSubstr("2"))); +} + +TEST(ModelDeathTest, VariableByIdDeleted) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + EXPECT_EQ(model.variable(x.id()).name(), "x"); + model.DeleteVariable(x); + EXPECT_DEATH(model.variable(x.id()), + AllOf(HasSubstr("variable"), HasSubstr("0"))); +} + +TEST(ModelDeathTest, VariableAccessorsInvalidModel) { + Model model_a("a"); + const Variable a_a = model_a.AddVariable("a_a"); + + Model model_b("b"); + + EXPECT_DEATH(model_b.name(a_a), internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.lower_bound(a_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.upper_bound(a_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.is_integer(a_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_lower_bound(a_a, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_upper_bound(a_a, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_is_integer(a_a, true), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_continuous(a_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_integer(a_a), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, LinearConstraintGetters) { + Model model; + const Model& const_model = model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + { + const LinearConstraint c = model.AddLinearConstraint( + /*lower_bound=*/-kInf, /*upper_bound=*/1.5, "upper_bounded"); + model.set_coefficient(c, x, 1.0); + model.set_coefficient(c, y, 2.0); + + EXPECT_EQ(const_model.name(c), "upper_bounded"); + EXPECT_EQ(const_model.lower_bound(c), -kInf); + EXPECT_EQ(const_model.upper_bound(c), 1.5); + + EXPECT_EQ(const_model.coefficient(c, x), 1.0); + EXPECT_EQ(const_model.coefficient(c, y), 2.0); + + EXPECT_TRUE(const_model.is_coefficient_nonzero(c, x)); + EXPECT_TRUE(const_model.is_coefficient_nonzero(c, y)); + EXPECT_FALSE(const_model.is_coefficient_nonzero(c, z)); + + EXPECT_THAT(model.RowNonzeros(c), UnorderedElementsAre(x, y)); + + const BoundedLinearExpression c_bounded_expr = + c.AsBoundedLinearExpression(); + // TODO(b/171883688): we should use expression matchers. + EXPECT_EQ(c_bounded_expr.lower_bound, -kInf); + EXPECT_EQ(c_bounded_expr.upper_bound, 1.5); + EXPECT_THAT(c_bounded_expr.expression.terms(), + UnorderedElementsAre(Pair(x, 1.0), Pair(y, 2.0))); + } + { + const LinearConstraint c = model.AddLinearConstraint( + /*lower_bound=*/0.5, /*upper_bound=*/kInf, "lower_bounded"); + model.set_coefficient(c, y, 2.0); + + EXPECT_EQ(const_model.name(c), "lower_bounded"); + EXPECT_EQ(const_model.lower_bound(c), 0.5); + EXPECT_EQ(const_model.upper_bound(c), kInf); + + EXPECT_EQ(const_model.coefficient(c, x), 0.0); + EXPECT_EQ(const_model.coefficient(c, y), 2.0); + + EXPECT_FALSE(const_model.is_coefficient_nonzero(c, x)); + EXPECT_TRUE(const_model.is_coefficient_nonzero(c, y)); + + EXPECT_THAT(model.RowNonzeros(c), UnorderedElementsAre(y)); + + const BoundedLinearExpression c_bounded_expr = + c.AsBoundedLinearExpression(); + // TODO(b/171883688): we should use expression matchers. + EXPECT_EQ(c_bounded_expr.lower_bound, 0.5); + EXPECT_EQ(c_bounded_expr.upper_bound, kInf); + EXPECT_THAT(c_bounded_expr.expression.terms(), + UnorderedElementsAre(Pair(y, 2.0))); + } +} + +TEST(ModelTest, LinearConstraintSetters) { + Model model; + const Variable x = model.AddVariable("x"); + const Model& const_model = model; + const LinearConstraint c = model.AddLinearConstraint("c"); + model.set_coefficient(c, x, 1.0); + + model.set_coefficient(c, x, 2.0); + model.set_lower_bound(c, 3.0); + model.set_upper_bound(c, 5.0); + + EXPECT_EQ(model.coefficient(c, x), 2.0); + EXPECT_EQ(const_model.lower_bound(c), 3.0); + EXPECT_EQ(const_model.upper_bound(c), 5.0); +} + +TEST(ModelTest, LinearConstraintById) { + Model model; + const LinearConstraint c0 = model.AddLinearConstraint("c0"); + const LinearConstraint c1 = model.AddLinearConstraint("c1"); + const LinearConstraint c2 = model.AddLinearConstraint("c2"); + model.DeleteLinearConstraint(c1); + EXPECT_TRUE(model.has_linear_constraint(c0.id())); + EXPECT_FALSE(model.has_linear_constraint(c1.id())); + EXPECT_TRUE(model.has_linear_constraint(c2.id())); + EXPECT_EQ(model.linear_constraint(c0.id()).name(), "c0"); + EXPECT_EQ(model.linear_constraint(c2.id()).name(), "c2"); + + EXPECT_TRUE(model.has_linear_constraint(c0.typed_id())); + EXPECT_FALSE(model.has_linear_constraint(c1.typed_id())); + EXPECT_TRUE(model.has_linear_constraint(c2.typed_id())); + EXPECT_EQ(model.linear_constraint(c0.typed_id()).name(), "c0"); + EXPECT_EQ(model.linear_constraint(c2.typed_id()).name(), "c2"); +} + +TEST(ModelTest, ValidateExistingLinearConstraintOfThisModel) { + Model model_a; + const LinearConstraint c0 = model_a.AddLinearConstraint("c0"); + const LinearConstraint c1 = model_a.AddLinearConstraint("c1"); + model_a.DeleteLinearConstraint(c0); + + Model model_b("b"); + + EXPECT_OK(model_a.ValidateExistingLinearConstraintOfThisModel(c1)); + EXPECT_THAT( + model_a.ValidateExistingLinearConstraintOfThisModel(c0), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("not found"))); + EXPECT_THAT(model_b.ValidateExistingLinearConstraintOfThisModel(c1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("different model"))); +} + +TEST(ModelDeathTest, LinearConstraintByIdOutOfBounds) { + Model model; + model.AddLinearConstraint("c"); + EXPECT_DEATH(model.linear_constraint(-1), + AllOf(HasSubstr("linear constraint"), HasSubstr("-1"))); + EXPECT_DEATH(model.linear_constraint(2), + AllOf(HasSubstr("linear constraint"), HasSubstr("2"))); +} + +TEST(ModelDeathTest, LinearConstraintByIdDeleted) { + Model model; + const LinearConstraint c = model.AddLinearConstraint("c"); + EXPECT_EQ(model.linear_constraint(c.id()).name(), "c"); + model.DeleteLinearConstraint(c); + EXPECT_DEATH(model.linear_constraint(c.id()), + AllOf(HasSubstr("linear constraint"), HasSubstr("0"))); +} + +TEST(ModelDeathTest, LinearConstraintAccessorsInvalidModel) { + Model model_a("a"); + const Variable x_a = model_a.AddVariable("x_a"); + const LinearConstraint c_a = model_a.AddLinearConstraint("c_a"); + + Model model_b("b"); + const Variable x_b = model_b.AddVariable("x_b"); + const LinearConstraint c_b = model_b.AddLinearConstraint("c_b"); + + EXPECT_DEATH(model_b.name(c_a), internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.lower_bound(c_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.upper_bound(c_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_lower_bound(c_a, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_upper_bound(c_a, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_coefficient(c_a, x_b, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.set_coefficient(c_b, x_a, 0.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.coefficient(c_a, x_b), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.coefficient(c_b, x_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.is_coefficient_nonzero(c_a, x_b), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.is_coefficient_nonzero(c_b, x_a), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.RowNonzeros(c_a), + internal::kObjectsFromOtherModelStorage); +} + +TEST_F(ModelingTest, ModelProperties) { + EXPECT_EQ(model_.name(), "math_opt_model"); + EXPECT_EQ(model_.num_variables(), 2); + EXPECT_EQ(model_.next_variable_id(), 2); + EXPECT_TRUE(model_.has_variable(0)); + EXPECT_TRUE(model_.has_variable(1)); + EXPECT_FALSE(model_.has_variable(2)); + EXPECT_FALSE(model_.has_variable(3)); + EXPECT_FALSE(model_.has_variable(-1)); + EXPECT_THAT(model_.Variables(), UnorderedElementsAre(x_, y_)); + EXPECT_THAT(model_.SortedVariables(), ElementsAre(x_, y_)); + + EXPECT_EQ(model_.num_linear_constraints(), 2); + EXPECT_EQ(model_.next_linear_constraint_id(), 2); + EXPECT_TRUE(model_.has_linear_constraint(0)); + EXPECT_TRUE(model_.has_linear_constraint(1)); + EXPECT_FALSE(model_.has_linear_constraint(2)); + EXPECT_FALSE(model_.has_linear_constraint(3)); + EXPECT_FALSE(model_.has_linear_constraint(-1)); + EXPECT_THAT(model_.LinearConstraints(), UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedLinearConstraints(), ElementsAre(c_, d_)); +} + +TEST(ModelTest, ColumnNonzeros) { + Model model("math_opt_model"); + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + const LinearConstraint c1 = model.AddLinearConstraint(x + y <= 2); + const LinearConstraint c2 = model.AddLinearConstraint(x <= 1); + const LinearConstraint c3 = model.AddLinearConstraint(x + y <= 2); + model.DeleteLinearConstraint(c3); + + EXPECT_THAT(model.ColumnNonzeros(x), UnorderedElementsAre(c1, c2)); + EXPECT_THAT(model.ColumnNonzeros(y), UnorderedElementsAre(c1)); + EXPECT_THAT(model.ColumnNonzeros(z), IsEmpty()); +} + +template +std::vector SortedValueNames( + const google::protobuf::Map& messages) { + std::vector> sorted_entries; + for (const auto& [id, message] : messages) { + sorted_entries.push_back( + {id, google::protobuf::StringCopy(message.name())}); + } + absl::c_sort(sorted_entries); + std::vector result; + for (const auto& [id, name] : sorted_entries) { + result.push_back(name); + } + return result; +} + +TEST(ModelTest, ExportModel_RemoveNames) { + Model model("my_model"); + const Variable x = model.AddVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + model.Maximize(x); + const Objective b = model.AddAuxiliaryObjective(1, "objB"); + model.set_objective_offset(b, 2.0); + model.AddLinearConstraint(x <= 1.0, "lin_con"); + model.AddQuadraticConstraint(x * x <= 1.0, "quad_con"); + model.AddIndicatorConstraint(y, x >= 3.0, /*activate_on_zero=*/false, + "ind_con"); + model.AddSos1Constraint({y, 1.0 - y}, {1.0, 1.0}, "sos1"); + model.AddSos2Constraint({y, 1.0 - y}, {1.0, 1.0}, "sos2"); + model.AddSecondOrderConeConstraint({x + y}, 1.0, "soc"); + { + ModelProto named_proto = model.ExportModel(/*remove_names=*/false); + EXPECT_EQ(named_proto.name(), "my_model"); + EXPECT_THAT(named_proto.variables().names(), ElementsAre("x", "y")); + EXPECT_THAT(SortedValueNames(named_proto.auxiliary_objectives()), + ElementsAre("objB")); + EXPECT_THAT(named_proto.linear_constraints().names(), + ElementsAre("lin_con")); + EXPECT_THAT(SortedValueNames(named_proto.quadratic_constraints()), + ElementsAre("quad_con")); + EXPECT_THAT(SortedValueNames(named_proto.indicator_constraints()), + ElementsAre("ind_con")); + EXPECT_THAT(SortedValueNames(named_proto.sos1_constraints()), + ElementsAre("sos1")); + EXPECT_THAT(SortedValueNames(named_proto.sos2_constraints()), + ElementsAre("sos2")); + EXPECT_THAT(SortedValueNames(named_proto.second_order_cone_constraints()), + ElementsAre("soc")); + } + + { + ModelProto unnamed_proto = model.ExportModel(/*remove_names=*/true); + EXPECT_EQ(unnamed_proto.name(), ""); + EXPECT_THAT(unnamed_proto.variables().names(), IsEmpty()); + EXPECT_THAT(SortedValueNames(unnamed_proto.auxiliary_objectives()), + ElementsAre("")); + EXPECT_THAT(unnamed_proto.linear_constraints().names(), IsEmpty()); + EXPECT_THAT(SortedValueNames(unnamed_proto.quadratic_constraints()), + ElementsAre("")); + EXPECT_THAT(SortedValueNames(unnamed_proto.indicator_constraints()), + ElementsAre("")); + EXPECT_THAT(SortedValueNames(unnamed_proto.sos1_constraints()), + ElementsAre("")); + EXPECT_THAT(SortedValueNames(unnamed_proto.sos2_constraints()), + ElementsAre("")); + EXPECT_THAT(SortedValueNames(unnamed_proto.second_order_cone_constraints()), + ElementsAre("")); + } +} + +TEST(ModelDeathTest, ColumnNonzerosOtherModel) { + Model model_a("a"); + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + EXPECT_DEATH(model_a.ColumnNonzeros(b_x), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelDeathTest, RowNonzerosOtherModel) { + Model model_a("a"); + Model model_b("b"); + const LinearConstraint b_c = model_b.AddLinearConstraint("c"); + EXPECT_DEATH(model_a.RowNonzeros(b_c), + internal::kObjectsFromOtherModelStorage); +} + +TEST_F(ModelingTest, DeleteVariable) { + model_.DeleteVariable(x_); + EXPECT_EQ(model_.num_variables(), 1); + EXPECT_EQ(model_.next_variable_id(), 2); + EXPECT_FALSE(model_.has_variable(0)); + EXPECT_TRUE(model_.has_variable(1)); + EXPECT_THAT(model_.Variables(), UnorderedElementsAre(y_)); + EXPECT_THAT(model_.RowNonzeros(c_), UnorderedElementsAre(y_)); + const BoundedLinearExpression c_bounded_expr = c_.AsBoundedLinearExpression(); + // TODO(b/171883688): we should use expression matchers. + EXPECT_DOUBLE_EQ(c_bounded_expr.lower_bound, -kInf); + EXPECT_DOUBLE_EQ(c_bounded_expr.upper_bound, 1.5); + EXPECT_THAT(c_bounded_expr.expression.terms(), + UnorderedElementsAre(Pair(y_, 1.0))); +} + +TEST_F(ModelingTest, DeleteLinearConstraint) { + model_.DeleteLinearConstraint(c_); + EXPECT_EQ(model_.num_linear_constraints(), 1); + EXPECT_EQ(model_.next_linear_constraint_id(), 2); + EXPECT_FALSE(model_.has_linear_constraint(0)); + EXPECT_TRUE(model_.has_linear_constraint(1)); + EXPECT_THAT(model_.LinearConstraints(), UnorderedElementsAre(d_)); +} + +TEST_F(ModelingTest, ExportModel) { + ASSERT_OK_AND_ASSIGN(const ModelProto expected, + ParseTextProto(R"pb( + name: "math_opt_model" + variables { + ids: [ 0, 1 ] + lower_bounds: [ -inf, 0.0 ] + upper_bounds: [ inf, 1.0 ] + integers: [ false, true ] + names: [ "x", "y" ] + } + objective { + offset: 3.5 + maximize: true + linear_coefficients: { + ids: [ 1 ] + values: [ 2.0 ] + } + } + linear_constraints { + ids: [ 0, 1 ] + lower_bounds: [ -inf, 0.5 ] + upper_bounds: [ 1.5, inf ] + names: [ "c", "d" ] + } + linear_constraint_matrix { + row_ids: [ 0, 0, 1 ] + column_ids: [ 0, 1, 1 ] + coefficients: [ 1.0, 1.0, 2.0 ] + } + )pb")); + EXPECT_THAT(model_.ExportModel(), EquivToProto(expected)); +} + +TEST(ModelTest, AddBoundedLinearConstraint) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = + model.AddLinearConstraint(3 <= 2 * x - y + 2 <= 5, "c"); + EXPECT_EQ(c.coefficient(x), 2.0); + EXPECT_EQ(c.coefficient(y), -1.0); + EXPECT_EQ(c.lower_bound(), 3.0 - 2.0); + EXPECT_EQ(c.upper_bound(), 5.0 - 2.0); +} + +TEST(ModelTest, AddEqualityLinearConstraint) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint(2 * x - 5 == x + y, "c"); + EXPECT_EQ(c.coefficient(x), 1.0); + EXPECT_EQ(c.coefficient(y), -1.0); + EXPECT_EQ(c.lower_bound(), 5.0); + EXPECT_EQ(c.upper_bound(), 5.0); +} + +TEST(ModelTest, AddVariablesEqualityLinearConstraint) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint(x == y, "c"); + EXPECT_EQ(c.coefficient(x), 1.0); + EXPECT_EQ(c.coefficient(y), -1.0); + EXPECT_EQ(c.lower_bound(), 0.0); + EXPECT_EQ(c.upper_bound(), 0.0); +} + +TEST(ModelTest, AddLowerBoundedLinearConstraint) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint(3 <= x - 1, "c"); + EXPECT_EQ(c.coefficient(x), 1.0); + EXPECT_EQ(c.coefficient(y), 0.0); + EXPECT_EQ(c.lower_bound(), 3.0 - -1.0); + EXPECT_EQ(c.upper_bound(), kInf); +} + +TEST(ModelTest, AddUpperBoundedLinearConstraint) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint(y <= 5, "c"); + EXPECT_EQ(c.coefficient(x), 0.0); + EXPECT_EQ(c.coefficient(y), 1.0); + EXPECT_EQ(c.lower_bound(), -kInf); + EXPECT_EQ(c.upper_bound(), 5.0); +} + +TEST(ModelDeathTest, AddLinearConstraintOtherModel) { + Model model_a("a"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + const Variable b_y = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.AddLinearConstraint(2 <= 2 * b_x - b_y + 2, "c"), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, AddLinearConstraintWithoutVariables) { + Model model; + + // Here we test a corner case that may not be very useful in practice: the + // case of a bounded LinearExpression that have no terms but its offset. + // + // We want to make sure the code don't assume all LinearExpression have a + // non-null storage(). + const LinearConstraint c = + model.AddLinearConstraint(LinearExpression(3) <= 5, "c"); + EXPECT_EQ(c.lower_bound(), -kInf); + EXPECT_EQ(c.upper_bound(), 2.0); +} + +TEST(ModelTest, ObjectiveAccessors) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + + const Model& const_model = model; + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y), 2.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, y), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, y), 0.0); + + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(y, x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(y, y)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 3.5); + EXPECT_TRUE(const_model.is_maximize()); + + // TODO(b/171883688): we should use expression matchers. + EXPECT_THAT(const_model.ObjectiveAsLinearExpression().terms(), + UnorderedElementsAre(Pair(y, 2.0))); + EXPECT_DOUBLE_EQ(const_model.ObjectiveAsLinearExpression().offset(), 3.5); + EXPECT_THAT(const_model.ObjectiveAsQuadraticExpression().quadratic_terms(), + IsEmpty()); + EXPECT_THAT(const_model.ObjectiveAsQuadraticExpression().linear_terms(), + UnorderedElementsAre(Pair(y, 2.0))); + EXPECT_DOUBLE_EQ(const_model.ObjectiveAsQuadraticExpression().offset(), 3.5); + + // Now we add a quadratic term + model.set_objective_coefficient(x, y, 3.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y), 2.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, y), 3.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, x), 3.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, y), 0.0); + + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y, x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(y, y)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 3.5); + EXPECT_TRUE(const_model.is_maximize()); + + // TODO(b/171883688): we should use expression matchers. + EXPECT_THAT(const_model.ObjectiveAsQuadraticExpression().quadratic_terms(), + UnorderedElementsAre(Pair(QuadraticTermKey(x, y), 3.0))); + EXPECT_THAT(const_model.ObjectiveAsQuadraticExpression().linear_terms(), + UnorderedElementsAre(Pair(y, 2.0))); + EXPECT_DOUBLE_EQ(const_model.ObjectiveAsQuadraticExpression().offset(), 3.5); +} + +TEST(ModelDeathTest, ObjectiveAsLinearExpressionWhenObjectiveIsQuadratic) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + model.set_objective_coefficient(x, y, 3.0); + + EXPECT_DEATH(model.ObjectiveAsLinearExpression(), "quadratic terms"); +} + +TEST(ModelTest, AddToObjective) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + + model.AddToObjective(5.0 * x - y + 7.0); + + const Model& const_model = model; + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y), 1.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, y), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, y), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(y, x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(y, y)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 10.5); + EXPECT_TRUE(const_model.is_maximize()); + + model.AddToObjective(6.0 * x * y + 7.0 * y * y + 8.0 * x); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x), 13.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y), 1.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x, y), 6.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, x), 6.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y, y), 7.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y, x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y, y)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 10.5); + EXPECT_TRUE(const_model.is_maximize()); +} + +TEST(ObjectiveDeathTest, AddToObjectiveOtherModel) { + Model model_a; + + Model model_b; + const Variable x_b = model_b.AddVariable("x"); + const Variable y_b = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.AddToObjective(5.0 * x_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_a.AddToObjective(5.0 * x_b * x_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, AddToObjectiveConstant) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + + model.AddToObjective(7.0); + + const Model& const_model = model; + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_DOUBLE_EQ(const_model.objective_coefficient(y), 2.0); + + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 10.5); + EXPECT_TRUE(const_model.is_maximize()); +} + +TEST(ModelTest, MinimizeLinear) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.Minimize(5.0 * x - y + 7.0); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 7.0); + EXPECT_FALSE(const_model.is_maximize()); +} + +TEST(ModelTest, MinimizeQuadratic) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.Minimize(5.0 * x * y - y + 7.0); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, y), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 7.0); + EXPECT_FALSE(const_model.is_maximize()); +} + +TEST(ModelDeathTest, MinimizeOtherModel) { + Model model_a; + + Model model_b; + const Variable x_b = model_b.AddVariable("x"); + const Variable y_b = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.Minimize(5.0 * x_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_a.Minimize(5.0 * x_b * y_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, MaximizeLinear) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_minimize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.Maximize(5.0 * x - y + 7.0); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 7.0); + EXPECT_TRUE(const_model.is_maximize()); +} + +TEST(ModelTest, MaximizeQuadratic) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_minimize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.Maximize(5.0 * x * y - y + 7.0); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, y), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_DOUBLE_EQ(const_model.objective_offset(), 7.0); + EXPECT_TRUE(const_model.is_maximize()); +} + +TEST(ModelDeathTest, MaximizeOtherModel) { + Model model_a; + + Model model_b; + const Variable x_b = model_b.AddVariable("x"); + const Variable y_b = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.Maximize(5.0 * x_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_a.Maximize(5.0 * x_b * y_b - y_b + 7.0), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, SetObjectiveLinear) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.SetObjective(5.0 * x - y + 7.0, false); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_EQ(const_model.objective_offset(), 7.0); + EXPECT_FALSE(const_model.is_maximize()); +} + +TEST(ModelTest, SetObjectiveQuadratic) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(y, 2.0); + model.set_objective_coefficient(z, 3.0); + model.set_objective_coefficient(x, z, 4.0); + + model.SetObjective(5.0 * x * y - y + 7.0, false); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(y), -1.0); + EXPECT_EQ(const_model.objective_coefficient(z), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, y), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x, z), 0.0); + + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(z)); + EXPECT_TRUE(const_model.is_objective_coefficient_nonzero(x, y)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, z)); + + EXPECT_EQ(const_model.objective_offset(), 7.0); + EXPECT_FALSE(const_model.is_maximize()); +} + +TEST(ModelDeathTest, SetObjectiveOtherModel) { + Model model_a; + + Model model_b; + const Variable x_b = model_b.AddVariable("x"); + const Variable y_b = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.SetObjective(5.0 * x_b + 7.0, true), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_a.SetObjective(5.0 * x_b * y_b + 7.0, true), + internal::kObjectsFromOtherModelStorage); +} + +TEST(ModelTest, SetObjectiveAsConstant) { + Model model; + const Variable x = model.AddVariable("x"); + + // Set a non trivial initial quadratic objective to test that SetObjective + // updates the offset and linear and quadratic coefficients, and resets to + // zero those coefficients not in the new objective. + model.set_maximize(); + model.set_objective_offset(3.5); + model.set_objective_coefficient(x, 2.0); + model.set_objective_coefficient(x, x, 3.0); + + model.SetObjective(7.0, false); + + const Model& const_model = model; + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x)); + EXPECT_FALSE(const_model.is_objective_coefficient_nonzero(x, x)); + + EXPECT_EQ(const_model.objective_offset(), 7.0); + EXPECT_FALSE(const_model.is_maximize()); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsDouble) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(3.0); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 3.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.Minimize(4.0); + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 4.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.SetObjective(5.0, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsVariable) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 1.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.Minimize(x); + + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 1.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.SetObjective(x, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 1.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsLinearTerm) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(3.0 * x); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 3.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.Minimize(4.0 * x); + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 4.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.SetObjective(5.0 * x, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsLinearExpression) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(3.0 * x + 4); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 4.0); + EXPECT_EQ(const_model.objective_coefficient(x), 3.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.Minimize(5.0 * x + 6); + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 6.0); + EXPECT_EQ(const_model.objective_coefficient(x), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); + + model.SetObjective(7.0 * x + 8, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 8.0); + EXPECT_EQ(const_model.objective_coefficient(x), 7.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 0.0); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsQuadraticTerm) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(3.0 * x * x); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 3.0); + + model.Minimize(4.0 * x * x); + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 4.0); + + model.SetObjective(5.0 * x * x, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x), 0.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 5.0); +} + +// TODO(b/207482515): Add tests against expression constructor counters +TEST(ModelTest, ObjectiveAsQuadraticExpression) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(3.0 * x * x + 4 * x + 5); + + const Model& const_model = model; + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 5.0); + EXPECT_EQ(const_model.objective_coefficient(x), 4.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 3.0); + + model.Minimize(6.0 * x * x + 7 * x + 8); + EXPECT_FALSE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), 8.0); + EXPECT_EQ(const_model.objective_coefficient(x), 7.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 6.0); + + model.SetObjective(9.0 * x * x - x - 2, true); + EXPECT_TRUE(const_model.is_maximize()); + EXPECT_EQ(const_model.objective_offset(), -2.0); + EXPECT_EQ(const_model.objective_coefficient(x), -1.0); + EXPECT_EQ(const_model.objective_coefficient(x, x), 9.0); +} + +TEST(ModelTest, NonzeroVariablesInLinearObjective) { + Model model; + model.AddVariable(); + const Variable y = model.AddVariable(); + const Variable z = model.AddVariable(); + model.Minimize(3.0 * y + 0.0 * z + 1.0 * z * z); + EXPECT_THAT(model.NonzeroVariablesInLinearObjective(), + UnorderedElementsAre(y)); +} + +TEST(ModelTest, NonzeroVariablesInQuadraticObjective) { + Model model; + model.AddVariable(); + const Variable y = model.AddVariable(); + const Variable z = model.AddVariable(); + const Variable u = model.AddVariable(); + const Variable v = model.AddVariable(); + model.Minimize(3.0 * y + 0.0 * z + 1.0 * u * v); + EXPECT_THAT(model.NonzeroVariablesInQuadraticObjective(), + UnorderedElementsAre(u, v)); +} + +TEST(UpdateTrackerTest, ExportModel) { + Model model; + model.AddVariable("x"); + + std::unique_ptr update_tracker = model.NewUpdateTracker(); + + EXPECT_THAT(update_tracker->ExportModel(), + IsOkAndHolds(EquivToProto(R"pb(variables { + ids: [ 0 ] + lower_bounds: [ -inf ] + upper_bounds: [ inf ] + integers: [ false ] + names: [ "x" ] + })pb"))); +} + +TEST(UpdateTrackerTest, ExportModelUpdate) { + Model model; + const Variable x = model.AddVariable("x"); + + std::unique_ptr update_tracker = model.NewUpdateTracker(); + + model.set_integer(x); + + EXPECT_THAT(update_tracker->ExportModelUpdate(), + IsOkAndHolds(Optional(EquivToProto(R"pb(variable_updates { + integers { + ids: [ 0 ] + values: [ true ] + } + })pb")))); +} + +TEST(ModelTest, ExportModelUpdate_RemoveNames) { + Model model("my_model"); + std::unique_ptr tracker = model.NewUpdateTracker(); + const Variable x = model.AddVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + model.Maximize(x); + Objective b = model.AddAuxiliaryObjective(1, "objB"); + model.set_objective_offset(b, 2.0); + model.AddLinearConstraint(x <= 1.0, "lin_con"); + model.AddQuadraticConstraint(x * x <= 1.0, "quad_con"); + model.AddIndicatorConstraint(y, x >= 3.0, /*activate_on_zero=*/false, + "ind_con"); + model.AddSos1Constraint({y, 1.0 - y}, {1.0, 1.0}, "sos1"); + model.AddSos2Constraint({y, 1.0 - y}, {1.0, 1.0}, "sos2"); + model.AddSecondOrderConeConstraint({x + y}, 1.0, "soc"); + { + ASSERT_OK_AND_ASSIGN(const std::optional update, + tracker->ExportModelUpdate(/*remove_names=*/false)); + ASSERT_TRUE(update.has_value()); + EXPECT_THAT(update->new_variables().names(), ElementsAre("x", "y")); + EXPECT_THAT(SortedValueNames( + update->auxiliary_objectives_updates().new_objectives()), + ElementsAre("objB")); + EXPECT_THAT(update->new_linear_constraints().names(), + ElementsAre("lin_con")); + EXPECT_THAT(SortedValueNames( + update->quadratic_constraint_updates().new_constraints()), + ElementsAre("quad_con")); + EXPECT_THAT(SortedValueNames( + update->indicator_constraint_updates().new_constraints()), + ElementsAre("ind_con")); + EXPECT_THAT( + SortedValueNames(update->sos1_constraint_updates().new_constraints()), + ElementsAre("sos1")); + EXPECT_THAT( + SortedValueNames(update->sos2_constraint_updates().new_constraints()), + ElementsAre("sos2")); + EXPECT_THAT( + SortedValueNames( + update->second_order_cone_constraint_updates().new_constraints()), + ElementsAre("soc")); + } + + { + ASSERT_OK_AND_ASSIGN(const std::optional update, + tracker->ExportModelUpdate(/*remove_names=*/true)); + ASSERT_TRUE(update.has_value()); + EXPECT_THAT(update->new_variables().names(), IsEmpty()); + EXPECT_THAT(SortedValueNames( + update->auxiliary_objectives_updates().new_objectives()), + ElementsAre("")); + EXPECT_THAT(update->new_linear_constraints().names(), IsEmpty()); + EXPECT_THAT(SortedValueNames( + update->quadratic_constraint_updates().new_constraints()), + ElementsAre("")); + EXPECT_THAT(SortedValueNames( + update->indicator_constraint_updates().new_constraints()), + ElementsAre("")); + EXPECT_THAT( + SortedValueNames(update->sos1_constraint_updates().new_constraints()), + ElementsAre("")); + EXPECT_THAT( + SortedValueNames(update->sos2_constraint_updates().new_constraints()), + ElementsAre("")); + EXPECT_THAT( + SortedValueNames( + update->second_order_cone_constraint_updates().new_constraints()), + ElementsAre("")); + } +} + +TEST(UpdateTrackerTest, Checkpoint) { + Model model; + std::unique_ptr update_tracker = model.NewUpdateTracker(); + + const Variable x = model.AddVariable("x"); + + ASSERT_OK(update_tracker->AdvanceCheckpoint()); + + model.set_integer(x); + + EXPECT_THAT(update_tracker->ExportModelUpdate(), + IsOkAndHolds(Optional(EquivToProto(R"pb(variable_updates { + integers { + ids: [ 0 ] + values: [ true ] + } + })pb")))); +} + +TEST(UpdateTrackerTest, DestructionAfterModelDestruction) { + auto model = std::make_unique(); + std::unique_ptr update_tracker = model->NewUpdateTracker(); + + // Destroy the model first. + model = nullptr; + + // Then destroy the tracker. + update_tracker = nullptr; +} + +TEST(UpdateTrackerTest, ExportModelAfterModelDestruction) { + auto model = std::make_unique(); + const std::unique_ptr update_tracker = + model->NewUpdateTracker(); + + model = nullptr; + + EXPECT_THAT(update_tracker->ExportModel(), + StatusIs(absl::StatusCode::kInvalidArgument, + internal::kModelIsDestroyed)); +} + +TEST(UpdateTrackerTest, ExportModelUpdateAfterModelDestruction) { + auto model = std::make_unique(); + const std::unique_ptr update_tracker = + model->NewUpdateTracker(); + + model = nullptr; + + EXPECT_THAT(update_tracker->ExportModelUpdate(), + StatusIs(absl::StatusCode::kInvalidArgument, + internal::kModelIsDestroyed)); +} + +TEST(UpdateTrackerTest, CheckpointAfterModelDestruction) { + auto model = std::make_unique(); + const std::unique_ptr update_tracker = + model->NewUpdateTracker(); + + model = nullptr; + + EXPECT_THAT(update_tracker->AdvanceCheckpoint(), + StatusIs(absl::StatusCode::kInvalidArgument, + internal::kModelIsDestroyed)); +} + +TEST(OstreamTest, EmptyModel) { + const Model model("empty_model"); + EXPECT_EQ(StreamToString(model), + R"model(Model empty_model: + Objective: + minimize 0 + Linear constraints: + Variables: +)model"); +} + +TEST(OstreamTest, MinimizingLinearObjective) { + Model model("minimize_linear_objective"); + const Variable noname = model.AddVariable(); + const Variable x = model.AddVariable("x"); + const Variable z = model.AddContinuousVariable(-15, 17, "z"); + const Variable n = model.AddIntegerVariable(7, 32, "n"); + const Variable t = model.AddContinuousVariable(-kInf, 7, "t"); + const Variable u = model.AddContinuousVariable(-4, kInf, "u"); + const Variable b = model.AddBinaryVariable("b"); + const Variable yy = model.AddVariable("y_y"); + model.AddLinearConstraint(z + x == 9, "c1"); + model.AddLinearConstraint(-3 * n + 2 * t + 2 >= 8); + model.AddLinearConstraint(7 * u - 2 * b + 7 * yy - z <= 32, "c2"); + model.AddLinearConstraint(78 <= yy + 4 * noname <= 256, "c3"); + model.Minimize(45 * z + 3 * u); + EXPECT_EQ(StreamToString(model), + R"model(Model minimize_linear_objective: + Objective: + minimize 45*z + 3*u + Linear constraints: + c1: x + z = 9 + __lin_con#1__: -3*n + 2*t ≥ 6 + c2: -z + 7*u - 2*b + 7*y_y ≤ 32 + c3: 78 ≤ 4*__var#0__ + y_y ≤ 256 + Variables: + __var#0__ in (-∞, +∞) + x in (-∞, +∞) + z in [-15, 17] + n (integer) in [7, 32] + t in (-∞, 7] + u in [-4, +∞) + b (binary) + y_y in (-∞, +∞) +)model"); +} + +TEST(OstreamTest, MaximizingLinearObjective) { + Model model("maximize_linear_objective"); + const Variable x = model.AddVariable("x"); + const Variable y = model.AddContinuousVariable(1, 5, "y"); + model.AddLinearConstraint(x + y == 9, "c1"); + model.Maximize(-2 * x + y); + EXPECT_EQ(StreamToString(model), + R"model(Model maximize_linear_objective: + Objective: + maximize -2*x + y + Linear constraints: + c1: x + y = 9 + Variables: + x in (-∞, +∞) + y in [1, 5] +)model"); +} + +TEST(OstreamTest, WithoutName) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddContinuousVariable(1, 5, "y"); + model.AddLinearConstraint(x + y == 9, "c1"); + model.Maximize(-2 * x + y); + EXPECT_EQ(StreamToString(model), + R"model(Model: + Objective: + maximize -2*x + y + Linear constraints: + c1: x + y = 9 + Variables: + x in (-∞, +∞) + y in [1, 5] +)model"); +} + +TEST(OstreamTest, MinimizingQuadraticObjective) { + Model model("minimize_quadratic_objective"); + const Variable x = model.AddVariable("x"); + const Variable y = model.AddContinuousVariable(1, 5, "y"); + model.AddLinearConstraint(x + y == 9, "c1"); + model.Minimize(-2 * x + y + 7 * y * x - 5 * x * x); + EXPECT_EQ(StreamToString(model), + R"model(Model minimize_quadratic_objective: + Objective: + minimize -5*x² + 7*x*y - 2*x + y + Linear constraints: + c1: x + y = 9 + Variables: + x in (-∞, +∞) + y in [1, 5] +)model"); +} + +TEST(OstreamTest, FloatingPointRoundTripVariableBounds) { + Model model("minimize_linear_objective"); + model.AddContinuousVariable(kRoundTripTestNumber, 17, "x"); + model.AddContinuousVariable(-kInf, kRoundTripTestNumber, "y"); + EXPECT_THAT( + StreamToString(model), + AllOf( + HasSubstr(absl::StrCat("x in [", kRoundTripTestNumberStr, ", 17]")), + HasSubstr(absl::StrCat("y in (-∞, ", kRoundTripTestNumberStr, "]")))); +} + +// -------------------------- Auxiliary objectives ----------------------------- + +// {max x, min 3, max 2y + 1} with priorities {2, 3, 5} +// s.t. x, y unbounded +class SimpleAuxiliaryObjectiveTest : public testing::Test { + protected: + SimpleAuxiliaryObjectiveTest() + : model_("auxiliary_objectives"), + x_(model_.AddVariable("x")), + y_(model_.AddVariable("y")), + primary_(model_.primary_objective()), + secondary_(model_.AddMinimizationObjective(3.0, 3, "secondary")), + tertiary_(model_.AddMaximizationObjective(0.0, 5, "tertiary")) { + // Maximize and Minimize wrap SetObjective, hence this tests them. + model_.Maximize(primary_, x_); + model_.set_objective_priority(primary_, 2); + // We also want to exercise AddToObjective. + model_.set_maximize(tertiary_); + model_.AddToObjective(tertiary_, 2.0 * y_); + model_.AddToObjective(tertiary_, 1.0); + } + + Model model_; + const Variable x_; + const Variable y_; + const Objective primary_; + const Objective secondary_; + const Objective tertiary_; +}; + +TEST_F(SimpleAuxiliaryObjectiveTest, Properties) { + EXPECT_EQ(model_.num_auxiliary_objectives(), 2); + EXPECT_EQ(model_.next_auxiliary_objective_id(), 2); + EXPECT_TRUE(model_.has_auxiliary_objective(0)); + EXPECT_TRUE(model_.has_auxiliary_objective(1)); + EXPECT_FALSE(model_.has_auxiliary_objective(2)); + EXPECT_FALSE(model_.has_auxiliary_objective(3)); + EXPECT_FALSE(model_.has_auxiliary_objective(-1)); + EXPECT_THAT(model_.AuxiliaryObjectives(), + UnorderedElementsAre(secondary_, tertiary_)); + EXPECT_THAT(model_.SortedAuxiliaryObjectives(), + ElementsAre(secondary_, tertiary_)); + + EXPECT_EQ(model_.auxiliary_objective(secondary_.id().value()).name(), + "secondary"); + EXPECT_EQ(model_.auxiliary_objective(tertiary_.id().value()).name(), + "tertiary"); + EXPECT_EQ(model_.auxiliary_objective(secondary_.typed_id().value()).name(), + "secondary"); + EXPECT_EQ(model_.auxiliary_objective(tertiary_.typed_id().value()).name(), + "tertiary"); +} + +TEST(AuxiliaryObjectiveTest, SenseSetting) { + Model model; + const Objective o = model.AddAuxiliaryObjective(3, "o"); + // set_maximize + ASSERT_FALSE(o.maximize()); + model.set_maximize(o); + ASSERT_TRUE(o.maximize()); + + // set_minimize + model.set_minimize(o); + ASSERT_FALSE(o.maximize()); + + model.set_is_maximize(o, true); + EXPECT_TRUE(o.maximize()); +} + +TEST(AuxiliaryObjectiveTest, PrioritySetting) { + Model model; + const Objective o = model.AddAuxiliaryObjective(3, "o"); + ASSERT_EQ(o.priority(), 3); + model.set_objective_priority(o, 4); + EXPECT_EQ(o.priority(), 4); +} + +TEST(AuxiliaryObjectiveTest, OffsetSetting) { + Model model; + const Objective o = model.AddAuxiliaryObjective(3, "o"); + ASSERT_EQ(o.offset(), 0.0); + model.set_objective_offset(o, 4.0); + EXPECT_EQ(o.offset(), 4.0); +} + +TEST(AuxiliaryObjectiveTest, LinearCoefficientSetting) { + Model model; + const Variable x = model.AddVariable("x"); + const Objective o = model.AddAuxiliaryObjective(3, "o"); + ASSERT_EQ(o.coefficient(x), 0.0); + model.set_objective_coefficient(o, x, 3.0); + EXPECT_EQ(o.coefficient(x), 3.0); +} + +TEST_F(SimpleAuxiliaryObjectiveTest, DeleteAuxiliaryObjective) { + model_.DeleteAuxiliaryObjective(secondary_); + EXPECT_EQ(model_.num_auxiliary_objectives(), 1); + EXPECT_EQ(model_.next_auxiliary_objective_id(), 2); + EXPECT_FALSE(model_.has_auxiliary_objective(0)); + EXPECT_TRUE(model_.has_auxiliary_objective(1)); + EXPECT_THAT(model_.AuxiliaryObjectives(), UnorderedElementsAre(tertiary_)); +} + +TEST(AuxiliaryObjectiveTest, NewObjectiveMethods) { + Model model; + const Variable x = model.AddVariable("x"); + { + const Objective a = model.AddAuxiliaryObjective(1); + model.Maximize(a, x + 2.0); + EXPECT_TRUE(a.maximize()); + EXPECT_EQ(a.offset(), 2.0); + EXPECT_EQ(a.coefficient(x), 1.0); + } + { + const Objective b = model.AddAuxiliaryObjective(2); + model.Minimize(b, x + 2.0); + EXPECT_FALSE(b.maximize()); + EXPECT_EQ(b.offset(), 2.0); + EXPECT_EQ(b.coefficient(x), 1.0); + } + { + const Objective c = model.AddMaximizationObjective(x + 2.0, 3); + EXPECT_TRUE(c.maximize()); + EXPECT_EQ(c.offset(), 2.0); + EXPECT_EQ(c.coefficient(x), 1.0); + } + { + const Objective d = model.AddMinimizationObjective(x + 2.0, 4); + EXPECT_FALSE(d.maximize()); + EXPECT_EQ(d.offset(), 2.0); + EXPECT_EQ(d.coefficient(x), 1.0); + } + { + const Objective e = model.AddAuxiliaryObjective(x + 2.0, true, 4); + EXPECT_TRUE(e.maximize()); + EXPECT_EQ(e.offset(), 2.0); + EXPECT_EQ(e.coefficient(x), 1.0); + } + { + const Objective f = model.AddMinimizationObjective(7.0 * x - 3.0, 4); + model.AddToObjective(f, -6.0 * x); + model.AddToObjective(f, 5.0); + EXPECT_FALSE(f.maximize()); + EXPECT_EQ(f.offset(), 2.0); + EXPECT_EQ(f.coefficient(x), 1.0); + } +} + +TEST_F(SimpleAuxiliaryObjectiveTest, ExportModel) { + EXPECT_THAT( + model_.ExportModel(), EquivToProto(R"pb( + name: "auxiliary_objectives" + variables { + ids: [ 0, 1 ] + lower_bounds: [ -inf, -inf ] + upper_bounds: [ inf, inf ] + integers: [ false, false ] + names: [ "x", "y" ] + } + objective { + maximize: true + linear_coefficients { + ids: [ 0 ] + values: [ 1.0 ] + } + priority: 2 + } + auxiliary_objectives { + key: 0 + value { maximize: false offset: 3.0 priority: 3 name: "secondary" } + } + auxiliary_objectives { + key: 1 + value { + maximize: true + offset: 1.0 + linear_coefficients { + ids: [ 1 ] + values: [ 2.0 ] + } + priority: 5 + name: "tertiary" + } + } + )pb")); +} + +TEST_F(SimpleAuxiliaryObjectiveTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model auxiliary_objectives: + Objectives: + __primary_obj__ (priority 2): maximize x + secondary (priority 3): minimize 3 + tertiary (priority 5): maximize 2*y + 1 + Linear constraints: + Variables: + x in (-∞, +∞) + y in (-∞, +∞) +)model"); +} + +TEST(AuxiliaryObjectiveDeathTest, ObjectiveByIdOutOfBounds) { + Model model; + model.AddAuxiliaryObjective(1); + EXPECT_DEATH(model.auxiliary_objective(-1), + AllOf(HasSubstr("auxiliary objective"), HasSubstr("-1"))); + EXPECT_DEATH(model.auxiliary_objective(2), + AllOf(HasSubstr("auxiliary objective"), HasSubstr("2"))); +} + +TEST(AuxiliaryObjectiveDeathTest, ObjectiveByIdDeleted) { + Model model; + const Objective o = model.AddAuxiliaryObjective(1, "o"); + EXPECT_EQ(model.auxiliary_objective(o.id().value()).name(), "o"); + model.DeleteAuxiliaryObjective(o); + EXPECT_DEATH(model.auxiliary_objective(o.id().value()), + AllOf(HasSubstr("auxiliary objective"), HasSubstr("0"))); +} + +TEST(AuxiliaryObjectiveDeathTest, DeletePrimaryObjective) { + Model model; + EXPECT_DEATH(model.DeleteAuxiliaryObjective(model.primary_objective()), + HasSubstr("primary objective")); +} + +TEST(AuxiliaryObjectiveDeathTest, DoubleDeleteObjective) { + Model model; + const Objective o = model.AddAuxiliaryObjective(3, "o"); + model.DeleteAuxiliaryObjective(o); + EXPECT_DEATH(model.DeleteAuxiliaryObjective(o), + HasSubstr("unrecognized auxiliary objective")); +} + +TEST(AuxiliaryObjectiveDeathTest, DeleteInvalidObjective) { + Model model; + const Objective o = + Objective::Auxiliary(model.storage(), AuxiliaryObjectiveId(0)); + EXPECT_DEATH(model.DeleteAuxiliaryObjective(o), + HasSubstr("unrecognized auxiliary objective")); +} + +TEST(AuxiliaryObjectiveDeathTest, SetObjectiveOtherModel) { + Model model_a("a"); + const Objective o_a = model_a.AddAuxiliaryObjective(3); + + Model model_b("b"); + const Variable x_b = model_b.AddVariable(); + + EXPECT_DEATH(model_a.SetObjective(o_a, x_b, false), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.SetObjective(o_a, x_b, false), + internal::kObjectsFromOtherModelStorage); +} + +TEST(AuxiliaryObjectiveDeathTest, AddToObjectiveOtherModel) { + Model model_a("a"); + const Objective o_a = model_a.AddAuxiliaryObjective(3); + + Model model_b("b"); + const Variable x_b = model_b.AddVariable(); + + EXPECT_DEATH(model_a.AddToObjective(o_a, x_b), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_b.AddToObjective(o_a, x_b), + internal::kObjectsFromOtherModelStorage); +} + +TEST(AuxiliaryObjectiveTest, NonzeroVariablesInLinarObjective) { + Model model; + const Objective o = model.AddAuxiliaryObjective(3); + model.AddVariable(); + const Variable y = model.AddVariable(); + const Variable z = model.AddVariable(); + model.set_objective_coefficient(o, y, 3.0); + model.set_objective_coefficient(o, z, 0.0); + EXPECT_THAT(model.NonzeroVariablesInLinearObjective(o), + UnorderedElementsAre(y)); +} + +// ------------------------- Quadratic constraints ----------------------------- + +// max 0 +// s.t. x^2 + y^2 <= 1.0 (c) +// 2x*y + 3x >= 0.5 (d) +// x unbounded +// y in {0, 1} +class SimpleQuadraticConstraintTest : public testing::Test { + protected: + SimpleQuadraticConstraintTest() + : model_("quadratic_constraints"), + x_(model_.AddVariable("x")), + y_(model_.AddBinaryVariable("y")), + c_(model_.AddQuadraticConstraint(x_ * x_ + y_ * y_ <= 1.0, "c")), + d_(model_.AddQuadraticConstraint(2.0 * x_ * y_ + 3.0 * y_ >= 0.5, + "d")) {} + + Model model_; + const Variable x_; + const Variable y_; + const QuadraticConstraint c_; + const QuadraticConstraint d_; +}; + +TEST_F(SimpleQuadraticConstraintTest, Properties) { + EXPECT_EQ(model_.num_quadratic_constraints(), 2); + EXPECT_EQ(model_.next_quadratic_constraint_id(), 2); + EXPECT_TRUE(model_.has_quadratic_constraint(0)); + EXPECT_TRUE(model_.has_quadratic_constraint(1)); + EXPECT_FALSE(model_.has_quadratic_constraint(2)); + EXPECT_FALSE(model_.has_quadratic_constraint(3)); + EXPECT_FALSE(model_.has_quadratic_constraint(-1)); + EXPECT_THAT(model_.QuadraticConstraints(), UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedQuadraticConstraints(), ElementsAre(c_, d_)); + + EXPECT_EQ(model_.quadratic_constraint(c_.id()).name(), "c"); + EXPECT_EQ(model_.quadratic_constraint(d_.id()).name(), "d"); + EXPECT_EQ(model_.quadratic_constraint(c_.typed_id()).name(), "c"); + EXPECT_EQ(model_.quadratic_constraint(d_.typed_id()).name(), "d"); +} + +TEST_F(SimpleQuadraticConstraintTest, DeleteConstraint) { + model_.DeleteQuadraticConstraint(c_); + EXPECT_EQ(model_.num_quadratic_constraints(), 1); + EXPECT_EQ(model_.next_quadratic_constraint_id(), 2); + EXPECT_FALSE(model_.has_quadratic_constraint(0)); + EXPECT_TRUE(model_.has_quadratic_constraint(1)); + EXPECT_THAT(model_.QuadraticConstraints(), UnorderedElementsAre(d_)); +} + +TEST_F(SimpleQuadraticConstraintTest, ExportModel) { + EXPECT_THAT(model_.ExportModel(), EquivToProto(R"pb( + name: "quadratic_constraints" + variables { + ids: [ 0, 1 ] + lower_bounds: [ -inf, 0.0 ] + upper_bounds: [ inf, 1.0 ] + integers: [ false, true ] + names: [ "x", "y" ] + } + quadratic_constraints { + key: 0 + value: { + lower_bound: -inf + upper_bound: 1.0 + quadratic_terms { + row_ids: [ 0, 1 ] + column_ids: [ 0, 1 ] + coefficients: [ 1.0, 1.0 ] + } + name: "c" + } + } + quadratic_constraints { + key: 1 + value: { + lower_bound: 0.5 + upper_bound: inf + linear_terms { + ids: [ 1 ] + values: [ 3.0 ] + } + quadratic_terms { + row_ids: [ 0 ] + column_ids: [ 1 ] + coefficients: [ 2.0 ] + } + name: "d" + } + } + )pb")); +} + +TEST_F(SimpleQuadraticConstraintTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model quadratic_constraints: + Objective: + minimize 0 + Linear constraints: + Quadratic constraints: + c: x² + y² ≤ 1 + d: 2*x*y + 3*y ≥ 0.5 + Variables: + x in (-∞, +∞) + y (binary) +)model"); +} + +TEST(QuadraticConstraintTest, AddQuadraticConstraintWithoutVariables) { + Model model; + + // Here we test a corner case that may not be very useful in practice: the + // case of a bounded LinearExpression that have no terms but its offset. + // + // We want to make sure the code don't assume all LinearExpression have a + // non-null storage(). + const QuadraticConstraint c = + model.AddQuadraticConstraint(BoundedQuadraticExpression(0.0, 1.0, 2.0)); + EXPECT_EQ(c.lower_bound(), 1.0); + EXPECT_EQ(c.upper_bound(), 2.0); + EXPECT_THAT(c.NonzeroVariables(), IsEmpty()); +} + +TEST(QuadraticConstraintDeathTest, ConstraintByIdOutOfBounds) { + Model model; + model.AddQuadraticConstraint(BoundedQuadraticExpression(0, 0, 0)); + EXPECT_DEATH(model.quadratic_constraint(-1), + AllOf(HasSubstr("quadratic constraint"), HasSubstr("-1"))); + EXPECT_DEATH(model.quadratic_constraint(2), + AllOf(HasSubstr("quadratic constraint"), HasSubstr("2"))); +} + +TEST(QuadraticConstraintDeathTest, ConstraintByIdDeleted) { + Model model; + const QuadraticConstraint c = + model.AddQuadraticConstraint(BoundedQuadraticExpression(0, 0, 0), "c"); + EXPECT_EQ(model.quadratic_constraint(c.id()).name(), "c"); + model.DeleteQuadraticConstraint(c); + EXPECT_DEATH(model.quadratic_constraint(c.id()), + AllOf(HasSubstr("quadratic constraint"), HasSubstr("0"))); +} + +TEST(QuadraticConstraintDeathTest, AddConstraintOtherModel) { + Model model_a("a"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + const Variable b_y = model_b.AddVariable("y"); + + EXPECT_DEATH(model_a.AddQuadraticConstraint(2 <= 2 * b_x * b_y + 2, "c"), + internal::kObjectsFromOtherModelStorage); +} + +// --------------------- Second-order cone constraints ------------------------- + +// max 0 +// s.t. ||{x, y}||_2 <= 1.0 (c) +// ||{1, 2x - y}||_2 <= 3y - 4 (d) +// x, y unbounded +class SimpleSecondOrderConeConstraintTest : public testing::Test { + protected: + SimpleSecondOrderConeConstraintTest() + : model_("soc_constraints"), + x_(model_.AddVariable("x")), + y_(model_.AddVariable("y")), + c_(model_.AddSecondOrderConeConstraint({x_, y_}, 1.0, "c")), + d_(model_.AddSecondOrderConeConstraint({1.0, 2.0 * x_ - y_}, + 3.0 * y_ - 4.0, "d")) {} + + Model model_; + const Variable x_; + const Variable y_; + const SecondOrderConeConstraint c_; + const SecondOrderConeConstraint d_; +}; + +TEST_F(SimpleSecondOrderConeConstraintTest, Properties) { + EXPECT_EQ(model_.num_second_order_cone_constraints(), 2); + EXPECT_EQ(model_.next_second_order_cone_constraint_id(), 2); + EXPECT_TRUE(model_.has_second_order_cone_constraint(0)); + EXPECT_TRUE(model_.has_second_order_cone_constraint(1)); + EXPECT_FALSE(model_.has_second_order_cone_constraint(2)); + EXPECT_FALSE(model_.has_second_order_cone_constraint(3)); + EXPECT_FALSE(model_.has_second_order_cone_constraint(-1)); + EXPECT_THAT(model_.SecondOrderConeConstraints(), + UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedSecondOrderConeConstraints(), ElementsAre(c_, d_)); + + EXPECT_EQ(model_.second_order_cone_constraint(c_.id()).name(), "c"); + EXPECT_EQ(model_.second_order_cone_constraint(d_.id()).name(), "d"); + EXPECT_EQ(model_.second_order_cone_constraint(c_.typed_id()).name(), "c"); + EXPECT_EQ(model_.second_order_cone_constraint(d_.typed_id()).name(), "d"); +} + +TEST_F(SimpleSecondOrderConeConstraintTest, DeleteConstraint) { + model_.DeleteSecondOrderConeConstraint(c_); + EXPECT_EQ(model_.num_second_order_cone_constraints(), 1); + EXPECT_EQ(model_.next_second_order_cone_constraint_id(), 2); + EXPECT_FALSE(model_.has_second_order_cone_constraint(0)); + EXPECT_TRUE(model_.has_second_order_cone_constraint(1)); + EXPECT_THAT(model_.SecondOrderConeConstraints(), UnorderedElementsAre(d_)); +} + +TEST_F(SimpleSecondOrderConeConstraintTest, ExportModel) { + EXPECT_THAT(model_.ExportModel(), EquivToProto(R"pb( + name: "soc_constraints" + variables { + ids: [ 0, 1 ] + lower_bounds: [ -inf, -inf ] + upper_bounds: [ inf, inf ] + integers: [ false, false ] + names: [ "x", "y" ] + } + second_order_cone_constraints { + key: 0 + value: { + upper_bound { offset: 1.0 } + arguments_to_norm { + ids: [ 0 ] + coefficients: [ 1.0 ] + } + arguments_to_norm { + ids: [ 1 ] + coefficients: [ 1.0 ] + } + name: "c" + } + } + second_order_cone_constraints { + key: 1 + value: { + upper_bound { + ids: [ 1 ] + coefficients: [ 3.0 ] + offset: -4.0 + } + arguments_to_norm { offset: 1.0 } + arguments_to_norm { + ids: [ 0, 1 ] + coefficients: [ 2.0, -1.0 ] + } + name: "d" + } + } + )pb")); +} + +TEST_F(SimpleSecondOrderConeConstraintTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model soc_constraints: + Objective: + minimize 0 + Linear constraints: + Second-order cone constraints: + c: ||{x, y}||₂ ≤ 1 + d: ||{1, 2*x - y}||₂ ≤ 3*y - 4 + Variables: + x in (-∞, +∞) + y in (-∞, +∞) +)model"); +} + +TEST(SecondOrderConeConstraintTest, + AddSecondOrderConeConstraintWithoutVariables) { + Model model; + + // Here we test a corner case that may not be very useful in practice: the + // case of a LinearExpression that has no terms but its offset. + // + // We want to make sure the code don't assume all LinearExpressions have a + // non-null storage(). + const SecondOrderConeConstraint c = + model.AddSecondOrderConeConstraint({2.0}, 1.0, "c"); + { + const LinearExpression ub = c.UpperBound(); + EXPECT_EQ(ub.offset(), 1.0); + EXPECT_THAT(ub.terms(), IsEmpty()); + } + { + const std::vector args = c.ArgumentsToNorm(); + ASSERT_EQ(args.size(), 1); + EXPECT_EQ(args[0].offset(), 2.0); + EXPECT_THAT(args[0].terms(), IsEmpty()); + } +} + +TEST(SecondOrderConeConstraintDeathTest, ConstraintByIdOutOfBounds) { + Model model; + model.AddSecondOrderConeConstraint({}, 1.0, "c"); + EXPECT_DEATH( + model.second_order_cone_constraint(-1), + AllOf(HasSubstr("second-order cone constraint"), HasSubstr("-1"))); + EXPECT_DEATH( + model.second_order_cone_constraint(2), + AllOf(HasSubstr("second-order cone constraint"), HasSubstr("2"))); +} + +TEST(SecondOrderConeConstraintDeathTest, ConstraintByIdDeleted) { + Model model; + const SecondOrderConeConstraint c = + model.AddSecondOrderConeConstraint({}, 1.0, "c"); + EXPECT_EQ(model.second_order_cone_constraint(c.id()).name(), "c"); + model.DeleteSecondOrderConeConstraint(c); + EXPECT_DEATH( + model.second_order_cone_constraint(c.id()), + AllOf(HasSubstr("second-order cone constraint"), HasSubstr("0"))); +} + +TEST(SecondOrderConeConstraintDeathTest, AddConstraintOtherModel) { + Model model_a("a"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + + EXPECT_DEATH(model_a.AddSecondOrderConeConstraint({b_x}, 1.0, "c"), + internal::kObjectsFromOtherModelStorage); + EXPECT_DEATH(model_a.AddSecondOrderConeConstraint({1.0}, b_x, "c"), + internal::kObjectsFromOtherModelStorage); +} + +// --------------------------- SOS1 constraints -------------------------------- + +// max 0 +// s.t. {x, y} is SOS1 with weights {3, 2} (c) +// {2 * y - 1, 1} is SOS1 (d) +// x, y unbounded +class SimpleSos1ConstraintTest : public testing::Test { + protected: + SimpleSos1ConstraintTest() + : model_("sos1_constraints"), + x_(model_.AddVariable("x")), + y_(model_.AddVariable("y")), + c_(model_.AddSos1Constraint({x_, y_}, {3.0, 2.0}, "c")), + d_(model_.AddSos1Constraint({2 * y_ - 1, 1.0}, {}, "d")) {} + + Model model_; + const Variable x_; + const Variable y_; + const Sos1Constraint c_; + const Sos1Constraint d_; +}; + +TEST_F(SimpleSos1ConstraintTest, Properties) { + EXPECT_EQ(model_.num_sos1_constraints(), 2); + EXPECT_EQ(model_.next_sos1_constraint_id(), 2); + EXPECT_TRUE(model_.has_sos1_constraint(0)); + EXPECT_TRUE(model_.has_sos1_constraint(1)); + EXPECT_FALSE(model_.has_sos1_constraint(2)); + EXPECT_FALSE(model_.has_sos1_constraint(3)); + EXPECT_FALSE(model_.has_sos1_constraint(-1)); + EXPECT_THAT(model_.Sos1Constraints(), UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedSos1Constraints(), ElementsAre(c_, d_)); + + EXPECT_EQ(model_.sos1_constraint(c_.id()).name(), "c"); + EXPECT_EQ(model_.sos1_constraint(d_.id()).name(), "d"); + EXPECT_EQ(model_.sos1_constraint(c_.typed_id()).name(), "c"); + EXPECT_EQ(model_.sos1_constraint(d_.typed_id()).name(), "d"); +} + +TEST_F(SimpleSos1ConstraintTest, DeleteConstraint) { + model_.DeleteSos1Constraint(c_); + EXPECT_EQ(model_.num_sos1_constraints(), 1); + EXPECT_EQ(model_.next_sos1_constraint_id(), 2); + EXPECT_FALSE(model_.has_sos1_constraint(0)); + EXPECT_TRUE(model_.has_sos1_constraint(1)); + EXPECT_THAT(model_.Sos1Constraints(), UnorderedElementsAre(d_)); +} + +TEST_F(SimpleSos1ConstraintTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model sos1_constraints: + Objective: + minimize 0 + Linear constraints: + SOS1 constraints: + c: {x, y} is SOS1 with weights {3, 2} + d: {2*y - 1, 1} is SOS1 + Variables: + x in (-∞, +∞) + y in (-∞, +∞) +)model"); +} + +TEST(SimpleSos1ConstraintDeathTest, ConstraintByIdOutOfBounds) { + Model model; + model.AddSos1Constraint({}, {}); + EXPECT_DEATH(model.sos1_constraint(-1), + AllOf(HasSubstr("SOS1 constraint"), HasSubstr("-1"))); + EXPECT_DEATH(model.sos1_constraint(2), + AllOf(HasSubstr("SOS1 constraint"), HasSubstr("2"))); +} + +TEST(SimpleSos1ConstraintDeathTest, ConstraintByIdDeleted) { + Model model; + const Sos1Constraint c = model.AddSos1Constraint({}, {}, "c"); + EXPECT_EQ(model.sos1_constraint(c.id()).name(), "c"); + model.DeleteSos1Constraint(c); + EXPECT_DEATH(model.sos1_constraint(c.id()), + AllOf(HasSubstr("SOS1 constraint"), HasSubstr("0"))); +} + +TEST(SimpleSos1ConstraintDeathTest, AddConstraintOtherModel) { + Model model_a("a"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + + EXPECT_DEATH(model_a.AddSos1Constraint({b_x}, {}), + internal::kObjectsFromOtherModelStorage); +} + +// --------------------------- SOS2 constraints -------------------------------- + +// max 0 +// s.t. {x, y} is SOS2 with weights {3, 2} (c) +// {2 * y - 1, 1} is SOS2 (d) +// x, y unbounded +class SimpleSos2ConstraintTest : public testing::Test { + protected: + SimpleSos2ConstraintTest() + : model_("sos2_constraints"), + x_(model_.AddVariable("x")), + y_(model_.AddVariable("y")), + c_(model_.AddSos2Constraint({x_, y_}, {3.0, 2.0}, "c")), + d_(model_.AddSos2Constraint({2 * y_ - 1, 1.0}, {}, "d")) {} + + Model model_; + const Variable x_; + const Variable y_; + const Sos2Constraint c_; + const Sos2Constraint d_; +}; + +TEST_F(SimpleSos2ConstraintTest, Properties) { + EXPECT_EQ(model_.num_sos2_constraints(), 2); + EXPECT_EQ(model_.next_sos2_constraint_id(), 2); + EXPECT_TRUE(model_.has_sos2_constraint(0)); + EXPECT_TRUE(model_.has_sos2_constraint(1)); + EXPECT_FALSE(model_.has_sos2_constraint(2)); + EXPECT_FALSE(model_.has_sos2_constraint(3)); + EXPECT_FALSE(model_.has_sos2_constraint(-1)); + EXPECT_THAT(model_.Sos2Constraints(), UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedSos2Constraints(), ElementsAre(c_, d_)); + + EXPECT_EQ(model_.sos2_constraint(c_.id()).name(), "c"); + EXPECT_EQ(model_.sos2_constraint(d_.id()).name(), "d"); + EXPECT_EQ(model_.sos2_constraint(c_.typed_id()).name(), "c"); + EXPECT_EQ(model_.sos2_constraint(d_.typed_id()).name(), "d"); +} + +TEST_F(SimpleSos2ConstraintTest, DeleteConstraint) { + model_.DeleteSos2Constraint(c_); + EXPECT_EQ(model_.num_sos2_constraints(), 1); + EXPECT_EQ(model_.next_sos2_constraint_id(), 2); + EXPECT_FALSE(model_.has_sos2_constraint(0)); + EXPECT_TRUE(model_.has_sos2_constraint(1)); + EXPECT_THAT(model_.Sos2Constraints(), UnorderedElementsAre(d_)); +} + +TEST_F(SimpleSos2ConstraintTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model sos2_constraints: + Objective: + minimize 0 + Linear constraints: + SOS2 constraints: + c: {x, y} is SOS2 with weights {3, 2} + d: {2*y - 1, 1} is SOS2 + Variables: + x in (-∞, +∞) + y in (-∞, +∞) +)model"); +} + +TEST(SimpleSos2ConstraintDeathTest, ConstraintByIdOutOfBounds) { + Model model; + model.AddSos2Constraint({}, {}); + EXPECT_DEATH(model.sos2_constraint(-1), + AllOf(HasSubstr("SOS2 constraint"), HasSubstr("-1"))); + EXPECT_DEATH(model.sos2_constraint(2), + AllOf(HasSubstr("SOS2 constraint"), HasSubstr("2"))); +} + +TEST(SimpleSos2ConstraintDeathTest, ConstraintByIdDeleted) { + Model model; + const Sos2Constraint c = model.AddSos2Constraint({}, {}, "c"); + EXPECT_EQ(model.sos2_constraint(c.id()).name(), "c"); + model.DeleteSos2Constraint(c); + EXPECT_DEATH(model.sos2_constraint(c.id()), + AllOf(HasSubstr("SOS2 constraint"), HasSubstr("0"))); +} + +TEST(SimpleSos2ConstraintDeathTest, AddConstraintOtherModel) { + Model model_a("a"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + + EXPECT_DEATH(model_a.AddSos2Constraint({b_x}, {}), + internal::kObjectsFromOtherModelStorage); +} + +// ------------------------ Indicator constraints ------------------------------ + +// max 0 +// s.t. x = 1 --> z + 2 <= 3 (c) +// y = 0 --> 1 <= 2 * z + 3 <= 4 (d) +// x, y in {0,1} +// z unbounded +class SimpleIndicatorConstraintTest : public testing::Test { + protected: + SimpleIndicatorConstraintTest() + : model_("indicator_constraints"), + x_(model_.AddBinaryVariable("x")), + y_(model_.AddBinaryVariable("y")), + z_(model_.AddVariable("z")), + c_(model_.AddIndicatorConstraint(x_, z_ + 2 <= 3, + /*activate_on_zero=*/false, "c")), + d_(model_.AddIndicatorConstraint(y_, 1 <= 2 * z_ + 3 <= 4, + /*activate_on_zero=*/true, "d")) {} + + Model model_; + const Variable x_; + const Variable y_; + const Variable z_; + const IndicatorConstraint c_; + const IndicatorConstraint d_; +}; + +TEST_F(SimpleIndicatorConstraintTest, Properties) { + EXPECT_EQ(model_.num_indicator_constraints(), 2); + EXPECT_EQ(model_.next_indicator_constraint_id(), 2); + EXPECT_TRUE(model_.has_indicator_constraint(0)); + EXPECT_TRUE(model_.has_indicator_constraint(1)); + EXPECT_FALSE(model_.has_indicator_constraint(2)); + EXPECT_FALSE(model_.has_indicator_constraint(3)); + EXPECT_FALSE(model_.has_indicator_constraint(-1)); + EXPECT_THAT(model_.IndicatorConstraints(), UnorderedElementsAre(c_, d_)); + EXPECT_THAT(model_.SortedIndicatorConstraints(), ElementsAre(c_, d_)); + + EXPECT_EQ(model_.indicator_constraint(c_.id()).name(), "c"); + EXPECT_EQ(model_.indicator_constraint(d_.id()).name(), "d"); + EXPECT_EQ(model_.indicator_constraint(c_.typed_id()).name(), "c"); + EXPECT_EQ(model_.indicator_constraint(d_.typed_id()).name(), "d"); +} + +TEST_F(SimpleIndicatorConstraintTest, DeleteConstraint) { + model_.DeleteIndicatorConstraint(c_); + EXPECT_EQ(model_.num_indicator_constraints(), 1); + EXPECT_EQ(model_.next_indicator_constraint_id(), 2); + EXPECT_FALSE(model_.has_indicator_constraint(0)); + EXPECT_TRUE(model_.has_indicator_constraint(1)); + EXPECT_THAT(model_.IndicatorConstraints(), UnorderedElementsAre(d_)); +} + +TEST_F(SimpleIndicatorConstraintTest, Streaming) { + EXPECT_EQ(StreamToString(model_), + R"model(Model indicator_constraints: + Objective: + minimize 0 + Linear constraints: + Indicator constraints: + c: x = 1 ⇒ z ≤ 1 + d: y = 0 ⇒ -2 ≤ 2*z ≤ 1 + Variables: + x (binary) + y (binary) + z in (-∞, +∞) +)model"); +} + +TEST(SimpleIndicatorConstraintDeathTest, ConstraintByIdOutOfBounds) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + model.AddIndicatorConstraint(x, x <= 1); + + EXPECT_DEATH(model.indicator_constraint(-1), + AllOf(HasSubstr("indicator_constraint"), HasSubstr("-1"))); + EXPECT_DEATH(model.indicator_constraint(2), + AllOf(HasSubstr("indicator_constraint"), HasSubstr("2"))); +} + +TEST(SimpleIndicatorConstraintDeathTest, ConstraintByIdDeleted) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const IndicatorConstraint c = + model.AddIndicatorConstraint(x, x <= 1, /*activate_on_zero=*/false, "c"); + + EXPECT_EQ(model.indicator_constraint(c.id()).name(), "c"); + model.DeleteIndicatorConstraint(c); + EXPECT_DEATH(model.indicator_constraint(c.id()), + AllOf(HasSubstr("indicator constraint"), HasSubstr("0"))); +} + +TEST(SimpleIndicatorConstraintDeathTest, AddConstraintOtherModel) { + Model model_a("a"); + const Variable a_x = model_a.AddVariable("x"); + + Model model_b("b"); + const Variable b_x = model_b.AddVariable("x"); + + // The indicator variable should trigger the crash. + EXPECT_DEATH(model_a.AddIndicatorConstraint(b_x, a_x <= 1), + internal::kObjectsFromOtherModelStorage); + + // The implied constraint should trigger the crash. + EXPECT_DEATH(model_a.AddIndicatorConstraint(a_x, b_x <= 1), + internal::kObjectsFromOtherModelStorage); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/remote_streaming_mode_test.cc b/ortools/math_opt/cpp/remote_streaming_mode_test.cc new file mode 100644 index 00000000000..9c72350b10a --- /dev/null +++ b/ortools/math_opt/cpp/remote_streaming_mode_test.cc @@ -0,0 +1,72 @@ +// Copyright 2010-2025 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/remote_streaming_mode.h" + +#include +#include + +#include "absl/flags/marshalling.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::HasSubstr; + +// Tests that the AbslParseFlag() and AbslUnparseFlag() functions properly +// roundtrips. +class RemoteStreamingSolveModeRoundtripTest + : public testing::TestWithParam {}; + +TEST_P(RemoteStreamingSolveModeRoundtripTest, Roundtrip) { + RemoteStreamingSolveMode output = RemoteStreamingSolveMode::kDefault; + std::string error; + EXPECT_TRUE(absl::ParseFlag(absl::UnparseFlag(GetParam()), &output, &error)) + << "error: '" << absl::CEscape(error) << "'"; + EXPECT_EQ(output, GetParam()); +} + +INSTANTIATE_TEST_SUITE_P( + All, RemoteStreamingSolveModeRoundtripTest, + testing::Values(RemoteStreamingSolveMode::kDefault, + RemoteStreamingSolveMode::kSubprocess, + RemoteStreamingSolveMode::kInProcess), + [](const testing::TestParamInfo< + RemoteStreamingSolveModeRoundtripTest::ParamType>& info) { + return AbslUnparseFlag(info.param); + }); + +TEST(RemoteStreamingSolveModeTest, InvalidValue) { + RemoteStreamingSolveMode output = RemoteStreamingSolveMode::kDefault; + std::string error; + EXPECT_FALSE(absl::ParseFlag("unknown", &output, &error)) + << "output: " << output; + EXPECT_THAT(error, HasSubstr("unknown value")); +} + +TEST(RemoteStreamingSolveModeTest, PrintToStdStream) { + std::ostringstream oss; + oss << RemoteStreamingSolveMode::kDefault; + EXPECT_EQ(oss.str(), "default"); +} + +TEST(RemoteStreamingSolveModeTest, PrintToAbsl) { + EXPECT_EQ(absl::StrCat(RemoteStreamingSolveMode::kDefault), "default"); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/sparse_containers_test.cc b/ortools/math_opt/cpp/sparse_containers_test.cc new file mode 100644 index 00000000000..a81b0d3ac02 --- /dev/null +++ b/ortools/math_opt/cpp/sparse_containers_test.cc @@ -0,0 +1,513 @@ +// Copyright 2010-2025 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/sparse_containers.h" + +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +TEST(DoubleVariableValuesFromProtoTest, Empty) { + ModelStorage model; + model.AddVariable("x"); + + const SparseDoubleVectorProto proto; + + EXPECT_THAT(VariableValuesFromProto(&model, proto), IsOkAndHolds(IsEmpty())); +} + +TEST(DoubleVariableValuesFromProtoTest, NonEmpty) { + ModelStorage model; + const VariableId x = model.AddVariable("x"); + model.AddVariable("y"); + const VariableId z = model.AddVariable("z"); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_ids(2); + proto.add_values(3.0); + proto.add_values(-2.0); + + VariableMap expected = {{Variable(&model, x), 3.0}, + {Variable(&model, z), -2.0}}; + + EXPECT_THAT(VariableValuesFromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0.0))); +} + +TEST(DoubleVariableValuesFromProtoTest, UnequalSize) { + ModelStorage model; + model.AddVariable("x"); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + + EXPECT_THAT(VariableValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(DoubleVariableValuesFromProtoTest, OutOfOrder) { + ModelStorage model; + model.AddVariable("x"); + model.AddVariable("y"); + + SparseDoubleVectorProto proto; + proto.add_ids(1); + proto.add_ids(0); + proto.add_values(1.0); + proto.add_values(1.0); + + EXPECT_THAT(VariableValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(DoubleVariableValuesFromProtoTest, NotInModel) { + ModelStorage model; + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_values(1.0); + + EXPECT_THAT(VariableValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no variable with id"))); +} + +TEST(DoubleVariableValuesFromProtoTest, SmallMap) { + ModelStorage model; + const VariableId x = model.AddVariable("x"); + model.AddVariable("y"); + const VariableId z = model.AddVariable("z"); + + const VariableMap input = {{Variable(&model, x), 3.0}, + {Variable(&model, z), -2.0}}; + + EXPECT_THAT(VariableValuesFromProto(&model, VariableValuesToProto(input)), + IsOkAndHolds(IsNear(input, /*tolerance=*/0.0))); +} + +TEST(Int32VariableValuesFromProtoTest, UnequalSize) { + Model model; + model.AddVariable("x"); + + SparseInt32VectorProto proto; + proto.add_ids(0); + + EXPECT_THAT(VariableValuesFromProto(model.storage(), proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(Int32VariableValuesFromProtoTest, VariableDoesNotExist) { + Model model; + model.AddVariable("x"); + + SparseInt32VectorProto proto; + proto.add_ids(1); + proto.add_values(12); + + EXPECT_THAT(VariableValuesFromProto(model.storage(), proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(Int32VariableValuesFromProtoTest, SmallMap) { + Model model; + const Variable x = model.AddVariable("x"); + model.AddVariable("y"); + const Variable z = model.AddVariable("z"); + + SparseInt32VectorProto proto; + proto.add_ids(0); + proto.add_ids(2); + proto.add_values(12); + proto.add_values(11); + + EXPECT_THAT(VariableValuesFromProto(model.storage(), proto), + IsOkAndHolds(UnorderedElementsAre(Pair(testing::Eq(x), 12), + Pair(testing::Eq(z), 11)))); +} + +TEST(AuxiliaryObjectiveValuesFromProtoTest, Empty) { + ModelStorage model; + EXPECT_THAT(AuxiliaryObjectiveValuesFromProto( + &model, google::protobuf::Map()), + IsOkAndHolds(IsEmpty())); +} + +TEST(AuxiliaryObjectiveValuesFromProtoTest, NonEmpty) { + ModelStorage model; + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(2)); + + google::protobuf::Map proto; + proto[o.id().value()] = 3.0; + + EXPECT_THAT(AuxiliaryObjectiveValuesFromProto(&model, proto), + IsOkAndHolds(UnorderedElementsAre(Pair(o, 3.0)))); +} + +TEST(AuxiliaryObjectiveValuesFromProtoTest, NotInModel) { + ModelStorage model; + + google::protobuf::Map proto; + proto[0] = 3.0; + + EXPECT_THAT(AuxiliaryObjectiveValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no auxiliary objective with id"))); +} + +TEST(AuxiliaryObjectiveValuesToProtoTest, Empty) { + EXPECT_THAT(AuxiliaryObjectiveValuesToProto({}), IsEmpty()); +} + +TEST(AuxiliaryObjectiveValuesToProtoTest, NonEmpty) { + ModelStorage model; + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(2)); + + const absl::flat_hash_map input = {{o, 3.0}}; + + EXPECT_THAT(AuxiliaryObjectiveValuesToProto(input), + UnorderedElementsAre(Pair(o.id().value(), 3.0))); +} + +TEST(AuxiliaryObjectiveValuesToProtoDeathTest, PrimaryObjectiveInMap) { + ModelStorage model; + const Objective o = Objective::Primary(&model); + + const absl::flat_hash_map input = {{o, 3.0}}; + EXPECT_DEATH(AuxiliaryObjectiveValuesToProto(input), "primary"); +} + +TEST(LinearConstraintValuesFromProtoTest, EmptySuccess) { + ModelStorage model; + model.AddLinearConstraint("c"); + + SparseDoubleVectorProto proto; + + EXPECT_THAT(LinearConstraintValuesFromProto(&model, proto), + IsOkAndHolds(IsEmpty())); +} + +TEST(LinearConstraintValuesFromProtoTest, NonEmptySuccess) { + ModelStorage model; + const LinearConstraintId c = model.AddLinearConstraint("c"); + model.AddLinearConstraint("d"); + const LinearConstraintId e = model.AddLinearConstraint("e"); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_ids(2); + proto.add_values(3.0); + proto.add_values(-2.0); + + LinearConstraintMap expected = {{LinearConstraint(&model, c), 3.0}, + {LinearConstraint(&model, e), -2.0}}; + + EXPECT_THAT(LinearConstraintValuesFromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0.0))); +} + +TEST(LinearConstraintValuesFromProtoTest, UnequalSize) { + ModelStorage model; + model.AddLinearConstraint("c"); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + + EXPECT_THAT(LinearConstraintValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(LinearConstraintValuesFromProtoTest, NotInModel) { + ModelStorage model; + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_values(1.0); + + EXPECT_THAT(LinearConstraintValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no linear constraint with id"))); +} + +TEST(LinearConstraintValuesToProtoTest, Empty) { + ModelStorage model; + EXPECT_THAT(LinearConstraintValuesFromProto( + &model, LinearConstraintValuesToProto({})), + IsOkAndHolds(IsEmpty())); +} + +TEST(LinearConstraintValuesToProtoTest, SmallMap) { + ModelStorage model; + const LinearConstraintId c = model.AddLinearConstraint("c"); + model.AddLinearConstraint("d"); + const LinearConstraintId e = model.AddLinearConstraint("e"); + + const LinearConstraintMap input = { + {LinearConstraint(&model, c), 3.0}, {LinearConstraint(&model, e), -2.0}}; + + EXPECT_THAT(LinearConstraintValuesFromProto( + &model, LinearConstraintValuesToProto(input)), + IsOkAndHolds(IsNear(input, /*tolerance=*/0.0))); +} + +TEST(QuadraticConstraintValuesFromProtoTest, EmptySuccess) { + ModelStorage model; + model.AddAtomicConstraint(QuadraticConstraintData{.name = "c"}); + + SparseDoubleVectorProto proto; + + EXPECT_THAT(QuadraticConstraintValuesFromProto(&model, proto), + IsOkAndHolds(IsEmpty())); +} + +TEST(QuadraticConstraintValuesFromProtoTest, NonEmptySuccess) { + ModelStorage model; + const QuadraticConstraintId c = + model.AddAtomicConstraint(QuadraticConstraintData{.name = "c"}); + model.AddAtomicConstraint(QuadraticConstraintData{.name = "d"}); + const QuadraticConstraintId e = + model.AddAtomicConstraint(QuadraticConstraintData{.name = "e"}); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_ids(2); + proto.add_values(3.0); + proto.add_values(-2.0); + + absl::flat_hash_map expected = { + {QuadraticConstraint(&model, c), 3.0}, + {QuadraticConstraint(&model, e), -2.0}}; + + EXPECT_THAT(QuadraticConstraintValuesFromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0.0))); +} + +TEST(QuadraticConstraintValuesFromProtoTest, UnequalSize) { + ModelStorage model; + model.AddAtomicConstraint(QuadraticConstraintData{.name = "c"}); + + SparseDoubleVectorProto proto; + proto.add_ids(0); + + EXPECT_THAT(QuadraticConstraintValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(QuadraticConstraintValuesFromProtoTest, NotInModel) { + ModelStorage model; + + SparseDoubleVectorProto proto; + proto.add_ids(0); + proto.add_values(1.0); + + EXPECT_THAT(QuadraticConstraintValuesFromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no quadratic constraint with id"))); +} + +TEST(QuadraticConstraintValuesToProtoTest, Empty) { + ModelStorage model; + EXPECT_THAT(QuadraticConstraintValuesFromProto( + &model, QuadraticConstraintValuesToProto({})), + IsOkAndHolds(IsEmpty())); +} + +TEST(QuadraticConstraintValuesToProtoTest, SmallMap) { + ModelStorage model; + const QuadraticConstraintId c = + model.AddAtomicConstraint(QuadraticConstraintData{.name = "c"}); + model.AddAtomicConstraint(QuadraticConstraintData{.name = "d"}); + const QuadraticConstraintId e = + model.AddAtomicConstraint(QuadraticConstraintData{.name = "e"}); + + const absl::flat_hash_map input = { + {QuadraticConstraint(&model, c), 3.0}, + {QuadraticConstraint(&model, e), -2.0}}; + + EXPECT_THAT(QuadraticConstraintValuesFromProto( + &model, QuadraticConstraintValuesToProto(input)), + IsOkAndHolds(IsNear(input, /*tolerance=*/0.0))); +} + +TEST(VariableBasis, EmptyRoundTrip) { + ModelStorage model; + const VariableMap variable_basis; + SparseBasisStatusVector proto; + + // Test one way + EXPECT_THAT(VariableBasisFromProto(&model, proto), + IsOkAndHolds(variable_basis)); + // Test round trip + EXPECT_THAT( + VariableBasisFromProto(&model, VariableBasisToProto(variable_basis)), + IsOkAndHolds(variable_basis)); +} + +TEST(VariableBasis, RoundTrip) { + ModelStorage model; + const VariableId x = model.AddVariable("x"); + const VariableId y = model.AddVariable("y"); + + const VariableMap variable_basis = { + {Variable(&model, x), BasisStatus::kAtUpperBound}, + {Variable(&model, y), BasisStatus::kFree}}; + + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_UPPER_BOUND); + proto.add_values(BASIS_STATUS_FREE); + + // Test one way + EXPECT_THAT(VariableBasisFromProto(&model, proto), + IsOkAndHolds(variable_basis)); + // Test round trip + EXPECT_THAT( + VariableBasisFromProto(&model, VariableBasisToProto(variable_basis)), + IsOkAndHolds(variable_basis)); +} + +TEST(VariableBasisFromProto, UnequalSize) { + ModelStorage storage; + storage.AddVariable("x"); + storage.AddVariable("y"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_LOWER_BOUND); + EXPECT_THAT(VariableBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(VariableBasisFromProto, VariableDoesNotExist) { + ModelStorage storage; + storage.AddVariable("x"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_LOWER_BOUND); + proto.add_values(BASIS_STATUS_AT_UPPER_BOUND); + EXPECT_THAT(VariableBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no variable with id"))); +} + +TEST(VariableBasisFromProto, UnspecifiedStatus) { + ModelStorage storage; + storage.AddVariable("x"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_values(BASIS_STATUS_UNSPECIFIED); + EXPECT_THAT(VariableBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("basis status not specified"))); +} + +TEST(LinearConstraintBasis, EmptyRoundTrip) { + ModelStorage model; + + const LinearConstraintMap linear_constraint_basis; + + SparseBasisStatusVector proto; + + // Test one way + EXPECT_THAT(LinearConstraintBasisFromProto(&model, proto), + IsOkAndHolds(linear_constraint_basis)); + // Test round trip + EXPECT_THAT( + LinearConstraintBasisFromProto( + &model, LinearConstraintBasisToProto(linear_constraint_basis)), + IsOkAndHolds(linear_constraint_basis)); +} + +TEST(LinearConstraintBasis, RoundTrip) { + ModelStorage model; + const LinearConstraintId c = model.AddLinearConstraint("c"); + const LinearConstraintId d = model.AddLinearConstraint("d"); + + const LinearConstraintMap linear_constraint_basis = { + {LinearConstraint(&model, c), BasisStatus::kAtLowerBound}, + {LinearConstraint(&model, d), BasisStatus::kFixedValue}}; + + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_LOWER_BOUND); + proto.add_values(BASIS_STATUS_FIXED_VALUE); + + // Test one way + EXPECT_THAT(LinearConstraintBasisFromProto(&model, proto), + IsOkAndHolds(linear_constraint_basis)); + // Test round trip + EXPECT_THAT( + LinearConstraintBasisFromProto( + &model, LinearConstraintBasisToProto(linear_constraint_basis)), + IsOkAndHolds(linear_constraint_basis)); +} + +TEST(LinearConstraintBasisFromProto, UnequalSize) { + ModelStorage storage; + storage.AddLinearConstraint("c"); + storage.AddLinearConstraint("d"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_LOWER_BOUND); + EXPECT_THAT(LinearConstraintBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(LinearConstraintBasisFromProto, LinearConstraintDoesNotExist) { + ModelStorage storage; + storage.AddLinearConstraint("c"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_ids(1); + proto.add_values(BASIS_STATUS_AT_LOWER_BOUND); + proto.add_values(BASIS_STATUS_AT_UPPER_BOUND); + EXPECT_THAT(LinearConstraintBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no linear constraint with id"))); +} + +TEST(LinearConstraintBasisFromProto, UnspecifiedStatus) { + ModelStorage storage; + storage.AddLinearConstraint("c"); + SparseBasisStatusVector proto; + proto.add_ids(0); + proto.add_values(BASIS_STATUS_UNSPECIFIED); + EXPECT_THAT(LinearConstraintBasisFromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("basis status not specified"))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/elemental/element_storage.h b/ortools/math_opt/elemental/element_storage.h index a796a115220..e4f011412aa 100644 --- a/ortools/math_opt/elemental/element_storage.h +++ b/ortools/math_opt/elemental/element_storage.h @@ -107,8 +107,7 @@ class SparseElementStorage { class ElementStorage { // Functions with deduced return must be defined before they are used. private: - // std::visit is very slow, see - // 5253596299885805568 + // std::visit is very slow, see yaqs/5253596299885805568. // // This function is static, taking Self as template argument, to avoid // having const and non-const versions. Post C++ 23, prefer: diff --git a/ortools/math_opt/elemental/elemental_test.cc b/ortools/math_opt/elemental/elemental_test.cc index 11744b4bf02..d973d14a566 100644 --- a/ortools/math_opt/elemental/elemental_test.cc +++ b/ortools/math_opt/elemental/elemental_test.cc @@ -78,8 +78,7 @@ TEST(ElementalTest, HaveNames) { //////////////////////////////////////////////////////////////////////////////// // Typed tests do not work with non-type template parameters, so we need to -// wrap the values with a type. See -// 8947385822189977600 +// wrap the values with a type. See yaqs/8947385822189977600. template class ElementTypeProxy { public: diff --git a/ortools/math_opt/python/BUILD.bazel b/ortools/math_opt/python/BUILD.bazel index 30889016385..877e801b48b 100644 --- a/ortools/math_opt/python/BUILD.bazel +++ b/ortools/math_opt/python/BUILD.bazel @@ -234,7 +234,7 @@ py_library( ":variables", "//ortools/math_opt:result_py_pb2", "//ortools/math_opt/solvers:osqp_py_pb2", - "//ortools/math_opt/solvers/gscip:gscip_proto_py_pb2", + "//ortools/math_opt/solvers/gscip:gscip_py_pb2", ], ) @@ -288,7 +288,7 @@ py_library( "//ortools/math_opt/solvers:gurobi_py_pb2", "//ortools/math_opt/solvers:highs_py_pb2", "//ortools/math_opt/solvers:osqp_py_pb2", - "//ortools/math_opt/solvers/gscip:gscip_proto_py_pb2", + "//ortools/math_opt/solvers/gscip:gscip_py_pb2", "//ortools/pdlp:solvers_py_pb2", "//ortools/sat:sat_parameters_py_pb2", ], @@ -308,7 +308,7 @@ py_test( "//ortools/math_opt/solvers:gurobi_py_pb2", "//ortools/math_opt/solvers:highs_py_pb2", "//ortools/math_opt/solvers:osqp_py_pb2", - "//ortools/math_opt/solvers/gscip:gscip_proto_py_pb2", + "//ortools/math_opt/solvers/gscip:gscip_py_pb2", "//ortools/pdlp:solvers_py_pb2", "//ortools/sat:sat_parameters_py_pb2", ], diff --git a/ortools/math_opt/python/bounded_expressions.py b/ortools/math_opt/python/bounded_expressions.py index 768c687ee28..d9d024f7bd5 100644 --- a/ortools/math_opt/python/bounded_expressions.py +++ b/ortools/math_opt/python/bounded_expressions.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/math_opt/python/linear_expression_test.py b/ortools/math_opt/python/linear_expression_test.py index 013a7dfda4d..d36be4c7692 100644 --- a/ortools/math_opt/python/linear_expression_test.py +++ b/ortools/math_opt/python/linear_expression_test.py @@ -2479,7 +2479,7 @@ def test_mult(self, lhs_type: str, rhs_type: str) -> None: # pylint: disable=pointless-statement # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): lhs * rhs @@ -2501,7 +2501,7 @@ def test_div(self, lhs_type: str, rhs_type: str) -> None: # pylint: disable=pointless-statement # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): lhs / rhs @@ -2566,7 +2566,7 @@ def test_add( # pylint: disable=pointless-statement # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): if linear_or_quadratic_first: linear_or_quadratic + other @@ -2602,7 +2602,7 @@ def test_sub( # pylint: disable=pointless-statement # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): if linear_or_quadratic_first: linear_or_quadratic - other @@ -2622,7 +2622,7 @@ def test_sub( class UnsupportedInitializationTest(parameterized.TestCase): def test_linear_sum_not_tuple(self): - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "object is not iterable"): variables.LinearSum(2.0) # pytype: enable=wrong-arg-types @@ -2630,19 +2630,19 @@ def test_linear_sum_not_tuple(self): def test_linear_sum_not_linear_in_tuple(self): mod = model.Model() x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "unsupported type in iterable argument"): variables.LinearSum((2.0, x * x)) # pytype: enable=wrong-arg-types def test_quadratic_sum_not_tuple(self): - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "object is not iterable"): variables.QuadraticSum(2.0) # pytype: enable=wrong-arg-types def test_quadratic_sum_not_linear_in_tuple(self): - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "unsupported type in iterable argument"): variables.QuadraticSum((2.0, "string")) # pytype: enable=wrong-arg-types @@ -2650,7 +2650,7 @@ def test_quadratic_sum_not_linear_in_tuple(self): def test_linear_product_not_scalar(self): mod = model.Model() x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for scalar argument in LinearProduct" ): @@ -2658,7 +2658,7 @@ def test_linear_product_not_scalar(self): # pytype: enable=wrong-arg-types def test_linear_product_not_linear(self): - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for linear argument in LinearProduct" ): @@ -2668,7 +2668,7 @@ def test_linear_product_not_linear(self): def test_quadratic_product_not_scalar(self): mod = model.Model() x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for scalar argument in QuadraticProduct" ): @@ -2676,7 +2676,7 @@ def test_quadratic_product_not_scalar(self): # pytype: enable=wrong-arg-types def test_quadratic_product_not_quadratic(self): - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for linear argument in QuadraticProduct" ): @@ -2686,7 +2686,7 @@ def test_quadratic_product_not_quadratic(self): def test_linear_linear_product_first_not_linear(self): mod = model.Model() x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for first_linear argument in LinearLinearProduct", @@ -2697,7 +2697,7 @@ def test_linear_linear_product_first_not_linear(self): def test_linear_linear_product_second_not_linear(self): mod = model.Model() x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( TypeError, "unsupported type for second_linear argument in LinearLinearProduct", diff --git a/ortools/math_opt/python/normalized_inequality_test.py b/ortools/math_opt/python/normalized_inequality_test.py index 7b8c99eeaa1..afaabe7ca1c 100644 --- a/ortools/math_opt/python/normalized_inequality_test.py +++ b/ortools/math_opt/python/normalized_inequality_test.py @@ -130,9 +130,7 @@ def test_expr_and_bounded_expr_error(self) -> None: def test_bounded_expr_bad_type_raise_error(self) -> None: with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): - normalized_inequality.as_normalized_linear_inequality( - "dogdog" - ) # pytype: disable=wrong-arg-types + normalized_inequality.as_normalized_linear_inequality("dogdog") def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: with self.assertRaisesRegex( @@ -141,9 +139,7 @@ def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: bounded = bounded_expressions.BoundedExpression( lower_bound=1.0, expression="dogdog", upper_bound=1.0 ) - normalized_inequality.as_normalized_linear_inequality( - bounded - ) # pytype: disable=wrong-arg-types + normalized_inequality.as_normalized_linear_inequality(bounded) def _quad_coef_dict( @@ -196,7 +192,7 @@ def test_init_expr_wrong_type_error(self) -> None: with self.assertRaises(TypeError): normalized_inequality.NormalizedQuadraticInequality( lb=1.0, expr="dog", ub=2.0 - ) # pytype: disable=wrong-arg-types + ) def test_as_normalized_inequality_from_parts(self) -> None: mod = model.Model() @@ -273,18 +269,14 @@ def test_expr_and_boundex_expr_error(self) -> None: def test_bounded_expr_bad_type_raise_error(self) -> None: with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): - normalized_inequality.as_normalized_quadratic_inequality( - "dogdog" - ) # pytype: disable=wrong-arg-types + normalized_inequality.as_normalized_quadratic_inequality("dogdog") def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: with self.assertRaisesRegex(TypeError, "bounded_expr.expression has bad type"): bounded = bounded_expressions.BoundedExpression( lower_bound=1.0, expression="dogdog", upper_bound=1.0 ) - normalized_inequality.as_normalized_quadratic_inequality( - bounded - ) # pytype: disable=wrong-arg-types + normalized_inequality.as_normalized_quadratic_inequality(bounded) if __name__ == "__main__": diff --git a/ortools/math_opt/python/result_test.py b/ortools/math_opt/python/result_test.py index af901b1a24e..6388c009e4b 100644 --- a/ortools/math_opt/python/result_test.py +++ b/ortools/math_opt/python/result_test.py @@ -293,7 +293,7 @@ def test_dual_solution_has_feasible(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.reduced_costs([y, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.reduced_costs(20) # pytype: disable=wrong-arg-types + res.reduced_costs(20) # Dual values. self.assertDictEqual(res.dual_values(), {c: 3.0, d: 4.0}) self.assertEqual(res.dual_values()[c], 3.0) @@ -305,7 +305,7 @@ def test_dual_solution_has_feasible(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.dual_values([d, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.dual_values(20) # pytype: disable=wrong-arg-types + res.dual_values(20) def test_dual_solution_no_feasible(self) -> None: mod = model.Model(name="test_model") @@ -391,7 +391,7 @@ def test_primal_ray_has_ray(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.ray_variable_values([y, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_variable_values(20) # pytype: disable=wrong-arg-types + res.ray_variable_values(20) def test_primal_ray_no_ray(self) -> None: res = result.SolveResult() @@ -426,7 +426,7 @@ def test_dual_ray_has_ray(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.ray_reduced_costs([y, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_reduced_costs(20) # pytype: disable=wrong-arg-types + res.ray_reduced_costs(20) # Dual values. self.assertDictEqual(res.ray_dual_values(), {c: 3.0, d: 4.0}) self.assertEqual(res.ray_dual_values()[c], 3.0) @@ -438,7 +438,7 @@ def test_dual_ray_has_ray(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.ray_dual_values([d, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_dual_values(20) # pytype: disable=wrong-arg-types + res.ray_dual_values(20) def test_dual_ray_no_ray(self) -> None: res = result.SolveResult() @@ -496,7 +496,7 @@ def test_basis_has_basis(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.variable_status([y, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.variable_status(20) # pytype: disable=wrong-arg-types + res.variable_status(20) # Constraint status self.assertDictEqual( res.constraint_status(), @@ -514,7 +514,7 @@ def test_basis_has_basis(self) -> None: with self.assertRaisesRegex(KeyError, ".*string"): res.constraint_status([d, "string"]) with self.assertRaisesRegex(TypeError, ".*int"): - res.constraint_status(20) # pytype: disable=wrong-arg-types + res.constraint_status(20) def test_basis_no_basis_in_best_solution(self) -> None: mod = model.Model(name="test_model") diff --git a/ortools/math_opt/samples/cpp/BUILD.bazel b/ortools/math_opt/samples/cpp/BUILD.bazel index 2577871d641..b3568e048a0 100644 --- a/ortools/math_opt/samples/cpp/BUILD.bazel +++ b/ortools/math_opt/samples/cpp/BUILD.bazel @@ -112,7 +112,6 @@ cc_binary( "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/solvers:glop_solver", "//ortools/math_opt/solvers:glpk_solver", - "//ortools/math_opt/solvers:gurobi_solver", "//ortools/math_opt/solvers:pdlp_solver", "//ortools/util:status_macros", "@abseil-cpp//absl/container:flat_hash_map", @@ -157,7 +156,6 @@ cc_binary( "//ortools/base", "//ortools/base:status_macros", "//ortools/math_opt/cpp:math_opt", - "//ortools/math_opt/solvers:gurobi_solver", "//ortools/math_opt/solvers:pdlp_solver", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/flags:flag", @@ -181,7 +179,6 @@ cc_binary( "//ortools/math_opt/solvers:glop_solver", "//ortools/math_opt/solvers:glpk_solver", "//ortools/math_opt/solvers:gscip_solver", - "//ortools/math_opt/solvers:gurobi_solver", "//ortools/math_opt/solvers:highs_solver", "//ortools/math_opt/solvers:pdlp_solver", ], @@ -196,7 +193,6 @@ cc_binary( "//ortools/base:status_macros", "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/solvers:gscip_solver", - "//ortools/math_opt/solvers:gurobi_solver", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/log:check", @@ -232,7 +228,6 @@ cc_binary( "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/solvers:cp_sat_solver", "//ortools/math_opt/solvers:gscip_solver", - "//ortools/math_opt/solvers:gurobi_solver", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/log", diff --git a/ortools/math_opt/solver_tests/invalid_input_tests.cc b/ortools/math_opt/solver_tests/invalid_input_tests.cc index 9c7907ef23a..3c4ce8d0739 100644 --- a/ortools/math_opt/solver_tests/invalid_input_tests.cc +++ b/ortools/math_opt/solver_tests/invalid_input_tests.cc @@ -23,7 +23,6 @@ #include "absl/strings/str_join.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/status_matchers.h" #include "ortools/math_opt/core/solver.h" #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/model.pb.h" diff --git a/ortools/math_opt/solvers/gscip/BUILD.bazel b/ortools/math_opt/solvers/gscip/BUILD.bazel index f211e12694e..8b3c6689e66 100644 --- a/ortools/math_opt/solvers/gscip/BUILD.bazel +++ b/ortools/math_opt/solvers/gscip/BUILD.bazel @@ -39,7 +39,7 @@ cc_proto_library( ) py_proto_library( - name = "gscip_proto_py_pb2", + name = "gscip_py_pb2", visibility = ["//visibility:public"], deps = [":gscip_proto"], ) From d7da95402540aecb7ff6e49e6561b1561beeb9ac Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 29 Aug 2025 16:27:53 +0200 Subject: [PATCH 017/491] math_opt: fixup --- ortools/math_opt/cpp/CMakeLists.txt | 2 +- ortools/math_opt/cpp/key_types.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ortools/math_opt/cpp/CMakeLists.txt b/ortools/math_opt/cpp/CMakeLists.txt index a0f57d18086..ba86195715f 100644 --- a/ortools/math_opt/cpp/CMakeLists.txt +++ b/ortools/math_opt/cpp/CMakeLists.txt @@ -16,7 +16,7 @@ add_library(${NAME} OBJECT) file(GLOB _SRCS "*.h" "*.cc") list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc") -list(FILTER _SRCS EXCLUDE REGEX "/matchers\\.") +list(FILTER _SRCS EXCLUDE REGEX "/matchers\.") target_sources(${NAME} PRIVATE ${_SRCS}) set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/ortools/math_opt/cpp/key_types.h b/ortools/math_opt/cpp/key_types.h index a1c304bfcf0..df72faad6af 100644 --- a/ortools/math_opt/cpp/key_types.h +++ b/ortools/math_opt/cpp/key_types.h @@ -149,17 +149,17 @@ std::vector Values(const Map& map, namespace internal { // The CHECK message to use when a KeyType::storage() is nullptr. -inline const std::string kKeyHasNullModelStorage = +inline constexpr const char kKeyHasNullModelStorage[] = "The input key has null .storage()."; // The CHECK message to use when two KeyType with different storage() are used // in the same collection. -inline const std::string kObjectsFromOtherModelStorage = +inline constexpr const char kObjectsFromOtherModelStorage[] = "The input objects belongs to another model."; // The Status message to use when an input KeyType is from an unexpected // storage(). -inline constexpr absl::string_view kInputFromInvalidModelStorage = +inline constexpr const char kInputFromInvalidModelStorage[] = "the input does not belong to the same model"; // Returns a failure when the input pointer is not nullptr and points to a From da07438f01469c99b16a2366720ce5670dcec157 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sun, 31 Aug 2025 14:07:02 +0200 Subject: [PATCH 018/491] misc --- ortools/util/BUILD.bazel | 1 + ortools/util/proto_tools.cc | 3 ++ ortools/util/proto_tools.h | 54 +++++++++++++++++++++++++++++-- ortools/util/sigint.cc | 5 +++ ortools/util/sigint.h | 6 ++++ ortools/util/solve_interrupter.cc | 2 -- 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index fca66bd277a..13479741eca 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -232,6 +232,7 @@ cc_library( hdrs = ["sigint.h"], deps = [ "//ortools/base", + "//ortools/port:os", ], ) diff --git a/ortools/util/proto_tools.cc b/ortools/util/proto_tools.cc index a21f9ffe1ae..510eb5a4b40 100644 --- a/ortools/util/proto_tools.cc +++ b/ortools/util/proto_tools.cc @@ -13,7 +13,10 @@ #include "ortools/util/proto_tools.h" +#include +#include #include +#include #include "absl/strings/str_cat.h" #include "google/protobuf/descriptor.h" diff --git a/ortools/util/proto_tools.h b/ortools/util/proto_tools.h index e6305c94609..8555bf5ee1b 100644 --- a/ortools/util/proto_tools.h +++ b/ortools/util/proto_tools.h @@ -42,6 +42,55 @@ absl::StatusOr SafeProtoConstDownCast( std::string FullProtocolMessageAsString( const google::protobuf::Message& message, int indent_level); +// This recursive function returns all the proto fields that are set(*) in a +// proto instance, along with how many times they appeared. A repeated field is +// only counted once as itself, regardless of its (non-zero) size, but then the +// nested child fields of a repeated message are counted once per instance. +// EXAMPLE: with these .proto message definitions: +// message YY { repeated int z; } +// message XX { int a; int b; repeated int c; repeated YY d; } +// and this instance of `XX`: +// XX x = Parse("a: 10 c: [ 11, 12] d: {z: [13, 14]} d: {z: [15]}""); +// We'd expect ExploreAndCountAllProtoPathsInInstance() to yield the map: +// {"a": 1, "c": 1, "d": 1, "d.z": 3} +// +// (*) The term 'set' has the usual semantic, which varies depending on proto +// version. Extensions and unknown fields are ignored by this function. +void ExploreAndCountAllProtoPathsInInstance( + const google::protobuf::Message& message, + // Output. Must be non-nullptr. Entries are added to the map, i.e., the map + // is not cleared. That allows cumulative use of this function across + // several proto instances to build a global count of proto paths. + absl::flat_hash_map* proto_path_counts); + +// This recursive function lists all the fields of a given proto *type* +// (not a proto instance), up to the given depth of nested sub-messages, and +// inserts their proto paths into `proto_paths`. +// +// SIMPLE EXAMPLE: with this .proto message definition: +// message YY { repeated int z; } +// message XX { int a; int b; repeated int c; repeated YY d; }, +// ExploreAndInsertAllProtoPathsInType( +// XX().GetDescriptor(), {}, {}, /*max_depth=*/3) +// Returns: {"a", "b", "c", "d", "d.z"}. +// See the unit test for more complex examples. +absl::flat_hash_set ExploreAndInsertAllProtoPathsInType( + const google::protobuf::Descriptor* descriptor, + // ADVANCED USAGE: Pruning the proto tree exploration with two possible + // mechanisms: 1) based on proto path, or 2) based on proto type. + // 1) List of proto paths to skip: those fields will not be output nor + // explored, meaning that their descendants is also skipped. + const absl::flat_hash_set& skip_these_proto_paths, + // 2) Maps a field's full *type* name (as in Descriptor::full_name(), e.g., + // "operations_research.MyProto") to the subset of its child field names + // that may be explored. + const absl::flat_hash_map>& + proto_type_names_to_field_name_allowlist, + // The maximum depth, when diving into sub-messages. Note that some protos + // may have infinite potential depth, e.g. message X { X x; }, so we must + // limit the recursion depth. + int max_depth); + // ============================================================================= // Implementation of function templates. @@ -51,8 +100,7 @@ absl::StatusOr SafeProtoDownCast(google::protobuf::Message* proto) { Proto::default_instance().GetDescriptor(); const google::protobuf::Descriptor* actual_descriptor = proto->GetDescriptor(); - if (actual_descriptor == expected_descriptor) - return reinterpret_cast(proto); + if (actual_descriptor == expected_descriptor) return down_cast(proto); return absl::InvalidArgumentError(absl::StrFormat( "Expected message type '%s', but got type '%s'", expected_descriptor->full_name(), actual_descriptor->full_name())); @@ -66,7 +114,7 @@ absl::StatusOr SafeProtoConstDownCast( const google::protobuf::Descriptor* actual_descriptor = proto->GetDescriptor(); if (actual_descriptor == expected_descriptor) { - return reinterpret_cast(proto); + return down_cast(proto); } return absl::InvalidArgumentError(absl::StrFormat( "Expected message type '%s', but got type '%s'", diff --git a/ortools/util/sigint.cc b/ortools/util/sigint.cc index bd4f40cfac7..bc27d669d97 100644 --- a/ortools/util/sigint.cc +++ b/ortools/util/sigint.cc @@ -18,6 +18,9 @@ #include #include "ortools/base/logging.h" +#include "ortools/port/os.h" + +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS namespace operations_research { @@ -67,3 +70,5 @@ SigtermHandler::~SigtermHandler() { thread_local std::function SigtermHandler::handler_; } // namespace operations_research + +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS diff --git a/ortools/util/sigint.h b/ortools/util/sigint.h index 1d9fcd1b814..11e78181ef9 100644 --- a/ortools/util/sigint.h +++ b/ortools/util/sigint.h @@ -17,6 +17,10 @@ #include #include +#include "ortools/port/os.h" + +#if ORTOOLS_TARGET_OS_SUPPORTS_THREADS + namespace operations_research { class SigintHandler { @@ -51,4 +55,6 @@ class SigtermHandler { } // namespace operations_research +#endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS + #endif // OR_TOOLS_UTIL_SIGINT_H_ diff --git a/ortools/util/solve_interrupter.cc b/ortools/util/solve_interrupter.cc index e494879385a..d8c44c0ac90 100644 --- a/ortools/util/solve_interrupter.cc +++ b/ortools/util/solve_interrupter.cc @@ -15,14 +15,12 @@ #include #include -#include #include #include #include "absl/synchronization/mutex.h" #include "ortools/base/linked_hash_map.h" #include "ortools/base/logging.h" -#include "ortools/base/strong_int.h" namespace operations_research { From 0e7b4c612c84b8080667cb9ed1814b5a42b40f9c Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sun, 31 Aug 2025 14:07:56 +0200 Subject: [PATCH 019/491] [CP-SAT] support enforcement literals for most constraints; support general linear expressions everywhere 1-var affine expressions were expected --- ortools/sat/BUILD.bazel | 134 +++- ortools/sat/all_different.cc | 56 +- ortools/sat/all_different.h | 26 +- ortools/sat/all_different_test.cc | 34 + ortools/sat/boolean_problem.cc | 5 +- ortools/sat/circuit.cc | 202 +++--- ortools/sat/circuit.h | 16 +- ortools/sat/circuit_test.cc | 14 +- ortools/sat/colab/cp_sat.ipynb | 16 +- ortools/sat/colab/flags.py | 1 + ortools/sat/colab/visualization.py | 1 + ortools/sat/cp_constraints.cc | 128 ++-- ortools/sat/cp_constraints.h | 29 +- ortools/sat/cp_constraints_test.cc | 30 +- ortools/sat/cp_model.proto | 26 +- ortools/sat/cp_model_checker.cc | 80 +-- ortools/sat/cp_model_checker_test.cc | 27 - ortools/sat/cp_model_copy.cc | 173 +++++- ortools/sat/cp_model_copy.h | 18 + ortools/sat/cp_model_copy_test.cc | 79 +++ ortools/sat/cp_model_expand.cc | 572 +++++++++++++---- ortools/sat/cp_model_expand_test.cc | 588 +++++++++++++++++- ortools/sat/cp_model_loader.cc | 34 +- ortools/sat/cp_model_presolve.cc | 37 +- ortools/sat/cp_model_solver.cc | 10 +- ortools/sat/cp_model_solver_helpers.cc | 22 +- ortools/sat/cp_model_solver_test.cc | 295 ++++++++- ortools/sat/cp_model_symmetries.cc | 16 +- ortools/sat/cp_model_utils.cc | 18 + ortools/sat/cp_model_utils.h | 20 + ortools/sat/cumulative.cc | 4 +- ortools/sat/disjunctive.cc | 57 +- ortools/sat/disjunctive.h | 2 +- ortools/sat/fuzz_testdata/AtMostOneModel | 2 +- ortools/sat/fuzz_testdata/AutomatonModel | 2 +- ortools/sat/fuzz_testdata/BadHintWithCore | 2 +- ortools/sat/fuzz_testdata/CircuitModel | 2 +- .../DualConnectedComponentsModel | 2 +- ortools/sat/fuzz_testdata/ElementModel | 2 +- .../sat/fuzz_testdata/EnumerateAllSolutions | 2 +- .../fuzz_testdata/EnumerateAllSolutionsBis | 2 +- .../EnumerateAllSolutionsOfEmptyModel | 2 +- ortools/sat/fuzz_testdata/ExactlyOneModel | 2 +- ortools/sat/fuzz_testdata/FixedBoxes | 2 +- ortools/sat/fuzz_testdata/HintWithCore | 2 +- ortools/sat/fuzz_testdata/IntProdModel | 2 +- ortools/sat/fuzz_testdata/InverseModel | 2 +- ortools/sat/fuzz_testdata/LinMaxModel | 2 +- .../sat/fuzz_testdata/MultipleCumulativesA | 2 +- .../sat/fuzz_testdata/MultipleCumulativesB | 2 +- .../fuzz_testdata/MultipleEnforcementLiteral | 2 +- .../sat/fuzz_testdata/NoOverlap2DOptimization | 2 +- .../fuzz_testdata/NonInstantiatedVariables | 2 +- .../fuzz_testdata/ObjectiveDomainLowerBound | 2 +- ortools/sat/fuzz_testdata/PureSatProblem | 2 +- .../sat/fuzz_testdata/PureSatProblemWithLimit | 2 +- ortools/sat/fuzz_testdata/ReservoirModel | 2 +- ortools/sat/fuzz_testdata/RoutingModel | 2 +- ortools/sat/fuzz_testdata/SimpleCumulative | 2 +- ortools/sat/fuzz_testdata/SimpleInterval | 2 +- .../SimpleLinearExampleWithMaximize | 2 +- .../SimpleOptionalIntervalFeasible | 2 +- .../SimpleOptionalIntervalInfeasible | 2 +- .../SmallDualConnectedComponentsModel | 2 +- .../sat/fuzz_testdata/SolutionHintBasicTest | 2 +- .../fuzz_testdata/SolutionHintEnumerateTest | 2 +- .../fuzz_testdata/SolutionHintObjectiveTest | 2 +- .../SolutionHintOptimalObjectiveTest | 2 +- ...lutionsAreCorrectlyPostsolvedInTheObserver | 2 +- ortools/sat/fuzz_testdata/TableProblem | 2 +- ortools/sat/fuzz_testdata/TightenedDomains | 2 +- .../TightenedDomainsIfInfeasible | 2 +- .../sat/fuzz_testdata/TrivialModelWithCore | 2 +- ortools/sat/fuzz_testdata/UnsatProblem | 2 +- ortools/sat/integer.cc | 12 + ortools/sat/integer.h | 4 + ortools/sat/integer_expr.cc | 120 ++-- ortools/sat/integer_expr.h | 44 +- ortools/sat/integer_expr_test.cc | 140 ++++- ortools/sat/linear_propagation.cc | 2 +- ortools/sat/linear_relaxation.cc | 141 +++-- ortools/sat/linear_relaxation.h | 13 +- ortools/sat/linear_relaxation_test.cc | 87 ++- ortools/sat/no_overlap_2d_helper.cc | 40 +- ortools/sat/parameters_validation.cc | 5 - ortools/sat/precedences.cc | 213 +++++++ ortools/sat/precedences.h | 22 +- ortools/sat/precedences_test.cc | 12 +- ortools/sat/python/cp_model.py | 1 + ortools/sat/python/wrappers.cc | 6 +- ortools/sat/routing_cuts.cc | 4 +- ortools/sat/routing_cuts_test.cc | 18 +- ortools/sat/samples/code_samples.bzl | 2 +- ortools/sat/sat_parameters.proto | 22 +- ortools/sat/scheduling_helpers.cc | 52 ++ ortools/sat/scheduling_helpers.h | 7 + ortools/sat/solution_crush.cc | 7 + ortools/sat/synchronization.h | 19 +- ortools/sat/table_test.cc | 17 - ortools/sat/timetable.cc | 113 ++-- ortools/sat/timetable.h | 23 +- ortools/sat/timetable_test.cc | 29 +- ortools/sat/util.cc | 40 +- ortools/sat/util.h | 7 +- ortools/sat/util_test.cc | 67 +- 105 files changed, 3209 insertions(+), 932 deletions(-) diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index 6bdd9ffd2fe..1c4c778779f 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -28,6 +28,7 @@ cc_library( name = "cp_model", srcs = ["cp_model.cc"], hdrs = ["cp_model.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_solver", @@ -55,9 +56,9 @@ cc_test( ":cp_model_solver", ":model", ":sat_parameters_cc_proto", - "//ortools/base", "//ortools/base:container_logging", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:parse_test_proto", "//ortools/util:sorted_interval_list", "@abseil-cpp//absl/types:span", @@ -67,6 +68,7 @@ cc_test( cc_library( name = "model", hdrs = ["model.h"], + visibility = ["//visibility:public"], deps = [ "//ortools/base", "//ortools/base:typeid", @@ -89,20 +91,24 @@ cc_test( proto_library( name = "sat_parameters_proto", srcs = ["sat_parameters.proto"], + visibility = ["//visibility:public"], ) cc_proto_library( name = "sat_parameters_cc_proto", + visibility = ["//visibility:public"], deps = [":sat_parameters_proto"], ) py_proto_library( name = "sat_parameters_py_pb2", + visibility = ["//visibility:public"], deps = [":sat_parameters_proto"], ) java_proto_library( name = "sat_parameters_java_proto", + visibility = ["//visibility:public"], deps = [":sat_parameters_proto"], ) @@ -110,11 +116,13 @@ go_proto_library( name = "sat_parameters_go_proto", importpath = "github.com/google/or-tools/ortools/sat/proto/satparameters", protos = [":sat_parameters_proto"], + visibility = ["//visibility:public"], ) proto_library( name = "cp_model_proto", srcs = ["cp_model.proto"], + visibility = ["//visibility:public"], ) cc_library( @@ -170,9 +178,10 @@ cc_library( ":cp_model_utils", ":presolve_context", ":sat_parameters_cc_proto", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:protobuf_util", "//ortools/util:sorted_interval_list", + "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", @@ -236,7 +245,7 @@ cc_library( ":sat_base", ":sat_solver", ":synchronization", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:stl_util", "//ortools/base:strong_vector", "//ortools/util:bitset", @@ -252,16 +261,19 @@ cc_library( cc_proto_library( name = "cp_model_cc_proto", + visibility = ["//visibility:public"], deps = [":cp_model_proto"], ) py_proto_library( name = "cp_model_py_pb2", + visibility = ["//visibility:public"], deps = [":cp_model_proto"], ) java_proto_library( name = "cp_model_java_proto", + visibility = ["//visibility:public"], deps = [":cp_model_proto"], ) @@ -269,12 +281,14 @@ go_proto_library( name = "cp_model_go_proto", importpath = "github.com/google/or-tools/ortools/sat/proto/cpmodel", protos = [":cp_model_proto"], + visibility = ["//visibility:public"], ) cc_library( name = "cp_model_utils", srcs = ["cp_model_utils.cc"], hdrs = ["cp_model_utils.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":sat_base", @@ -314,6 +328,7 @@ cc_library( name = "synchronization", srcs = ["synchronization.cc"], hdrs = ["synchronization.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_utils", @@ -380,6 +395,7 @@ cc_library( name = "cp_model_checker", srcs = ["cp_model_checker.cc"], hdrs = ["cp_model_checker.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_utils", @@ -420,6 +436,7 @@ cc_library( name = "constraint_violation", srcs = ["constraint_violation.cc"], hdrs = ["constraint_violation.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_utils", @@ -450,9 +467,9 @@ cc_test( ":constraint_violation", ":cp_model_cc_proto", ":cp_model_utils", - "//ortools/base", "//ortools/base:dump_vars", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:parse_test_proto", "//ortools/util:sorted_interval_list", "//ortools/util:time_limit", @@ -464,6 +481,7 @@ cc_library( name = "feasibility_jump", srcs = ["feasibility_jump.cc"], hdrs = ["feasibility_jump.h"], + visibility = ["//visibility:public"], deps = [ ":combine_solutions", ":constraint_violation", @@ -510,6 +528,7 @@ cc_library( name = "linear_model", srcs = ["linear_model.cc"], hdrs = ["linear_model.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_utils", @@ -538,6 +557,7 @@ cc_library( name = "parameters_validation", srcs = ["parameters_validation.cc"], hdrs = ["parameters_validation.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_search", ":sat_parameters_cc_proto", @@ -609,6 +629,7 @@ cc_library( name = "cp_model_solver_helpers", srcs = ["cp_model_solver_helpers.cc"], hdrs = ["cp_model_solver_helpers.h"], + visibility = ["//visibility:public"], deps = [ ":circuit", ":clause", @@ -660,6 +681,7 @@ cc_library( "//ortools/base", "//ortools/base:status_macros", "//ortools/base:strong_vector", + "//ortools/base:threadpool", "//ortools/base:timer", "//ortools/base:types", "//ortools/graph:connected_components", @@ -697,6 +719,7 @@ cc_library( name = "shaving_solver", srcs = ["shaving_solver.cc"], hdrs = ["shaving_solver.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_copy", @@ -729,6 +752,7 @@ cc_library( name = "cp_model_solver", srcs = ["cp_model_solver.cc"], hdrs = ["cp_model_solver.h"], + visibility = ["//visibility:public"], deps = [ ":circuit", ":clause", @@ -839,6 +863,7 @@ cc_test( "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/port:os", "//ortools/util:logging", + "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log", "@abseil-cpp//absl/strings", ], @@ -847,6 +872,7 @@ cc_test( cc_library( name = "cp_model_mapping", hdrs = ["cp_model_mapping.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_utils", @@ -866,6 +892,7 @@ cc_library( name = "cp_model_loader", srcs = ["cp_model_loader.cc"], hdrs = ["cp_model_loader.h"], + visibility = ["//visibility:public"], deps = [ ":2d_distances_propagator", ":all_different", @@ -937,6 +964,11 @@ proto_library( cc_proto_library( name = "boolean_problem_cc_proto", + visibility = [ + "//ops/netarch/wand/transport:__subpackages__", + "//ortools:__subpackages__", + "//ortools/bop:__pkg__", + ], deps = [":boolean_problem_proto"], ) @@ -1007,7 +1039,7 @@ cc_library( ":sat_solver", ":solution_crush", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/port:proto_utils", "//ortools/util:affine_relation", "//ortools/util:bitset", @@ -1216,9 +1248,9 @@ cc_test( ":cp_model_solver", ":cp_model_utils", ":sat_parameters_cc_proto", - "//ortools/base", "//ortools/base:file", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:path", "//ortools/util:sorted_interval_list", "@abseil-cpp//absl/flags:flag", @@ -1275,7 +1307,7 @@ cc_library( ":sat_parameters_cc_proto", ":solution_crush", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:stl_util", "//ortools/port:proto_utils", "//ortools/util:logging", @@ -1310,6 +1342,7 @@ cc_test( "//ortools/base:container_logging", "//ortools/base:gmock_main", "//ortools/base:parse_test_proto", + "//ortools/base:parse_text_proto", "//ortools/util:sorted_interval_list", "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/strings", @@ -1397,7 +1430,7 @@ cc_library( ":model", ":sat_decision", ":sat_parameters_cc_proto", - "//ortools/base", + "//ortools/base:logging", "//ortools/port:proto_utils", "//ortools/util:bitset", "//ortools/util:running_stat", @@ -1744,7 +1777,8 @@ cc_library( ":solution_crush", ":util", "//ortools/algorithms:dynamic_partition", - "//ortools/base", + "//ortools/base:hash", + "//ortools/base:logging", "//ortools/base:mathutil", "//ortools/base:stl_util", "//ortools/base:strong_vector", @@ -1782,6 +1816,10 @@ cc_library( name = "integer_base", srcs = ["integer_base.cc"], hdrs = ["integer_base.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":sat_base", "//ortools/base", @@ -1812,6 +1850,10 @@ cc_library( name = "integer", srcs = ["integer.cc"], hdrs = ["integer.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":integer_base", ":model", @@ -1865,7 +1907,7 @@ cc_library( ":scheduling_helpers", ":synchronization", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/util:strong_integers", "//ortools/util:time_limit", "@abseil-cpp//absl/container:flat_hash_set", @@ -1891,8 +1933,8 @@ cc_test( ":integer_search", ":model", ":sat_parameters_cc_proto", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/util:random_engine", "@abseil-cpp//absl/container:flat_hash_set", ], @@ -1917,7 +1959,7 @@ cc_library( ":sat_solver", ":synchronization", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:strong_vector", "//ortools/glop:variables_info", "//ortools/util:strong_integers", @@ -1958,7 +2000,7 @@ cc_library( ":sat_base", ":sat_parameters_cc_proto", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:strong_vector", "//ortools/util:strong_integers", "@abseil-cpp//absl/log:check", @@ -2037,7 +2079,7 @@ cc_library( ":precedences", ":sat_base", ":sat_solver", - "//ortools/base", + "//ortools/base:logging", "//ortools/util:bitset", "//ortools/util:sort", "//ortools/util:strong_integers", @@ -2089,6 +2131,10 @@ cc_library( name = "precedences", srcs = ["precedences.cc"], hdrs = ["precedences.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":clause", ":cp_constraints", @@ -2154,8 +2200,8 @@ cc_test( ":model", ":sat_base", ":sat_solver", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/util:sorted_interval_list", "//ortools/util:strong_integers", "@abseil-cpp//absl/log:check", @@ -2168,6 +2214,10 @@ cc_library( name = "integer_expr", srcs = ["integer_expr.cc"], hdrs = ["integer_expr.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":cp_constraints", ":integer", @@ -2209,9 +2259,10 @@ cc_test( ":sat_base", ":sat_parameters_cc_proto", ":sat_solver", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:parse_test_proto", + "//ortools/base:parse_text_proto", "//ortools/port:proto_utils", "//ortools/util:saturated_arithmetic", "//ortools/util:sorted_interval_list", @@ -2309,6 +2360,7 @@ cc_test( ":integer_base", ":integer_search", ":model", + ":sat_base", ":sat_solver", "//ortools/base", "//ortools/base:gmock_main", @@ -2331,6 +2383,7 @@ cc_library( ":precedences", ":sat_base", ":sat_parameters_cc_proto", + ":scheduling_helpers", ":synchronization", ":timetable", ":util", @@ -2378,9 +2431,9 @@ cc_library( srcs = ["timetable.cc"], hdrs = ["timetable.h"], deps = [ + ":cp_constraints", ":integer", ":integer_base", - ":intervals", ":model", ":sat_base", ":scheduling_helpers", @@ -2464,7 +2517,6 @@ cc_test( name = "cumulative_test", size = "large", srcs = ["cumulative_test.cc"], - shard_count = 32, deps = [ ":cumulative", ":integer", @@ -2605,7 +2657,7 @@ cc_library( ":scheduling_cuts", ":scheduling_helpers", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:mathutil", "//ortools/base:stl_util", "//ortools/base:strong_vector", @@ -2649,6 +2701,10 @@ cc_library( name = "linear_constraint", srcs = ["linear_constraint.cc"], hdrs = ["linear_constraint.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":integer", ":integer_base", @@ -2768,8 +2824,8 @@ cc_library( ":sat_parameters_cc_proto", ":synchronization", ":util", - "//ortools/base", "//ortools/base:hash", + "//ortools/base:logging", "//ortools/base:strong_vector", "//ortools/glop:variables_info", "//ortools/lp_data:base", @@ -2868,10 +2924,12 @@ cc_test( proto_library( name = "routes_support_graph_proto", srcs = ["routes_support_graph.proto"], + visibility = ["//visibility:public"], ) cc_proto_library( name = "routes_support_graph_cc_proto", + visibility = ["//visibility:public"], deps = [":routes_support_graph_proto"], ) @@ -3041,7 +3099,7 @@ cc_library( deps = [ ":integer_base", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/lp_data:base", "//ortools/util:strong_integers", "@abseil-cpp//absl/log:check", @@ -3201,6 +3259,7 @@ cc_library( ":sat_base", ":sat_parameters_cc_proto", "//ortools/base", + "//ortools/base:logging", "//ortools/base:mathutil", "//ortools/base:stl_util", "//ortools/util:random_engine", @@ -3236,8 +3295,8 @@ cc_test( ":sat_base", ":sat_parameters_cc_proto", ":util", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:mathutil", "//ortools/base:stl_util", "//ortools/util:random_engine", @@ -3303,9 +3362,9 @@ cc_test( ":sat_parameters_cc_proto", ":sat_solver", ":table", - "//ortools/base", "//ortools/base:container_logging", "//ortools/base:gmock_main", + "//ortools/base:logging", "//ortools/base:parse_test_proto", "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/types:span", @@ -3316,6 +3375,10 @@ cc_library( name = "cp_constraints", srcs = ["cp_constraints.cc"], hdrs = ["cp_constraints.h"], + visibility = [ + "//learning/brain/experimental/telamon:__pkg__", + "//ortools:__subpackages__", + ], deps = [ ":integer", ":integer_base", @@ -3345,8 +3408,8 @@ cc_test( ":precedences", ":sat_base", ":sat_solver", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "@abseil-cpp//absl/types:span", ], ) @@ -3359,7 +3422,7 @@ cc_library( ":integer_base", ":scheduling_helpers", ":util", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:stl_util", "//ortools/base:strong_vector", "//ortools/graph:connected_components", @@ -3575,8 +3638,8 @@ cc_test( ":diffn_util", ":integer_base", ":util", - "//ortools/base", "//ortools/base:gmock", + "//ortools/base:logging", "//ortools/graph:connected_components", "//ortools/graph:strongly_connected_components", "//ortools/util:saturated_arithmetic", @@ -3652,8 +3715,8 @@ cc_test( ":sat_base", ":sat_parameters_cc_proto", ":sat_solver", - "//ortools/base", "//ortools/base:gmock_main", + "//ortools/base:logging", "@abseil-cpp//absl/strings", ], ) @@ -3665,6 +3728,7 @@ cc_library( deps = [ ":all_different", ":clause", + ":cp_constraints", ":integer", ":model", ":sat_base", @@ -3678,7 +3742,6 @@ cc_library( "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/meta:type_traits", "@abseil-cpp//absl/types:span", ], ) @@ -3740,6 +3803,7 @@ cc_library( name = "cp_model_lns", srcs = ["cp_model_lns.cc"], hdrs = ["cp_model_lns.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_cc_proto", ":cp_model_copy", @@ -3818,6 +3882,7 @@ cc_library( name = "feasibility_pump", srcs = ["feasibility_pump.cc"], hdrs = ["feasibility_pump.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_mapping", ":integer", @@ -3871,6 +3936,7 @@ cc_library( name = "rins", srcs = ["rins.cc"], hdrs = ["rins.h"], + visibility = ["//visibility:public"], deps = [ ":cp_model_mapping", ":integer_base", @@ -3907,6 +3973,7 @@ cc_library( name = "subsolver", srcs = ["subsolver.cc"], hdrs = ["subsolver.h"], + visibility = ["//visibility:public"], deps = [ ":util", "//ortools/base", @@ -3961,8 +4028,8 @@ cc_library( hdrs = ["drat_checker.h"], deps = [ ":sat_base", - "//ortools/base", "//ortools/base:hash", + "//ortools/base:logging", "//ortools/base:strong_vector", "//ortools/util:strong_integers", "//ortools/util:time_limit", @@ -4009,7 +4076,7 @@ cc_library( deps = [ ":cp_model_cc_proto", ":cp_model_utils", - "//ortools/base", + "//ortools/base:logging", "//ortools/base:stl_util", "//ortools/util:filelineiter", "@abseil-cpp//absl/base:core_headers", @@ -4084,6 +4151,7 @@ cc_binary( cc_library( name = "sat_cnf_reader", hdrs = ["sat_cnf_reader.h"], + visibility = ["//visibility:public"], deps = [ ":boolean_problem_cc_proto", ":cp_model_cc_proto", @@ -4136,8 +4204,8 @@ cc_library( "//ortools/algorithms:binary_search", "//ortools/algorithms:find_graph_symmetries", "//ortools/algorithms:sparse_permutation", - "//ortools/base", "//ortools/base:hash", + "//ortools/base:logging", "//ortools/graph", "//ortools/util:affine_relation", "//ortools/util:logging", @@ -4180,6 +4248,10 @@ cc_library( name = "swig_helper", srcs = ["swig_helper.cc"], hdrs = ["swig_helper.h"], + visibility = [ + "//ortools/sat/java:__pkg__", + "//ortools/sat/python:__pkg__", + ], deps = [ ":cp_model_cc_proto", ":cp_model_checker", diff --git a/ortools/sat/all_different.cc b/ortools/sat/all_different.cc index 6b47c5d96fc..f47c021b875 100644 --- a/ortools/sat/all_different.cc +++ b/ortools/sat/all_different.cc @@ -14,9 +14,7 @@ #include "ortools/sat/all_different.h" #include -#include #include -#include #include #include @@ -27,6 +25,7 @@ #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/graph/strongly_connected_components.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" @@ -74,14 +73,13 @@ std::function AllDifferentBinary( } std::function AllDifferentOnBounds( + absl::Span enforcement_literals, absl::Span expressions) { return [=, expressions = std::vector( expressions.begin(), expressions.end())](Model* model) { if (expressions.empty()) return; - auto* constraint = new AllDifferentBoundsPropagator( - expressions, model->GetOrCreate()); - constraint->RegisterWith(model->GetOrCreate()); - model->TakeOwnership(constraint); + model->TakeOwnership(new AllDifferentBoundsPropagator(enforcement_literals, + expressions, model)); }; } @@ -95,10 +93,8 @@ std::function AllDifferentOnBounds( for (const IntegerVariable var : vars) { expressions.push_back(AffineExpression(var)); } - auto* constraint = new AllDifferentBoundsPropagator( - expressions, model->GetOrCreate()); - constraint->RegisterWith(model->GetOrCreate()); - model->TakeOwnership(constraint); + model->TakeOwnership(new AllDifferentBoundsPropagator( + /*enforcement_literals=*/{}, expressions, model)); }; } @@ -417,8 +413,10 @@ bool AllDifferentConstraint::Propagate() { } AllDifferentBoundsPropagator::AllDifferentBoundsPropagator( - absl::Span expressions, IntegerTrail* integer_trail) - : integer_trail_(integer_trail) { + absl::Span enforcement_literals, + absl::Span expressions, Model* model) + : integer_trail_(*model->GetOrCreate()), + enforcement_propagator_(*model->GetOrCreate()) { CHECK(!expressions.empty()); // We need +2 for sentinels. @@ -432,9 +430,19 @@ AllDifferentBoundsPropagator::AllDifferentBoundsPropagator( bounds_.push_back({expressions[i]}); negated_bounds_.push_back({expressions[i].Negated()}); } + + GenericLiteralWatcher* watcher = model->GetOrCreate(); + enforcement_id_ = enforcement_propagator_.Register( + enforcement_literals, watcher, RegisterWith(watcher)); } bool AllDifferentBoundsPropagator::Propagate() { + const EnforcementStatus status = + enforcement_propagator_.Status(enforcement_id_); + if (status != EnforcementStatus::IS_ENFORCED && + status != EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { + return true; + } if (!PropagateLowerBounds()) return false; // Note that it is not required to swap back bounds_ and negated_bounds_. @@ -478,8 +486,8 @@ int AllDifferentBoundsPropagator::FindStartIndexAndCompressPath(int index) { bool AllDifferentBoundsPropagator::PropagateLowerBounds() { // Start by filling the cached bounds and sorting by increasing lb. for (CachedBounds& entry : bounds_) { - entry.lb = integer_trail_->LowerBound(entry.expr); - entry.ub = integer_trail_->UpperBound(entry.expr); + entry.lb = integer_trail_.LowerBound(entry.expr); + entry.ub = integer_trail_.UpperBound(entry.expr); } IncrementalSort(bounds_.begin(), bounds_.end(), [](CachedBounds a, CachedBounds b) { return a.lb < b.lb; }); @@ -561,9 +569,17 @@ bool AllDifferentBoundsPropagator::PropagateLowerBoundsInternal( const IntegerValue he = hall_ends_[hall_index]; FillHallReason(hs, he); integer_reason_.push_back(expr.GreaterOrEqual(hs)); - if (!integer_trail_->SafeEnqueue(expr.GreaterOrEqual(he + 1), - integer_reason_)) { - return false; + if (enforcement_propagator_.Status(enforcement_id_) == + EnforcementStatus::IS_ENFORCED) { + if (!enforcement_propagator_.SafeEnqueue(enforcement_id_, + expr.GreaterOrEqual(he + 1), + integer_reason_)) { + return false; + } + } else if (he >= entry.ub) { + integer_reason_.push_back(expr.LowerOrEqual(entry.ub)); + return enforcement_propagator_.PropagateWhenFalse( + enforcement_id_, /*literal_reason=*/{}, integer_reason_); } } } @@ -607,7 +623,7 @@ bool AllDifferentBoundsPropagator::PropagateLowerBoundsInternal( // we abort so that the conflict reason will be better on the next call to // the propagator. const IntegerValue end = GetValue(end_index); - if (end > integer_trail_->UpperBound(expr)) return true; + if (end > integer_trail_.UpperBound(expr)) return true; // If we have a new Hall interval, add it to the set. Note that it will // always be last, and if it overlaps some previous Hall intervals, it @@ -630,13 +646,13 @@ bool AllDifferentBoundsPropagator::PropagateLowerBoundsInternal( return true; } -void AllDifferentBoundsPropagator::RegisterWith( - GenericLiteralWatcher* watcher) { +int AllDifferentBoundsPropagator::RegisterWith(GenericLiteralWatcher* watcher) { const int id = watcher->Register(this); for (const CachedBounds& entry : bounds_) { watcher->WatchAffineExpression(entry.expr, id); } watcher->NotifyThatPropagatorMayNotReachFixedPointInOnePass(id); + return id; } } // namespace sat diff --git a/ortools/sat/all_different.h b/ortools/sat/all_different.h index 4b202720f9c..1bac51a9035 100644 --- a/ortools/sat/all_different.h +++ b/ortools/sat/all_different.h @@ -19,13 +19,16 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/logging.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_base.h" +#include "ortools/sat/util.h" #include "ortools/util/bitset.h" #include "ortools/util/strong_integers.h" @@ -49,6 +52,7 @@ std::function AllDifferentBinary( std::function AllDifferentOnBounds( absl::Span vars); std::function AllDifferentOnBounds( + absl::Span enforcement_literals, absl::Span expressions); // This constraint forces all variables to take different values. This is meant @@ -141,10 +145,10 @@ class AllDifferentConstraint : PropagatorInterface { // Implements the all different bound consistent propagator with explanation. // That is, given n affine expressions that must take different values, this // propagates the bounds of each expression as much as possible. The key is to -// detect the so called Hall interval which are interval of size k that contains -// the domain of k expressinos. Because all the variables must take different -// values, we can deduce that the domain of the other variables cannot contains -// such Hall interval. +// detect the so called Hall intervals which are intervals of size k that +// contain the domain of k expressions. Because all the variables must take +// different values, we can deduce that the domain of the other variables cannot +// contain such Hall interval. // // We use a "fast" O(n log n) algorithm. // @@ -153,8 +157,9 @@ class AllDifferentConstraint : PropagatorInterface { // https://cs.uwaterloo.ca/~vanbeek/Publications/ijcai03_TR.pdf class AllDifferentBoundsPropagator : public PropagatorInterface { public: - AllDifferentBoundsPropagator(absl::Span expressions, - IntegerTrail* integer_trail); + AllDifferentBoundsPropagator(absl::Span enforcement_literals, + absl::Span expressions, + Model* model); // This type is neither copyable nor movable. AllDifferentBoundsPropagator(const AllDifferentBoundsPropagator&) = delete; @@ -162,7 +167,6 @@ class AllDifferentBoundsPropagator : public PropagatorInterface { delete; bool Propagate() final; - void RegisterWith(GenericLiteralWatcher* watcher); private: // We locally cache the lb/ub for faster sorting and to guarantee some @@ -173,6 +177,8 @@ class AllDifferentBoundsPropagator : public PropagatorInterface { IntegerValue ub; }; + int RegisterWith(GenericLiteralWatcher* watcher); + // Fills integer_reason_ with the reason why we have the given hall interval. void FillHallReason(IntegerValue hall_lb, IntegerValue hall_ub); @@ -208,13 +214,15 @@ class AllDifferentBoundsPropagator : public PropagatorInterface { IntegerValue GetValue(int index) const { return base_ + IntegerValue(index); } - IntegerTrail* integer_trail_; + const IntegerTrail& integer_trail_; + EnforcementPropagator& enforcement_propagator_; + EnforcementId enforcement_id_; // These vector will be either sorted by lb or by -ub. std::vector bounds_; std::vector negated_bounds_; - // The list of Hall intervalls detected so far, sorted. + // The list of Hall intervals detected so far, sorted. std::vector hall_starts_; std::vector hall_ends_; diff --git a/ortools/sat/all_different_test.cc b/ortools/sat/all_different_test.cc index a69afc5eb76..fe8db965cc1 100644 --- a/ortools/sat/all_different_test.cc +++ b/ortools/sat/all_different_test.cc @@ -27,6 +27,7 @@ #include "ortools/sat/integer_base.h" #include "ortools/sat/integer_search.h" #include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" #include "ortools/sat/sat_solver.h" #include "ortools/util/sorted_interval_list.h" @@ -156,6 +157,39 @@ TEST_P(AllDifferentTest, EnumerateAllInjections) { EXPECT_EQ(num_solutions, Factorial(m) / Factorial(m - n)); } +TEST(AllDifferentOnBoundsTest, AlwaysFalseWithUnassignedEnforcementLiteral) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 1)), + model.Add(NewIntegerVariable(1, 2))}; + const Literal b = Literal(model.Add(NewBooleanVariable()), true); + IntegerTrail* integer_trail = model.GetOrCreate(); + integer_trail->UpdateInitialDomain(vars[0], Domain::FromValues({1, 1})); + integer_trail->UpdateInitialDomain(vars[1], Domain::FromValues({0, 0})); + // b => all_diff(x, y+1, 2-z), x=y=1 (always false if enforced). + model.Add(AllDifferentOnBounds( + {b}, {AffineExpression(vars[0], 1), AffineExpression(vars[1], 1, 1), + AffineExpression(vars[2], -1, 3)})); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsFalse(b)); + EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); +} + +TEST(AllDifferentOnBoundsTest, NotAlwaysFalseWithUnassignedEnforcementLiteral) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 3)), + model.Add(NewIntegerVariable(0, 2)), + model.Add(NewIntegerVariable(1, 3))}; + const Literal b = Literal(model.Add(NewBooleanVariable()), true); + // b => all_diff(x, y+1, 2-z), x,y+1,3-z in [1, 3]. + model.Add(AllDifferentOnBounds( + {b}, {AffineExpression(vars[0], 1), AffineExpression(vars[1], 1, 1), + AffineExpression(vars[2], -1, 3)})); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.GetOrCreate()->Assignment().LiteralIsAssigned(b)); + EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); +} + } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/sat/boolean_problem.cc b/ortools/sat/boolean_problem.cc index e9b37bc923d..958b89b8a2e 100644 --- a/ortools/sat/boolean_problem.cc +++ b/ortools/sat/boolean_problem.cc @@ -26,7 +26,6 @@ #include "absl/container/flat_hash_map.h" #include "absl/flags/flag.h" #include "absl/log/check.h" -#include "absl/meta/type_traits.h" #include "absl/status/status.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" @@ -845,8 +844,8 @@ void ProbeAndSimplifyProblem(SatPostsolver* postsolver, } util_intops::StrongVector equiv_map; - ProbeAndFindEquivalentLiteral(&solver, postsolver, /*drat_writer=*/nullptr, - &equiv_map); + ProbeAndFindEquivalentLiteral(&solver, postsolver, + /*drat_proof_handler=*/nullptr, &equiv_map); // We can abort if no information is learned. if (equiv_map.empty() && solver.LiteralTrail().Index() == 0) break; diff --git a/ortools/sat/circuit.cc b/ortools/sat/circuit.cc index 8ae427ce787..15f81da6d45 100644 --- a/ortools/sat/circuit.cc +++ b/ortools/sat/circuit.cc @@ -20,12 +20,12 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" -#include "absl/meta/type_traits.h" #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/graph/strongly_connected_components.h" #include "ortools/sat/all_different.h" #include "ortools/sat/clause.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_base.h" @@ -35,15 +35,15 @@ namespace operations_research { namespace sat { -CircuitPropagator::CircuitPropagator(const int num_nodes, - absl::Span tails, - absl::Span heads, - absl::Span literals, - Options options, Model* model) +CircuitPropagator::CircuitPropagator( + const int num_nodes, absl::Span tails, + absl::Span heads, absl::Span enforcement_literals, + absl::Span literals, Options options, Model* model) : num_nodes_(num_nodes), options_(options), - trail_(model->GetOrCreate()), - assignment_(trail_->Assignment()) { + trail_(*model->GetOrCreate()), + enforcement_propagator_(*model->GetOrCreate()), + assignment_(trail_.Assignment()) { CHECK(!tails.empty()) << "Empty constraint, shouldn't be constructed!"; next_.resize(num_nodes_, -1); prev_.resize(num_nodes_, -1); @@ -60,6 +60,7 @@ CircuitPropagator::CircuitPropagator(const int num_nodes, graph_.reserve(num_arcs); self_arcs_.resize(num_nodes_, kFalseLiteralIndex); + enabled_ = true; for (int arc = 0; arc < num_arcs; ++arc) { const int head = heads[arc]; const int tail = tails[arc]; @@ -74,9 +75,19 @@ CircuitPropagator::CircuitPropagator(const int num_nodes, if (assignment_.LiteralIsTrue(literal)) { if (next_[tail] != -1 || prev_[head] != -1) { - VLOG(1) << "Trivially UNSAT or duplicate arcs while adding " << tail - << " -> " << head; - model->GetOrCreate()->NotifyThatModelIsUnsat(); + SatSolver* sat_solver = model->GetOrCreate(); + if (enforcement_literals.empty()) { + VLOG(1) << "Trivially UNSAT or duplicate arcs while adding " << tail + << " -> " << head; + sat_solver->NotifyThatModelIsUnsat(); + } else { + std::vector negated_enforcement_literals; + for (const Literal literal : enforcement_literals) { + negated_enforcement_literals.push_back(literal.Negated()); + } + sat_solver->AddProblemClause(negated_enforcement_literals); + enabled_ = false; + } return; } AddArc(tail, head, kNoLiteralIndex); @@ -108,9 +119,13 @@ CircuitPropagator::CircuitPropagator(const int num_nodes, } } } + + GenericLiteralWatcher* watcher = model->GetOrCreate(); + enforcement_id_ = enforcement_propagator_.Register( + enforcement_literals, watcher, RegisterWith(watcher)); } -void CircuitPropagator::RegisterWith(GenericLiteralWatcher* watcher) { +int CircuitPropagator::RegisterWith(GenericLiteralWatcher* watcher) { const int id = watcher->Register(this); for (int w = 0; w < watch_index_to_literal_.size(); ++w) { watcher->WatchLiteral(watch_index_to_literal_[w], id, w); @@ -123,9 +138,11 @@ void CircuitPropagator::RegisterWith(GenericLiteralWatcher* watcher) { // // TODO(user): come up with a test that fail when this is not here. watcher->NotifyThatPropagatorMayNotReachFixedPointInOnePass(id); + return id; } void CircuitPropagator::SetLevel(int level) { + if (!enabled_) return; if (level == level_ends_.size()) return; if (level > level_ends_.size()) { while (level > level_ends_.size()) { @@ -158,6 +175,19 @@ void CircuitPropagator::FillReasonForPath(int start_node, } } +bool CircuitPropagator::ReportConflictOrPropagateEnforcement( + std::vector* reason) { + if (enforcement_propagator_.Status(enforcement_id_) == + EnforcementStatus::IS_ENFORCED) { + enforcement_propagator_.AddEnforcementReason(enforcement_id_, reason); + trail_.MutableConflict()->assign(reason->begin(), reason->end()); + return false; + } else { + return enforcement_propagator_.PropagateWhenFalse(enforcement_id_, *reason, + /*integer_reason=*/{}); + } +} + // If multiple_subcircuit_through_zero is true, we never fill next_[0] and // prev_[0]. void CircuitPropagator::AddArc(int tail, int head, LiteralIndex literal_index) { @@ -172,6 +202,14 @@ void CircuitPropagator::AddArc(int tail, int head, LiteralIndex literal_index) { bool CircuitPropagator::IncrementalPropagate( const std::vector& watch_indices) { + if (!enabled_) return true; + const EnforcementStatus status = + enforcement_propagator_.Status(enforcement_id_); + if (status != EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT && + status != EnforcementStatus::IS_ENFORCED) { + return true; + } + for (const int w : watch_indices) { const Literal literal = watch_index_to_literal_[w]; for (const Arc arc : watch_index_to_arcs_[w]) { @@ -184,24 +222,22 @@ bool CircuitPropagator::IncrementalPropagate( // Get rid of the trivial conflicts: At most one incoming and one outgoing // arc for each nodes. if (next_[arc.tail] != -1) { - std::vector* conflict = trail_->MutableConflict(); if (next_literal_[arc.tail] != kNoLiteralIndex) { - *conflict = {Literal(next_literal_[arc.tail]).Negated(), - literal.Negated()}; + temp_reason_ = {Literal(next_literal_[arc.tail]).Negated(), + literal.Negated()}; } else { - *conflict = {literal.Negated()}; + temp_reason_ = {literal.Negated()}; } - return false; + return ReportConflictOrPropagateEnforcement(&temp_reason_); } if (prev_[arc.head] != -1) { - std::vector* conflict = trail_->MutableConflict(); if (next_literal_[prev_[arc.head]] != kNoLiteralIndex) { - *conflict = {Literal(next_literal_[prev_[arc.head]]).Negated(), - literal.Negated()}; + temp_reason_ = {Literal(next_literal_[prev_[arc.head]]).Negated(), + literal.Negated()}; } else { - *conflict = {literal.Negated()}; + temp_reason_ = {literal.Negated()}; } - return false; + return ReportConflictOrPropagateEnforcement(&temp_reason_); } // Add the arc. @@ -215,6 +251,14 @@ bool CircuitPropagator::IncrementalPropagate( // This function assumes that next_, prev_, next_literal_ and must_be_in_cycle_ // are all up to date. bool CircuitPropagator::Propagate() { + if (!enabled_) return true; + const EnforcementStatus status = + enforcement_propagator_.Status(enforcement_id_); + if (status != EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT && + status != EnforcementStatus::IS_ENFORCED) { + return true; + } + processed_.assign(num_nodes_, false); for (int n = 0; n < num_nodes_; ++n) { if (processed_[n]) continue; @@ -249,21 +293,23 @@ bool CircuitPropagator::Propagate() { if (options_.multiple_subcircuit_through_zero) { // Any cycle must contain zero. if (start_node == end_node && !in_current_path_[0]) { - FillReasonForPath(start_node, trail_->MutableConflict()); - return false; + FillReasonForPath(start_node, &temp_reason_); + return ReportConflictOrPropagateEnforcement(&temp_reason_); } // An incomplete path cannot be closed except if one of the end-points // is zero. - if (start_node != end_node && start_node != 0 && end_node != 0) { + if (start_node != end_node && start_node != 0 && end_node != 0 && + status == EnforcementStatus::IS_ENFORCED) { const auto it = graph_.find({end_node, start_node}); if (it == graph_.end()) continue; const Literal literal = it->second; if (assignment_.LiteralIsFalse(literal)) continue; - std::vector* reason = trail_->GetEmptyVectorToStoreReason(); + std::vector* reason = trail_.GetEmptyVectorToStoreReason(); FillReasonForPath(start_node, reason); - if (!trail_->EnqueueWithStoredReason(literal.Negated())) { + enforcement_propagator_.AddEnforcementReason(enforcement_id_, reason); + if (!trail_.EnqueueWithStoredReason(literal.Negated())) { return false; } } @@ -291,27 +337,28 @@ bool CircuitPropagator::Propagate() { if (miss_some_nodes) { // A circuit that miss a mandatory node is a conflict. if (start_node == end_node) { - FillReasonForPath(start_node, trail_->MutableConflict()); + FillReasonForPath(start_node, &temp_reason_); if (extra_reason != kFalseLiteralIndex) { - trail_->MutableConflict()->push_back(Literal(extra_reason)); + temp_reason_.push_back(Literal(extra_reason)); } - return false; + return ReportConflictOrPropagateEnforcement(&temp_reason_); } // We have an unclosed path. Propagate the fact that it cannot // be closed into a cycle, i.e. not(end_node -> start_node). - if (start_node != end_node) { + if (start_node != end_node && status == EnforcementStatus::IS_ENFORCED) { const auto it = graph_.find({end_node, start_node}); if (it == graph_.end()) continue; const Literal literal = it->second; if (assignment_.LiteralIsFalse(literal)) continue; - std::vector* reason = trail_->GetEmptyVectorToStoreReason(); + std::vector* reason = trail_.GetEmptyVectorToStoreReason(); FillReasonForPath(start_node, reason); + enforcement_propagator_.AddEnforcementReason(enforcement_id_, reason); if (extra_reason != kFalseLiteralIndex) { reason->push_back(Literal(extra_reason)); } - const bool ok = trail_->EnqueueWithStoredReason(literal.Negated()); + const bool ok = trail_.EnqueueWithStoredReason(literal.Negated()); if (!ok) return false; continue; } @@ -337,22 +384,26 @@ bool CircuitPropagator::Propagate() { // many arcs, and we just propagated it here. if (self_arcs_[node] == kFalseLiteralIndex || assignment_.LiteralIsFalse(Literal(self_arcs_[node]))) { - FillReasonForPath(start_node, trail_->MutableConflict()); + FillReasonForPath(start_node, &temp_reason_); if (self_arcs_[node] != kFalseLiteralIndex) { - trail_->MutableConflict()->push_back(Literal(self_arcs_[node])); + temp_reason_.push_back(Literal(self_arcs_[node])); } - return false; + return ReportConflictOrPropagateEnforcement(&temp_reason_); } // Propagate. - const Literal literal(self_arcs_[node]); - if (variable_with_same_reason == kNoBooleanVariable) { - variable_with_same_reason = literal.Variable(); - FillReasonForPath(start_node, trail_->GetEmptyVectorToStoreReason()); - const bool ok = trail_->EnqueueWithStoredReason(literal); - if (!ok) return false; - } else { - trail_->EnqueueWithSameReasonAs(literal, variable_with_same_reason); + if (status == EnforcementStatus::IS_ENFORCED) { + const Literal literal(self_arcs_[node]); + if (variable_with_same_reason == kNoBooleanVariable) { + variable_with_same_reason = literal.Variable(); + std::vector* reason = trail_.GetEmptyVectorToStoreReason(); + FillReasonForPath(start_node, reason); + enforcement_propagator_.AddEnforcementReason(enforcement_id_, reason); + const bool ok = trail_.EnqueueWithStoredReason(literal); + if (!ok) return false; + } else { + trail_.EnqueueWithSameReasonAs(literal, variable_with_same_reason); + } } } } @@ -649,6 +700,7 @@ std::function ExactlyOnePerRowAndPerColumn( void LoadSubcircuitConstraint(int num_nodes, absl::Span tails, absl::Span heads, + absl::Span enforcement_literals, absl::Span literals, Model* model, bool multiple_subcircuit_through_zero) { const int num_arcs = tails.size(); @@ -661,44 +713,50 @@ void LoadSubcircuitConstraint(int num_nodes, absl::Span tails, auto sat_solver = model->GetOrCreate(); auto implications = model->GetOrCreate(); - std::vector> exactly_one_incoming(num_nodes); - std::vector> exactly_one_outgoing(num_nodes); - for (int arc = 0; arc < num_arcs; arc++) { - const int tail = tails[arc]; - const int head = heads[arc]; - exactly_one_outgoing[tail].push_back(literals[arc]); - exactly_one_incoming[head].push_back(literals[arc]); - } - for (int i = 0; i < exactly_one_incoming.size(); ++i) { - if (i == 0 && multiple_subcircuit_through_zero) continue; - if (!implications->AddAtMostOne(exactly_one_incoming[i])) { - sat_solver->NotifyThatModelIsUnsat(); - return; + if (enforcement_literals.empty()) { + // TODO(user): how to generalize this to support enforcement literals? + // (we can easily add the negated enforcement literals to AddProblemClause() + // calls, but the AddAtMostOne() calls are trickier). For now this is done + // with additional constraints added by ExpandCircuit() during expansion. + std::vector> exactly_one_incoming(num_nodes); + std::vector> exactly_one_outgoing(num_nodes); + for (int arc = 0; arc < num_arcs; arc++) { + const int tail = tails[arc]; + const int head = heads[arc]; + exactly_one_outgoing[tail].push_back(literals[arc]); + exactly_one_incoming[head].push_back(literals[arc]); } - sat_solver->AddProblemClause(exactly_one_incoming[i]); - if (sat_solver->ModelIsUnsat()) return; - } - for (int i = 0; i < exactly_one_outgoing.size(); ++i) { - if (i == 0 && multiple_subcircuit_through_zero) continue; - if (!implications->AddAtMostOne(exactly_one_outgoing[i])) { - sat_solver->NotifyThatModelIsUnsat(); - return; + for (int i = 0; i < exactly_one_incoming.size(); ++i) { + if (i == 0 && multiple_subcircuit_through_zero) continue; + if (!implications->AddAtMostOne(exactly_one_incoming[i])) { + sat_solver->NotifyThatModelIsUnsat(); + return; + } + sat_solver->AddProblemClause(exactly_one_incoming[i]); + if (sat_solver->ModelIsUnsat()) return; + } + for (int i = 0; i < exactly_one_outgoing.size(); ++i) { + if (i == 0 && multiple_subcircuit_through_zero) continue; + if (!implications->AddAtMostOne(exactly_one_outgoing[i])) { + sat_solver->NotifyThatModelIsUnsat(); + return; + } + sat_solver->AddProblemClause(exactly_one_outgoing[i]); + if (sat_solver->ModelIsUnsat()) return; } - sat_solver->AddProblemClause(exactly_one_outgoing[i]); - if (sat_solver->ModelIsUnsat()) return; } CircuitPropagator::Options options; options.multiple_subcircuit_through_zero = multiple_subcircuit_through_zero; - CircuitPropagator* constraint = - new CircuitPropagator(num_nodes, tails, heads, literals, options, model); - constraint->RegisterWith(model->GetOrCreate()); - model->TakeOwnership(constraint); + model->TakeOwnership(new CircuitPropagator( + num_nodes, tails, heads, enforcement_literals, literals, options, model)); // TODO(user): Just ignore node zero if multiple_subcircuit_through_zero is // true. + // TODO(user): add support for enforcement literals in + // AllDifferentConstraint? if (model->GetOrCreate()->use_all_different_for_circuit() && - !multiple_subcircuit_through_zero) { + enforcement_literals.empty() && !multiple_subcircuit_through_zero) { AllDifferentConstraint* constraint = new AllDifferentConstraint(num_nodes, tails, heads, literals, model); constraint->RegisterWith(model->GetOrCreate()); diff --git a/ortools/sat/circuit.h b/ortools/sat/circuit.h index 97443a562c6..4462e9efb1e 100644 --- a/ortools/sat/circuit.h +++ b/ortools/sat/circuit.h @@ -22,6 +22,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/types/span.h" #include "ortools/graph/strongly_connected_components.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_base.h" @@ -53,6 +54,7 @@ class CircuitPropagator : PropagatorInterface, ReversibleInterface { // being present when the given literal is true. CircuitPropagator(int num_nodes, absl::Span tails, absl::Span heads, + absl::Span enforcement_literals, absl::Span literals, Options options, Model* model); @@ -63,9 +65,10 @@ class CircuitPropagator : PropagatorInterface, ReversibleInterface { void SetLevel(int level) final; bool Propagate() final; bool IncrementalPropagate(const std::vector& watch_indices) final; - void RegisterWith(GenericLiteralWatcher* watcher); private: + int RegisterWith(GenericLiteralWatcher* watcher); + // Updates the structures when the given arc is added to the paths. void AddArc(int tail, int head, LiteralIndex literal_index); @@ -74,10 +77,17 @@ class CircuitPropagator : PropagatorInterface, ReversibleInterface { // start (not like a rho shape). void FillReasonForPath(int start_node, std::vector* reason) const; + // Reports a conflict if the constraint is enforced, or propagates the unique + // unassigned enforcement literal otherwise. + bool ReportConflictOrPropagateEnforcement(std::vector* reason); + const int num_nodes_; const Options options_; - Trail* trail_; + Trail& trail_; + EnforcementPropagator& enforcement_propagator_; + EnforcementId enforcement_id_; const VariablesAssignment& assignment_; + bool enabled_; // We use this to query in O(1) for an arc existence. The self-arcs are // accessed often, so we use a more efficient std::vector<> for them. Note @@ -114,6 +124,7 @@ class CircuitPropagator : PropagatorInterface, ReversibleInterface { // Temporary vectors. std::vector processed_; std::vector in_current_path_; + std::vector temp_reason_; }; // Enforce the fact that there is no cycle in the given directed graph. @@ -246,6 +257,7 @@ int ReindexArcs(IntContainer* tails, IntContainer* heads, // problem to be UNSAT. One can call ReindexArcs() first to ignore such nodes. void LoadSubcircuitConstraint(int num_nodes, absl::Span tails, absl::Span heads, + absl::Span enforcement_literals, absl::Span literals, Model* model, bool multiple_subcircuit_through_zero = false); diff --git a/ortools/sat/circuit_test.cc b/ortools/sat/circuit_test.cc index afd3c1ff6dc..ce75fad149c 100644 --- a/ortools/sat/circuit_test.cc +++ b/ortools/sat/circuit_test.cc @@ -49,7 +49,8 @@ std::function DenseCircuitConstraint( literals.push_back(Literal(model->Add(NewBooleanVariable()), true)); } } - LoadSubcircuitConstraint(num_nodes, tails, heads, literals, model, + LoadSubcircuitConstraint(num_nodes, tails, heads, + /*enforcement_literals=*/{}, literals, model, allow_multiple_subcircuit_through_zero); }; } @@ -110,7 +111,8 @@ TEST(CircuitConstraintTest, NodeWithNoArcsIsUnsat) { tails.push_back(0); heads.push_back(1); literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); - LoadSubcircuitConstraint(kNumNodes, tails, heads, literals, &model); + LoadSubcircuitConstraint(kNumNodes, tails, heads, /*enforcement_literals=*/{}, + literals, &model); EXPECT_TRUE(model.GetOrCreate()->ModelIsUnsat()); } @@ -217,14 +219,15 @@ TEST(CircuitConstraintTest, InfeasibleBecauseOfMissingArcs) { heads.push_back(arcs.second); literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); } - LoadSubcircuitConstraint(3, tails, heads, literals, &model, false); + LoadSubcircuitConstraint(3, tails, heads, /*enforcement_literals=*/{}, + literals, &model, false); const SatSolver::Status status = SolveIntegerProblemWithLazyEncoding(&model); EXPECT_EQ(status, SatSolver::Status::INFEASIBLE); } // The graph look like this with a self-loop at 2. If 2 is not selected // (self-loop) then there is one solution (0,1,3,0) and (0,3,5,0). Otherwise, -// there is 2 more solutions with 2 inserteed in one of the two routes. +// there is 2 more solutions with 2 inserted in one of the two routes. // // 0 ---> 1 ---> 4 ------------- // | | ^ | @@ -253,7 +256,8 @@ TEST(CircuitConstraintTest, RouteConstraint) { heads.push_back(arcs.second); literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); } - LoadSubcircuitConstraint(6, tails, heads, literals, &model, true); + LoadSubcircuitConstraint(6, tails, heads, /*enforcement_literals=*/{}, + literals, &model, true); const int64_t num_solutions = CountSolutions(&model); EXPECT_EQ(num_solutions, 3); } diff --git a/ortools/sat/colab/cp_sat.ipynb b/ortools/sat/colab/cp_sat.ipynb index 474fb8e3a3d..4da360c4737 100644 --- a/ortools/sat/colab/cp_sat.ipynb +++ b/ortools/sat/colab/cp_sat.ipynb @@ -1,3 +1,17 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 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. + { "cells": [ { @@ -22,7 +36,7 @@ "To build and run this runtime locally execute the following command at the root of a CitC:\n", "\n", "```\n", - "blaze run -c opt ortools/colab:or_notebook -- --logtostderr\n", + "blaze run -c opt util/operations_research/colab:or_notebook -- --logtostderr\n", "```\n", "\n", "Then, click Connect button and click CONNECT under **Local runtime**.\n", diff --git a/ortools/sat/colab/flags.py b/ortools/sat/colab/flags.py index 7057db3684a..48d2cebe176 100644 --- a/ortools/sat/colab/flags.py +++ b/ortools/sat/colab/flags.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/sat/colab/visualization.py b/ortools/sat/colab/visualization.py index 725042fb273..10bcb82bc99 100644 --- a/ortools/sat/colab/visualization.py +++ b/ortools/sat/colab/visualization.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/sat/cp_constraints.cc b/ortools/sat/cp_constraints.cc index 341ca496bd1..8f7fce2cb3d 100644 --- a/ortools/sat/cp_constraints.cc +++ b/ortools/sat/cp_constraints.cc @@ -40,8 +40,8 @@ std::ostream& operator<<(std::ostream& os, const EnforcementStatus& e) { case EnforcementStatus::CANNOT_PROPAGATE: os << "CANNOT_PROPAGATE"; break; - case EnforcementStatus::CAN_PROPAGATE: - os << "CAN_PROPAGATE"; + case EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT: + os << "CAN_PROPAGATE_ENFORCEMENT"; break; case EnforcementStatus::IS_ENFORCED: os << "IS_ENFORCED"; @@ -160,7 +160,7 @@ EnforcementId EnforcementPropagator::Register( // The default status at level zero. statuses_.push_back(temp_literals_.size() == 1 - ? EnforcementStatus::CAN_PROPAGATE + ? EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT : EnforcementStatus::CANNOT_PROPAGATE); if (temp_literals_.size() == 1) { @@ -199,11 +199,11 @@ EnforcementId EnforcementPropagator::Register( } else if (num_true == temp_literals_.size()) { ChangeStatus(id, EnforcementStatus::IS_ENFORCED); } else if (num_true + 1 == temp_literals_.size()) { - ChangeStatus(id, EnforcementStatus::CAN_PROPAGATE); + ChangeStatus(id, EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); // Because this is the default status, we still need to call the callback. if (temp_literals_.size() == 1) { if (callbacks_[id] != nullptr) { - callbacks_[id](id, EnforcementStatus::CAN_PROPAGATE); + callbacks_[id](id, EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); } } } @@ -221,13 +221,13 @@ EnforcementId EnforcementPropagator::Register( EnforcementId EnforcementPropagator::Register( absl::Span enforcement_literals, GenericLiteralWatcher* watcher, int literal_watcher_id) { - return Register(enforcement_literals, - [=](EnforcementId, EnforcementStatus status) { - if (status == EnforcementStatus::CAN_PROPAGATE || - status == EnforcementStatus::IS_ENFORCED) { - watcher->CallOnNextPropagate(literal_watcher_id); - } - }); + return Register( + enforcement_literals, [=](EnforcementId, EnforcementStatus status) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT || + status == EnforcementStatus::IS_ENFORCED) { + watcher->CallOnNextPropagate(literal_watcher_id); + } + }); } // Add the enforcement reason to the given vector. @@ -268,6 +268,17 @@ bool EnforcementPropagator::PropagateWhenFalse( return true; } +bool EnforcementPropagator::Enqueue( + EnforcementId id, IntegerLiteral i_lit, + absl::Span literal_reason, + absl::Span integer_reason) { + temp_reason_.clear(); + temp_reason_.insert(temp_reason_.end(), literal_reason.begin(), + literal_reason.end()); + AddEnforcementReason(id, &temp_reason_); + return integer_trail_->Enqueue(i_lit, temp_reason_, integer_reason); +} + bool EnforcementPropagator::SafeEnqueue( EnforcementId id, IntegerLiteral i_lit, absl::Span integer_reason) { @@ -276,9 +287,37 @@ bool EnforcementPropagator::SafeEnqueue( return integer_trail_->SafeEnqueue(i_lit, temp_reason_, integer_reason); } +bool EnforcementPropagator::ConditionalEnqueue( + EnforcementId id, Literal lit, IntegerLiteral i_lit, + absl::Span literal_reason, + absl::Span integer_reason) { + temp_reason_.clear(); + temp_reason_.insert(temp_reason_.end(), literal_reason.begin(), + literal_reason.end()); + AddEnforcementReason(id, &temp_reason_); + temp_integer_reason_.clear(); + temp_integer_reason_.insert(temp_integer_reason_.end(), + integer_reason.begin(), integer_reason.end()); + return integer_trail_->ConditionalEnqueue(lit, i_lit, &temp_reason_, + &temp_integer_reason_); +} + +void EnforcementPropagator::EnqueueLiteral( + EnforcementId id, Literal literal, absl::Span literal_reason, + absl::Span integer_reason) { + temp_reason_.clear(); + temp_reason_.insert(temp_reason_.end(), literal_reason.begin(), + literal_reason.end()); + AddEnforcementReason(id, &temp_reason_); + return integer_trail_->EnqueueLiteral(literal, temp_reason_, integer_reason); +} + bool EnforcementPropagator::ReportConflict( - EnforcementId id, absl::Span integer_reason) { + EnforcementId id, absl::Span literal_reason, + absl::Span integer_reason) { temp_reason_.clear(); + temp_reason_.insert(temp_reason_.end(), literal_reason.begin(), + literal_reason.end()); AddEnforcementReason(id, &temp_reason_); return integer_trail_->ReportConflict(temp_reason_, integer_reason); } @@ -307,7 +346,7 @@ LiteralIndex EnforcementPropagator::ProcessIdOnTrue(Literal watched, const auto span = GetSpan(id); if (span.size() == 1) { - CHECK_EQ(status, EnforcementStatus::CAN_PROPAGATE); + CHECK_EQ(status, EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); ChangeStatus(id, EnforcementStatus::IS_ENFORCED); return kNoLiteralIndex; } @@ -341,7 +380,7 @@ LiteralIndex EnforcementPropagator::ProcessIdOnTrue(Literal watched, } else { // The other watched literal is the last unassigned CHECK_EQ(status, EnforcementStatus::CANNOT_PROPAGATE); - ChangeStatus(id, EnforcementStatus::CAN_PROPAGATE); + ChangeStatus(id, EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); return kNoLiteralIndex; } } @@ -369,7 +408,7 @@ EnforcementStatus EnforcementPropagator::DebugStatus(EnforcementId id) { } const int size = GetSpan(id).size(); if (num_true == size) return EnforcementStatus::IS_ENFORCED; - if (num_true + 1 == size) return EnforcementStatus::CAN_PROPAGATE; + if (num_true + 1 == size) return EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT; return EnforcementStatus::CANNOT_PROPAGATE; } @@ -378,17 +417,17 @@ BooleanXorPropagator::BooleanXorPropagator( const std::vector& literals, bool value, Model* model) : literals_(literals), value_(value), - trail_(model->GetOrCreate()), - integer_trail_(model->GetOrCreate()), - enforcement_propagator_(model->GetOrCreate()) { + trail_(*model->GetOrCreate()), + integer_trail_(*model->GetOrCreate()), + enforcement_propagator_(*model->GetOrCreate()) { GenericLiteralWatcher* watcher = model->GetOrCreate(); - enforcement_id_ = enforcement_propagator_->Register( + enforcement_id_ = enforcement_propagator_.Register( enforcement_literals, watcher, RegisterWith(watcher)); } bool BooleanXorPropagator::Propagate() { const EnforcementStatus status = - enforcement_propagator_->Status(enforcement_id_); + enforcement_propagator_.Status(enforcement_id_); if (status == EnforcementStatus::IS_FALSE || status == EnforcementStatus::CANNOT_PROPAGATE) { return true; @@ -398,9 +437,9 @@ bool BooleanXorPropagator::Propagate() { int unassigned_index = -1; for (int i = 0; i < literals_.size(); ++i) { const Literal l = literals_[i]; - if (trail_->Assignment().LiteralIsFalse(l)) { + if (trail_.Assignment().LiteralIsFalse(l)) { sum ^= false; - } else if (trail_->Assignment().LiteralIsTrue(l)) { + } else if (trail_.Assignment().LiteralIsTrue(l)) { sum ^= true; } else { // If we have more than one unassigned literal, we can't deduce anything. @@ -409,29 +448,23 @@ bool BooleanXorPropagator::Propagate() { } } - // Propagates? - if (status == EnforcementStatus::IS_ENFORCED && unassigned_index != -1) { + auto fill_literal_reason = [&]() { literal_reason_.clear(); - enforcement_propagator_->AddEnforcementReason(enforcement_id_, - &literal_reason_); for (int i = 0; i < literals_.size(); ++i) { if (i == unassigned_index) continue; const Literal l = literals_[i]; literal_reason_.push_back( - trail_->Assignment().LiteralIsFalse(l) ? l : l.Negated()); + trail_.Assignment().LiteralIsFalse(l) ? l : l.Negated()); } + }; + + // Propagates? + if (status == EnforcementStatus::IS_ENFORCED && unassigned_index != -1) { + fill_literal_reason(); const Literal u = literals_[unassigned_index]; - integer_trail_->EnqueueLiteral(sum == value_ ? u.Negated() : u, - literal_reason_, {}); - return true; - } - if (status == EnforcementStatus::CAN_PROPAGATE && unassigned_index == -1 && - sum != value_) { - return enforcement_propagator_->PropagateWhenFalse(enforcement_id_, - literals_, - /*integer_reason=*/{}); - } - if (status != EnforcementStatus::IS_ENFORCED || unassigned_index != -1) { + enforcement_propagator_.EnqueueLiteral( + enforcement_id_, sum == value_ ? u.Negated() : u, literal_reason_, + /*integer_reason=*/{}); return true; } @@ -439,14 +472,17 @@ bool BooleanXorPropagator::Propagate() { if (sum == value_) return true; // Conflict. - std::vector* conflict = trail_->MutableConflict(); - conflict->clear(); - enforcement_propagator_->AddEnforcementReason(enforcement_id_, conflict); - for (const Literal& l : literals_) { - conflict->push_back(trail_->Assignment().LiteralIsFalse(l) ? l - : l.Negated()); + if (status == EnforcementStatus::IS_ENFORCED) { + fill_literal_reason(); + return enforcement_propagator_.ReportConflict( + enforcement_id_, literal_reason_, /*integer_reason=*/{}); + } else if (unassigned_index == -1) { + fill_literal_reason(); + return enforcement_propagator_.PropagateWhenFalse( + enforcement_id_, literal_reason_, /*integer_reason=*/{}); + } else { + return true; } - return false; } int BooleanXorPropagator::RegisterWith(GenericLiteralWatcher* watcher) { diff --git a/ortools/sat/cp_constraints.h b/ortools/sat/cp_constraints.h index 4e8a296366d..b4efeda8be6 100644 --- a/ortools/sat/cp_constraints.h +++ b/ortools/sat/cp_constraints.h @@ -45,7 +45,7 @@ enum class EnforcementStatus { // More than two literals are unassigned. CANNOT_PROPAGATE = 1, // All enforcement literals are true but one. - CAN_PROPAGATE = 2, + CAN_PROPAGATE_ENFORCEMENT = 2, // All enforcement literals are true. IS_ENFORCED = 3, }; @@ -89,11 +89,31 @@ class EnforcementPropagator : public SatPropagator { EnforcementId id, absl::Span literal_reason, absl::Span integer_reason); + ABSL_MUST_USE_RESULT bool Enqueue( + EnforcementId id, IntegerLiteral i_lit, + absl::Span literal_reason, + absl::Span integer_reason); + ABSL_MUST_USE_RESULT bool SafeEnqueue( EnforcementId id, IntegerLiteral i_lit, absl::Span integer_reason); + ABSL_MUST_USE_RESULT bool ConditionalEnqueue( + EnforcementId id, Literal lit, IntegerLiteral i_lit, + absl::Span literal_reason, + absl::Span integer_reason); + + void EnqueueLiteral(EnforcementId id, Literal literal, + absl::Span literal_reason, + absl::Span integer_reason); + bool ReportConflict(EnforcementId id, + absl::Span integer_reason) { + return ReportConflict(id, /*literal_reason=*/{}, integer_reason); + } + + bool ReportConflict(EnforcementId id, + absl::Span literal_reason, absl::Span integer_reason); EnforcementStatus Status(EnforcementId id) const { @@ -148,6 +168,7 @@ class EnforcementPropagator : public SatPropagator { std::vector temp_literals_; std::vector temp_reason_; + std::vector temp_integer_reason_; std::vector ids_to_fix_until_next_root_level_; }; @@ -175,9 +196,9 @@ class BooleanXorPropagator : public PropagatorInterface { const std::vector literals_; const bool value_; std::vector literal_reason_; - Trail* trail_; - IntegerTrail* integer_trail_; - EnforcementPropagator* enforcement_propagator_; + const Trail& trail_; + const IntegerTrail& integer_trail_; + EnforcementPropagator& enforcement_propagator_; EnforcementId enforcement_id_; }; diff --git a/ortools/sat/cp_constraints_test.cc b/ortools/sat/cp_constraints_test.cc index 88efa3d1676..05408adac60 100644 --- a/ortools/sat/cp_constraints_test.cc +++ b/ortools/sat/cp_constraints_test.cc @@ -44,15 +44,15 @@ TEST(EnforcementPropagatorTest, BasicTest) { const EnforcementId id3 = propag->Register(Literals({-2})); EXPECT_TRUE(propag->Propagate(trail)); - EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); EXPECT_EQ(propag->Status(id2), EnforcementStatus::CANNOT_PROPAGATE); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); sat_solver->EnqueueDecisionIfNotConflicting(Literal(+1)); EXPECT_TRUE(propag->Propagate(trail)); EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); - EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); sat_solver->EnqueueDecisionIfNotConflicting(Literal(+2)); EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); @@ -60,9 +60,9 @@ TEST(EnforcementPropagatorTest, BasicTest) { EXPECT_EQ(propag->Status(id3), EnforcementStatus::IS_FALSE); CHECK(sat_solver->ResetToLevelZero()); - EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); EXPECT_EQ(propag->Status(id2), EnforcementStatus::CANNOT_PROPAGATE); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); } TEST(EnforcementPropagatorTest, UntrailWork) { @@ -77,21 +77,21 @@ TEST(EnforcementPropagatorTest, UntrailWork) { const EnforcementId id3 = propag->Register(Literals({+3})); EXPECT_TRUE(propag->Propagate(trail)); - EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); - EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); sat_solver->EnqueueDecisionIfNotConflicting(Literal(+1)); EXPECT_TRUE(propag->Propagate(trail)); EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); - EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); sat_solver->EnqueueDecisionIfNotConflicting(Literal(+2)); EXPECT_TRUE(propag->Propagate(trail)); EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); const int level = sat_solver->CurrentDecisionLevel(); sat_solver->EnqueueDecisionIfNotConflicting(Literal(+3)); @@ -103,7 +103,7 @@ TEST(EnforcementPropagatorTest, UntrailWork) { sat_solver->Backtrack(level); EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); - EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); } TEST(EnforcementPropagatorTest, AddingAtPositiveLevelTrue) { @@ -122,7 +122,7 @@ TEST(EnforcementPropagatorTest, AddingAtPositiveLevelTrue) { sat_solver->Backtrack(0); EXPECT_TRUE(propag->Propagate(trail)); - EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); } TEST(EnforcementPropagatorTest, AddingAtPositiveLevelFalse) { @@ -141,7 +141,7 @@ TEST(EnforcementPropagatorTest, AddingAtPositiveLevelFalse) { sat_solver->Backtrack(0); EXPECT_TRUE(propag->Propagate(trail)); - EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT); } TEST(LiteralXorIsTest, OneVariable) { diff --git a/ortools/sat/cp_model.proto b/ortools/sat/cp_model.proto index 92e0d2fa638..a30299e939f 100644 --- a/ortools/sat/cp_model.proto +++ b/ortools/sat/cp_model.proto @@ -85,7 +85,7 @@ message LinearArgumentProto { repeated LinearExpressionProto exprs = 2; } -// All affine expressions must take different values. +// All expressions must take different values. message AllDifferentConstraintProto { repeated LinearExpressionProto exprs = 1; } @@ -108,7 +108,6 @@ message ElementConstraintProto { int32 index = 1; // Legacy field. int32 target = 2; // Legacy field. repeated int32 vars = 3; // Legacy field. - // All expressions below must be affine function with at most one variable. LinearExpressionProto linear_index = 4; LinearExpressionProto linear_target = 5; repeated LinearExpressionProto exprs = 6; @@ -121,10 +120,6 @@ message ElementConstraintProto { // components, and it is up to the client to add in the model: // - enforcement => start + size == end. // - enforcement => size >= 0 // Only needed if size is not already >= 0. -// -// IMPORTANT: For now, we just support affine relation. We could easily -// create an intermediate variable to support full linear expression, but this -// isn't done currently. message IntervalConstraintProto { LinearExpressionProto start = 4; LinearExpressionProto end = 5; @@ -188,9 +183,8 @@ message CumulativeConstraintProto { message ReservoirConstraintProto { int64 min_level = 1; int64 max_level = 2; - repeated LinearExpressionProto time_exprs = 3; // affine expressions. - // Currently, we only support constant level changes. - repeated LinearExpressionProto level_changes = 6; // affine expressions. + repeated LinearExpressionProto time_exprs = 3; + repeated LinearExpressionProto level_changes = 6; repeated int32 active_literals = 5; reserved 4; } @@ -242,7 +236,6 @@ message RoutesConstraintProto { // A set of linear expressions associated with the nodes. message NodeExpressions { // The i-th element is the linear expression associated with the i-th node. - // All expressions must be affine expressions (a * var + b). repeated LinearExpressionProto exprs = 1; } @@ -261,7 +254,6 @@ message RoutesConstraintProto { // The values of the n-tuple formed by the given expression can only be one of // the listed n-tuples in values. The n-tuples are encoded in a flattened way: // [tuple0_v0, tuple0_v1, ..., tuple0_v{n-1}, tuple1_v0, ...]. -// Expressions must be affine (a * var + b). // Corner cases: // - If all `vars`, `values` and `exprs` are empty, the constraint is trivially // true, irrespective of the value of `negated`. @@ -304,9 +296,8 @@ message AutomatonConstraintProto { // Legacy field. repeated int32 vars = 7; - // The sequence of affine expressions (a * var + b). The automaton is ran for - // exprs_size() "steps" and the value of exprs[i] corresponds to the - // transition label at step i. + // The sequence of expressions. The automaton is ran for exprs_size() "steps" + // and the value of exprs[i] corresponds to the transition label at step i. repeated LinearExpressionProto exprs = 8; } @@ -330,11 +321,10 @@ message ConstraintProto { // (controlled by a literal l) and its negation (controlled by the negation of // l). // - // Important: as of July 2025, only a few constraint support enforcement: - // - bool_or, bool_and, at_most_one, exactly_one, bool_xor, int_div, int_mod, - // int_prod, linear, table: fully supported. + // Important: as of August 2025, some constraints do not support enforcement: // - interval: only support a single enforcement literal. - // - other: no support (but can be added on a per-demand basis). + // - routes, no_overlap, no_overlap_2d, cumulative: no support (but can be + // added on a per-demand basis). repeated int32 enforcement_literal = 2; // The actual constraint with its arguments. diff --git a/ortools/sat/cp_model_checker.cc b/ortools/sat/cp_model_checker.cc index 88ccd6485fe..ff1b81e18ab 100644 --- a/ortools/sat/cp_model_checker.cc +++ b/ortools/sat/cp_model_checker.cc @@ -29,7 +29,6 @@ #include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" #include "absl/log/check.h" -#include "absl/meta/type_traits.h" #include "absl/strings/str_cat.h" #include "absl/types/span.h" #include "ortools/base/logging.h" @@ -285,17 +284,8 @@ std::string ValidateLinearExpression(const CpModelProto& model, return ""; } -std::string ValidateAffineExpression(const CpModelProto& model, - const LinearExpressionProto& expr) { - if (expr.vars_size() > 1) { - return absl::StrCat("expression must be affine: ", - ProtobufShortDebugString(expr)); - } - return ValidateLinearExpression(model, expr); -} - -std::string ValidateConstantAffineExpression( - const CpModelProto& model, const LinearExpressionProto& expr) { +std::string ValidateConstantExpression(const CpModelProto& model, + const LinearExpressionProto& expr) { if (!expr.vars().empty()) { return absl::StrCat("expression must be constant: ", ProtobufShortDebugString(expr)); @@ -338,9 +328,9 @@ std::string ValidateIntModConstraint(const CpModelProto& model, ProtobufShortDebugString(ct)); } - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_mod().exprs(0))); - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_mod().exprs(1))); - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_mod().target())); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_mod().exprs(0))); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_mod().exprs(1))); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_mod().target())); const LinearExpressionProto mod_expr = ct.int_mod().exprs(1); if (MinOfExpression(model, mod_expr) <= 0) { @@ -360,9 +350,9 @@ std::string ValidateIntProdConstraint(const CpModelProto& model, } for (const LinearExpressionProto& expr : ct.int_prod().exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_prod().target())); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_prod().target())); // Detect potential overflow. Domain product_domain(1); @@ -406,9 +396,9 @@ std::string ValidateIntDivConstraint(const CpModelProto& model, ProtobufShortDebugString(ct)); } - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_div().exprs(0))); - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_div().exprs(1))); - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, ct.int_div().target())); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_div().exprs(0))); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_div().exprs(1))); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, ct.int_div().target())); const LinearExpressionProto& denom = ct.int_div().exprs(1); const int64_t offset = denom.offset(); @@ -498,11 +488,11 @@ std::string ValidateElementConstraint(const CpModelProto& model, if (in_linear_format) { RETURN_IF_NOT_EMPTY( - ValidateAffineExpression(model, element.linear_index())); + ValidateLinearExpression(model, element.linear_index())); RETURN_IF_NOT_EMPTY( - ValidateAffineExpression(model, element.linear_target())); + ValidateLinearExpression(model, element.linear_target())); for (const LinearExpressionProto& expr : element.exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); LinearExpressionProto overflow_detection = ct.element().linear_target(); AppendToOverflowValidator(expr, &overflow_detection, -1); const int64_t offset = CapSub(overflow_detection.offset(), expr.offset()); @@ -549,7 +539,7 @@ std::string ValidateTableConstraint(const CpModelProto& model, } } for (const LinearExpressionProto& expr : arg.exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } return ""; } @@ -576,7 +566,7 @@ std::string ValidateAutomatonConstraint(const CpModelProto& model, } } for (const LinearExpressionProto& expr : automaton.exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } absl::flat_hash_map, int64_t> tail_label_to_head; for (int i = 0; i < num_transistions; ++i) { @@ -670,7 +660,7 @@ std::string ValidateRoutesConstraint(const CpModelProto& model, ProtobufShortDebugString(ct)); } } - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } } @@ -809,9 +799,6 @@ std::string ValidateNoOverlap2DConstraint(const CpModelProto& model, std::string ValidateReservoirConstraint(const CpModelProto& model, const ConstraintProto& ct) { - if (ct.enforcement_literal_size() > 0) { - return "Reservoir does not support enforcement literals."; - } if (ct.reservoir().time_exprs().size() != ct.reservoir().level_changes().size()) { return absl::StrCat( @@ -819,7 +806,7 @@ std::string ValidateReservoirConstraint(const CpModelProto& model, ProtobufShortDebugString(ct)); } for (const LinearExpressionProto& expr : ct.reservoir().time_exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); // We want to be able to safely put time_exprs[i]-time_exprs[j] in a linear. if (MinOfExpression(model, expr) <= -std::numeric_limits::max() / 4 || @@ -831,7 +818,7 @@ std::string ValidateReservoirConstraint(const CpModelProto& model, } } for (const LinearExpressionProto& expr : ct.reservoir().level_changes()) { - RETURN_IF_NOT_EMPTY(ValidateConstantAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } if (ct.reservoir().min_level() > 0) { return absl::StrCat( @@ -1002,13 +989,17 @@ std::string ValidateSearchStrategies(const CpModelProto& model) { } } for (const LinearExpressionProto& expr : strategy.exprs()) { + if (expr.vars_size() > 1) { + return absl::StrCat("expression must be affine in strategy: ", + ProtobufShortDebugString(strategy)); + } for (const int var : expr.vars()) { if (!VariableReferenceIsValid(model, var)) { return absl::StrCat("Invalid variable reference in strategy: ", ProtobufShortDebugString(strategy)); } } - if (!ValidateAffineExpression(model, expr).empty()) { + if (!ValidateLinearExpression(model, expr).empty()) { return absl::StrCat("Invalid affine expr in strategy: ", ProtobufShortDebugString(strategy)); } @@ -1132,30 +1123,18 @@ std::string ValidateCpModel(const CpModelProto& model, bool after_presolve) { for (int c = 0; c < model.constraints_size(); ++c) { RETURN_IF_NOT_EMPTY(ValidateVariablesUsedInConstraint(model, c)); - // By default, a constraint does not support enforcement literals except if - // explicitly stated by setting this to true below. - bool support_enforcement = false; + bool support_enforcement = true; // Other non-generic validations. const ConstraintProto& ct = model.constraints(c); switch (ct.constraint_case()) { case ConstraintProto::ConstraintCase::kBoolOr: - support_enforcement = true; - break; case ConstraintProto::ConstraintCase::kBoolAnd: - support_enforcement = true; - break; case ConstraintProto::ConstraintCase::kAtMostOne: - support_enforcement = true; - break; case ConstraintProto::ConstraintCase::kExactlyOne: - support_enforcement = true; - break; case ConstraintProto::ConstraintCase::kBoolXor: - support_enforcement = true; break; case ConstraintProto::ConstraintCase::kLinear: - support_enforcement = true; RETURN_IF_NOT_EMPTY(ValidateLinearConstraint(model, ct)); break; case ConstraintProto::ConstraintCase::kLinMax: { @@ -1167,15 +1146,12 @@ std::string ValidateCpModel(const CpModelProto& model, bool after_presolve) { break; } case ConstraintProto::ConstraintCase::kIntProd: - support_enforcement = true; RETURN_IF_NOT_EMPTY(ValidateIntProdConstraint(model, ct)); break; case ConstraintProto::ConstraintCase::kIntDiv: - support_enforcement = true; RETURN_IF_NOT_EMPTY(ValidateIntDivConstraint(model, ct)); break; case ConstraintProto::ConstraintCase::kIntMod: - support_enforcement = true; RETURN_IF_NOT_EMPTY(ValidateIntModConstraint(model, ct)); break; case ConstraintProto::ConstraintCase::kInverse: @@ -1186,7 +1162,7 @@ std::string ValidateCpModel(const CpModelProto& model, bool after_presolve) { break; case ConstraintProto::ConstraintCase::kAllDiff: for (const LinearExpressionProto& expr : ct.all_diff().exprs()) { - RETURN_IF_NOT_EMPTY(ValidateAffineExpression(model, expr)); + RETURN_IF_NOT_EMPTY(ValidateLinearExpression(model, expr)); } break; case ConstraintProto::ConstraintCase::kElement: @@ -1194,7 +1170,6 @@ std::string ValidateCpModel(const CpModelProto& model, bool after_presolve) { break; case ConstraintProto::ConstraintCase::kTable: RETURN_IF_NOT_EMPTY(ValidateTableConstraint(model, ct)); - support_enforcement = true; break; case ConstraintProto::ConstraintCase::kAutomaton: RETURN_IF_NOT_EMPTY(ValidateAutomatonConstraint(model, ct)); @@ -1204,19 +1179,22 @@ std::string ValidateCpModel(const CpModelProto& model, bool after_presolve) { ValidateGraphInput(/*is_route=*/false, ct.circuit())); break; case ConstraintProto::ConstraintCase::kRoutes: + support_enforcement = false; RETURN_IF_NOT_EMPTY(ValidateRoutesConstraint(model, ct)); break; case ConstraintProto::ConstraintCase::kInterval: RETURN_IF_NOT_EMPTY(ValidateIntervalConstraint(model, ct)); - support_enforcement = true; break; case ConstraintProto::ConstraintCase::kCumulative: + support_enforcement = false; constraints_using_intervals.push_back(c); break; case ConstraintProto::ConstraintCase::kNoOverlap: + support_enforcement = false; constraints_using_intervals.push_back(c); break; case ConstraintProto::ConstraintCase::kNoOverlap2D: + support_enforcement = false; constraints_using_intervals.push_back(c); break; case ConstraintProto::ConstraintCase::kReservoir: diff --git a/ortools/sat/cp_model_checker_test.cc b/ortools/sat/cp_model_checker_test.cc index 23f96789593..8d7d3439e32 100644 --- a/ortools/sat/cp_model_checker_test.cc +++ b/ortools/sat/cp_model_checker_test.cc @@ -726,33 +726,6 @@ TEST(ValidateCpModelTest, InvalidNodeExpressionsCount) { EXPECT_THAT(ValidateCpModel(model), HasSubstr("must be of size num_nodes:2")); } -TEST(ValidateCpModelTest, NonAffineExpressionInRoutesConstraint) { - const CpModelProto model = ParseTestProto(R"pb( - variables { domain: [ 0, 1 ] } - variables { domain: [ 0, 1 ] } - variables { domain: [ 0, 10 ] } - variables { domain: [ 0, 10 ] } - constraints { - routes { - tails: [ 0, 1 ] - heads: [ 1, 0 ] - literals: [ 0, 1 ] - dimensions { - exprs { - vars: [ 2, 3 ] - coeffs: [ 1, 2 ] - } - exprs { - vars: [ 3 ] - coeffs: [ 1 ] - } - } - } - } - )pb"); - EXPECT_THAT(ValidateCpModel(model), HasSubstr("expression must be affine")); -} - TEST(ValidateCpModelTest, InvalidNodeExpressionInRoutesConstraint) { const CpModelProto model = ParseTestProto(R"pb( variables { domain: [ 0, 1 ] } diff --git a/ortools/sat/cp_model_copy.cc b/ortools/sat/cp_model_copy.cc index 590410ffcda..cf7c9da618d 100644 --- a/ortools/sat/cp_model_copy.cc +++ b/ortools/sat/cp_model_copy.cc @@ -15,10 +15,13 @@ #include #include +#include #include #include +#include #include #include +#include #include #include "absl/container/flat_hash_set.h" @@ -216,6 +219,9 @@ bool ModelCopy::ImportAndSimplifyConstraints( } } + if (first_copy) { + ExpandNonAffineExpressions(); + } return true; } @@ -544,6 +550,8 @@ bool ModelCopy::CopyElement(const ConstraintProto& ct) { if (ct.element().vars().empty() && !ct.element().exprs().empty()) { // New format, just copy. *new_ct = ct; + new_ct->mutable_enforcement_literal()->Clear(); + FinishEnforcementCopy(new_ct); return true; } @@ -559,6 +567,7 @@ bool ModelCopy::CopyElement(const ConstraintProto& ct) { } }; + FinishEnforcementCopy(new_ct); fill_expr(ct.element().index(), new_ct->mutable_element()->mutable_linear_index()); fill_expr(ct.element().target(), @@ -571,8 +580,20 @@ bool ModelCopy::CopyElement(const ConstraintProto& ct) { bool ModelCopy::CopyAutomaton(const ConstraintProto& ct) { ConstraintProto* new_ct = context_->working_model->add_constraints(); - *new_ct = ct; - if (new_ct->automaton().vars().empty()) return true; + new_ct->mutable_automaton()->set_starting_state( + ct.automaton().starting_state()); + *new_ct->mutable_automaton()->mutable_final_states() = + ct.automaton().final_states(); + *new_ct->mutable_automaton()->mutable_transition_tail() = + ct.automaton().transition_tail(); + *new_ct->mutable_automaton()->mutable_transition_head() = + ct.automaton().transition_head(); + *new_ct->mutable_automaton()->mutable_transition_label() = + ct.automaton().transition_label(); + for (const LinearExpressionProto& expr : ct.automaton().exprs()) { + CopyLinearExpression(expr, new_ct->mutable_automaton()->add_exprs()); + } + FinishEnforcementCopy(new_ct); auto fill_expr = [this](int var, LinearExpressionProto* expr) mutable { if (context_->IsFixed(var)) { @@ -589,7 +610,6 @@ bool ModelCopy::CopyAutomaton(const ConstraintProto& ct) { for (const int var : ct.automaton().vars()) { fill_expr(var, new_ct->mutable_automaton()->add_exprs()); } - new_ct->mutable_automaton()->clear_vars(); return true; } @@ -599,6 +619,8 @@ bool ModelCopy::CopyTable(const ConstraintProto& ct) { if (ct.table().vars().empty() && !ct.table().exprs().empty()) { // New format, just copy. *new_ct = ct; + new_ct->mutable_enforcement_literal()->Clear(); + FinishEnforcementCopy(new_ct); return true; } @@ -614,6 +636,7 @@ bool ModelCopy::CopyTable(const ConstraintProto& ct) { } }; + FinishEnforcementCopy(new_ct); for (const int var : ct.table().vars()) { fill_expr(var, new_ct->mutable_table()->add_exprs()); } @@ -630,6 +653,7 @@ bool ModelCopy::CopyAllDiff(const ConstraintProto& ct) { for (const LinearExpressionProto& expr : ct.all_diff().exprs()) { CopyLinearExpression(expr, new_ct->mutable_all_diff()->add_exprs()); } + FinishEnforcementCopy(new_ct); return true; } @@ -673,28 +697,10 @@ bool ModelCopy::CopyLinMax(const ConstraintProto& ct) { if (new_ct == nullptr) return false; // No expr == unsat. CopyLinearExpression(ct.lin_max().target(), new_ct->mutable_lin_max()->mutable_target()); + FinishEnforcementCopy(new_ct); return true; } -namespace { -void LiteralsToLinear(absl::Span literals, int64_t lb, int64_t ub, - LinearConstraintProto* linear) { - for (const int lit : literals) { - if (RefIsPositive(lit)) { - linear->add_vars(lit); - linear->add_coeffs(1); - } else { - linear->add_vars(NegatedRef(lit)); - linear->add_coeffs(-1); - lb -= 1; - ub -= 1; - } - } - linear->add_domain(lb); - linear->add_domain(ub); -} -} // namespace - bool ModelCopy::CopyAtMostOne(const ConstraintProto& ct) { if (!ct.enforcement_literal().empty()) { ConstraintProto new_ct; @@ -937,6 +943,129 @@ bool ModelCopy::CreateUnsatModel(int c, const ConstraintProto& ct) { return context_->NotifyThatModelIsUnsat(message); } +void ModelCopy::ExpandNonAffineExpressions() { + // Make sure all domains are initialized (they are used in + // MaybeExpandNonAffineExpression()). + context_->InitializeNewDomains(); + + non_affine_expression_to_new_var_.clear(); + for (int c = 0; c < context_->working_model->constraints_size(); ++c) { + ConstraintProto* const ct = context_->working_model->mutable_constraints(c); + switch (ct->constraint_case()) { + case ConstraintProto::kIntDiv: + MaybeExpandNonAffineExpressions(ct->mutable_int_div()); + break; + case ConstraintProto::kIntMod: + MaybeExpandNonAffineExpressions(ct->mutable_int_mod()); + break; + case ConstraintProto::kIntProd: + MaybeExpandNonAffineExpressions(ct->mutable_int_prod()); + break; + case ConstraintProto::kAllDiff: + for (LinearExpressionProto& expr : + *ct->mutable_all_diff()->mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + break; + case ConstraintProto::kElement: + if (!ct->element().exprs().empty()) { + MaybeExpandNonAffineExpression( + ct->mutable_element()->mutable_linear_index()); + MaybeExpandNonAffineExpression( + ct->mutable_element()->mutable_linear_target()); + for (LinearExpressionProto& expr : + *ct->mutable_element()->mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + } + break; + case ConstraintProto::kInterval: + MaybeExpandNonAffineExpression(ct->mutable_interval()->mutable_start()); + MaybeExpandNonAffineExpression(ct->mutable_interval()->mutable_end()); + MaybeExpandNonAffineExpression(ct->mutable_interval()->mutable_size()); + break; + case ConstraintProto::kReservoir: + for (LinearExpressionProto& expr : + *ct->mutable_reservoir()->mutable_time_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + break; + case ConstraintProto::kRoutes: + for (RoutesConstraintProto::NodeExpressions& node_exprs : + *ct->mutable_routes()->mutable_dimensions()) { + for (LinearExpressionProto& expr : *node_exprs.mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + } + break; + case ConstraintProto::kTable: + for (LinearExpressionProto& expr : + *ct->mutable_table()->mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + break; + case ConstraintProto::kAutomaton: + for (LinearExpressionProto& expr : + *ct->mutable_automaton()->mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } + break; + default: + break; + } + } +} + +// Replaces the expression sum a_i * x_i + c with gcd * y + c, where y is a new +// variable defined with an additional constraint y = sum a_i / gcd * x_i. +void ModelCopy::MaybeExpandNonAffineExpression(LinearExpressionProto* expr) { + if (expr->vars_size() < 2) return; + + int64_t gcd = std::abs(expr->coeffs(0)); + for (int i = 1; i < expr->coeffs().size(); ++i) { + gcd = std::gcd(gcd, std::abs(expr->coeffs(i))); + } + Domain domain(0); + std::vector> definition; + for (int i = 0; i < expr->vars().size(); ++i) { + const int var = expr->vars(i); + const int64_t coeff = expr->coeffs(i) / gcd; + domain = + domain.AdditionWith(context_->DomainOf(var).MultiplicationBy(coeff)); + definition.push_back({var, coeff}); + } + int new_var; + auto it = non_affine_expression_to_new_var_.find(definition); + if (it != non_affine_expression_to_new_var_.end()) { + new_var = it->second; + } else { + new_var = context_->NewIntVar(domain); + auto* new_linear = + context_->working_model->add_constraints()->mutable_linear(); + new_linear->add_vars(new_var); + new_linear->add_coeffs(-1); + for (const auto [var, coeff] : definition) { + new_linear->add_vars(var); + new_linear->add_coeffs(coeff); + } + new_linear->add_domain(0); + new_linear->add_domain(0); + context_->solution_crush().SetVarToLinearExpression(new_var, definition); + } + expr->clear_vars(); + expr->clear_coeffs(); + expr->add_vars(new_var); + expr->add_coeffs(gcd); +} + +void ModelCopy::MaybeExpandNonAffineExpressions( + LinearArgumentProto* linear_argument) { + MaybeExpandNonAffineExpression(linear_argument->mutable_target()); + for (LinearExpressionProto& expr : *linear_argument->mutable_exprs()) { + MaybeExpandNonAffineExpression(&expr); + } +} + bool ImportModelWithBasicPresolveIntoContext(const CpModelProto& in_model, PresolveContext* context) { ModelCopy copier(context); diff --git a/ortools/sat/cp_model_copy.h b/ortools/sat/cp_model_copy.h index ff9b438b312..b438fb571d0 100644 --- a/ortools/sat/cp_model_copy.h +++ b/ortools/sat/cp_model_copy.h @@ -16,8 +16,10 @@ #include #include +#include #include +#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/types/span.h" #include "ortools/sat/cp_model.pb.h" @@ -118,6 +120,17 @@ class ModelCopy { void CopyAndMapNoOverlap2D(const ConstraintProto& ct); bool CopyAndMapCumulative(const ConstraintProto& ct); + // Expands linear expressions with more than one variable in constraints which + // internally only support affine expressions (such as all_diff, element, + // interval, reservoir, table, etc). This creates new variables for each such + // expression, and replaces the original expressions with the new variables in + // the constraints. + void ExpandNonAffineExpressions(); + // Replaces the expression sum a_i * x_i + c with gcd * y + c, where y is a + // new variable defined with an additional constraint y = sum a_i / gcd * x_i. + void MaybeExpandNonAffineExpression(LinearExpressionProto* expr); + void MaybeExpandNonAffineExpressions(LinearArgumentProto* linear_argument); + PresolveContext* context_; // Temp vectors. @@ -133,6 +146,11 @@ class ModelCopy { absl::flat_hash_set temp_literals_set_; ConstraintProto tmp_constraint_; + + // Map used in ExpandNonAffineExpressions() to avoid creating the duplicate + // variables for the identical non affine expressions. + absl::flat_hash_map>, int> + non_affine_expression_to_new_var_; }; // Copy in_model to the model in the presolve context. diff --git a/ortools/sat/cp_model_copy_test.cc b/ortools/sat/cp_model_copy_test.cc index 4675f66f415..c4eb51e212d 100644 --- a/ortools/sat/cp_model_copy_test.cc +++ b/ortools/sat/cp_model_copy_test.cc @@ -255,6 +255,85 @@ TEST(ModelCopyTest, ChangeEnforcedAtMostOrExactlyOneToLinear) { EXPECT_THAT(new_cp_model, EqualsProto(expected_moded)); } +TEST(ModelCopyTest, LegacyElementConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1, 1 ] + element { + index: 0 + target: 1 + vars: [ 2, 3 ] + } + } + )pb"); + const CpModelProto expected_moded = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1 ] + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + CpModelProto new_cp_model; + Model model; + model.GetOrCreate() + ->set_keep_all_feasible_solutions_in_presolve(true); + PresolveContext context(&model, &new_cp_model, nullptr); + ImportModelWithBasicPresolveIntoContext(initial_model, &context); + EXPECT_THAT(new_cp_model, EqualsProto(expected_moded)); +} + +TEST(ModelCopyTest, ElementConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1, 1 ] + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_moded = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1 ] + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + CpModelProto new_cp_model; + Model model; + model.GetOrCreate() + ->set_keep_all_feasible_solutions_in_presolve(true); + PresolveContext context(&model, &new_cp_model, nullptr); + ImportModelWithBasicPresolveIntoContext(initial_model, &context); + EXPECT_THAT(new_cp_model, EqualsProto(expected_moded)); +} + } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/sat/cp_model_expand.cc b/ortools/sat/cp_model_expand.cc index 618a51b5bfd..df4f4ce80e0 100644 --- a/ortools/sat/cp_model_expand.cc +++ b/ortools/sat/cp_model_expand.cc @@ -28,8 +28,8 @@ #include "absl/container/flat_hash_set.h" #include "absl/container/inlined_vector.h" #include "absl/log/check.h" -#include "absl/meta/type_traits.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/types/span.h" #include "google/protobuf/message.h" #include "ortools/base/logging.h" @@ -50,6 +50,153 @@ namespace operations_research { namespace sat { namespace { +void ExpandAlwaysFalseConstraint(ConstraintProto* ct, PresolveContext* context, + absl::string_view message = "") { + if (ct->enforcement_literal().empty()) { + return (void)context->NotifyThatModelIsUnsat(message); + } + BoolArgumentProto& bool_or = + *context->working_model->add_constraints()->mutable_bool_or(); + for (const int literal : ct->enforcement_literal()) { + bool_or.add_literals(NegatedRef(literal)); + } + ct->Clear(); +} + +// Keeps track of the domains of the variables used in a constraint when this +// constraint is enforced. These domains cannot be modified in the presolve +// context because they are not valid when the constraint is not enforced. +class EnforcedDomains { + public: + EnforcedDomains(ConstraintProto* ct, PresolveContext* context) + : constraint_(ct), context_(context) {} + + int size() { return domains_.size(); } + + bool IsFixed(const LinearExpressionProto& expr) const { + CHECK_LE(expr.vars_size(), 1); + return expr.vars().empty() || DomainOf(expr.vars(0)).IsFixed(); + } + + Domain DomainOf(int ref) const { + auto it = domains_.find(PositiveRef(ref)); + if (it != domains_.end()) { + return RefIsPositive(ref) ? it->second : it->second.Negation(); + } + return context_->DomainOf(ref); + } + + bool DomainContains(const LinearExpressionProto& expr, int64_t value) const { + CHECK_LE(expr.vars_size(), 1); + const int64_t offset = expr.offset(); + if (expr.vars().empty()) { + return offset == value; + } + const Domain domain = DomainOf(expr.vars(0)); + const int64_t coeff = expr.coeffs(0); + if (value > coeff * (coeff > 0 ? domain.Max() : domain.Min()) + offset) { + return false; + } + if (value < coeff * (coeff > 0 ? domain.Min() : domain.Max()) + offset) { + return false; + } + // We assume expression is validated for overflow initially, and the code + // below should be overflow safe. + if ((value - expr.offset()) % expr.coeffs(0) != 0) return false; + return DomainOf(expr.vars(0)) + .Contains((value - expr.offset()) / expr.coeffs(0)); + } + + // Returns false if the new domain is empty. This does not necessarily mean + // that the problem is infeasible though (if the constraint has enforcement + // literals, it just means that the enforcement must be false). + bool IntersectDomainWith(int ref, const Domain& domain, + absl::string_view message = "", + bool* domain_modified = nullptr) { + if (constraint_->enforcement_literal().empty() && + !context_->IntersectDomainWith(ref, domain, domain_modified)) { + return context_->NotifyThatModelIsUnsat(message); + } + const Domain current_domain = DomainOf(ref); + if (current_domain.IsIncludedIn(domain)) return true; + if (domain_modified != nullptr) { + *domain_modified = true; + } + const Domain new_domain = current_domain.IntersectionWith(domain); + if (new_domain.IsEmpty()) { + ExpandAlwaysFalseConstraint(constraint_, context_, message); + return false; + } + domains_[PositiveRef(ref)] = new_domain; + return true; + }; + + bool SetLiteralToFalse(int lit) { + const int var = PositiveRef(lit); + const int64_t value = RefIsPositive(lit) ? 0 : 1; + return IntersectDomainWith(var, Domain(value)); + } + + bool IntersectDomainWith(const LinearExpressionProto& expr, + const Domain& domain, absl::string_view message = "", + bool* domain_modified = nullptr) { + CHECK_LE(expr.vars_size(), 1); + if (constraint_->enforcement_literal().empty() && + !context_->IntersectDomainWith(expr, domain, domain_modified)) { + return context_->NotifyThatModelIsUnsat(message); + } + if (expr.vars().empty()) { + if (domain.Contains(expr.offset())) return true; + ExpandAlwaysFalseConstraint(constraint_, context_, message); + return false; + } + return IntersectDomainWith(expr.vars(0), + domain.AdditionWith(Domain(-expr.offset())) + .InverseMultiplicationBy(expr.coeffs(0)), + message, domain_modified); + } + + void MaybeAddEnforcedDomainConstraints() const { + if (constraint_->enforcement_literal().empty()) return; + std::vector vars; + for (const auto& [var, _] : domains_) { + vars.push_back(var); + } + std::sort(vars.begin(), vars.end()); + for (const int var : vars) { + // enforcement_literal => var in domain + ConstraintProto* const imply = context_->working_model->add_constraints(); + *imply->mutable_enforcement_literal() = + constraint_->enforcement_literal(); + LinearConstraintProto* const lin = imply->mutable_linear(); + lin->add_vars(var); + lin->add_coeffs(1); + FillDomainInProto(domains_.at(var), lin); + } + } + + private: + ConstraintProto* constraint_; + PresolveContext* context_; + absl::flat_hash_map domains_; +}; + +void ExpandEnforcedAtMostOneOrExactlyOneConstraint(ConstraintProto* ct, int c, + PresolveContext* context) { + if (ct->enforcement_literal().empty()) { + return; + } + LinearConstraintProto linear; + if (ct->has_at_most_one()) { + LiteralsToLinear(ct->at_most_one().literals(), /*lb=*/0, /*ub=*/1, &linear); + } else { + LiteralsToLinear(ct->exactly_one().literals(), /*lb=*/1, /*ub=*/1, &linear); + } + ct->mutable_linear()->Swap(&linear); + context->CanonicalizeLinearConstraint(ct); + context->UpdateConstraintVariableUsage(c); +} + // Different encoding that support general demands. This is usually a pretty bad // encoding, at least until we improve the solver on such models. void ExpandReservoirUsingCircuit(int64_t sum_of_positive_demand, @@ -102,6 +249,8 @@ void ExpandReservoirUsingCircuit(int64_t sum_of_positive_demand, // Add enforced linear for demand. { ConstraintProto* new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = + reservoir_ct->enforcement_literal(); new_ct->add_enforcement_literal(start_var); LinearConstraintProto* lin = new_ct->mutable_linear(); lin->add_domain(0); @@ -141,6 +290,8 @@ void ExpandReservoirUsingCircuit(int64_t sum_of_positive_demand, // Add enforced linear for time. { ConstraintProto* new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = + reservoir_ct->enforcement_literal(); new_ct->add_enforcement_literal(arc_i_j); LinearConstraintProto* lin = new_ct->mutable_linear(); lin->add_domain(0); @@ -153,6 +304,8 @@ void ExpandReservoirUsingCircuit(int64_t sum_of_positive_demand, // Add enforced linear for demand. { ConstraintProto* new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = + reservoir_ct->enforcement_literal(); new_ct->add_enforcement_literal(arc_i_j); LinearConstraintProto* lin = new_ct->mutable_linear(); lin->add_domain(0); @@ -201,6 +354,8 @@ void ExpandReservoirUsingPrecedences(bool max_level_is_constraining, if (demand_i < 0 && !min_level_is_constraining) continue; ConstraintProto* new_cumul = context->working_model->add_constraints(); + *new_cumul->mutable_enforcement_literal() = + reservoir_ct->enforcement_literal(); LinearConstraintProto* new_linear = new_cumul->mutable_linear(); int64_t offset = 0; @@ -257,8 +412,9 @@ void ExpandReservoirUsingPrecedences(bool max_level_is_constraining, void ExpandReservoir(ConstraintProto* reservoir_ct, PresolveContext* context) { if (reservoir_ct->reservoir().min_level() > reservoir_ct->reservoir().max_level()) { - VLOG(1) << "Empty level domain in reservoir constraint."; - return (void)context->NotifyThatModelIsUnsat(); + ExpandAlwaysFalseConstraint(reservoir_ct, context, + "Empty level domain in reservoir constraint."); + return; } const ReservoirConstraintProto& reservoir = reservoir_ct->reservoir(); @@ -298,7 +454,11 @@ void ExpandReservoir(ConstraintProto* reservoir_ct, PresolveContext* context) { if (num_negatives == 0 || num_positives == 0) { const int true_literal = context->GetTrueLiteral(); ConstraintProto* new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = + reservoir_ct->enforcement_literal(); LinearConstraintProto* sum = new_ct->mutable_linear(); + sum->add_domain(reservoir.min_level()); + sum->add_domain(reservoir.max_level()); for (int i = 0; i < num_events; ++i) { const int active = reservoir.active_literals().empty() ? true_literal @@ -347,8 +507,6 @@ void ExpandReservoir(ConstraintProto* reservoir_ct, PresolveContext* context) { NegatedRef(active)); } } - sum->add_domain(reservoir.min_level()); - sum->add_domain(reservoir.max_level()); context->CanonicalizeLinearConstraint(new_ct); context->UpdateRuleStats("reservoir: simple expansion with sum"); @@ -467,6 +625,10 @@ void ExpandIntMod(ConstraintProto* ct, PresolveContext* context) { .ContinuousMultiplicationBy(context->DomainSuperSetOf(mod_expr)) .IntersectionWith(context->DomainSuperSetOf(expr).AdditionWith( context->DomainSuperSetOf(target_expr).Negation())); + if (prod_domain.IsEmpty()) { + return (void)context->NotifyThatModelIsUnsat( + "int_mod: empty target domain"); + } const int prod_var = context->NewIntVar(prod_domain); LinearExpressionProto prod_expr; prod_expr.add_vars(prod_var); @@ -546,25 +708,25 @@ void ExpandInverse(ConstraintProto* ct, PresolveContext* context) { // // TODO(user): Add support for UNSAT at expansion. This should create empty // domain if UNSAT, so it should still work correctly. - absl::flat_hash_set used_variables; + EnforcedDomains enforced_domains(ct, context); for (const int ref : f_direct) { - used_variables.insert(PositiveRef(ref)); - if (!context->IntersectDomainWith(ref, Domain(0, n - 1))) { - VLOG(1) << "Empty domain for a variable in ExpandInverse()"; + if (!enforced_domains.IntersectDomainWith( + ref, Domain(0, n - 1), + "Empty domain for a variable in ExpandInverse()")) { return; } } for (const int ref : f_inverse) { - used_variables.insert(PositiveRef(ref)); - if (!context->IntersectDomainWith(ref, Domain(0, n - 1))) { - VLOG(1) << "Empty domain for a variable in ExpandInverse()"; + if (!enforced_domains.IntersectDomainWith( + ref, Domain(0, n - 1), + "Empty domain for a variable in ExpandInverse()")) { return; } } // If we have duplicate variables, we make sure the domain are reduced // as the loop below might not detect incompatibilities. - if (used_variables.size() != 2 * n) { + if (enforced_domains.size() != 2 * n) { for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { // Note that if we don't have the same sign, both domain are at zero. @@ -572,8 +734,9 @@ void ExpandInverse(ConstraintProto* ct, PresolveContext* context) { // We can't have i or j as value if i != j. if (i == j) continue; - if (!context->IntersectDomainWith( - f_direct[i], Domain::FromValues({i, j}).Complement())) { + if (!enforced_domains.IntersectDomainWith( + f_direct[i], Domain::FromValues({i, j}).Complement(), + "Empty domain for a variable in ExpandInverse()")) { return; } } @@ -585,54 +748,74 @@ void ExpandInverse(ConstraintProto* ct, PresolveContext* context) { std::vector possible_values; // Propagate from one vector to its counterpart. - const auto filter_inverse_domain = - [context, n, &possible_values](const auto& direct, const auto& inverse) { - // Propagate from the inverse vector to the direct vector. - for (int i = 0; i < n; ++i) { - possible_values.clear(); - const Domain domain = context->DomainOf(direct[i]); - bool removed_value = false; - for (const int64_t j : domain.Values()) { - if (context->DomainOf(inverse[j]).Contains(i)) { - possible_values.push_back(j); - } else { - removed_value = true; - } - } - if (removed_value) { - if (!context->IntersectDomainWith( - direct[i], Domain::FromValues(possible_values))) { - VLOG(1) << "Empty domain for a variable in ExpandInverse()"; - return false; - } - } + const auto filter_inverse_domain = [&enforced_domains, n, &possible_values]( + const auto& direct, + const auto& inverse) { + // Propagate from the inverse vector to the direct vector. + for (int i = 0; i < n; ++i) { + possible_values.clear(); + const Domain domain = enforced_domains.DomainOf(direct[i]); + bool removed_value = false; + for (const int64_t j : domain.Values()) { + if (enforced_domains.DomainOf(inverse[j]).Contains(i)) { + possible_values.push_back(j); + } else { + removed_value = true; } - return true; - }; + } + if (removed_value) { + if (!enforced_domains.IntersectDomainWith( + direct[i], Domain::FromValues(possible_values), + "Empty domain for a variable in ExpandInverse()")) { + return false; + } + } + } + return true; + }; // Note that this should reach the fixed point in one pass. // However, if we have duplicate variable, I am not sure. if (!filter_inverse_domain(f_direct, f_inverse)) return; if (!filter_inverse_domain(f_inverse, f_direct)) return; + enforced_domains.MaybeAddEnforcedDomainConstraints(); + // Expand the inverse constraint by associating literal to var == value // and sharing them between the direct and inverse variables. // // Note that this is only correct because the domain are tight now. for (int i = 0; i < n; ++i) { const int f_i = f_direct[i]; - for (const int64_t j : context->DomainOf(f_i).Values()) { - // We have f[i] == j <=> r[j] == i; + for (const int64_t j : enforced_domains.DomainOf(f_i).Values()) { const int r_j = f_inverse[j]; - int r_j_i; - if (context->HasVarValueEncoding(r_j, i, &r_j_i)) { - if (!context->InsertVarValueEncoding(r_j_i, f_i, j)) { - return; + if (ct->enforcement_literal().empty()) { + // We have f[i] == j <=> r[j] == i; + int r_j_i; + if (context->HasVarValueEncoding(r_j, i, &r_j_i)) { + if (!context->InsertVarValueEncoding(r_j_i, f_i, j)) { + return; + } + } else { + const int f_i_j = context->GetOrCreateVarValueEncoding(f_i, j); + if (!context->InsertVarValueEncoding(f_i_j, r_j, i)) { + return; + } } } else { + // We have enforcement_literal && f[i] == j => r[j] == i; + // We have enforcement_literal && r[j] == i => f[i] == j; const int f_i_j = context->GetOrCreateVarValueEncoding(f_i, j); - if (!context->InsertVarValueEncoding(f_i_j, r_j, i)) { - return; + const int r_j_i = context->GetOrCreateVarValueEncoding(r_j, i); + if (f_i_j != r_j_i) { + ConstraintProto* eq = context->working_model->add_constraints(); + *eq->mutable_enforcement_literal() = ct->enforcement_literal(); + eq->add_enforcement_literal(f_i_j); + eq->mutable_bool_and()->add_literals(r_j_i); + eq = context->working_model->add_constraints(); + *eq->mutable_enforcement_literal() = ct->enforcement_literal(); + eq->add_enforcement_literal(r_j_i); + eq->mutable_bool_and()->add_literals(f_i_j); } } } @@ -653,9 +836,10 @@ void ExpandLinMax(ConstraintProto* ct, PresolveContext* context) { // We will create 2 * num_exprs constraints for target = max(a1, ..., an). // First. - // - target >= ai + // - enforcement literals => target >= ai for (const LinearExpressionProto& expr : ct->lin_max().exprs()) { ConstraintProto* new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = ct->enforcement_literal(); LinearConstraintProto* lin = new_ct->mutable_linear(); lin->add_domain(0); lin->add_domain(std::numeric_limits::max()); @@ -665,15 +849,16 @@ void ExpandLinMax(ConstraintProto* ct, PresolveContext* context) { } // Second, for each expr, create a new boolean bi, and add bi => target <= ai - // With exactly_one(bi) + // With enforcement literals => exactly_one(bi) std::vector enforcement_literals; enforcement_literals.reserve(num_exprs); - if (num_exprs == 2) { + if (num_exprs == 2 && ct->enforcement_literal().empty()) { const int new_bool = context->NewBoolVar("lin max expansion"); enforcement_literals.push_back(new_bool); enforcement_literals.push_back(NegatedRef(new_bool)); } else { ConstraintProto* exactly_one = context->working_model->add_constraints(); + *exactly_one->mutable_enforcement_literal() = ct->enforcement_literal(); for (int i = 0; i < num_exprs; ++i) { const int new_bool = context->NewBoolVar("lin max expansion"); exactly_one->mutable_exactly_one()->add_literals(new_bool); @@ -699,8 +884,9 @@ void ExpandLinMax(ConstraintProto* ct, PresolveContext* context) { } // A[V] == V means for all i, V == i => A_i == i -void ExpandElementWhenTargetShareVarWithIndex(ConstraintProto* ct, - PresolveContext* context) { +void ExpandElementWhenTargetShareVarWithIndex( + ConstraintProto* ct, PresolveContext* context, + const Domain& reduced_index_var_domain) { const ElementConstraintProto& element = ct->element(); const LinearExpressionProto& index = element.linear_index(); @@ -710,11 +896,12 @@ void ExpandElementWhenTargetShareVarWithIndex(ConstraintProto* ct, DCHECK_EQ(target.vars_size(), 1); DCHECK_EQ(target.vars(0), index_var); - for (const int64_t v : context->DomainOf(index_var).Values()) { + for (const int64_t v : reduced_index_var_domain.Values()) { const int64_t index_value = AffineExpressionValueAt(index, v); const int64_t target_value = AffineExpressionValueAt(target, v); const LinearExpressionProto& expr = element.exprs(index_value); ConstraintProto* imply = context->working_model->add_constraints(); + *imply->mutable_enforcement_literal() = ct->enforcement_literal(); imply->add_enforcement_literal( context->GetOrCreateVarValueEncoding(index_var, v)); imply->mutable_linear()->add_domain(target_value); @@ -729,23 +916,20 @@ void ExpandElementWhenTargetShareVarWithIndex(ConstraintProto* ct, } // Special case if the array of the element is filled with constant values. -void ExpandConstantArrayElement(ConstraintProto* ct, PresolveContext* context) { +void ExpandConstantArrayElement(ConstraintProto* ct, PresolveContext* context, + const Domain& reduced_index_var_domain) { const ElementConstraintProto& element = ct->element(); const LinearExpressionProto& index = element.linear_index(); DCHECK_EQ(index.vars_size(), 1); const int index_var = index.vars(0); const LinearExpressionProto& target = element.linear_target(); - // Index and target domain have been reduced before calling this function. - const Domain index_var_domain = context->DomainOf(index_var); - const Domain target_domain = context->DomainSuperSetOf(target); - // This BoolOrs implements the deduction that if all index literals pointing // to the same value in the constant array are false, then this value is no // no longer valid for the target variable. They are created only for values // that have multiples literals supporting them. absl::btree_map> supports; - for (const int64_t v : index_var_domain.Values()) { + for (const int64_t v : reduced_index_var_domain.Values()) { const int64_t index_value = AffineExpressionValueAt(index, v); const int64_t expr_value = context->FixedValue(element.exprs(index_value)); supports[expr_value].push_back(v); @@ -755,25 +939,31 @@ void ExpandConstantArrayElement(ConstraintProto* ct, PresolveContext* context) { // covered, it allows to easily detect this fact in the presolve. // // TODO(user): Do we need the support part ? Is this discovered by probing? - auto* exactly_one = - context->working_model->add_constraints()->mutable_exactly_one(); + ConstraintProto* const exactly_one = + context->working_model->add_constraints(); + *exactly_one->mutable_enforcement_literal() = ct->enforcement_literal(); for (const auto& [expr_value, support] : supports) { const int target_literal = context->GetOrCreateAffineValueEncoding(target, expr_value); - // not(indices supporting value) -> target != value - BoolArgumentProto* bool_or = - context->working_model->add_constraints()->mutable_bool_or(); - bool_or->add_literals(NegatedRef(target_literal)); + // enforcement_literal && not(indices supporting value) => target != value + ConstraintProto* const bool_or = context->working_model->add_constraints(); + *bool_or->mutable_enforcement_literal() = ct->enforcement_literal(); + bool_or->mutable_bool_or()->add_literals(NegatedRef(target_literal)); for (const int64_t v : support) { const int index_literal = context->GetOrCreateVarValueEncoding(index_var, v); - bool_or->add_literals(index_literal); - - // index == v => target == value - context->AddImplication(index_literal, target_literal); + bool_or->mutable_bool_or()->add_literals(index_literal); + + // enforcement_literal && index == v => target == value + if (index_literal != target_literal) { + ConstraintProto* const eq = context->working_model->add_constraints(); + *eq->mutable_enforcement_literal() = ct->enforcement_literal(); + eq->add_enforcement_literal(index_literal); + eq->mutable_bool_and()->add_literals(target_literal); + } // Helps presolve. - exactly_one->add_literals(index_literal); + exactly_one->mutable_exactly_one()->add_literals(index_literal); } } @@ -782,25 +972,26 @@ void ExpandConstantArrayElement(ConstraintProto* ct, PresolveContext* context) { } // General element when the array contains non fixed variables. -void ExpandVariableElement(ConstraintProto* ct, PresolveContext* context) { +void ExpandVariableElement(ConstraintProto* ct, PresolveContext* context, + const Domain& reduced_index_var_domain) { const ElementConstraintProto& element = ct->element(); const LinearExpressionProto& index = element.linear_index(); DCHECK_EQ(index.vars_size(), 1); const int index_var = index.vars(0); - const Domain index_var_domain = context->DomainOf(index_var); const LinearExpressionProto& target = element.linear_target(); - BoolArgumentProto* exactly_one = - context->working_model->add_constraints()->mutable_exactly_one(); + ConstraintProto* exactly_one = context->working_model->add_constraints(); + *exactly_one->mutable_enforcement_literal() = ct->enforcement_literal(); - for (const int64_t v : index_var_domain.Values()) { + for (const int64_t v : reduced_index_var_domain.Values()) { const int64_t index_value = AffineExpressionValueAt(index, v); DCHECK_GE(index_value, 0); DCHECK_LT(index_value, element.exprs_size()); const int index_lit = context->GetOrCreateVarValueEncoding(index_var, v); - exactly_one->add_literals(index_lit); + exactly_one->mutable_exactly_one()->add_literals(index_lit); ConstraintProto* const imply = context->working_model->add_constraints(); + *imply->mutable_enforcement_literal() = ct->enforcement_literal(); imply->add_enforcement_literal(index_lit); imply->mutable_linear()->add_domain(0); imply->mutable_linear()->add_domain(0); @@ -829,18 +1020,50 @@ void ExpandElement(ConstraintProto* ct, PresolveContext* context) { // Reduce the domain of the index to be compatible with the array of // variables. Note that the element constraint is 0 based. - if (!context->IntersectDomainWith(index, Domain(0, size - 1))) { - VLOG(1) << "Empty domain for the index variable in ExpandElement()"; - return; + const Domain reduced_index_var_domain = + index.vars_size() == 1 + ? context->DomainOf(index.vars(0)) + .IntersectionWith(Domain(0, size - 1) + .AdditionWith(Domain(-index.offset())) + .InverseMultiplicationBy(index.coeffs(0))) + : Domain(); + + if (ct->enforcement_literal().empty()) { + if (!context->IntersectDomainWith(index, Domain(0, size - 1))) { + VLOG(1) << "Empty domain for the index variable in ExpandElement()"; + return; + } + } else { + const bool reduced_index_domain_is_empty = + index.vars_size() == 1 ? reduced_index_var_domain.IsEmpty() + : (index.offset() < 0 || index.offset() >= size); + if (reduced_index_domain_is_empty) { + ExpandAlwaysFalseConstraint(ct, context); + return; + } + // enforcement_literal => index in [0, size - 1] + ConstraintProto* const index_ct = context->working_model->add_constraints(); + *index_ct->mutable_enforcement_literal() = ct->enforcement_literal(); + index_ct->mutable_linear()->add_domain(0); + index_ct->mutable_linear()->add_domain(size - 1); + AddLinearExpressionToLinearConstraint(index, 1, index_ct->mutable_linear()); + context->CanonicalizeLinearConstraint(index_ct); } - if (context->IsFixed(index)) { + const bool reduced_index_domain_is_fixed = + index.vars_size() == 0 || reduced_index_var_domain.IsFixed(); + if (reduced_index_domain_is_fixed) { + DCHECK(!ct->enforcement_literal().empty() || context->IsFixed(index)); ConstraintProto* const eq = context->working_model->add_constraints(); + *eq->mutable_enforcement_literal() = ct->enforcement_literal(); eq->mutable_linear()->add_domain(0); eq->mutable_linear()->add_domain(0); AddLinearExpressionToLinearConstraint(target, 1, eq->mutable_linear()); + const int64_t reduced_index_fixed_value = + index.vars_size() == 0 ? index.offset() + : reduced_index_var_domain.FixedValue(); AddLinearExpressionToLinearConstraint( - ct->element().exprs(context->FixedValue(index)), -1, + ct->element().exprs(reduced_index_fixed_value), -1, eq->mutable_linear()); context->CanonicalizeLinearConstraint(eq); context->UpdateRuleStats("element: expanded with fixed index"); @@ -851,13 +1074,14 @@ void ExpandElement(ConstraintProto* ct, PresolveContext* context) { // Special case when index.var = target.var. if (index.vars_size() == 1 && target.vars_size() == 1 && index.vars(0) == target.vars(0)) { - ExpandElementWhenTargetShareVarWithIndex(ct, context); + ExpandElementWhenTargetShareVarWithIndex(ct, context, + reduced_index_var_domain); return; } // Checks if all elements are constant. bool all_constants = true; - for (const int64_t v : context->DomainOf(index.vars(0)).Values()) { + for (const int64_t v : reduced_index_var_domain.Values()) { const int64_t index_value = AffineExpressionValueAt(index, v); if (!context->IsFixed(element.exprs(index_value))) { all_constants = false; @@ -866,15 +1090,19 @@ void ExpandElement(ConstraintProto* ct, PresolveContext* context) { } if (all_constants) { - ExpandConstantArrayElement(ct, context); + ExpandConstantArrayElement(ct, context, reduced_index_var_domain); } else { - ExpandVariableElement(ct, context); + ExpandVariableElement(ct, context, reduced_index_var_domain); } } -// Adds clauses so that literals[i] true <=> encoding[values[i]] true. -// This also implicitly use the fact that exactly one alternative is true. -void LinkLiteralsAndValues(absl::Span literals, +// Adds clauses so that: +// enforcement_literals && literals[i] true => encoding[values[i]] true +// enforcement_literals => one of literals[i in I(j)] true || encoding[j] false +// where I(j) = {i | values[i] = j}. This also implicitly uses the fact that +// exactly one alternative is true. +void LinkLiteralsAndValues(absl::Span enforcement_literals, + absl::Span literals, absl::Span values, const absl::flat_hash_map& encoding, PresolveContext* context) { @@ -885,36 +1113,46 @@ void LinkLiteralsAndValues(absl::Span literals, // TODO(user): Make sure this does not appear in the profile. absl::btree_map> encoding_lit_to_support; - // If a value is false (i.e not possible), then the tuple with this - // value is false too (i.e not possible). Conversely, if the tuple is - // selected, the value must be selected. for (int i = 0; i < values.size(); ++i) { encoding_lit_to_support[encoding.at(values[i])].push_back(literals[i]); } - // If all tuples supporting a value are false, then this value must be - // false. for (const auto& [encoding_lit, support] : encoding_lit_to_support) { CHECK(!support.empty()); - if (support.size() == 1) { + if (support.size() == 1 && enforcement_literals.empty()) { if (!context->StoreBooleanEqualityRelation(encoding_lit, support[0])) { return; } } else { - BoolArgumentProto* bool_or = - context->working_model->add_constraints()->mutable_bool_or(); + // The `ct` constraint ensures that if all tuples supporting a value are + // false, then this value must be false (if the automaton constraint is + // enforced). + ConstraintProto* ct = context->working_model->add_constraints(); + *ct->mutable_enforcement_literal() = {enforcement_literals.begin(), + enforcement_literals.end()}; + BoolArgumentProto* bool_or = ct->mutable_bool_or(); bool_or->add_literals(NegatedRef(encoding_lit)); for (const int lit : support) { bool_or->add_literals(lit); - context->AddImplication(lit, encoding_lit); + // Conversely, if a tuple supporting a value is selected, this value + // must be selected (if the automaton constraint is enforced). + if (lit != encoding_lit) { + ConstraintProto* inv_ct = context->working_model->add_constraints(); + *inv_ct->mutable_enforcement_literal() = { + enforcement_literals.begin(), enforcement_literals.end()}; + inv_ct->add_enforcement_literal(lit); + inv_ct->mutable_bool_and()->add_literals(encoding_lit); + } } } } } -// Add the constraint literal => one_of(encoding[v]), for v in reachable_values. -// Note that all possible values are the ones appearing in encoding. -void AddImplyInReachableValues(int literal, +// Add the constraint enforcement_literals && literal => one_of(encoding[v]), +// for v in reachable_values. Note that all possible values are the ones +// appearing in encoding. +void AddImplyInReachableValues(absl::Span enforcement_literals, + int literal, std::vector& reachable_values, const absl::flat_hash_map encoding, PresolveContext* context) { @@ -923,6 +1161,8 @@ void AddImplyInReachableValues(int literal, if (reachable_values.size() <= encoding.size() / 2) { // Bool or encoding. ConstraintProto* ct = context->working_model->add_constraints(); + *ct->mutable_enforcement_literal() = {enforcement_literals.begin(), + enforcement_literals.end()}; ct->add_enforcement_literal(literal); BoolArgumentProto* bool_or = ct->mutable_bool_or(); for (const int64_t v : reachable_values) { @@ -933,6 +1173,8 @@ void AddImplyInReachableValues(int literal, absl::flat_hash_set set(reachable_values.begin(), reachable_values.end()); ConstraintProto* ct = context->working_model->add_constraints(); + *ct->mutable_enforcement_literal() = {enforcement_literals.begin(), + enforcement_literals.end()}; ct->add_enforcement_literal(literal); BoolArgumentProto* bool_and = ct->mutable_bool_and(); for (const auto [value, literal] : encoding) { @@ -955,11 +1197,15 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { return; } } - return (void)context->NotifyThatModelIsUnsat( + + ExpandAlwaysFalseConstraint( + ct, context, "automaton: empty with an initial state not in the final states."); + return; } else if (proto.transition_label_size() == 0) { - return (void)context->NotifyThatModelIsUnsat( - "automaton: non-empty with no transition."); + ExpandAlwaysFalseConstraint(ct, context, + "automaton: non-empty with no transition."); + return; } std::vector> reachable_states; @@ -975,6 +1221,7 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { absl::flat_hash_map in_encoding; absl::flat_hash_map out_encoding; bool removed_values = false; + EnforcedDomains enforced_domains(ct, context); const int n = proto.exprs_size(); std::vector new_state_vars; @@ -995,7 +1242,7 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { if (!reachable_states[time].contains(tail)) continue; if (!reachable_states[time + 1].contains(head)) continue; - if (!context->DomainContains(proto.exprs(time), label)) continue; + if (!enforced_domains.DomainContains(proto.exprs(time), label)) continue; still_reachable_after_domain_change.insert(head); // TODO(user): if this transition correspond to just one in-state or @@ -1014,9 +1261,9 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { // Deal with single tuple. const int num_tuples = in_states.size(); if (num_tuples == 1) { - if (!context->IntersectDomainWith(proto.exprs(time), - Domain(labels.front()))) { - VLOG(1) << "Infeasible automaton."; + if (!enforced_domains.IntersectDomainWith(proto.exprs(time), + Domain(labels.front()), + "Infeasible automaton.")) { return; } @@ -1026,7 +1273,7 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { std::vector at_false; for (const auto [value, literal] : in_encoding) { if (value != in_states[0]) { - if (!context->SetLiteralToFalse(literal)) return; + if (!enforced_domains.SetLiteralToFalse(literal)) return; } } @@ -1041,17 +1288,17 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { gtl::STLSortAndRemoveDuplicates(&transitions); encoding.clear(); - if (!context->IntersectDomainWith(expr, Domain::FromValues(transitions), - &removed_values)) { - VLOG(1) << "Infeasible automaton."; + if (!enforced_domains.IntersectDomainWith( + expr, Domain::FromValues(transitions), "Infeasible automaton.", + &removed_values)) { return; } // Fully encode the variable. // We can leave the encoding empty for fixed vars. - if (!context->IsFixed(expr)) { + if (!enforced_domains.IsFixed(expr)) { const int var = expr.vars(0); - for (const int64_t v : context->DomainOf(var).Values()) { + for (const int64_t v : enforced_domains.DomainOf(var).Values()) { encoding[AffineExpressionValueAt(expr, v)] = context->GetOrCreateVarValueEncoding(var, v); } @@ -1166,16 +1413,18 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { in_encoding.begin(), in_encoding.end()); absl::c_sort(in_to_label_pairs); for (const auto [in_value, in_literal] : in_to_label_pairs) { - AddImplyInReachableValues(in_literal, in_to_label[in_value], encoding, - context); - AddImplyInReachableValues(in_literal, in_to_out[in_value], out_encoding, - context); + AddImplyInReachableValues(ct->enforcement_literal(), in_literal, + in_to_label[in_value], encoding, context); + AddImplyInReachableValues(ct->enforcement_literal(), in_literal, + in_to_out[in_value], out_encoding, context); } - // Part2, add all 3-clauses: (in_state, label) => out_state. + // Part2, add all 3-clauses: enforcement_literal && (in_state, label) => + // out_state. for (int i = 0; i < num_tuples; ++i) { - auto* bool_or = - context->working_model->add_constraints()->mutable_bool_or(); + ConstraintProto* bool_or_ct = context->working_model->add_constraints(); + *bool_or_ct->mutable_enforcement_literal() = ct->enforcement_literal(); + auto* bool_or = bool_or_ct->mutable_bool_or(); bool_or->add_literals(NegatedRef(in_encoding.at(in_states[i]))); bool_or->add_literals(NegatedRef(encoding.at(labels[i]))); bool_or->add_literals(out_encoding.at(out_states[i])); @@ -1200,8 +1449,11 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { // Note that we do not need the ExactlyOneConstraint(tuple_literals) // because it is already implicitly encoded since we have exactly one // transition value. But adding one seems to help. - BoolArgumentProto* exactly_one = - context->working_model->add_constraints()->mutable_exactly_one(); + ConstraintProto* exactly_one_ct = + context->working_model->add_constraints(); + *exactly_one_ct->mutable_enforcement_literal() = + ct->enforcement_literal(); + BoolArgumentProto* exactly_one = exactly_one_ct->mutable_exactly_one(); for (int i = 0; i < num_tuples; ++i) { int tuple_literal; if (in_count[in_states[i]] == 1 && !in_encoding.empty()) { @@ -1222,19 +1474,24 @@ void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) { } if (!in_encoding.empty()) { - LinkLiteralsAndValues(tuple_literals, in_states, in_encoding, context); + LinkLiteralsAndValues(ct->enforcement_literal(), tuple_literals, + in_states, in_encoding, context); } if (!encoding.empty()) { - LinkLiteralsAndValues(tuple_literals, labels, encoding, context); + LinkLiteralsAndValues(ct->enforcement_literal(), tuple_literals, labels, + encoding, context); } if (!out_encoding.empty()) { - LinkLiteralsAndValues(tuple_literals, out_states, out_encoding, context); + LinkLiteralsAndValues(ct->enforcement_literal(), tuple_literals, + out_states, out_encoding, context); } in_encoding.swap(out_encoding); out_encoding.clear(); } + enforced_domains.MaybeAddEnforcedDomainConstraints(); + context->solution_crush().SetAutomatonExpandedVars(proto, new_state_vars, new_transition_vars); if (removed_values) { @@ -1965,12 +2222,13 @@ void ExpandPositiveTable(ConstraintProto* ct, PresolveContext* context) { } bool AllDiffShouldBeExpanded(const Domain& union_of_domains, - ConstraintProto* ct, PresolveContext* context) { + const ConstraintProto* ct, + PresolveContext* context) { if (union_of_domains.Size() > context->params().max_alldiff_domain_size()) { return false; } - const AllDifferentConstraintProto& proto = *ct->mutable_all_diff(); + const AllDifferentConstraintProto& proto = ct->all_diff(); const int num_exprs = proto.exprs_size(); int num_fully_encoded = 0; for (int i = 0; i < num_exprs; ++i) { @@ -2227,7 +2485,7 @@ bool IsVarEqOrNeqValue(PresolveContext* context, // terms, and with a tight domain ( == cst). // TODO(user): The above rule is complex. Revisit. void ScanModelAndDecideAllDiffExpansion( - ConstraintProto* all_diff_ct, PresolveContext* context, + const ConstraintProto* all_diff_ct, PresolveContext* context, absl::flat_hash_set& domain_of_var_is_used, absl::flat_hash_set& bounds_of_var_are_used, absl::flat_hash_set& processed_variables, bool& expand, bool& keep) { @@ -2418,8 +2676,10 @@ void MaybeExpandAllDiff(ConstraintProto* ct, PresolveContext* context, if (fixed_expression_count > 1) { // Violates the definition of AllDifferent. - return (void)context->NotifyThatModelIsUnsat(); - } else if (fixed_expression_count == 1) { + ExpandAlwaysFalseConstraint(ct, context); + return; + } else if (fixed_expression_count == 1 && + ct->enforcement_literal().empty()) { // Remove values from other domains. for (const LinearExpressionProto& expr : possible_exprs) { if (context->IsFixed(expr)) continue; @@ -2430,10 +2690,11 @@ void MaybeExpandAllDiff(ConstraintProto* ct, PresolveContext* context, } } + ConstraintProto* const new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = ct->enforcement_literal(); BoolArgumentProto* at_most_or_equal_one = - is_a_permutation - ? context->working_model->add_constraints()->mutable_exactly_one() - : context->working_model->add_constraints()->mutable_at_most_one(); + is_a_permutation ? new_ct->mutable_exactly_one() + : new_ct->mutable_at_most_one(); for (const LinearExpressionProto& expr : possible_exprs) { // The above propagation can remove a value after the expressions was // added to possible_exprs. @@ -2453,6 +2714,44 @@ void MaybeExpandAllDiff(ConstraintProto* ct, PresolveContext* context, if (!keep_after_expansion) ct->Clear(); } +void ExpandCircuit(ConstraintProto* ct, PresolveContext* context) { + // The constraints added below are added by LoadSubcircuitConstraint() when + // enforcement_literal is empty. + // TODO(user): ideally we don't want to do that here, but only create + // these constraints at loading time so that the presolve does not have to + // deal with redundant constraints. + if (ct->enforcement_literal().empty()) return; + const auto& circuit = ct->circuit(); + const int num_arcs = circuit.tails_size(); + // At this point the node indices can have arbitrary values. + absl::flat_hash_map dense_node_index; + for (int arc = 0; arc < num_arcs; arc++) { + dense_node_index.insert({circuit.tails(arc), dense_node_index.size()}); + dense_node_index.insert({circuit.heads(arc), dense_node_index.size()}); + } + const int num_nodes = dense_node_index.size(); + std::vector> exactly_one_incoming(num_nodes); + std::vector> exactly_one_outgoing(num_nodes); + for (int arc = 0; arc < num_arcs; arc++) { + const int tail = dense_node_index[circuit.tails(arc)]; + const int head = dense_node_index[circuit.heads(arc)]; + exactly_one_outgoing[tail].push_back(circuit.literals(arc)); + exactly_one_incoming[head].push_back(circuit.literals(arc)); + } + for (int i = 0; i < exactly_one_incoming.size(); ++i) { + ConstraintProto* const new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = ct->enforcement_literal(); + LiteralsToLinear(exactly_one_incoming[i], /*lb=*/1, /*ub=*/1, + new_ct->mutable_linear()); + } + for (int i = 0; i < exactly_one_outgoing.size(); ++i) { + ConstraintProto* const new_ct = context->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = ct->enforcement_literal(); + LiteralsToLinear(exactly_one_outgoing[i], /*lb=*/1, /*ub=*/1, + new_ct->mutable_linear()); + } +} + } // namespace void ExpandCpModel(PresolveContext* context) { @@ -2530,6 +2829,9 @@ void ExpandCpModel(PresolveContext* context) { has_all_diffs = true; skip = true; break; + case ConstraintProto::kCircuit: + ExpandCircuit(ct, context); + break; default: skip = true; break; @@ -2561,6 +2863,12 @@ void ExpandCpModel(PresolveContext* context) { ConstraintProto* const ct = context->working_model->mutable_constraints(i); bool skip = false; switch (ct->constraint_case()) { + case ConstraintProto::kAtMostOne: + case ConstraintProto::kExactlyOne: + // We do those in the second pass since MaybeExpandAllDiff() below may + // create such constraints. + ExpandEnforcedAtMostOneOrExactlyOneConstraint(ct, i, context); + break; case ConstraintProto::kAllDiff: MaybeExpandAllDiff(ct, context, domain_of_var_is_used, bounds_of_var_are_used, processed_variables); diff --git a/ortools/sat/cp_model_expand_test.cc b/ortools/sat/cp_model_expand_test.cc index 8114f6892ff..06004e61386 100644 --- a/ortools/sat/cp_model_expand_test.cc +++ b/ortools/sat/cp_model_expand_test.cc @@ -14,7 +14,6 @@ #include "ortools/sat/cp_model_expand.h" #include -#include #include #include "absl/container/btree_set.h" @@ -24,6 +23,7 @@ #include "ortools/base/gmock.h" #include "ortools/base/logging.h" #include "ortools/base/parse_test_proto.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_checker.h" #include "ortools/sat/cp_model_solver.h" @@ -38,6 +38,7 @@ namespace sat { namespace { using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; CpSolverResponse SolveAndCheck( const CpModelProto& initial_model, absl::string_view extra_parameters = "", @@ -48,7 +49,7 @@ CpSolverResponse SolveAndCheck( params.set_debug_crash_if_presolve_breaks_hint(true); params.set_log_search_progress(true); if (!extra_parameters.empty()) { - params.MergeFromString(extra_parameters); + params.MergeFrom(ParseTextOrDie(extra_parameters)); } auto observer = [&](const CpSolverResponse& response) { VLOG(1) << response; @@ -117,6 +118,31 @@ TEST(ReservoirExpandTest, SimpleSemaphore) { EXPECT_EQ(187, solutions.size()); } +TEST(ReservoirExpandTest, SimpleSemaphoreWithEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 3 + reservoir { + max_level: 2 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + active_literals: [ 2, 2 ] + level_changes { offset: -1 } + level_changes { offset: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(187 + 11 * 11 * 2, solutions.size()); +} + TEST(ReservoirExpandTest, GizaReport) { const CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: 0 domain: 10 } @@ -229,7 +255,7 @@ TEST(ReservoirExpandTest, RepeatedTimesWithDifferentActivationVariables) { )pb"); absl::btree_set> solutions; const CpSolverResponse response = - SolveAndCheck(initial_model, "", &solutions); + SolveAndCheck(initial_model, "cp_model_presolve:false", &solutions); EXPECT_EQ(OPTIMAL, response.status()); // First two time variables should be unconstrained giving us 3x3 solutions. EXPECT_EQ(9, solutions.size()); @@ -317,7 +343,7 @@ TEST(ReservoirExpandTest, OneUnschedulableOptionalAndInitiallyFeasible) { )pb"); absl::btree_set> solutions; const CpSolverResponse response = - SolveAndCheck(initial_model, "", &solutions); + SolveAndCheck(initial_model, "cp_model_presolve:false", &solutions); EXPECT_EQ(OPTIMAL, response.status()); EXPECT_EQ(3, solutions.size()); } @@ -412,12 +438,78 @@ TEST(ReservoirExpandTest, ExpandReservoirUsingCircuitPreservesSolutionHint) { } )pb"); - SatParameters params; - params.set_expand_reservoir_using_circuit(true); - params.set_log_search_progress(true); - params.set_debug_crash_if_presolve_breaks_hint(true); - CpSolverResponse response = SolveWithParameters(initial_model, params); - EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + absl::btree_set> solutions; + const CpSolverResponse response = SolveAndCheck( + initial_model, "expand_reservoir_using_circuit:true", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); +} + +TEST(ReservoirExpandTest, ExpandReservoirUsingCircuitWithEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + reservoir { + max_level: 2 + time_exprs { offset: 1 } + time_exprs { offset: 1 } + time_exprs { offset: 1 } + time_exprs { offset: 2 } + time_exprs { offset: 3 } + level_changes: { offset: -1 } + level_changes: { offset: -1 } + level_changes: { offset: 2 } + level_changes: { offset: -2 } + level_changes: { offset: 1 } + active_literals: 0 + active_literals: 0 + active_literals: 0 + active_literals: 1 + active_literals: 0 + } + } + solution_hint { + vars: [ 0, 1, 2 ] + values: [ 1, 0, 1 ] + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = SolveAndCheck( + initial_model, "expand_reservoir_using_circuit:true", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(2 + 2 * 2, solutions.size()); +} + +TEST(ReservoirExpandTest, ExpandReservoirUsingSumWithEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + reservoir { + max_level: 2 + time_exprs { offset: 1 } + time_exprs { offset: 1 } + time_exprs { offset: 2 } + time_exprs { offset: 2 } + level_changes: { offset: 1 } + level_changes: { offset: 2 } + level_changes: { offset: 1 } + level_changes: { offset: 2 } + active_literals: [ 0, -1, 1, -2 ] + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + // There is a single solution when the constraint is enforced. Otherwise the + // all the variable values are possible. + EXPECT_EQ(1 + 2 * 2, solutions.size()); } TEST(IntModExpandTest, FzTest) { @@ -778,6 +870,159 @@ TEST(IntProdExpansionTest, ExpandNonBinaryIntProdPreservesSolutionHint) { EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); } +TEST(ElementExpandTest, InfeasibleFixedIndex) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 4, 4 ] } + variables { domain: [ 0, 7 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 4 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 2 } + } + } + )pb"); + + EXPECT_EQ(SolveAndCheck(initial_model, "cp_model_presolve:false").status(), + CpSolverStatus::INFEASIBLE); +} + +TEST(ElementExpandTest, FixedIndex) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 2, 2 ] } + variables { domain: [ 0, 7 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 4 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 2 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); + absl::btree_set> expected{{2, 3}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(ElementExpandTest, InfeasibleFixedIndexWithEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 4, 5 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 4 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 2 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); + absl::btree_set> expected; + for (int index = 4; index <= 5; ++index) { + for (int target = 0; target <= 7; ++target) { + expected.insert({index, target, 0}); + } + } + EXPECT_EQ(found_solutions, expected); +} + +TEST(ElementExpandTest, FixedIndexWithEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 3, 5 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 4 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 2 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); + absl::btree_set> expected{{3, 2, 1}}; + for (int index = 3; index <= 5; ++index) { + for (int target = 0; target <= 7; ++target) { + expected.insert({index, target, 0}); + } + } + EXPECT_EQ(found_solutions, expected); +} + +TEST(ElementExpandTest, SharedVariable) { + // 2i+1 = {3, 5, 4, 2}[i-1] + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 7 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 offset: -1 } + linear_target { vars: 0 coeffs: 2 offset: 1 } + exprs { offset: 3 } + exprs { offset: 5 } + exprs { offset: 4 } + exprs { offset: 2 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); + absl::btree_set> expected{{1}, {2}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(ElementExpandTest, SharedVariableWithEnforcementLiteral) { + // 2i+1 = {3, 5, 4, 2}[i-1] + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 1 + element { + linear_index { vars: 0 coeffs: 1 offset: -1 } + linear_target { vars: 0 coeffs: 2 offset: 1 } + exprs { offset: 3 } + exprs { offset: 5 } + exprs { offset: 4 } + exprs { offset: 2 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); + absl::btree_set> expected{{0, 0}, {1, 0}, {2, 0}, {3, 0}, + {4, 0}, {5, 0}, {6, 0}, {7, 0}, + {1, 1}, {2, 1}}; + EXPECT_EQ(found_solutions, expected); +} + TEST(ElementExpandTest, ConstantArray) { CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ -1, 5 ] } @@ -805,6 +1050,38 @@ TEST(ElementExpandTest, ConstantArray) { EXPECT_EQ(found_solutions, expected); } +TEST(ElementExpandTest, ConstantArrayWithEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -1, 5 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 1 } + exprs { offset: 3 } + exprs { offset: 4 } + exprs { offset: 5 } + exprs { offset: 1 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{ + {0, 1, 1}, {1, 3, 1}, {2, 4, 1}, {3, 5, 1}, {4, 1, 1}}; + for (int index = -1; index <= 5; ++index) { + for (int target = 0; target <= 7; ++target) { + expected.insert({index, target, 0}); + } + } + EXPECT_EQ(found_solutions, expected); +} + TEST(AutomatonExpandTest, NonogramRule) { // Accept sequences with 3 '1', then 2 '1', then 1 '1', separated by at least // one '0'. @@ -875,6 +1152,44 @@ TEST(AutomatonExpandTest, Bug1753_1) { EXPECT_EQ(found_solutions, expected); } +TEST(AutomatonExpandTest, Bug1753WithEnforcementLiteral_1) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "0" domain: 0 domain: 2 } + variables { name: "1" domain: 0 domain: 2 } + variables { name: "2" domain: 0 domain: 2 } + variables { name: "3" domain: 0 domain: 1 } + constraints { + enforcement_literal: 3 + automaton { + starting_state: 1 + final_states: 1 + final_states: 2 + transition_tail: 1 + transition_tail: 2 + transition_head: 2 + transition_head: 1 + transition_label: 1 + transition_label: 2 + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{{1, 2, 1, 1}}; + for (int i = 0; i <= 2; ++i) { + for (int j = 0; j <= 2; ++j) { + for (int k = 0; k <= 2; ++k) { + expected.insert({i, j, k, 0}); + } + } + } + EXPECT_EQ(found_solutions, expected); +} + TEST(AutomatonExpandTest, Bug1753_2) { CpModelProto initial_model = ParseTestProto(R"pb( variables { name: "0" domain: 0 domain: 2 } @@ -921,6 +1236,59 @@ TEST(AutomatonExpandTest, Bug1753_2) { EXPECT_EQ(found_solutions, expected); } +TEST(AutomatonExpandTest, Bug1753WithEnforcementLiteral_2) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "0" domain: 0 domain: 2 } + variables { name: "1" domain: 0 domain: 2 } + variables { name: "2" domain: 0 domain: 2 } + variables { name: "3" domain: 0 domain: 1 } + constraints { linear { vars: 2 coeffs: 1 domain: 1 domain: 1 } } + constraints { + enforcement_literal: 3 + automaton { + starting_state: 1 + final_states: 1 + final_states: 2 + transition_tail: 1 + transition_tail: 0 + transition_tail: 1 + transition_tail: 2 + transition_tail: 0 + transition_tail: 2 + transition_head: 2 + transition_head: 2 + transition_head: 1 + transition_head: 1 + transition_head: 1 + transition_head: 2 + transition_label: 1 + transition_label: 1 + transition_label: 0 + transition_label: 2 + transition_label: 2 + transition_label: 0 + vars: 0 + vars: 1 + vars: 2 + } + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 0, 0, 1, 1 ] + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{{0, 0, 1, 1}, {1, 2, 1, 1}}; + for (int i = 0; i <= 2; ++i) { + for (int j = 0; j <= 2; ++j) { + expected.insert({i, j, 1, 0}); + } + } + EXPECT_EQ(found_solutions, expected); +} + TEST(AutomatonExpandTest, EverythingZero) { CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ 0, 1 ] } @@ -961,6 +1329,62 @@ TEST(AutomatonExpandTest, EverythingZero) { EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); } +TEST(AutomatonExpandTest, EverythingZeroWithEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 0 + automaton { + starting_state: 1, + final_states: [ 1 ], + transition_tail: 1, + transition_head: 1, + transition_label: 0, + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + Model model; + PresolveContext context(&model, &initial_model, nullptr); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints {} + constraints { + enforcement_literal: 0 + linear { + vars: 0 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: 0 + linear { + vars: 1 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: 0 + linear { + vars: 2 + coeffs: 1 + domain: [ 0, 0 ] + } + } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + TEST(AutomatonExpandTest, LoopingAutomatonMultipleFinalStates) { // These tuples accept "0*(12)+0*". CpModelProto initial_model = ParseTestProto(R"pb( @@ -1520,12 +1944,74 @@ TEST(ExpandAllDiffTest, Permutation) { absl::btree_set> found_solutions; const CpSolverResponse response = - SolveAndCheck(initial_model, "presolve_cp_model:false", &found_solutions); + SolveAndCheck(initial_model, "cp_model_presolve:false", &found_solutions); absl::btree_set> expected{ {0, 2, 1}, {2, 0, 1}, {1, 2, 0}, {2, 1, 0}}; EXPECT_EQ(found_solutions, expected); } +TEST(ExpandAllDiffTest, GoldenTestWithEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: -1 + all_diff { + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 offset: 1 } + exprs { vars: 3 coeffs: -1 offset: 3 } + } + })pb"); + Model model; + PresolveContext context(&model, &initial_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints {} + constraints { + enforcement_literal: -1 + linear { + vars: 1 + coeffs: -1 + domain: [ -1, 0 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 1, 2 ] + coeffs: [ 1, -1 ] + domain: [ -1, 0 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 2, 3 ] + coeffs: [ 1, 1 ] + domain: [ 0, 1 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: 3 + coeffs: -1 + domain: [ -1, 0 ] + } + } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + TEST(ExpandInverseTest, CountInvolution) { const CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ 0, 10 ] } @@ -1547,6 +2033,30 @@ TEST(ExpandInverseTest, CountInvolution) { EXPECT_EQ(4, solutions.size()); } +TEST(ExpandInverseTest, CountInvolutionWithEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 3 + inverse { + f_direct: [ 0, 1, 2 ] + f_inverse: [ 0, 1, 2 ] + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + + // On 3 elements, we either have the identity or one of the 3 two cycle (when + // the constraint is enforced; otherwise all 10^3 combinations are valid). + EXPECT_EQ(1004, solutions.size()); +} + TEST(ExpandInverseTest, DuplicateAtDifferentPosition) { const CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ 0, 10 ] } @@ -1939,6 +2449,62 @@ TEST(LinMaxExpansionTest, GoldenTest) { EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); } +TEST(LinMaxExpansionTest, GoldenTestWithEnforcementLiterals) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 3 + lin_max { + target { vars: 0 coeffs: 1 offset: 1 } + exprs { vars: 1 coeffs: 2 } + exprs: { vars: 2 coeffs: 1 offset: -3 } + } + } + )pb"); + Model model; + model.GetOrCreate()->set_max_lin_max_size_for_expansion(4); + PresolveContext context(&model, &initial_model, nullptr); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints {} + constraints { + enforcement_literal: 3 + linear { vars: 0 vars: 1 coeffs: 1 coeffs: -2 domain: -1 domain: 5 } + } + constraints { + enforcement_literal: 3 + linear { vars: 0 vars: 2 coeffs: 1 coeffs: -1 domain: -4 domain: 5 } + } + constraints { + enforcement_literal: 3 + linear { + vars: [ 4, 5 ] + coeffs: [ 1, 1 ] + domain: [ 1, 1 ] + } + } + constraints { + enforcement_literal: 4 + linear { vars: 0 vars: 1 coeffs: 1 coeffs: -2 domain: -10 domain: -1 } + } + constraints { + enforcement_literal: 5 + linear { vars: 0 vars: 2 coeffs: 1 coeffs: -1 domain: -6 domain: -4 } + } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + TEST(LinMaxExpansionTest, ExpandLinMaxPreservesSolutionHint) { CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ 1, 4 ] } diff --git a/ortools/sat/cp_model_loader.cc b/ortools/sat/cp_model_loader.cc index fcecb826709..5d923e963c1 100644 --- a/ortools/sat/cp_model_loader.cc +++ b/ortools/sat/cp_model_loader.cc @@ -1512,9 +1512,11 @@ void LoadLinearConstraint(const ConstraintProto& ct, Model* m) { void LoadAllDiffConstraint(const ConstraintProto& ct, Model* m) { auto* mapping = m->GetOrCreate(); + const std::vector enforcement_literals = + mapping->Literals(ct.enforcement_literal()); const std::vector expressions = mapping->Affines(ct.all_diff().exprs()); - m->Add(AllDifferentOnBounds(expressions)); + m->Add(AllDifferentOnBounds(enforcement_literals, expressions)); } void LoadAlwaysFalseConstraint(const ConstraintProto& ct, Model* m) { @@ -1545,11 +1547,16 @@ void LoadIntProdConstraint(const ConstraintProto& ct, Model* m) { if (prod.constant.value() != 1) { LoadAlwaysFalseConstraint(ct, m); } - } else { + } else if (enforcement_literals.empty()) { if (!integer_trail->Enqueue(prod.LowerOrEqual(1)) || !integer_trail->Enqueue(prod.GreaterOrEqual(1))) { - LoadAlwaysFalseConstraint(ct, m); + m->GetOrCreate()->NotifyThatModelIsUnsat(); } + } else { + LinearConstraintBuilder builder(m, /*lb=*/1, /*ub=*/1); + builder.AddTerm(prod, 1); + LoadConditionalLinearConstraint(enforcement_literals, builder.Build(), + m); } break; } @@ -1611,7 +1618,7 @@ void LoadIntModConstraint(const ConstraintProto& ct, Model* m) { void LoadLinMaxConstraint(const ConstraintProto& ct, Model* m) { if (ct.lin_max().exprs().empty()) { - m->GetOrCreate()->NotifyThatModelIsUnsat(); + LoadAlwaysFalseConstraint(ct, m); return; } @@ -1624,7 +1631,8 @@ void LoadLinMaxConstraint(const ConstraintProto& ct, Model* m) { NegationOf(mapping->GetExprFromProto(ct.lin_max().exprs(i)))); } // TODO(user): Consider replacing the min propagator by max. - m->Add(IsEqualToMinOf(NegationOf(max), negated_exprs)); + AddIsEqualToMinOf(mapping->Literals(ct.enforcement_literal()), + NegationOf(max), negated_exprs, m); } void LoadNoOverlapConstraint(const ConstraintProto& ct, Model* m) { @@ -1655,6 +1663,8 @@ void LoadCumulativeConstraint(const ConstraintProto& ct, Model* m) { void LoadReservoirConstraint(const ConstraintProto& ct, Model* m) { auto* mapping = m->GetOrCreate(); auto* encoder = m->GetOrCreate(); + const std::vector enforcement_literals = + mapping->Literals(ct.enforcement_literal()); const std::vector times = mapping->Affines(ct.reservoir().time_exprs()); const std::vector level_changes = @@ -1668,7 +1678,7 @@ void LoadReservoirConstraint(const ConstraintProto& ct, Model* m) { presences.push_back(encoder->GetTrueLiteral()); } } - AddReservoirConstraint(times, level_changes, presences, + AddReservoirConstraint(enforcement_literals, times, level_changes, presences, ct.reservoir().min_level(), ct.reservoir().max_level(), m); } @@ -1679,10 +1689,13 @@ void LoadCircuitConstraint(const ConstraintProto& ct, Model* m) { std::vector tails(circuit.tails().begin(), circuit.tails().end()); std::vector heads(circuit.heads().begin(), circuit.heads().end()); - std::vector literals = - m->GetOrCreate()->Literals(circuit.literals()); + auto* mapping = m->GetOrCreate(); + const std::vector enforcement_literals = + mapping->Literals(ct.enforcement_literal()); + const std::vector literals = mapping->Literals(circuit.literals()); const int num_nodes = ReindexArcs(&tails, &heads); - LoadSubcircuitConstraint(num_nodes, tails, heads, literals, m); + LoadSubcircuitConstraint(num_nodes, tails, heads, enforcement_literals, + literals, m); } void LoadRoutesConstraint(const ConstraintProto& ct, Model* m) { @@ -1694,7 +1707,8 @@ void LoadRoutesConstraint(const ConstraintProto& ct, Model* m) { std::vector literals = m->GetOrCreate()->Literals(routes.literals()); const int num_nodes = ReindexArcs(&tails, &heads); - LoadSubcircuitConstraint(num_nodes, tails, heads, literals, m, + LoadSubcircuitConstraint(num_nodes, tails, heads, /*enforcement_literals=*/{}, + literals, m, /*multiple_subcircuit_through_zero=*/true); } diff --git a/ortools/sat/cp_model_presolve.cc b/ortools/sat/cp_model_presolve.cc index b5bd658cac7..6b40c706243 100644 --- a/ortools/sat/cp_model_presolve.cc +++ b/ortools/sat/cp_model_presolve.cc @@ -318,10 +318,10 @@ bool CpModelPresolver::PresolveBoolXor(ConstraintProto* ct) { return false; } } - } - context_->UpdateNewConstraintsVariableUsage(); - context_->UpdateRuleStats("bool_xor: two active literals"); - return RemoveConstraint(ct); + context_->UpdateNewConstraintsVariableUsage(); + context_->UpdateRuleStats("bool_xor: two active literals"); + return RemoveConstraint(ct); + } // TODO(user): maybe replace the enforced XOR by an enforced equality? } if (num_true_literals % 2 == 1) { @@ -1086,6 +1086,7 @@ bool CpModelPresolver::PropagateAndReduceLinMax(ConstraintProto* ct) { bool CpModelPresolver::PresolveLinMax(int c, ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; + // TODO(user): add support for this case. if (HasEnforcementLiteral(*ct)) return false; const LinearExpressionProto& target = ct->lin_max().target(); @@ -1655,11 +1656,18 @@ bool CpModelPresolver::PresolveIntProd(ConstraintProto* ct) { return false; } context_->UpdateRuleStats("int_prod: constant product"); - return RemoveConstraint(ct); } else { - context_->UpdateRuleStats("TODO enforced int_prod: constant product"); // Replace ct with an enforced linear "target == constant_factor". + ConstraintProto* new_ct = context_->working_model->add_constraints(); + *new_ct->mutable_enforcement_literal() = ct->enforcement_literal(); + LinearConstraintProto* const lin = new_ct->mutable_linear(); + lin->add_domain(constant_factor); + lin->add_domain(constant_factor); + AddLinearExpressionToLinearConstraint(ct->int_prod().target(), 1, lin); + context_->UpdateNewConstraintsVariableUsage(); + context_->UpdateRuleStats("enforced int_prod: constant product"); } + return RemoveConstraint(ct); } // If target is fixed to zero, we can forget the constant factor. @@ -3176,7 +3184,7 @@ bool CpModelPresolver::PresolveDiophantine(ConstraintProto* ct) { // TODO(user): Make sure the newly generated linear constraint // satisfy our no-overflow precondition on the min/max activity. // We should check that the model still satisfy conditions in - // 3/ortools/sat/cp_model_checker.cc;l=165;bpv=0 + // https://source.corp.google.com/piper///depot/ortools/sat/cp_model_checker.cc;l=165;bpv=0 // Create new variables. std::vector new_variables(num_new_variables); @@ -5018,6 +5026,8 @@ bool CpModelPresolver::PresolveInterval(int c, ConstraintProto* ct) { // TODO(user): avoid code duplication between expand and presolve. bool CpModelPresolver::PresolveInverse(ConstraintProto* ct) { + // TODO(user): add support for this case. + if (HasEnforcementLiteral(*ct)) return false; const int size = ct->inverse().f_direct().size(); bool changed = false; @@ -5119,8 +5129,7 @@ bool CpModelPresolver::PresolveElement(int c, ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; if (ct->element().exprs().empty()) { - context_->UpdateRuleStats("element: empty array"); - return context_->NotifyThatModelIsUnsat(); + return MarkConstraintAsFalse(ct, "element: empty array"); } bool changed = false; @@ -5136,8 +5145,8 @@ bool CpModelPresolver::PresolveElement(int c, ConstraintProto* ct) { const LinearExpressionProto& index = ct->element().linear_index(); const LinearExpressionProto& target = ct->element().linear_target(); - // TODO(user): think about this once we do have such constraint. - if (HasEnforcementLiteral(*ct)) return false; + // TODO(user): add support for this case. + if (HasEnforcementLiteral(*ct)) return changed; // Reduce index domain from the array size. { @@ -5607,6 +5616,7 @@ class UniqueNonNegativeValue { bool CpModelPresolver::PresolveAllDiff(ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; + // TODO(user): add support for this case. if (HasEnforcementLiteral(*ct)) return false; AllDifferentConstraintProto& all_diff = *ct->mutable_all_diff(); @@ -5812,7 +5822,7 @@ void ExtractClauses(bool merge_into_bool_and, // bool_or. ConstraintProto* ct = proto->add_constraints(); if (!debug_name.empty()) { - ct->set_name(std::string(debug_name)); + ct->set_name(debug_name); } ct->mutable_bool_or()->mutable_literals()->Reserve(clause.size()); for (const Literal l : clause) { @@ -7195,6 +7205,7 @@ bool CpModelPresolver::PresolveRoutes(ConstraintProto* ct) { bool CpModelPresolver::PresolveCircuit(ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; + // TODO(user): add support for this case. if (HasEnforcementLiteral(*ct)) return false; CircuitConstraintProto& proto = *ct->mutable_circuit(); @@ -7394,6 +7405,7 @@ bool CpModelPresolver::PresolveCircuit(ConstraintProto* ct) { bool CpModelPresolver::PresolveAutomaton(ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; + // TODO(user): add support for this case. if (HasEnforcementLiteral(*ct)) return false; AutomatonConstraintProto* proto = ct->mutable_automaton(); @@ -7439,6 +7451,7 @@ bool CpModelPresolver::PresolveAutomaton(ConstraintProto* ct) { bool CpModelPresolver::PresolveReservoir(ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; + // TODO(user): add support for this case. if (HasEnforcementLiteral(*ct)) return false; ReservoirConstraintProto& proto = *ct->mutable_reservoir(); diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 7f263e2f3c4..78ed910aeea 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -119,9 +119,6 @@ ABSL_FLAG(std::string, cp_model_params, "", "This is interpreted as a text SatParameters proto. The " "specified fields will override the normal ones for all solves."); -ABSL_FLAG(bool, debug_model_copy, false, - "If true, copy the input model as if with no basic presolve"); - ABSL_FLAG(bool, cp_model_ignore_objective, false, "If true, ignore the objective."); ABSL_FLAG(bool, cp_model_ignore_hints, false, @@ -1719,8 +1716,6 @@ class LnsSolver : public SubSolver { void SolveCpModelParallel(SharedClasses* shared, Model* global_model) { const SatParameters& params = *global_model->GetOrCreate(); - CHECK(!params.enumerate_all_solutions()) - << "Enumerating all solutions in parallel is not supported."; if (global_model->GetOrCreate()->LimitReached()) return; // If specified by the user, we might disable some parameters based on their @@ -2492,10 +2487,7 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { auto context = std::make_unique(model, new_cp_model_proto, mapping_proto); - if (absl::GetFlag(FLAGS_debug_model_copy)) { - *new_cp_model_proto = model_proto; - } else if (!ImportModelWithBasicPresolveIntoContext(model_proto, - context.get())) { + if (!ImportModelWithBasicPresolveIntoContext(model_proto, context.get())) { const std::string info = "Problem proven infeasible during initial copy."; SOLVER_LOG(logger, info); CpSolverResponse status_response; diff --git a/ortools/sat/cp_model_solver_helpers.cc b/ortools/sat/cp_model_solver_helpers.cc index 9d6a72423f8..b4aeda2295c 100644 --- a/ortools/sat/cp_model_solver_helpers.cc +++ b/ortools/sat/cp_model_solver_helpers.cc @@ -2047,22 +2047,24 @@ void AdaptGlobalParameters(const CpModelProto& model_proto, Model* model) { } if (params->enumerate_all_solutions()) { - if (params->num_workers() >= 1) { + if (params->num_workers() == 0) { SOLVER_LOG(logger, - "Forcing sequential search as enumerating all solutions is " - "not supported in multi-thread."); + "Setting num_workers to 1 since it is not specified and " + "enumerate_all_solutions is true."); + params->set_num_workers(1); + } else if (params->num_workers() > 1) { + SOLVER_LOG( + logger, + "WARNING: enumerating all solutions in multi-thread works but might " + "lead to the same solution being found up to num_workers times."); } - params->set_num_workers(1); - // TODO(user): This was the old behavior, but consider switching this to - // just a warning? it might be a valid usage to enumerate all solution of - // a presolved model. - if (!params->keep_all_feasible_solutions_in_presolve()) { + if (!params->has_keep_all_feasible_solutions_in_presolve()) { SOLVER_LOG(logger, "Forcing presolve to keep all feasible solution given that " - "enumerate_all_solutions is true"); + "enumerate_all_solutions is true and that option is unset."); + params->set_keep_all_feasible_solutions_in_presolve(true); } - params->set_keep_all_feasible_solutions_in_presolve(true); } if (!model_proto.assumptions().empty()) { diff --git a/ortools/sat/cp_model_solver_test.cc b/ortools/sat/cp_model_solver_test.cc index 6a978b070db..b4509240907 100644 --- a/ortools/sat/cp_model_solver_test.cc +++ b/ortools/sat/cp_model_solver_test.cc @@ -17,6 +17,7 @@ #include #include +#include "absl/container/flat_hash_set.h" #include "absl/log/log.h" #include "absl/strings/str_join.h" #include "gtest/gtest.h" @@ -872,7 +873,7 @@ TEST(SolveCpModelTest, IntProdWithEnforcementLiteral) { } TEST(SolveCpModelTest, SquareIntProdWithEnforcementLiteral) { - // not(b) => x.y.z = 17 + // not(b) => x^2 = 17 CpModelProto model_proto = ParseTestProto(R"pb( variables { domain: [ 0, 1 ] } variables { domain: [ 2, 20 ] } @@ -891,6 +892,20 @@ TEST(SolveCpModelTest, SquareIntProdWithEnforcementLiteral) { EXPECT_EQ(response.solution(0), 1); } +TEST(SolveCpModelTest, IntProdWithEnforcementLiteralButNoTerms) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 0 + int_prod { target { vars: 0 coeffs: -4096 offset: 4096 } } + } + )pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + TEST(SolveCpModelTest, LinMaxObjectiveDomainLowerBoundInfeasible) { const CpModelProto model_proto = ParseTestProto(R"pb( variables { domain: [ 0, 5 ] } @@ -989,6 +1004,151 @@ TEST(SolveCpModelTest, LinMaxUniqueTarget) { EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); } +TEST(SolveCpModelTest, LinMaxWithEnforcementLiteral) { + // not(b) => z = max(x, y), x in [2, 5], y in [0, 4], z in [6, 9]. + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 2, 5 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 6, 9 ] } + constraints { + enforcement_literal: -1 + lin_max { + target { vars: 3 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + })pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.solution(0), 1); +} + +TEST(SolveCpModelTest, AllDifferentWithEnforcementLiteral) { + // not(b) => all_diff(x, y+1, 2-z), x,y+1,3-z in [1, 2]. + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 2 ] } + constraints { + enforcement_literal: -1 + all_diff { + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 offset: 1 } + exprs { vars: 3 coeffs: -1 offset: 3 } + } + })pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.solution(0), 1); +} + +TEST(SolveCpModelTest, FeasibleCircuitWithEnforcementLiteral) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 3 + circuit { + tails: [ 0, 1, 2, 3, 0, 2, 1 ] + heads: [ 1, 2, 3, 0, 2, 1, 3 ] + literals: [ 0, 1, 2, 3, 4, 5, 6 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + // Two feasible circuits (0, 1, 2, 3) and (0, 2, 1, 3), plus 2^6 solutions + // when the circuit constraint is not enforced. + EXPECT_EQ(count, 2 + 2 * 2 * 2 * 2 * 2 * 2); +} + +TEST(SolveCpModelTest, InfeasibleCircuitDueToMultiArcsWithEnforcementLiteral) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 2 + circuit { + tails: [ 0, 0, 1 ] + heads: [ 1, 1, 0 ] + literals: [ 0, 0, 1 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + // No feasible circuit, but 2 solutions with a false enforcement literal. + EXPECT_EQ(count, 2); +} + +TEST(SolveCpModelTest, + InfeasibleCircuitDueToMissingArcsWithEnforcementLiteral) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 4 + circuit { + tails: [ 0, 1, 0, 2 ] + heads: [ 1, 1, 2, 2 ] + literals: [ 0, 1, 2, 3 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + // No feasible circuit, but 2^4 solutions with a false enforcement literal. + EXPECT_EQ(count, 2 * 2 * 2 * 2); +} + TEST(SolveCpModelTest, HintWithCore) { const CpModelProto model_proto = ParseTestProto(R"pb( variables { domain: [ 0, 5 ] } @@ -1360,6 +1520,61 @@ TEST(SolveCpModelTest, MultipleEnforcementLiteral) { EXPECT_EQ(count, 25 + 25 + 25 + /*when enforced*/ 5); } +TEST(SolveCpModelTest, IntProdWithNonAffineExpressions) { + // z = (x-2y-1)^2 = (3-x)(1+x) + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "x" + domain: [ -5, 5 ] + } + variables { + name: "y" + domain: [ -5, 5 ] + } + variables { + name: "z" + domain: [ -5, 5 ] + } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { + vars: [ 0, 1 ] + coeffs: [ 1, -2 ] + offset: -1 + } + exprs { + vars: [ 0, 1 ] + coeffs: [ 1, -2 ] + offset: -1 + } + } + } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: -1 offset: 3 } + exprs { vars: 0 coeffs: 1 offset: 1 } + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + absl::flat_hash_set> solutions; + model.Add(NewFeasibleSolutionObserver([&solutions]( + const CpSolverResponse& response) { + solutions.insert({response.solution().begin(), response.solution().end()}); + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(solutions, + testing::UnorderedElementsAre(testing::ElementsAre(-1, -1, 0), + testing::ElementsAre(1, -1, 4), + testing::ElementsAre(1, 1, 4), + testing::ElementsAre(3, 1, 0))); +} + TEST(SolveCpModelTest, TightenedDomains) { const CpModelProto model_proto = ParseTestProto(R"pb( variables { domain: 0 domain: 10 } @@ -4959,6 +5174,84 @@ TEST(PresolveCpModelTest, CumulativeBug4) { EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); } +TEST(PresolveCpModelTest, AutomatonNonCanonical) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + automaton { + final_states: [ 3 ] + transition_tail: [ 0, 0, 1, 2, 1, 2 ] + transition_head: [ 1, 2, 1, 2, 3, 3 ] + transition_label: [ 0, 1, 0, 1, 1, 0 ] + exprs { + vars: [ 0 ] + coeffs: [ 1 ] + } + exprs { + vars: [ 1 ] + coeffs: [ 0 ] + } + exprs { + vars: [ 2 ] + coeffs: [ 1 ] + } + exprs { + vars: [ 3 ] + coeffs: [ 1 ] + } + } + })pb"); + + SatParameters params; + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + params.set_cp_model_presolve(false); + params.set_cp_model_probing_level(0); + params.set_symmetry_level(0); + + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, SolutionCrushBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + constraints { + int_mod { + target {} + exprs { offset: -1 } + exprs { vars: 1 coeffs: 1 offset: 2647 } + } + } + constraints { at_most_one {} } + floating_point_objective { vars: 0 coeffs: 1 offset: 5.12549424074614 } + )pb"); + + SatParameters params; + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + params.set_cp_model_presolve(false); + params.set_cp_model_probing_level(0); + params.set_symmetry_level(0); + + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + #endif // ORTOOLS_TARGET_OS_SUPPORTS_THREADS } // namespace diff --git a/ortools/sat/cp_model_symmetries.cc b/ortools/sat/cp_model_symmetries.cc index a93b81c0670..f45495f9b6a 100644 --- a/ortools/sat/cp_model_symmetries.cc +++ b/ortools/sat/cp_model_symmetries.cc @@ -379,7 +379,8 @@ std::unique_ptr GenerateGraphForSymmetryDetection( break; } case ConstraintProto::kAtMostOne: { - if (constraint.at_most_one().literals().size() == 2) { + if (constraint.at_most_one().literals().size() == 2 && + constraint.enforcement_literal().empty()) { // Treat it as an implication to avoid creating a node. add_implication(constraint.at_most_one().literals(0), NegatedRef(constraint.at_most_one().literals(1))); @@ -427,6 +428,7 @@ std::unique_ptr GenerateGraphForSymmetryDetection( constraint.lin_max().target(); const int target_node = make_linear_expr_node(target_expr, color); + CHECK_EQ(constraint_node, target_node); for (int i = 0; i < constraint.lin_max().exprs_size(); ++i) { const LinearExpressionProto& expr = constraint.lin_max().exprs(i); @@ -538,9 +540,13 @@ std::unique_ptr GenerateGraphForSymmetryDetection( case ConstraintProto::kCircuit: { // Note that this implementation will generate the same graph for a // circuit constraint with two disconnected components and two circuit - // constraints with one component each. + // constraints with one component each, unless there is an enforcement + // literal. const int num_arcs = constraint.circuit().literals().size(); absl::flat_hash_map circuit_node_to_symmetry_node; + if (!constraint.enforcement_literal().empty()) { + CHECK_EQ(constraint_node, new_node(color)); + } std::vector arc_color = color; arc_color.push_back(1); for (int i = 0; i < num_arcs; ++i) { @@ -558,6 +564,9 @@ std::unique_ptr GenerateGraphForSymmetryDetection( const int tail_node = circuit_node_to_symmetry_node[tail]; // To make the graph directed, we add two arcs on the head but not on // the tail. + if (!constraint.enforcement_literal().empty()) { + graph->AddArc(constraint_node, arc_node); + } graph->AddArc(tail_node, arc_node); graph->AddArc(arc_node, get_literal_node(literal)); graph->AddArc(arc_node, head_node); @@ -583,6 +592,9 @@ std::unique_ptr GenerateGraphForSymmetryDetection( // part. This way we can reuse the same get_literal_node() function. if (constraint.constraint_case() != ConstraintProto::kBoolAnd || constraint.enforcement_literal().size() > 1) { + if (!constraint.enforcement_literal().empty()) { + CHECK_LT(constraint_node, initial_equivalence_classes->size()); + } for (const int ref : constraint.enforcement_literal()) { graph->AddArc(constraint_node, get_literal_node(ref)); } diff --git a/ortools/sat/cp_model_utils.cc b/ortools/sat/cp_model_utils.cc index 6af621ec36b..fb49d7f7980 100644 --- a/ortools/sat/cp_model_utils.cc +++ b/ortools/sat/cp_model_utils.cc @@ -678,6 +678,24 @@ void AddWeightedLiteralToLinearConstraint(int lit, int64_t coeff, } } +void LiteralsToLinear(absl::Span literals, int64_t lb, int64_t ub, + LinearConstraintProto* linear) { + linear->Clear(); + for (const int lit : literals) { + if (RefIsPositive(lit)) { + linear->add_vars(lit); + linear->add_coeffs(1); + } else { + linear->add_vars(NegatedRef(lit)); + linear->add_coeffs(-1); + lb -= 1; + ub -= 1; + } + } + linear->add_domain(lb); + linear->add_domain(ub); +} + bool SafeAddLinearExpressionToLinearConstraint( const LinearExpressionProto& expr, int64_t coefficient, LinearConstraintProto* linear) { diff --git a/ortools/sat/cp_model_utils.h b/ortools/sat/cp_model_utils.h index e00d556306e..c0ae1925988 100644 --- a/ortools/sat/cp_model_utils.h +++ b/ortools/sat/cp_model_utils.h @@ -145,6 +145,22 @@ void FillDomainInProto(const Domain& domain, ProtoWithDomain* proto) { } } +template +void FillDomainInProto(int64_t lb, int64_t ub, ProtoWithDomain* proto) { + proto->clear_domain(); + proto->mutable_domain()->Reserve(2); + proto->add_domain(lb); + proto->add_domain(ub); +} + +template +void FillDomainInProto(int64_t value, ProtoWithDomain* proto) { + proto->clear_domain(); + proto->mutable_domain()->Reserve(2); + proto->add_domain(value); + proto->add_domain(value); +} + // Reads a Domain from the domain field of a proto. template Domain ReadDomainFromProto(const ProtoWithDomain& proto) { @@ -261,6 +277,10 @@ void AddWeightedLiteralToLinearConstraint(int lit, int64_t coeff, LinearConstraintProto* linear, int64_t* offset); +// Sets `linear` to the constraint "lb <= sum(`literals`) <= ub". +void LiteralsToLinear(absl::Span literals, int64_t lb, int64_t ub, + LinearConstraintProto* linear); + // Same method, but returns if the addition was possible without overflowing. bool SafeAddLinearExpressionToLinearConstraint( const LinearExpressionProto& expr, int64_t coefficient, diff --git a/ortools/sat/cumulative.cc b/ortools/sat/cumulative.cc index 8cde3428c07..cc41ced4b4d 100644 --- a/ortools/sat/cumulative.cc +++ b/ortools/sat/cumulative.cc @@ -411,8 +411,8 @@ std::function CumulativeUsingReservoir( presences.push_back(encoder->GetTrueLiteral()); } } - AddReservoirConstraint(times, deltas, presences, 0, fixed_capacity.value(), - model); + AddReservoirConstraint(/*enforcement_literals=*/{}, times, deltas, + presences, 0, fixed_capacity.value(), model); }; } diff --git a/ortools/sat/disjunctive.cc b/ortools/sat/disjunctive.cc index 869cf1b07e3..c3934dfb34b 100644 --- a/ortools/sat/disjunctive.cc +++ b/ortools/sat/disjunctive.cc @@ -32,6 +32,7 @@ #include "ortools/sat/precedences.h" #include "ortools/sat/sat_base.h" #include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/scheduling_helpers.h" #include "ortools/sat/timetable.h" #include "ortools/util/scheduling.h" #include "ortools/util/sort.h" @@ -66,7 +67,7 @@ void AddDisjunctive(const std::vector& intervals, for (const IntervalVariable interval : intervals) { starts.push_back(repository->Start(interval)); } - model->Add(AllDifferentOnBounds(starts)); + model->Add(AllDifferentOnBounds(/*enforcement_literals=*/{}, starts)); return; } @@ -263,6 +264,9 @@ bool DisjunctiveWithTwoItems::Propagate() { // We can't propagate anything if one of the interval is absent for sure. if (helper_->IsAbsent(0) || helper_->IsAbsent(1)) return true; + // We can't propagate anything for two intervals if neither are present. + if (!helper_->IsPresent(0) && !helper_->IsPresent(0)) return true; + // Note that this propagation also take care of the "overload checker" part. // It also propagates as much as possible, even in the presence of task with // variable sizes. @@ -274,8 +278,8 @@ bool DisjunctiveWithTwoItems::Propagate() { int task_before = 0; int task_after = 1; - const bool task_0_before_task_1 = helper_->StartMax(0) < helper_->EndMin(1); - const bool task_1_before_task_0 = helper_->StartMax(1) < helper_->EndMin(0); + const bool task_0_before_task_1 = helper_->TaskIsBeforeOrIsOverlapping(0, 1); + const bool task_1_before_task_0 = helper_->TaskIsBeforeOrIsOverlapping(1, 0); if (task_0_before_task_1 && task_1_before_task_0 && helper_->IsPresent(task_before) && helper_->IsPresent(task_after)) { @@ -296,40 +300,11 @@ bool DisjunctiveWithTwoItems::Propagate() { return true; } - if (helper_->IsPresent(task_before)) { - const IntegerValue end_min_before = helper_->EndMin(task_before); - if (helper_->StartMin(task_after) < end_min_before) { - // Reason for precedences if both present. - helper_->ClearReason(); - helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, - task_after); - - // Reason for the bound push. - helper_->AddPresenceReason(task_before); - helper_->AddEndMinReason(task_before, end_min_before); - if (!helper_->IncreaseStartMin(task_after, end_min_before)) { - return false; - } - } - } - - if (helper_->IsPresent(task_after)) { - const IntegerValue start_max_after = helper_->StartMax(task_after); - if (helper_->EndMax(task_before) > start_max_after) { - // Reason for precedences if both present. - helper_->ClearReason(); - helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, - task_after); - - // Reason for the bound push. - helper_->AddPresenceReason(task_after); - helper_->AddStartMaxReason(task_after, start_max_after); - if (!helper_->DecreaseEndMax(task_before, start_max_after)) { - return false; - } - } + helper_->ClearReason(); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, task_after); + if (!helper_->PushTaskOrderWhenPresent(task_before, task_after)) { + return false; } - return true; } @@ -767,16 +742,8 @@ bool DisjunctiveSimplePrecedences::Push(TaskTime before, int t) { DCHECK_NE(t_before, t); helper_->ClearReason(); - helper_->AddPresenceReason(t_before); helper_->AddReasonForBeingBeforeAssumingNoOverlap(t_before, t); - helper_->AddEndMinReason(t_before, before.time); - if (!helper_->IncreaseStartMin(t, before.time)) { - return false; - } - if (helper_->CurrentDecisionLevel() == 0 && helper_->IsPresent(t_before) && - helper_->IsPresent(t)) { - if (!helper_->NotifyLevelZeroPrecedence(t_before, t)) return false; - } + if (!helper_->PushTaskOrderWhenPresent(t_before, t)) return false; ++stats_.num_propagations; return true; } diff --git a/ortools/sat/disjunctive.h b/ortools/sat/disjunctive.h index 442c5ebfb63..09d28fc51ec 100644 --- a/ortools/sat/disjunctive.h +++ b/ortools/sat/disjunctive.h @@ -27,9 +27,9 @@ #include "absl/types/span.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" -#include "ortools/sat/intervals.h" #include "ortools/sat/model.h" #include "ortools/sat/precedences.h" +#include "ortools/sat/scheduling_helpers.h" #include "ortools/sat/synchronization.h" #include "ortools/sat/util.h" #include "ortools/util/scheduling.h" diff --git a/ortools/sat/fuzz_testdata/AtMostOneModel b/ortools/sat/fuzz_testdata/AtMostOneModel index 27e246669d5..89a9e1fa799 100644 --- a/ortools/sat/fuzz_testdata/AtMostOneModel +++ b/ortools/sat/fuzz_testdata/AtMostOneModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 1 ] } diff --git a/ortools/sat/fuzz_testdata/AutomatonModel b/ortools/sat/fuzz_testdata/AutomatonModel index 9a5254cb9b8..266fc2aa3ab 100644 --- a/ortools/sat/fuzz_testdata/AutomatonModel +++ b/ortools/sat/fuzz_testdata/AutomatonModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: 0 domain: 1 } diff --git a/ortools/sat/fuzz_testdata/BadHintWithCore b/ortools/sat/fuzz_testdata/BadHintWithCore index a80bf4a5bde..0f8918f1124 100644 --- a/ortools/sat/fuzz_testdata/BadHintWithCore +++ b/ortools/sat/fuzz_testdata/BadHintWithCore @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/CircuitModel b/ortools/sat/fuzz_testdata/CircuitModel index 8c9a7bf3235..4017ee238ee 100644 --- a/ortools/sat/fuzz_testdata/CircuitModel +++ b/ortools/sat/fuzz_testdata/CircuitModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 1 ] } # 0->1 diff --git a/ortools/sat/fuzz_testdata/DualConnectedComponentsModel b/ortools/sat/fuzz_testdata/DualConnectedComponentsModel index 9800ba1fb16..3bc1648dcd9 100644 --- a/ortools/sat/fuzz_testdata/DualConnectedComponentsModel +++ b/ortools/sat/fuzz_testdata/DualConnectedComponentsModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/ElementModel b/ortools/sat/fuzz_testdata/ElementModel index 99f98c8134a..6595db29790 100644 --- a/ortools/sat/fuzz_testdata/ElementModel +++ b/ortools/sat/fuzz_testdata/ElementModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 5 ] } diff --git a/ortools/sat/fuzz_testdata/EnumerateAllSolutions b/ortools/sat/fuzz_testdata/EnumerateAllSolutions index 73065fe184f..10729fd5a1f 100644 --- a/ortools/sat/fuzz_testdata/EnumerateAllSolutions +++ b/ortools/sat/fuzz_testdata/EnumerateAllSolutions @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/EnumerateAllSolutionsBis b/ortools/sat/fuzz_testdata/EnumerateAllSolutionsBis index 9fb226f29aa..c64eaea7633 100644 --- a/ortools/sat/fuzz_testdata/EnumerateAllSolutionsBis +++ b/ortools/sat/fuzz_testdata/EnumerateAllSolutionsBis @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/EnumerateAllSolutionsOfEmptyModel b/ortools/sat/fuzz_testdata/EnumerateAllSolutionsOfEmptyModel index c362ee8e5bc..8861fa9eb4a 100644 --- a/ortools/sat/fuzz_testdata/EnumerateAllSolutionsOfEmptyModel +++ b/ortools/sat/fuzz_testdata/EnumerateAllSolutionsOfEmptyModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/ExactlyOneModel b/ortools/sat/fuzz_testdata/ExactlyOneModel index 74218fabdcd..07e84d9cf95 100644 --- a/ortools/sat/fuzz_testdata/ExactlyOneModel +++ b/ortools/sat/fuzz_testdata/ExactlyOneModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 1 ] } diff --git a/ortools/sat/fuzz_testdata/FixedBoxes b/ortools/sat/fuzz_testdata/FixedBoxes index 9d0a96100bc..55f0022938c 100644 --- a/ortools/sat/fuzz_testdata/FixedBoxes +++ b/ortools/sat/fuzz_testdata/FixedBoxes @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [0, 41] } diff --git a/ortools/sat/fuzz_testdata/HintWithCore b/ortools/sat/fuzz_testdata/HintWithCore index c7c051ed10f..0756a7ed7f4 100644 --- a/ortools/sat/fuzz_testdata/HintWithCore +++ b/ortools/sat/fuzz_testdata/HintWithCore @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/IntProdModel b/ortools/sat/fuzz_testdata/IntProdModel index 76705062fe7..56d9bbe1828 100644 --- a/ortools/sat/fuzz_testdata/IntProdModel +++ b/ortools/sat/fuzz_testdata/IntProdModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/InverseModel b/ortools/sat/fuzz_testdata/InverseModel index 85c9225a9d8..ec553db64bb 100644 --- a/ortools/sat/fuzz_testdata/InverseModel +++ b/ortools/sat/fuzz_testdata/InverseModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 3 ] } diff --git a/ortools/sat/fuzz_testdata/LinMaxModel b/ortools/sat/fuzz_testdata/LinMaxModel index ce56cec764b..0c014274326 100644 --- a/ortools/sat/fuzz_testdata/LinMaxModel +++ b/ortools/sat/fuzz_testdata/LinMaxModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 1 ] } diff --git a/ortools/sat/fuzz_testdata/MultipleCumulativesA b/ortools/sat/fuzz_testdata/MultipleCumulativesA index 72f6445312a..a2f4eb99bab 100644 --- a/ortools/sat/fuzz_testdata/MultipleCumulativesA +++ b/ortools/sat/fuzz_testdata/MultipleCumulativesA @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [0, 1024] } diff --git a/ortools/sat/fuzz_testdata/MultipleCumulativesB b/ortools/sat/fuzz_testdata/MultipleCumulativesB index 91ad2be6696..9ef2c432d34 100644 --- a/ortools/sat/fuzz_testdata/MultipleCumulativesB +++ b/ortools/sat/fuzz_testdata/MultipleCumulativesB @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [0, 1024] } diff --git a/ortools/sat/fuzz_testdata/MultipleEnforcementLiteral b/ortools/sat/fuzz_testdata/MultipleEnforcementLiteral index ac3446011d2..7e212c28ee9 100644 --- a/ortools/sat/fuzz_testdata/MultipleEnforcementLiteral +++ b/ortools/sat/fuzz_testdata/MultipleEnforcementLiteral @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/NoOverlap2DOptimization b/ortools/sat/fuzz_testdata/NoOverlap2DOptimization index db846c7b19a..1bd03e76777 100644 --- a/ortools/sat/fuzz_testdata/NoOverlap2DOptimization +++ b/ortools/sat/fuzz_testdata/NoOverlap2DOptimization @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables: { diff --git a/ortools/sat/fuzz_testdata/NonInstantiatedVariables b/ortools/sat/fuzz_testdata/NonInstantiatedVariables index 29bff4ad1b1..698ebcc87c7 100644 --- a/ortools/sat/fuzz_testdata/NonInstantiatedVariables +++ b/ortools/sat/fuzz_testdata/NonInstantiatedVariables @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/ObjectiveDomainLowerBound b/ortools/sat/fuzz_testdata/ObjectiveDomainLowerBound index 2b41ef772eb..8ffac89573d 100644 --- a/ortools/sat/fuzz_testdata/ObjectiveDomainLowerBound +++ b/ortools/sat/fuzz_testdata/ObjectiveDomainLowerBound @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/PureSatProblem b/ortools/sat/fuzz_testdata/PureSatProblem index 2fb98665246..39ddf02d753 100644 --- a/ortools/sat/fuzz_testdata/PureSatProblem +++ b/ortools/sat/fuzz_testdata/PureSatProblem @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/PureSatProblemWithLimit b/ortools/sat/fuzz_testdata/PureSatProblemWithLimit index 0ec5cc0fc7e..5b057393fb2 100644 --- a/ortools/sat/fuzz_testdata/PureSatProblemWithLimit +++ b/ortools/sat/fuzz_testdata/PureSatProblemWithLimit @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/ReservoirModel b/ortools/sat/fuzz_testdata/ReservoirModel index 9acc7cbc30f..1e1061a424b 100644 --- a/ortools/sat/fuzz_testdata/ReservoirModel +++ b/ortools/sat/fuzz_testdata/ReservoirModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [ 0, 2 ] } diff --git a/ortools/sat/fuzz_testdata/RoutingModel b/ortools/sat/fuzz_testdata/RoutingModel index 0a3953d1486..fafbd44137c 100644 --- a/ortools/sat/fuzz_testdata/RoutingModel +++ b/ortools/sat/fuzz_testdata/RoutingModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { domain: [0, 1] } diff --git a/ortools/sat/fuzz_testdata/SimpleCumulative b/ortools/sat/fuzz_testdata/SimpleCumulative index d937e2e97a9..d20e3ceb836 100644 --- a/ortools/sat/fuzz_testdata/SimpleCumulative +++ b/ortools/sat/fuzz_testdata/SimpleCumulative @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SimpleInterval b/ortools/sat/fuzz_testdata/SimpleInterval index b566ec24a14..e7af34f821a 100644 --- a/ortools/sat/fuzz_testdata/SimpleInterval +++ b/ortools/sat/fuzz_testdata/SimpleInterval @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SimpleLinearExampleWithMaximize b/ortools/sat/fuzz_testdata/SimpleLinearExampleWithMaximize index 4bb5b29aba2..0a845ea4b8f 100644 --- a/ortools/sat/fuzz_testdata/SimpleLinearExampleWithMaximize +++ b/ortools/sat/fuzz_testdata/SimpleLinearExampleWithMaximize @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SimpleOptionalIntervalFeasible b/ortools/sat/fuzz_testdata/SimpleOptionalIntervalFeasible index a3e26b240c5..4581d9fe484 100644 --- a/ortools/sat/fuzz_testdata/SimpleOptionalIntervalFeasible +++ b/ortools/sat/fuzz_testdata/SimpleOptionalIntervalFeasible @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SimpleOptionalIntervalInfeasible b/ortools/sat/fuzz_testdata/SimpleOptionalIntervalInfeasible index 604b1980971..c12ca534022 100644 --- a/ortools/sat/fuzz_testdata/SimpleOptionalIntervalInfeasible +++ b/ortools/sat/fuzz_testdata/SimpleOptionalIntervalInfeasible @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SmallDualConnectedComponentsModel b/ortools/sat/fuzz_testdata/SmallDualConnectedComponentsModel index 338e156655c..607b55eef34 100644 --- a/ortools/sat/fuzz_testdata/SmallDualConnectedComponentsModel +++ b/ortools/sat/fuzz_testdata/SmallDualConnectedComponentsModel @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SolutionHintBasicTest b/ortools/sat/fuzz_testdata/SolutionHintBasicTest index cf32fe066de..88999ad985c 100644 --- a/ortools/sat/fuzz_testdata/SolutionHintBasicTest +++ b/ortools/sat/fuzz_testdata/SolutionHintBasicTest @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SolutionHintEnumerateTest b/ortools/sat/fuzz_testdata/SolutionHintEnumerateTest index d31cf002ecb..cf2b1a6ff7c 100644 --- a/ortools/sat/fuzz_testdata/SolutionHintEnumerateTest +++ b/ortools/sat/fuzz_testdata/SolutionHintEnumerateTest @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SolutionHintObjectiveTest b/ortools/sat/fuzz_testdata/SolutionHintObjectiveTest index 555d4451bfb..ffa2d7aa960 100644 --- a/ortools/sat/fuzz_testdata/SolutionHintObjectiveTest +++ b/ortools/sat/fuzz_testdata/SolutionHintObjectiveTest @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SolutionHintOptimalObjectiveTest b/ortools/sat/fuzz_testdata/SolutionHintOptimalObjectiveTest index 4e2dc624247..f83002be03f 100644 --- a/ortools/sat/fuzz_testdata/SolutionHintOptimalObjectiveTest +++ b/ortools/sat/fuzz_testdata/SolutionHintOptimalObjectiveTest @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/SolutionsAreCorrectlyPostsolvedInTheObserver b/ortools/sat/fuzz_testdata/SolutionsAreCorrectlyPostsolvedInTheObserver index e1d3ba8908d..5f603ddc187 100644 --- a/ortools/sat/fuzz_testdata/SolutionsAreCorrectlyPostsolvedInTheObserver +++ b/ortools/sat/fuzz_testdata/SolutionsAreCorrectlyPostsolvedInTheObserver @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/TableProblem b/ortools/sat/fuzz_testdata/TableProblem index 463d98f6c8f..a3ddc6329d0 100644 --- a/ortools/sat/fuzz_testdata/TableProblem +++ b/ortools/sat/fuzz_testdata/TableProblem @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/TightenedDomains b/ortools/sat/fuzz_testdata/TightenedDomains index 3df0ce979ec..fde0986105c 100644 --- a/ortools/sat/fuzz_testdata/TightenedDomains +++ b/ortools/sat/fuzz_testdata/TightenedDomains @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/TightenedDomainsIfInfeasible b/ortools/sat/fuzz_testdata/TightenedDomainsIfInfeasible index 8ad97046883..037185934d1 100644 --- a/ortools/sat/fuzz_testdata/TightenedDomainsIfInfeasible +++ b/ortools/sat/fuzz_testdata/TightenedDomainsIfInfeasible @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/TrivialModelWithCore b/ortools/sat/fuzz_testdata/TrivialModelWithCore index d18325dc55a..0426a32e8ce 100644 --- a/ortools/sat/fuzz_testdata/TrivialModelWithCore +++ b/ortools/sat/fuzz_testdata/TrivialModelWithCore @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/fuzz_testdata/UnsatProblem b/ortools/sat/fuzz_testdata/UnsatProblem index 9390f9a6de0..50777232ef7 100644 --- a/ortools/sat/fuzz_testdata/UnsatProblem +++ b/ortools/sat/fuzz_testdata/UnsatProblem @@ -1,4 +1,4 @@ -# proto-file: ortools/sat/cp_model.proto +# proto-file: util/operations_research/sat/cp_model.proto # proto-message: operations_research.sat.CpModelProto variables { diff --git a/ortools/sat/integer.cc b/ortools/sat/integer.cc index 076ea5810e4..e8597b26921 100644 --- a/ortools/sat/integer.cc +++ b/ortools/sat/integer.cc @@ -1397,6 +1397,18 @@ void IntegerTrail::EnqueueLiteral( EnqueueLiteralInternal(literal, false, literal_reason, integer_reason); } +bool IntegerTrail::SafeEnqueueLiteral( + Literal literal, absl::Span literal_reason, + absl::Span integer_reason) { + if (trail_->Assignment().LiteralIsTrue(literal)) { + return true; + } else if (trail_->Assignment().LiteralIsFalse(literal)) { + return ReportConflict(literal_reason, integer_reason); + } + EnqueueLiteralInternal(literal, false, literal_reason, integer_reason); + return true; +} + void IntegerTrail::EnqueueLiteralInternal( Literal literal, bool use_lazy_reason, absl::Span literal_reason, diff --git a/ortools/sat/integer.h b/ortools/sat/integer.h index c127aa6be54..ad6fe2ae42b 100644 --- a/ortools/sat/integer.h +++ b/ortools/sat/integer.h @@ -689,6 +689,10 @@ class IntegerTrail final : public SatPropagator { void EnqueueLiteral(Literal literal, absl::Span literal_reason, absl::Span integer_reason); + bool SafeEnqueueLiteral(Literal literal, + absl::Span literal_reason, + absl::Span integer_reason); + // Returns the reason (as set of Literal currently false) for a given integer // literal. Note that the bound must be less restrictive than the current // bound (checked). diff --git a/ortools/sat/integer_expr.cc b/ortools/sat/integer_expr.cc index 498a2152fb5..2d655afdca0 100644 --- a/ortools/sat/integer_expr.cc +++ b/ortools/sat/integer_expr.cc @@ -644,20 +644,28 @@ void MinPropagator::RegisterWith(GenericLiteralWatcher* watcher) { watcher->WatchUpperBound(min_var_, id); } -LinMinPropagator::LinMinPropagator(std::vector exprs, - IntegerVariable min_var, Model* model) +GreaterThanMinOfExprsPropagator::GreaterThanMinOfExprsPropagator( + absl::Span enforcement_literals, + std::vector exprs, IntegerVariable min_var, Model* model) : exprs_(std::move(exprs)), min_var_(min_var), model_(model), - integer_trail_(model_->GetOrCreate()) {} + integer_trail_(*model_->GetOrCreate()), + enforcement_propagator_(*model_->GetOrCreate()) { + GenericLiteralWatcher* watcher = model->GetOrCreate(); + enforcement_id_ = enforcement_propagator_.Register( + enforcement_literals, watcher, RegisterWith(watcher)); +} -void LinMinPropagator::Explain(int id, IntegerValue propagation_slack, - IntegerVariable var_to_explain, int trail_index, - std::vector* literals_reason, - std::vector* trail_indices_reason) { +void GreaterThanMinOfExprsPropagator::Explain( + int id, IntegerValue propagation_slack, IntegerVariable var_to_explain, + int trail_index, std::vector* literals_reason, + std::vector* trail_indices_reason) { const auto& vars = exprs_[id].vars; const auto& coeffs = exprs_[id].coeffs; literals_reason->clear(); + enforcement_propagator_.AddEnforcementReason(enforcement_id_, + literals_reason); trail_indices_reason->clear(); std::vector reason_coeffs; const int size = vars.size(); @@ -667,7 +675,7 @@ void LinMinPropagator::Explain(int id, IntegerValue propagation_slack, continue; } const int index = - integer_trail_->FindTrailIndexOfVarBefore(var, trail_index); + integer_trail_.FindTrailIndexOfVarBefore(var, trail_index); if (index >= 0) { trail_indices_reason->push_back(index); if (propagation_slack > 0) { @@ -676,20 +684,20 @@ void LinMinPropagator::Explain(int id, IntegerValue propagation_slack, } } if (propagation_slack > 0) { - integer_trail_->RelaxLinearReason(propagation_slack, reason_coeffs, - trail_indices_reason); + integer_trail_.RelaxLinearReason(propagation_slack, reason_coeffs, + trail_indices_reason); } // Now add the old integer_reason that triggered this propagation. for (IntegerLiteral reason_lit : integer_reason_for_unique_candidate_) { const int index = - integer_trail_->FindTrailIndexOfVarBefore(reason_lit.var, trail_index); + integer_trail_.FindTrailIndexOfVarBefore(reason_lit.var, trail_index); if (index >= 0) { trail_indices_reason->push_back(index); } } } -bool LinMinPropagator::PropagateLinearUpperBound( +bool GreaterThanMinOfExprsPropagator::PropagateLinearUpperBound( int id, absl::Span vars, absl::Span coeffs, const IntegerValue upper_bound) { IntegerValue sum_lb = IntegerValue(0); @@ -700,8 +708,8 @@ bool LinMinPropagator::PropagateLinearUpperBound( const IntegerValue coeff = coeffs[i]; // The coefficients are assumed to be positive for this to work properly. DCHECK_GE(coeff, 0); - const IntegerValue lb = integer_trail_->LowerBound(var); - const IntegerValue ub = integer_trail_->UpperBound(var); + const IntegerValue lb = integer_trail_.LowerBound(var); + const IntegerValue ub = integer_trail_.UpperBound(var); max_variations_[i] = (ub - lb) * coeff; sum_lb += lb * coeff; } @@ -709,6 +717,8 @@ bool LinMinPropagator::PropagateLinearUpperBound( model_->GetOrCreate()->AdvanceDeterministicTime( static_cast(num_vars) * 1e-9); + const EnforcementStatus status = + enforcement_propagator_.Status(enforcement_id_); const IntegerValue slack = upper_bound - sum_lb; if (slack < 0) { // Conflict. @@ -716,30 +726,37 @@ bool LinMinPropagator::PropagateLinearUpperBound( reason_coeffs_.clear(); for (int i = 0; i < num_vars; ++i) { const IntegerVariable var = vars[i]; - if (!integer_trail_->VariableLowerBoundIsFromLevelZero(var)) { - local_reason_.push_back(integer_trail_->LowerBoundAsLiteral(var)); + if (!integer_trail_.VariableLowerBoundIsFromLevelZero(var)) { + local_reason_.push_back(integer_trail_.LowerBoundAsLiteral(var)); reason_coeffs_.push_back(coeffs[i]); } } - integer_trail_->RelaxLinearReason(-slack - 1, reason_coeffs_, - &local_reason_); + integer_trail_.RelaxLinearReason(-slack - 1, reason_coeffs_, + &local_reason_); local_reason_.insert(local_reason_.end(), integer_reason_for_unique_candidate_.begin(), integer_reason_for_unique_candidate_.end()); - return integer_trail_->ReportConflict({}, local_reason_); + if (status == EnforcementStatus::IS_ENFORCED) { + return enforcement_propagator_.ReportConflict(enforcement_id_, + local_reason_); + } else { + return enforcement_propagator_.PropagateWhenFalse( + enforcement_id_, /*literal_reason=*/{}, local_reason_); + } } // The lower bound of all the variables except one can be used to update the // upper bound of the last one. + if (status != EnforcementStatus::IS_ENFORCED) return true; for (int i = 0; i < num_vars; ++i) { if (max_variations_[i] <= slack) continue; const IntegerVariable var = vars[i]; const IntegerValue coeff = coeffs[i]; const IntegerValue div = slack / coeff; - const IntegerValue new_ub = integer_trail_->LowerBound(var) + div; + const IntegerValue new_ub = integer_trail_.LowerBound(var) + div; const IntegerValue propagation_slack = (div + 1) * coeff - slack - 1; - if (!integer_trail_->EnqueueWithLazyReason( + if (!integer_trail_.EnqueueWithLazyReason( IntegerLiteral::LowerOrEqual(var, new_ub), id, propagation_slack, this)) { return false; @@ -748,19 +765,27 @@ bool LinMinPropagator::PropagateLinearUpperBound( return true; } -bool LinMinPropagator::Propagate() { - if (exprs_.empty()) return true; +bool GreaterThanMinOfExprsPropagator::Propagate() { + // The case of empty exprs is handled in cp_model_loader.cc. + DCHECK(!exprs_.empty()); + + const EnforcementStatus status = + enforcement_propagator_.Status(enforcement_id_); + if (status != EnforcementStatus::IS_ENFORCED && + status != EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { + return true; + } // Count the number of interval that are possible candidate for the min. // Only the intervals for which lb > current_min_ub cannot. - const IntegerValue current_min_ub = integer_trail_->UpperBound(min_var_); + const IntegerValue current_min_ub = integer_trail_.UpperBound(min_var_); int num_intervals_that_can_be_min = 0; int last_possible_min_interval = 0; expr_lbs_.clear(); IntegerValue min_of_linear_expression_lb = kMaxIntegerValue; for (int i = 0; i < exprs_.size(); ++i) { - const IntegerValue lb = exprs_[i].Min(*integer_trail_); + const IntegerValue lb = exprs_[i].Min(integer_trail_); expr_lbs_.push_back(lb); min_of_linear_expression_lb = std::min(min_of_linear_expression_lb, lb); if (lb <= current_min_ub) { @@ -776,17 +801,28 @@ bool LinMinPropagator::Propagate() { if (min_of_linear_expression_lb > current_min_ub) { min_of_linear_expression_lb = current_min_ub + 1; } - if (min_of_linear_expression_lb > integer_trail_->LowerBound(min_var_)) { + if (min_of_linear_expression_lb > integer_trail_.LowerBound(min_var_)) { local_reason_.clear(); for (int i = 0; i < exprs_.size(); ++i) { const IntegerValue slack = expr_lbs_[i] - min_of_linear_expression_lb; - integer_trail_->AppendRelaxedLinearReason(slack, exprs_[i].coeffs, - exprs_[i].vars, &local_reason_); + integer_trail_.AppendRelaxedLinearReason(slack, exprs_[i].coeffs, + exprs_[i].vars, &local_reason_); } - if (!integer_trail_->Enqueue(IntegerLiteral::GreaterOrEqual( - min_var_, min_of_linear_expression_lb), - {}, local_reason_)) { - return false; + if (status == EnforcementStatus::IS_ENFORCED) { + if (!enforcement_propagator_.SafeEnqueue( + enforcement_id_, + IntegerLiteral::GreaterOrEqual(min_var_, + min_of_linear_expression_lb), + local_reason_)) { + return false; + } + } else if (min_of_linear_expression_lb > current_min_ub) { + // If the upper bound of min_var_ is strictly smaller than the lower bound + // of all the expressions, then the enforcement must be false. + local_reason_.push_back( + IntegerLiteral::LowerOrEqual(min_var_, current_min_ub)); + return enforcement_propagator_.PropagateWhenFalse( + enforcement_id_, /*literal_reason=*/{}, local_reason_); } } @@ -796,7 +832,7 @@ bool LinMinPropagator::Propagate() { // In this case, ub(min) >= ub(e). if (num_intervals_that_can_be_min == 1) { const IntegerValue ub_of_only_candidate = - exprs_[last_possible_min_interval].Max(*integer_trail_); + exprs_[last_possible_min_interval].Max(integer_trail_); if (current_min_ub < ub_of_only_candidate) { // For this propagation, we only need to fill the integer reason once at // the lowest level. At higher levels this reason still remains valid. @@ -806,11 +842,11 @@ bool LinMinPropagator::Propagate() { // The reason is that all the other interval start after current_min_ub. // And that min_ub has its current value. integer_reason_for_unique_candidate_.push_back( - integer_trail_->UpperBoundAsLiteral(min_var_)); + integer_trail_.UpperBoundAsLiteral(min_var_)); for (int i = 0; i < exprs_.size(); ++i) { if (i == last_possible_min_interval) continue; const IntegerValue slack = expr_lbs_[i] - (current_min_ub + 1); - integer_trail_->AppendRelaxedLinearReason( + integer_trail_.AppendRelaxedLinearReason( slack, exprs_[i].coeffs, exprs_[i].vars, &integer_reason_for_unique_candidate_); } @@ -827,7 +863,8 @@ bool LinMinPropagator::Propagate() { return true; } -void LinMinPropagator::RegisterWith(GenericLiteralWatcher* watcher) { +int GreaterThanMinOfExprsPropagator::RegisterWith( + GenericLiteralWatcher* watcher) { const int id = watcher->Register(this); bool has_var_also_in_exprs = false; for (const LinearExpression& expr : exprs_) { @@ -847,6 +884,7 @@ void LinMinPropagator::RegisterWith(GenericLiteralWatcher* watcher) { if (has_var_also_in_exprs) { watcher->NotifyThatPropagatorMayNotReachFixedPointInOnePass(id); } + return id; } ProductPropagator::ProductPropagator( @@ -1022,7 +1060,7 @@ bool ProductPropagator::PropagateMaxOnPositiveProduct(AffineExpression a, bool ProductPropagator::Propagate() { const EnforcementStatus status = enforcement_propagator_.Status(enforcement_id_); - if (status == EnforcementStatus::CAN_PROPAGATE) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { const int64_t min_a = integer_trail_.LowerBound(a_).value(); const int64_t max_a = integer_trail_.UpperBound(a_).value(); const int64_t min_b = integer_trail_.LowerBound(b_).value(); @@ -1239,7 +1277,7 @@ bool SquarePropagator::Propagate() { const EnforcementStatus status = enforcement_propagator_.Status(enforcement_id_); - if (status == EnforcementStatus::CAN_PROPAGATE) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { // If the bounds of x * x and s are disjoint, the enforcement must be false. // TODO(user): relax the reason in a better way. if (min_x_square > max_s) { @@ -1337,7 +1375,7 @@ bool DivisionPropagator::Propagate() { const EnforcementStatus status = enforcement_propagator_.Status(enforcement_id_); - if (status == EnforcementStatus::CAN_PROPAGATE) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { const IntegerValue min_num = integer_trail_.LowerBound(num); const IntegerValue max_num = integer_trail_.UpperBound(num); const IntegerValue min_denom = integer_trail_.LowerBound(denom); @@ -1567,7 +1605,7 @@ bool FixedDivisionPropagator::Propagate() { const EnforcementStatus status = enforcement_propagator_.Status(enforcement_id_); - if (status == EnforcementStatus::CAN_PROPAGATE) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { // If the bounds of a / b and c are disjoint, the enforcement must be false. // TODO(user): relax the reason in a better way. if (min_a / b_ > max_c) { @@ -1652,7 +1690,7 @@ FixedModuloPropagator::FixedModuloPropagator( bool FixedModuloPropagator::Propagate() { const EnforcementStatus status = enforcement_propagator_.Status(enforcement_id_); - if (status == EnforcementStatus::CAN_PROPAGATE) { + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT) { const IntegerValue min_target = integer_trail_.LowerBound(target_); const IntegerValue max_target = integer_trail_.UpperBound(target_); if (min_target >= mod_) { diff --git a/ortools/sat/integer_expr.h b/ortools/sat/integer_expr.h index e95fa9d50a8..577026e0afb 100644 --- a/ortools/sat/integer_expr.h +++ b/ortools/sat/integer_expr.h @@ -243,15 +243,19 @@ class MinPropagator : public PropagatorInterface { // Same as MinPropagator except this works on min = MIN(exprs) where exprs are // linear expressions. It uses IntegerSumLE to propagate bounds on the exprs. // Assumes Canonical expressions (all positive coefficients). -class LinMinPropagator : public PropagatorInterface, LazyReasonInterface { +class GreaterThanMinOfExprsPropagator : public PropagatorInterface, + LazyReasonInterface { public: - LinMinPropagator(std::vector exprs, IntegerVariable min_var, - Model* model); - LinMinPropagator(const LinMinPropagator&) = delete; - LinMinPropagator& operator=(const LinMinPropagator&) = delete; + GreaterThanMinOfExprsPropagator( + absl::Span enforcement_literals, + std::vector exprs, IntegerVariable min_var, + Model* model); + GreaterThanMinOfExprsPropagator(const GreaterThanMinOfExprsPropagator&) = + delete; + GreaterThanMinOfExprsPropagator& operator=( + const GreaterThanMinOfExprsPropagator&) = delete; bool Propagate() final; - void RegisterWith(GenericLiteralWatcher* watcher); // For LazyReasonInterface. void Explain(int id, IntegerValue propagation_slack, @@ -260,6 +264,8 @@ class LinMinPropagator : public PropagatorInterface, LazyReasonInterface { std::vector* trail_indices_reason) final; private: + int RegisterWith(GenericLiteralWatcher* watcher); + // Lighter version of IntegerSumLE. This uses the current value of // integer_reason_ in addition to the reason for propagating the linear // constraint. The coeffs are assumed to be positive here. @@ -271,7 +277,9 @@ class LinMinPropagator : public PropagatorInterface, LazyReasonInterface { const IntegerVariable min_var_; std::vector expr_lbs_; Model* model_; - IntegerTrail* integer_trail_; + IntegerTrail& integer_trail_; + EnforcementPropagator& enforcement_propagator_; + EnforcementId enforcement_id_; std::vector max_variations_; std::vector reason_coeffs_; std::vector local_reason_; @@ -732,9 +740,10 @@ inline std::function NewWeightedSum( // Expresses the fact that an existing integer variable is equal to the minimum // of linear expressions. Assumes Canonical expressions (all positive // coefficients). -inline void AddIsEqualToMinOf(const LinearExpression& min_expr, - std::vector exprs, - Model* model) { +inline void AddIsEqualToMinOf( + const absl::Span enforcement_literals, + const LinearExpression& min_expr, std::vector exprs, + Model* model) { IntegerTrail* integer_trail = model->GetOrCreate(); IntegerVariable min_var; @@ -758,17 +767,18 @@ inline void AddIsEqualToMinOf(const LinearExpression& min_expr, LoadLinearConstraint(builder.Build(), model); } - // Add for all i, min <= exprs[i]. + // Add for all i, enforcement_literals => min <= exprs[i]. for (const LinearExpression& expr : exprs) { LinearConstraintBuilder builder(0, kMaxIntegerValue); builder.AddLinearExpression(expr, 1); builder.AddTerm(min_var, -1); - LoadLinearConstraint(builder.Build(), model); + LoadConditionalLinearConstraint(enforcement_literals, builder.Build(), + model); } - LinMinPropagator* constraint = - new LinMinPropagator(std::move(exprs), min_var, model); - constraint->RegisterWith(model->GetOrCreate()); + GreaterThanMinOfExprsPropagator* constraint = + new GreaterThanMinOfExprsPropagator(enforcement_literals, + std::move(exprs), min_var, model); model->TakeOwnership(constraint); } @@ -776,7 +786,9 @@ ABSL_DEPRECATED("Use AddIsEqualToMinOf() instead.") inline std::function IsEqualToMinOf( const LinearExpression& min_expr, const std::vector& exprs) { - return [&](Model* model) { AddIsEqualToMinOf(min_expr, exprs, model); }; + return [&](Model* model) { + AddIsEqualToMinOf(/*enforcement_literals=*/{}, min_expr, exprs, model); + }; } // Adds the constraint: a * b = p. diff --git a/ortools/sat/integer_expr_test.cc b/ortools/sat/integer_expr_test.cc index b35f3fc4d54..a076dc30694 100644 --- a/ortools/sat/integer_expr_test.cc +++ b/ortools/sat/integer_expr_test.cc @@ -31,6 +31,7 @@ #include "gtest/gtest.h" #include "ortools/base/logging.h" #include "ortools/base/parse_test_proto.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/port/proto_utils.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_checker.h" @@ -87,6 +88,7 @@ void AddFixedWeightedSumReif(Literal is_eq, } using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; CpSolverResponse SolveAndCheck( const CpModelProto& initial_model, absl::string_view extra_parameters = "", @@ -94,7 +96,7 @@ CpSolverResponse SolveAndCheck( SatParameters params; params.set_enumerate_all_solutions(true); if (!extra_parameters.empty()) { - params.MergeFromString(extra_parameters); + params.MergeFrom(ParseTextOrDie(extra_parameters)); } auto observer = [&](const CpSolverResponse& response) { VLOG(2) << response; @@ -476,7 +478,7 @@ TEST(LinMinTest, OnlyOnePossibleCandidate) { LinearExpression min_expr; min_expr.vars.push_back(min); min_expr.coeffs.push_back(1); - AddIsEqualToMinOf(min_expr, exprs, &model); + AddIsEqualToMinOf(/*enforcement_literals=*/{}, min_expr, exprs, &model); // So far everything is normal. EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); @@ -531,7 +533,7 @@ TEST(LinMinTest, OnlyOnePossibleExpr) { LinearExpression min_expr; min_expr.vars.push_back(min); min_expr.coeffs.push_back(1); - AddIsEqualToMinOf(min_expr, exprs, &model); + AddIsEqualToMinOf(/*enforcement_literals=*/{}, min_expr, exprs, &model); // So far everything is normal. EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); @@ -554,6 +556,134 @@ TEST(LinMinTest, OnlyOnePossibleExpr) { EXPECT_EQ(SatSolver::INFEASIBLE, model.GetOrCreate()->Solve()); } +TEST(LinMinTest, AlwaysFalseWithUnassignedEnforcementLiteral) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 3)), + model.Add(NewIntegerVariable(-2, 4))}; + IntegerTrail* integer_trail = model.GetOrCreate(); + LinearExpression expr1; // 2x0 + 3x1 - 5 + expr1.vars = {vars[0], vars[1]}; + expr1.coeffs = {2, 3}; + expr1.offset = -5; + expr1 = CanonicalizeExpr(expr1); + EXPECT_EQ(-3, expr1.Min(*integer_trail)); + EXPECT_EQ(8, expr1.Max(*integer_trail)); + + LinearExpression expr2; // 2x1 - 5x2 + 6 + expr2.vars = {vars[1], vars[2]}; + expr2.coeffs = {2, -5}; + expr2.offset = 6; + expr2 = CanonicalizeExpr(expr2); + EXPECT_EQ(-14, expr2.Min(*integer_trail)); + EXPECT_EQ(22, expr2.Max(*integer_trail)); + + LinearExpression min_expr; // 2x0 + 3x2 + min_expr.vars = {vars[0], vars[2]}; + min_expr.coeffs = {2, 3}; + min_expr.offset = -50; + min_expr = CanonicalizeExpr(min_expr); + EXPECT_EQ(-54, min_expr.Min(*integer_trail)); + EXPECT_EQ(-34, min_expr.Max(*integer_trail)); + + const Literal b = Literal(model.Add(NewBooleanVariable()), true); + // Always false if enforced (min_expr is always strictly smaller than expr1 + // and expr2). + AddIsEqualToMinOf({b}, min_expr, {expr1, expr2}, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsFalse(b)); + EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); +} + +TEST(LinMinTest, NotAlwaysFalseWithUnassignedEnforcementLiteral) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 3)), + model.Add(NewIntegerVariable(-2, 4))}; + IntegerTrail* integer_trail = model.GetOrCreate(); + LinearExpression expr1; // 2x0 + 3x1 - 5 + expr1.vars = {vars[0], vars[1]}; + expr1.coeffs = {2, 3}; + expr1.offset = -5; + expr1 = CanonicalizeExpr(expr1); + EXPECT_EQ(-3, expr1.Min(*integer_trail)); + EXPECT_EQ(8, expr1.Max(*integer_trail)); + + LinearExpression expr2; // 2x1 - 5x2 + 6 + expr2.vars = {vars[1], vars[2]}; + expr2.coeffs = {2, -5}; + expr2.offset = 6; + expr2 = CanonicalizeExpr(expr2); + EXPECT_EQ(-14, expr2.Min(*integer_trail)); + EXPECT_EQ(22, expr2.Max(*integer_trail)); + + LinearExpression min_expr; // 2x0 + 3x2 + min_expr.vars = {vars[0], vars[2]}; + min_expr.coeffs = {2, 3}; + min_expr = CanonicalizeExpr(min_expr); + EXPECT_EQ(-4, min_expr.Min(*integer_trail)); + EXPECT_EQ(16, min_expr.Max(*integer_trail)); + + const Literal b = Literal(model.Add(NewBooleanVariable()), true); + AddIsEqualToMinOf({b}, min_expr, {expr1, expr2}, &model); + // Nothing should be propagated. + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.GetOrCreate()->Assignment().LiteralIsAssigned(b)); + EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); +} + +TEST(LinMinTest, CheckEnumerateAllSolutionsWithoutEnforcementLiteral) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { + name: 'b' + domain: [ 0, 1 ] + } + variables { + name: 'x' + domain: [ 0, 6 ] + } + variables { + name: 'y' + domain: [ 1, 7 ] + } + variables { + name: 'z' + domain: [ -5, 5 ] + } + constraints { + enforcement_literal: 0 + lin_max { + target { + vars: [ 1, 2, 3 ] + coeffs: [ 2, -2, 1 ] + offset: 5 + } + exprs { vars: 1 coeffs: 1 offset: 1 } + exprs { vars: 2 coeffs: 1 offset: 2 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "linearization_level:2", &solutions); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + CpModelProto reference_model = initial_model; + reference_model.mutable_constraints(0)->clear_enforcement_literal(); + absl::btree_set> reference_solutions; + for (int x = 0; x <= 6; ++x) { + for (int y = 1; y <= 7; ++y) { + for (int z = -5; z <= 5; ++z) { + reference_solutions.insert({0, x, y, z}); + } + } + } + const CpSolverResponse reference_response = SolveAndCheck( + reference_model, "linearization_level:2", &reference_solutions); + EXPECT_EQ(reference_response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(solutions, reference_solutions); +} + // Propagates a * b = p by hand. Return false if the domains are empty, // otherwise returns true and the expected domains value. This is slow and // work in O(product of domain(a).size() * domain(b).size())!. @@ -1098,7 +1228,7 @@ TEST(ProductPropagationTest, AlwaysFalseWithOneUnassignedEnforcementLiteral) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 5)); const IntegerVariable p = model.Add(NewIntegerVariable(50, 100)); // Always false if enforced (x.y always less than p). - model.Add(ProductConstraint({b}, x, x, p)); + model.Add(ProductConstraint({b}, x, y, p)); EXPECT_TRUE(model.GetOrCreate()->Propagate()); EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsFalse(b)); EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); @@ -1114,7 +1244,7 @@ TEST(ProductPropagationTest, AlwaysFalseWithOneUnassignedEnforcementLiteral2) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 5)); const IntegerVariable p = model.Add(NewIntegerVariable(-100, -50)); // Always false if enforced (x.y always greater than p). - model.Add(ProductConstraint({b}, x, x, p)); + model.Add(ProductConstraint({b}, x, y, p)); EXPECT_TRUE(model.GetOrCreate()->Propagate()); EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsFalse(b)); EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); diff --git a/ortools/sat/linear_propagation.cc b/ortools/sat/linear_propagation.cc index 2446dd2ecd6..03805fb840f 100644 --- a/ortools/sat/linear_propagation.cc +++ b/ortools/sat/linear_propagation.cc @@ -388,7 +388,7 @@ bool LinearPropagator::AddConstraint( // TODO(user): With some care, when we cannot propagate or the // constraint is not enforced, we could leave in_queue_[] at true but // not put the constraint in the queue. - if (status == EnforcementStatus::CAN_PROPAGATE || + if (status == EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT || status == EnforcementStatus::IS_ENFORCED) { AddToQueueIfNeeded(id); watcher_->CallOnNextPropagate(watcher_id_); diff --git a/ortools/sat/linear_relaxation.cc b/ortools/sat/linear_relaxation.cc index 29e57fbf8e0..a17cfa1cd6d 100644 --- a/ortools/sat/linear_relaxation.cc +++ b/ortools/sat/linear_relaxation.cc @@ -26,7 +26,6 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" -#include "absl/meta/type_traits.h" #include "absl/types/span.h" #include "google/protobuf/message.h" #include "ortools/base/logging.h" @@ -1111,7 +1110,8 @@ void AppendNoOverlap2dRelaxation(const ConstraintProto& ct, Model* model, } void AppendLinMaxRelaxationPart1(const ConstraintProto& ct, Model* model, - LinearRelaxation* relaxation) { + LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper) { auto* mapping = model->GetOrCreate(); // We want to linearize target = max(exprs[1], exprs[2], ..., exprs[d]). @@ -1124,7 +1124,8 @@ void AppendLinMaxRelaxationPart1(const ConstraintProto& ct, Model* model, LinearConstraintBuilder lc(model, kMinIntegerValue, IntegerValue(0)); lc.AddLinearExpression(negated_target); lc.AddLinearExpression(expr); - relaxation->linear_constraints.push_back(lc.Build()); + AppendLinearConstraintRelaxation(ct.enforcement_literal(), lc.Build(), + model, relaxation, activity_helper); } } @@ -1133,7 +1134,8 @@ void AppendLinMaxRelaxationPart1(const ConstraintProto& ct, Model* model, // 2) keep this code // 3) remove this code and create the cut generator at level 1. void AppendMaxAffineRelaxation(const ConstraintProto& ct, Model* model, - LinearRelaxation* relaxation) { + LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper) { IntegerVariable var; std::vector> affines; auto* mapping = model->GetOrCreate(); @@ -1148,7 +1150,8 @@ void AppendMaxAffineRelaxation(const ConstraintProto& ct, Model* model, PositiveVarExpr(mapping->GetExprFromProto(ct.lin_max().target())); LinearConstraintBuilder builder(model); if (BuildMaxAffineUpConstraint(target_expr, var, affines, model, &builder)) { - relaxation->linear_constraints.push_back(builder.Build()); + AppendLinearConstraintRelaxation(ct.enforcement_literal(), builder.Build(), + model, relaxation, activity_helper); } } @@ -1250,13 +1253,15 @@ void AppendLinMaxRelaxationPart2(IntegerVariable target, } } -void AppendLinearConstraintRelaxation(const ConstraintProto& ct, - bool linearize_enforced_constraints, - Model* model, +namespace { +bool LinearizeEnforcedConstraints(Model* model) { + return model->GetOrCreate()->linearization_level() > 1; +} +} // namespace + +void AppendLinearConstraintRelaxation(const ConstraintProto& ct, Model* model, LinearRelaxation* relaxation, ActivityBoundHelper* activity_helper) { - auto* mapping = model->Get(); - // Note that we ignore the holes in the domain. // // TODO(user): In LoadLinearConstraint() we already created intermediate @@ -1271,19 +1276,23 @@ void AppendLinearConstraintRelaxation(const ConstraintProto& ct, rhs_domain_max == std::numeric_limits::max()) return; + LinearConstraintBuilder lc(model, rhs_domain_min, rhs_domain_max); + auto* mapping = model->Get(); + for (int i = 0; i < ct.linear().vars_size(); i++) { + const int ref = ct.linear().vars(i); + const IntegerVariable int_var = mapping->Integer(ref); + // Everything here should have a view. + CHECK_NE(int_var, kNoIntegerVariable); + const int64_t coeff = ct.linear().coeffs(i); + lc.AddTerm(int_var, IntegerValue(coeff)); + } if (!HasEnforcementLiteral(ct)) { - LinearConstraintBuilder lc(model, rhs_domain_min, rhs_domain_max); - for (int i = 0; i < ct.linear().vars_size(); i++) { - const int ref = ct.linear().vars(i); - const int64_t coeff = ct.linear().coeffs(i); - lc.AddTerm(mapping->Integer(ref), IntegerValue(coeff)); - } relaxation->linear_constraints.push_back(lc.Build()); return; } // Reified version. - if (!linearize_enforced_constraints) return; + if (!LinearizeEnforcedConstraints(model)) return; // We linearize fully reified constraints of size 1 all together for a given // variable. But we need to process half-reified ones or constraint with @@ -1296,9 +1305,25 @@ void AppendLinearConstraintRelaxation(const ConstraintProto& ct, return; } + AppendLinearConstraintRelaxation(ct.enforcement_literal(), lc.Build(), model, + relaxation, activity_helper); +} + +void AppendLinearConstraintRelaxation(absl::Span enforcement, + LinearConstraint&& linear_constraint, + Model* model, + LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper) { + if (enforcement.empty()) { + relaxation->linear_constraints.push_back(std::move(linear_constraint)); + return; + } + if (!LinearizeEnforcedConstraints(model)) return; + + auto* mapping = model->Get(); std::vector enforcing_literals; - enforcing_literals.reserve(ct.enforcement_literal_size()); - for (const int enforcement_ref : ct.enforcement_literal()) { + enforcing_literals.reserve(enforcement.size()); + for (const int enforcement_ref : enforcement) { enforcing_literals.push_back(mapping->Literal(enforcement_ref)); } @@ -1307,17 +1332,14 @@ void AppendLinearConstraintRelaxation(const ConstraintProto& ct, IntegerValue min_activity(0); IntegerValue max_activity(0); const auto integer_trail = model->GetOrCreate(); - for (int i = 0; i < ct.linear().vars_size(); i++) { - const int ref = ct.linear().vars(i); - const IntegerValue coeff(ct.linear().coeffs(i)); - const IntegerVariable int_var = mapping->Integer(ref); - - // Everything here should have a view. - CHECK_NE(int_var, kNoIntegerVariable); + for (int i = 0; i < linear_constraint.num_terms; i++) { + const IntegerValue coeff = linear_constraint.coeffs[i]; + const IntegerVariable int_var = linear_constraint.vars[i]; + const int ref = mapping->GetProtoVariableFromIntegerVariable(int_var); const IntegerValue lb = integer_trail->LowerBound(int_var); const IntegerValue ub = integer_trail->UpperBound(int_var); - if (lb == 0 && ub == 1 && activity_helper != nullptr) { + if (lb == 0 && ub == 1 && activity_helper != nullptr && ref != -1) { bool_terms.push_back({ref, coeff.value()}); } else { if (coeff > 0) { @@ -1336,42 +1358,36 @@ void AppendLinearConstraintRelaxation(const ConstraintProto& ct, IntegerValue(activity_helper->ComputeMaxActivity(bool_terms)); } - if (rhs_domain_min > min_activity) { - // And(ei) => terms >= rhs_domain_min - // <=> Sum_i (~ei * (rhs_domain_min - min_activity)) + terms >= + if (linear_constraint.lb > min_activity) { + // And(ei) => terms >= linear_constraint.lb + // <=> Sum_i (~ei * (linear_constraint.lb - min_activity)) + terms >= // rhs_domain_min - LinearConstraintBuilder lc(model, rhs_domain_min, kMaxIntegerValue); - const IntegerValue term = CapSubI(rhs_domain_min, min_activity); + LinearConstraintBuilder lc(model, linear_constraint.lb, kMaxIntegerValue); + const IntegerValue term = CapSubI(linear_constraint.lb, min_activity); if (AtMinOrMaxInt64I(term) && !enforcing_literals.empty()) return; for (const Literal& literal : enforcing_literals) { CHECK(lc.AddLiteralTerm(literal.Negated(), term)); } - for (int i = 0; i < ct.linear().vars_size(); i++) { - const int ref = ct.linear().vars(i); - const IntegerValue coeff(ct.linear().coeffs(i)); - const IntegerVariable int_var = mapping->Integer(ref); - lc.AddTerm(int_var, coeff); + for (int i = 0; i < linear_constraint.num_terms; i++) { + lc.AddTerm(linear_constraint.vars[i], linear_constraint.coeffs[i]); } LinearConstraint built_ct = lc.Build(); if (!PossibleOverflow(*integer_trail, built_ct)) { relaxation->linear_constraints.push_back(std::move(built_ct)); } } - if (rhs_domain_max < max_activity) { - // And(ei) => terms <= rhs_domain_max - // <=> Sum_i (~ei * (rhs_domain_max - max_activity)) + terms <= - // rhs_domain_max - LinearConstraintBuilder lc(model, kMinIntegerValue, rhs_domain_max); - const IntegerValue term = CapSubI(rhs_domain_max, max_activity); + if (linear_constraint.ub < max_activity) { + // And(ei) => terms <= linear_constraint.ub + // <=> Sum_i (~ei * (linear_constraint.ub - max_activity)) + terms <= + // linear_constraint.ub + LinearConstraintBuilder lc(model, kMinIntegerValue, linear_constraint.ub); + const IntegerValue term = CapSubI(linear_constraint.ub, max_activity); if (AtMinOrMaxInt64I(term) && !enforcing_literals.empty()) return; for (const Literal& literal : enforcing_literals) { CHECK(lc.AddLiteralTerm(literal.Negated(), term)); } - for (int i = 0; i < ct.linear().vars_size(); i++) { - const int ref = ct.linear().vars(i); - const IntegerValue coeff(ct.linear().coeffs(i)); - const IntegerVariable int_var = mapping->Integer(ref); - lc.AddTerm(int_var, coeff); + for (int i = 0; i < linear_constraint.num_terms; i++) { + lc.AddTerm(linear_constraint.vars[i], linear_constraint.coeffs[i]); } LinearConstraint built_ct = lc.Build(); if (!PossibleOverflow(*integer_trail, built_ct)) { @@ -1422,7 +1438,7 @@ void TryToLinearizeConstraint(const CpModelProto& /*model_proto*/, break; } case ConstraintProto::ConstraintCase::kIntProd: { - // TODO(user): add support for enforcement literals? + // TODO(user): add support for enforcement literals. if (!HasEnforcementLiteral(ct)) { const LinearArgumentProto& int_prod = ct.int_prod(); if (int_prod.exprs_size() == 2 && @@ -1438,15 +1454,16 @@ void TryToLinearizeConstraint(const CpModelProto& /*model_proto*/, break; } case ConstraintProto::ConstraintCase::kLinMax: { - AppendLinMaxRelaxationPart1(ct, model, relaxation); + AppendLinMaxRelaxationPart1(ct, model, relaxation, activity_helper); const bool is_affine_max = ExpressionsContainsOnlyOneVar(ct.lin_max().exprs()); if (is_affine_max) { - AppendMaxAffineRelaxation(ct, model, relaxation); + AppendMaxAffineRelaxation(ct, model, relaxation, activity_helper); } // Add cut generators. - if (linearization_level > 1) { + // TODO(user): add support for enforcement literals. + if (linearization_level > 1 && !HasEnforcementLiteral(ct)) { if (is_affine_max) { AddMaxAffineCutGenerator(ct, model, relaxation); } else if (ct.lin_max().exprs().size() < 100) { @@ -1456,20 +1473,24 @@ void TryToLinearizeConstraint(const CpModelProto& /*model_proto*/, break; } case ConstraintProto::ConstraintCase::kAllDiff: { - AddAllDiffRelaxationAndCutGenerator(ct, linearization_level, model, - relaxation); + // TODO(user): add support for enforcement literals. + if (!HasEnforcementLiteral(ct)) { + AddAllDiffRelaxationAndCutGenerator(ct, linearization_level, model, + relaxation); + } break; } case ConstraintProto::ConstraintCase::kLinear: { - AppendLinearConstraintRelaxation( - ct, /*linearize_enforced_constraints=*/linearization_level > 1, model, - relaxation, activity_helper); + AppendLinearConstraintRelaxation(ct, model, relaxation, activity_helper); break; } case ConstraintProto::ConstraintCase::kCircuit: { - AppendCircuitRelaxation(ct, model, relaxation); - if (linearization_level > 1) { - AddCircuitCutGenerator(ct, model, relaxation); + // TODO(user): add support for enforcement literals? + if (!HasEnforcementLiteral(ct)) { + AppendCircuitRelaxation(ct, model, relaxation); + if (linearization_level > 1) { + AddCircuitCutGenerator(ct, model, relaxation); + } } break; } diff --git a/ortools/sat/linear_relaxation.h b/ortools/sat/linear_relaxation.h index c4f8e3cd7eb..67bba950fa2 100644 --- a/ortools/sat/linear_relaxation.h +++ b/ortools/sat/linear_relaxation.h @@ -112,8 +112,9 @@ void AppendExactlyOneRelaxation(const ConstraintProto& ct, Model* model, // Reference: "Strong mixed-integer programming formulations for trained neural // networks" by Ross Anderson et. (https://arxiv.org/pdf/1811.01988.pdf). // TODO(user): Support linear expression as target. -void AppendLinMaxRelaxationPart1(const ConstraintProto& ct, Model* model, - LinearRelaxation* relaxation); +void AppendLinMaxRelaxationPart1( + const ConstraintProto& ct, Model* model, LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper = nullptr); void AppendLinMaxRelaxationPart2(IntegerVariable target, absl::Span alternative_literals, @@ -122,7 +123,8 @@ void AppendLinMaxRelaxationPart2(IntegerVariable target, // Note: This only works if all affine expressions share the same variable. void AppendMaxAffineRelaxation(const ConstraintProto& ct, Model* model, - LinearRelaxation* relaxation); + LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper = nullptr); // Appends linear constraints to the relaxation. This also handles the // relaxation of linear constraints with enforcement literals. @@ -133,7 +135,10 @@ void AppendMaxAffineRelaxation(const ConstraintProto& ct, Model* model, // Where implied_lb and implied_ub are trivial lower and upper bounds of the // constraint. void AppendLinearConstraintRelaxation( - const ConstraintProto& ct, bool linearize_enforced_constraints, + const ConstraintProto& ct, Model* model, LinearRelaxation* relaxation, + ActivityBoundHelper* activity_helper = nullptr); +void AppendLinearConstraintRelaxation( + absl::Span enforcement, LinearConstraint&& linear_constraint, Model* model, LinearRelaxation* relaxation, ActivityBoundHelper* activity_helper = nullptr); diff --git a/ortools/sat/linear_relaxation_test.cc b/ortools/sat/linear_relaxation_test.cc index 2f6e33eb069..13b046f2ef5 100644 --- a/ortools/sat/linear_relaxation_test.cc +++ b/ortools/sat/linear_relaxation_test.cc @@ -386,6 +386,41 @@ TEST(TryToLinearizeConstraint, LinMaxLevel1Bis) { EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), "-1*X2 -1*X3 <= 0"); } +TEST(TryToLinearizeConstraint, EnforcedLinMaxLevel2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ -1, 7 ] } + variables { domain: [ -2, 9 ] } + variables { domain: [ -5, 10 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 4 + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 2 coeffs: -1 } + } + } + )pb"); + + Model model; + model.GetOrCreate()->set_linearization_level(2); + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 3); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1*X0 -1*X3 10*X4 <= 10"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "1*X1 -1*X3 12*X4 <= 12"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "-1*X2 -1*X3 7*X4 <= 7"); +} + TEST(TryToLinearizeConstraint, LinMaxSmall) { const CpModelProto initial_model = ParseTestProto(R"pb( variables { domain: [ 0, 5 ] } @@ -960,12 +995,12 @@ TEST(AppendLinearConstraintRelaxation, NoEnforcementLiteral) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 1); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), @@ -991,9 +1026,8 @@ TEST(AppendLinearConstraintRelaxation, SmallLinearizationLevel) { LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/false, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 0); } @@ -1015,9 +1049,8 @@ TEST(AppendLinearConstraintRelaxation, PbConstraint) { LoadVariables(initial_model, false, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/false, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 1); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "3 <= 2*X0 1*X1 3*X2 <= 5"); @@ -1038,12 +1071,12 @@ TEST(AppendLinearConstraintRelaxation, SmallConstraint) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 0); } @@ -1064,12 +1097,12 @@ TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralLowerBound) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 1); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), @@ -1092,12 +1125,12 @@ TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralUpperBound) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 1); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), @@ -1120,12 +1153,12 @@ TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralBothBounds) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 2); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), @@ -1152,12 +1185,12 @@ TEST(AppendLinearConstraintRelaxation, MultipleEnforcementLiteral) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 2); EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), @@ -1186,12 +1219,12 @@ TEST(AppendLinearConstraintRelaxation, BoundsNotTight) { )pb"); Model model; + model.GetOrCreate()->set_linearization_level(2); LoadVariables(initial_model, true, &model); LinearRelaxation relaxation; - AppendLinearConstraintRelaxation(initial_model.constraints(0), - /*linearize_enforced_constraints=*/true, - &model, &relaxation); + AppendLinearConstraintRelaxation(initial_model.constraints(0), &model, + &relaxation); EXPECT_EQ(relaxation.linear_constraints.size(), 0); } diff --git a/ortools/sat/no_overlap_2d_helper.cc b/ortools/sat/no_overlap_2d_helper.cc index 94484b160ee..b776bf7b2fc 100644 --- a/ortools/sat/no_overlap_2d_helper.cc +++ b/ortools/sat/no_overlap_2d_helper.cc @@ -156,37 +156,15 @@ namespace { bool LeftBoxBeforeRightBoxOnFirstDimension(int left, int right, SchedulingConstraintHelper* x, SchedulingConstraintHelper* y) { - // left box2 pushes right box2. - const IntegerValue left_end_min = x->EndMin(left); - if (left_end_min > x->StartMin(right)) { - x->ClearReason(); - x->AddPresenceReason(left); - x->AddPresenceReason(right); - x->AddReasonForBeingBeforeAssumingNoOverlap(left, right); - x->AddEndMinReason(left, left_end_min); - // left and right must overlap on y. - ClearAndAddMandatoryOverlapReason(left, right, y); - // Propagate with the complete reason. - x->ImportOtherReasons(*y); - if (!x->IncreaseStartMin(right, left_end_min)) return false; - } - - // right box2 pushes left box2. - const IntegerValue right_start_max = x->StartMax(right); - if (right_start_max < x->EndMax(left)) { - x->ClearReason(); - x->AddPresenceReason(left); - x->AddPresenceReason(right); - x->AddReasonForBeingBeforeAssumingNoOverlap(left, right); - x->AddStartMaxReason(right, right_start_max); - // left and right must overlap on y. - ClearAndAddMandatoryOverlapReason(left, right, y); - // Propagate with the complete reason. - x->ImportOtherReasons(*y); - if (!x->DecreaseEndMax(left, right_start_max)) return false; - } - - return true; + x->ClearReason(); + x->AddPresenceReason(left); + x->AddPresenceReason(right); + x->AddReasonForBeingBeforeAssumingNoOverlap(left, right); + // left and right must overlap on y. + ClearAndAddMandatoryOverlapReason(left, right, y); + // Propagate with the complete reason. + x->ImportOtherReasons(*y); + return x->PushTaskOrderWhenPresent(left, right); } } // namespace diff --git a/ortools/sat/parameters_validation.cc b/ortools/sat/parameters_validation.cc index 7c0de26f542..1e7f52e628a 100644 --- a/ortools/sat/parameters_validation.cc +++ b/ortools/sat/parameters_validation.cc @@ -161,11 +161,6 @@ std::string ValidateParameters(const SatParameters& params) { TEST_NON_NEGATIVE(symmetry_detection_deterministic_time_limit); TEST_POSITIVE(share_glue_clauses_dtime); - if (params.enumerate_all_solutions() && - (params.num_search_workers() > 1 || params.num_workers() > 1)) { - return "Enumerating all solutions does not work in parallel"; - } - if (params.enumerate_all_solutions() && (!params.subsolvers().empty() || !params.extra_subsolvers().empty() || !params.ignore_subsolvers().empty())) { diff --git a/ortools/sat/precedences.cc b/ortools/sat/precedences.cc index 2ce6788e9ee..6d91ba3074b 100644 --- a/ortools/sat/precedences.cc +++ b/ortools/sat/precedences.cc @@ -736,6 +736,16 @@ void EnforcedLinear2Bounds::CollectPrecedences( } } +BinaryRelationRepository::~BinaryRelationRepository() { + if (!VLOG_IS_ON(1)) return; + std::vector> stats; + stats.push_back({"BinaryRelationRepository/num_enforced_relations", + num_enforced_relations_}); + stats.push_back({"BinaryRelationRepository/num_encoded_equivalences", + num_encoded_equivalences_}); + shared_stats_->AddStats(stats); +} + void BinaryRelationRepository::Add(Literal lit, LinearExpression2 expr, IntegerValue lhs, IntegerValue rhs) { expr.SimpleCanonicalization(); @@ -774,6 +784,91 @@ void BinaryRelationRepository::Build() { literal_key_values.emplace_back(r.enforcement.Index(), i); } lit_to_relations_.ResetFromPairs(literal_key_values); + lit_to_relations_.Add({}); // One extra unit size to make sure the negation + // cannot be out of bounds in lit_to_relations_. + + // If we have "l => (x <= 6)" and "~l => (x >= 9)" we can push + // "l <=> (x <= 6)" to the repository of fully encoded linear2 bounds. + // More generally, if we have: + // + // l => (expr <= a) + // ~l => (expr >= b) + // + // And if moreover a < b, we have the following truth table: + // + // l | expr <= a | a < expr < b | expr >= b + // --+-----------+---------------+---------- + // 0 | false | false | true (from "~l => (expr >= b)") + // 1 | true | false | false (from "l => (expr <= a)") + // + // So we can generalize the expressions to equivalences: + // l <=> (expr <= a) + // ~l <=> (expr >= b) + // (a < expr < b) is impossible + absl::flat_hash_map lin2_to_upper_bound; + for (LiteralIndex lit_index{0}; lit_index < lit_to_relations_.size() - 1; + ++lit_index) { + const Literal lit(lit_index); + lin2_to_upper_bound.clear(); + const absl::Span relations = lit_to_relations_[lit_index]; + const absl::Span relations_negation = + lit_to_relations_[lit.NegatedIndex()]; + if (relations.empty() || relations_negation.empty()) continue; + for (const int relation_index : relations) { + const Relation& r = relations_[relation_index]; + LinearExpression2 expr = r.expr; + if (expr.coeffs[0] == 0) continue; + expr.SimpleCanonicalization(); // Since relations_ uses a different + // canonicalization convention. + DCHECK_EQ(expr.DivideByGcd(), 1); + { + const auto [it, inserted] = lin2_to_upper_bound.insert({expr, r.rhs}); + if (!inserted) { + it->second = std::min(it->second, r.rhs); + } + } + { + expr.Negate(); + const auto [it, inserted] = lin2_to_upper_bound.insert({expr, -r.lhs}); + if (!inserted) { + it->second = std::min(it->second, -r.lhs); + } + } + } + for (const int relation_index : relations_negation) { + const Relation& r = relations_[relation_index]; + LinearExpression2 canonical_expr = r.expr; + canonical_expr.SimpleCanonicalization(); + if (canonical_expr.coeffs[0] == 0) continue; + DCHECK_EQ(canonical_expr.DivideByGcd(), 1); + + // Let's work with lower bounds only. + const IntegerValue lower_bounds[2] = {r.lhs, -r.rhs}; + LinearExpression2 exprs[2] = {canonical_expr, canonical_expr}; + exprs[1].Negate(); + for (int i = 0; i < 2; ++i) { + // We have here "~l => (exprs[i] >= lower_bounds[i])". + const auto it = lin2_to_upper_bound.find(exprs[i]); + if (it != lin2_to_upper_bound.end()) { + const IntegerValue ub = it->second; + // Here we have "l => expr <= ub". + if (ub >= lower_bounds[i]) { + // Don't obey the "a < b" condition + continue; + } + num_encoded_equivalences_++; + + // Make both relationships two-way. + reified_linear2_bounds_->AddBoundEncodingIfNonTrivial(lit, exprs[i], + ub); + LinearExpression2 expr = exprs[i]; + expr.Negate(); + reified_linear2_bounds_->AddBoundEncodingIfNonTrivial( + lit.Negated(), expr, -lower_bounds[i]); + } + } + } + } } bool BinaryRelationRepository::PropagateLocalBounds( @@ -1413,5 +1508,123 @@ RelationStatus Linear2Bounds::GetStatus(LinearExpression2 expr, IntegerValue lb, return RelationStatus::IS_UNKNOWN; } +bool Linear2Bounds::EnqueueLowerOrEqual( + LinearExpression2 expr, IntegerValue ub, + absl::Span literal_reason, + absl::Span integer_reason) { + using ReifiedBoundType = ReifiedLinear2Bounds::ReifiedBoundType; + expr.SimpleCanonicalization(); + const IntegerValue gcd = expr.DivideByGcd(); + ub = FloorRatio(ub, gcd); + // We have many different scenarios here, each one pushing something different + // in the trail. + + // Trivial. + if (expr.coeffs[0] == 0 && expr.coeffs[1] == 0) { + if (ub >= 0) { + return true; + } else { + return integer_trail_->ReportConflict(literal_reason, integer_reason); + } + } + + // Degenerate single variable case, just push the IntegerLiteral. + if (expr.coeffs[0] == 0) { + return integer_trail_->Enqueue( + IntegerLiteral::LowerOrEqual(expr.vars[1], ub), literal_reason, + integer_reason); + } + + // TODO(user): also check partially-encoded bounds, e.g. (expr <= ub) => l, + // which might be in BinaryRelationRepository as ~l => (-expr <= - ub - 1). + const auto reified_bound = reified_lin2_bounds_->GetEncodedBound(expr, ub); + + // Already true. + if (std::holds_alternative(reified_bound) && + std::get(reified_bound) == + ReifiedBoundType::kAlwaysTrue) { + return true; + } + + // Conflict (ub < lb). + if (std::holds_alternative(reified_bound) && + std::get(reified_bound) == + ReifiedBoundType::kAlwaysFalse) { + LinearExpression2 negated_expr = expr; + negated_expr.Negate(); + DCHECK_LT(ub, -UpperBound(negated_expr)); + std::vector tmp_literal_reason(literal_reason.begin(), + literal_reason.end()); + std::vector tmp_integer_reason(integer_reason.begin(), + integer_reason.end()); + AddReasonForUpperBoundLowerThan(negated_expr, -ub - 1, &tmp_literal_reason, + &tmp_integer_reason); + return integer_trail_->ReportConflict(tmp_literal_reason, + tmp_integer_reason); + } + + // Now all the cases below are pushing a proper linear2 bound. If we are at + // level zero, store this in the root_level_bounds_. + if (trail_->CurrentDecisionLevel() == 0) { + root_level_bounds_->AddUpperBound(expr, ub); + } + + // We don't have anything encoding this linear2 bound. Push the bounds of + // its two variables. + if (std::holds_alternative(reified_bound) && + std::get(reified_bound) == + ReifiedBoundType::kNoLiteralStored) { + // TODO(user): create a Linear2 trail and enqueue this bound there. + std::vector tmp_integer_reason(integer_reason.begin(), + integer_reason.end()); + const IntegerValue var_0_ub = FloorRatio( + ub - integer_trail_->LowerBound(expr.vars[1]) * expr.coeffs[1], + expr.coeffs[0]); + const IntegerValue var_1_ub = FloorRatio( + ub - integer_trail_->LowerBound(expr.vars[0]) * expr.coeffs[0], + expr.coeffs[1]); + + tmp_integer_reason.push_back(IntegerLiteral::GreaterOrEqual( + expr.vars[1], integer_trail_->LowerBound(expr.vars[1]))); + if (!integer_trail_->Enqueue( + IntegerLiteral::LowerOrEqual(expr.vars[0], var_0_ub), + literal_reason, tmp_integer_reason)) { + return false; + } + tmp_integer_reason.pop_back(); + + tmp_integer_reason.push_back(IntegerLiteral::GreaterOrEqual( + expr.vars[0], integer_trail_->LowerBound(expr.vars[0]))); + if (!integer_trail_->Enqueue( + IntegerLiteral::LowerOrEqual(expr.vars[1], var_1_ub), + literal_reason, tmp_integer_reason)) { + return false; + } + return true; + } + + // We have a literal encoding for this linear2 bound, push it. + if (std::holds_alternative(reified_bound)) { + // TODO(user): push this to EnforcedLinear2Bounds so the same + // propagator that enqueued this bound will get the new linear2 bound if + // request it to this class without needing to wait the propagation fix + // point. + const Literal literal = std::get(reified_bound); + + integer_trail_->SafeEnqueueLiteral(literal, literal_reason, integer_reason); + return true; + } + + // We can encode this linear2 bound with an IntegerLiteral (probably coming + // from a linear3). Push the IntegerLiteral. + if (std::holds_alternative(reified_bound)) { + // TODO(user): update Linear2BoundsFromLinear3. + const IntegerLiteral literal = std::get(reified_bound); + return integer_trail_->Enqueue(literal, literal_reason, integer_reason); + } + LOG(FATAL) << "Unknown reified bound type"; + return false; +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/precedences.h b/ortools/sat/precedences.h index 2b120268e21..9cb99df8dbf 100644 --- a/ortools/sat/precedences.h +++ b/ortools/sat/precedences.h @@ -496,12 +496,19 @@ struct Relation { } }; +class ReifiedLinear2Bounds; + // A repository of all the enforced linear constraints of size 1 or 2. // // TODO(user): This is not always needed, find a way to clean this once we // don't need it. class BinaryRelationRepository { public: + explicit BinaryRelationRepository(Model* model) + : reified_linear2_bounds_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()) {} + ~BinaryRelationRepository(); + int size() const { return relations_.size(); } // The returned relation is guaranteed to only have positive variables. @@ -543,8 +550,11 @@ class BinaryRelationRepository { absl::flat_hash_map* output) const; private: + ReifiedLinear2Bounds* reified_linear2_bounds_; + SharedStatistics* shared_stats_; bool is_built_ = false; int num_enforced_relations_ = 0; + int num_encoded_equivalences_ = 0; std::vector relations_; CompactVectorVector lit_to_relations_; }; @@ -677,7 +687,9 @@ class Linear2Bounds { root_level_bounds_(model->GetOrCreate()), enforced_bounds_(model->GetOrCreate()), linear3_bounds_(model->GetOrCreate()), - lin2_indices_(model->GetOrCreate()) {} + lin2_indices_(model->GetOrCreate()), + reified_lin2_bounds_(model->GetOrCreate()), + trail_(model->GetOrCreate()) {} // Returns the best known upper-bound of the given LinearExpression2 at the // current decision level. If its explanation is needed, it can be queried @@ -697,12 +709,20 @@ class Linear2Bounds { // the individual variable bounds. This is faster. IntegerValue NonTrivialUpperBound(LinearExpression2Index lin2_index) const; + // Given the new linear2 bounds and its reason, inspect our various repository + // to find the strongest way to push this new upper bound. + bool EnqueueLowerOrEqual(LinearExpression2 expr, IntegerValue ub, + absl::Span literal_reason, + absl::Span integer_reason); + private: IntegerTrail* integer_trail_; RootLevelLinear2Bounds* root_level_bounds_; EnforcedLinear2Bounds* enforced_bounds_; Linear2BoundsFromLinear3* linear3_bounds_; Linear2Indices* lin2_indices_; + ReifiedLinear2Bounds* reified_lin2_bounds_; + Trail* trail_; }; // Detects if at least one of a subset of linear of size 2 or 1, touching the diff --git a/ortools/sat/precedences_test.cc b/ortools/sat/precedences_test.cc index d1c098d69e5..59a64fcf591 100644 --- a/ortools/sat/precedences_test.cc +++ b/ortools/sat/precedences_test.cc @@ -532,7 +532,7 @@ TEST(BinaryRelationRepositoryTest, Build) { const IntegerVariable z = model.Add(NewIntegerVariable(-100, 100)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); RootLevelLinear2Bounds* root_level_bounds = model.GetOrCreate(); repository.Add(lit_a, LinearExpression2(NegationOf(x), y, 1, 1), 2, 8); @@ -836,7 +836,7 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedRelation) { const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); RootLevelLinear2Bounds* root_level_bounds = model.GetOrCreate(); repository.Add(lit_a, LinearExpression2::Difference(y, x), 2, @@ -861,7 +861,7 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_UnenforcedRelation) { const IntegerVariable x = model.Add(NewIntegerVariable(-100, 100)); const IntegerVariable y = model.Add(NewIntegerVariable(-100, 100)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(lit_a, LinearExpression2(x, y, -1, 1), -5, 10); // lit_a => y => x - 5 root_level_bounds->Add(LinearExpression2(x, y, -1, 1), 2, @@ -888,7 +888,7 @@ TEST(BinaryRelationRepositoryTest, const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(lit_a, LinearExpression2::Difference(y, x), -5, 10); // lit_a => y => x - 5 repository.Add(lit_b, LinearExpression2::Difference(y, x), 2, @@ -911,7 +911,7 @@ TEST(BinaryRelationRepositoryTest, const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); RootLevelLinear2Bounds* root_level_bounds = model.GetOrCreate(); repository.Add(lit_a, LinearExpression2::Difference(y, x), 2, @@ -934,7 +934,7 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_Infeasible) { const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); RootLevelLinear2Bounds* root_level_bounds = model.GetOrCreate(); repository.Add(lit_a, LinearExpression2::Difference(y, x), 8, diff --git a/ortools/sat/python/cp_model.py b/ortools/sat/python/cp_model.py index 3eefdfbdbde..035ef78f22b 100644 --- a/ortools/sat/python/cp_model.py +++ b/ortools/sat/python/cp_model.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ortools/sat/python/wrappers.cc b/ortools/sat/python/wrappers.cc index 2fd15391999..021619f5bc2 100644 --- a/ortools/sat/python/wrappers.cc +++ b/ortools/sat/python/wrappers.cc @@ -337,7 +337,8 @@ class Generator { []($1 self) { return self->mutable_$2(); }, py::return_value_policy::reference, py::keep_alive<0, 1>()))", field.name(), current_context_.self_mutable_name, field.name()); - // We'll need to generate the wrapping for `proto2::RepeatedPtrField<$3>`. + // We'll need to generate the wrapping for + // `google::protobuf::RepeatedPtrField<$3>`. repeated_ptr_types_.insert(msg_type); // We'll need to generate the wrapping for this message type. message_stack_.push_back(ABSL_DIE_IF_NULL(field.message_type())); @@ -350,7 +351,8 @@ class Generator { py::return_value_policy::reference, py::keep_alive<0, 1>()))", field.name(), current_context_.self_mutable_name); - // We'll need to generate the wrapping for `proto2::RepeatedField<$2>`. + // We'll need to generate the wrapping for + // `google::protobuf::RepeatedField<$2>`. repeated_scalar_types_.insert(GetCppType(field.cpp_type(), field)); } } diff --git a/ortools/sat/routing_cuts.cc b/ortools/sat/routing_cuts.cc index 80f32678f82..a2d63f468cd 100644 --- a/ortools/sat/routing_cuts.cc +++ b/ortools/sat/routing_cuts.cc @@ -1851,7 +1851,8 @@ int ToNodeVariableIndex(IntegerVariable var) { // in routes constraints. BinaryRelationRepository ComputePartialBinaryRelationRepository( const CpModelProto& model) { - BinaryRelationRepository repository; + Model empty_model; + BinaryRelationRepository repository(&empty_model); for (const ConstraintProto& ct : model.constraints()) { if (ct.constraint_case() != ConstraintProto::kLinear) continue; const absl::Span vars = ct.linear().vars(); @@ -1861,7 +1862,6 @@ BinaryRelationRepository ComputePartialBinaryRelationRepository( ToPositiveIntegerVariable(vars[0]), ToPositiveIntegerVariable(vars[1])); } - Model empty_model; repository.Build(); return repository; } diff --git a/ortools/sat/routing_cuts_test.cc b/ortools/sat/routing_cuts_test.cc index 39bb0469ee4..201d599d151 100644 --- a/ortools/sat/routing_cuts_test.cc +++ b/ortools/sat/routing_cuts_test.cc @@ -1409,7 +1409,7 @@ TEST(RouteRelationsHelperTest, Basic) { const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(a, b), 50, 1000); repository.Add(literals[1], LinearExpression2::Difference(a, c), 70, 1000); repository.Add(literals[2], LinearExpression2::Difference(c, b), 40, 1000); @@ -1504,7 +1504,7 @@ TEST(RouteRelationsHelperTest, UnenforcedRelations) { const IntegerVariable b = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable d = model.Add(NewIntegerVariable(0, 100)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); RootLevelLinear2Bounds* bounds = model.GetOrCreate(); repository.Add(literals[0], LinearExpression2::Difference(b, a), 1, 1); repository.Add(literals[1], LinearExpression2::Difference(c, b), 2, 2); @@ -1554,7 +1554,7 @@ TEST(RouteRelationsHelperTest, SeveralVariablesPerNode) { const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000); repository.Add(literals[0], LinearExpression2::Difference(z, y), 5, 100); @@ -1585,7 +1585,7 @@ TEST(RouteRelationsHelperTest, ComplexVariableRelations) { // and 1, respectively. const IntegerVariable a = model.Add(NewIntegerVariable(0, 150)); const IntegerVariable b = model.Add(NewIntegerVariable(0, 1)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); // "complex" relation with non +1/-1 coefficients. repository.Add(literals[0], LinearExpression2(b, a, 10, 1), 0, 150); repository.Build(); @@ -1620,7 +1620,7 @@ TEST(RouteRelationsHelperTest, TwoUnaryRelationsPerArc) { IntegerEncoder& encoder = *model.GetOrCreate(); encoder.AssociateToIntegerEqualValue(literals[0], a, 20); encoder.AssociateToIntegerLiteral(literals[0], {b, 50}); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Build(); const RoutingCumulExpressions cumuls = { @@ -1650,7 +1650,7 @@ TEST(RouteRelationsHelperTest, SeveralRelationsPerArc) { const IntegerVariable a = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable b = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000); // Add a second relation for some arc. @@ -1686,7 +1686,7 @@ TEST(RouteRelationsHelperTest, SeveralArcsPerLiteral) { const IntegerVariable a = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable b = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); repository.Add(literals[0], LinearExpression2::Difference(c, b), 40, 1000); repository.Build(); @@ -1728,7 +1728,7 @@ TEST(RouteRelationsHelperTest, InconsistentRelationIsSkipped) { const IntegerVariable d = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable e = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable f = model.Add(NewIntegerVariable(0, 100)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0); repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1); repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2); @@ -1788,7 +1788,7 @@ TEST(RouteRelationsHelperTest, InconsistentRelationWithMultipleArcsPerLiteral) { const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable d = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable e = model.Add(NewIntegerVariable(0, 100)); - BinaryRelationRepository repository; + BinaryRelationRepository repository(&model); repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0); repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1); repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2); diff --git a/ortools/sat/samples/code_samples.bzl b/ortools/sat/samples/code_samples.bzl index 5c7f5ace284..1b5971cafd0 100644 --- a/ortools/sat/samples/code_samples.bzl +++ b/ortools/sat/samples/code_samples.bzl @@ -13,10 +13,10 @@ """Helper macro to compile and test code samples.""" +load("@rules_go//go:def.bzl", "go_test") load("@pip_deps//:requirements.bzl", "requirement") load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_test.bzl", "cc_test") -load("@rules_go//go:def.bzl", "go_test") load("@rules_java//java:java_test.bzl", "java_test") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index f6502cdfdb0..bccc4d69da0 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -24,7 +24,7 @@ option java_multiple_files = true; // Contains the definitions for all the sat algorithm parameters and their // default values. // -// NEXT TAG: 329 +// NEXT TAG: 330 message SatParameters { // In some context, like in a portfolio of search, it makes sense to name a // given parameters set for logging purpose. @@ -1282,13 +1282,15 @@ message SatParameters { // the number of too-easy subtrees that are generates. optional double shared_tree_split_min_dtime = 328 [default = 0.1]; - // Whether we enumerate all solutions of a problem without objective. Note - // that setting this to true automatically disable some presolve reduction - // that can remove feasible solution. That is it has the same effect as - // setting keep_all_feasible_solutions_in_presolve. + // Whether we enumerate all solutions of a problem without objective. // - // TODO(user): Do not do that and let the user choose what behavior is best by - // setting keep_all_feasible_solutions_in_presolve ? + // WARNING: + // - This can be used with num_workers > 1 but then each solutions can be + // found more than once, so it is up to the client to deduplicate them. + // - If keep_all_feasible_solutions_in_presolve is unset, we will set it to + // true as otherwise, many feasible solution can just be removed by the + // presolve. It is still possible to manually set this to false if one only + // wants to enumerate all solutions of the presolved model. optional bool enumerate_all_solutions = 87 [default = false]; // If true, we disable the presolve reductions that remove feasible solutions @@ -1357,6 +1359,12 @@ message SatParameters { // impact the "base" solution chosen for a LNS/LS fragment. optional int32 solution_pool_size = 193 [default = 3]; + // If solution_pool_size is <= this, we will use DP to keep a "diverse" set + // of solutions (the one further apart via hamming distance) in the pool. + // Setting this to large value might be slow, especially if your solution are + // large. + optional int32 solution_pool_diversity_limit = 329 [default = 10]; + // In order to not get stuck in local optima, when this is non-zero, we try to // also work on "older" solutions with a worse objective value so we get a // chance to follow a different LS/LNS trajectory. diff --git a/ortools/sat/scheduling_helpers.cc b/ortools/sat/scheduling_helpers.cc index 7016ed3280d..1b5928a890b 100644 --- a/ortools/sat/scheduling_helpers.cc +++ b/ortools/sat/scheduling_helpers.cc @@ -477,6 +477,13 @@ SchedulingConstraintHelper::GetEnergyProfile() { return energy_profile_; } +bool SchedulingConstraintHelper::TaskIsBeforeOrIsOverlapping(int before, + int after) { + const auto [expr, ub] = + EncodeDifferenceLowerThan(starts_[before], ends_[after], -1); + return linear2_bounds_->UpperBound(expr) <= ub; +} + void SchedulingConstraintHelper::AddReasonForBeingBeforeAssumingNoOverlap( int before, int after) { AddOtherReason(before); @@ -663,6 +670,51 @@ bool SchedulingConstraintHelper::PushTaskPresence(int t) { return true; } +bool SchedulingConstraintHelper::PushTaskOrderWhenPresent(int t_before, + int t_after) { + CHECK_NE(t_before, t_after); + if (IsAbsent(t_before)) return true; + if (IsAbsent(t_after)) return true; + if (!IsPresent(t_before) && !IsPresent(t_after)) return true; + + const auto [expr, rhs] = + EncodeDifferenceLowerThan(ends_[t_before], starts_[t_after], 0); + const auto status = linear2_bounds_->GetStatus(expr, kMinIntegerValue, rhs); + + if (status == RelationStatus::IS_TRUE) return true; + + ImportOtherReasons(); + + if (status == RelationStatus::IS_FALSE) { + LinearExpression2 negated_expr = expr; + negated_expr.Negate(); + linear2_bounds_->AddReasonForUpperBoundLowerThan( + negated_expr, -rhs - 1, &literal_reason_, &integer_reason_); + if (!IsPresent(t_before)) { + AddPresenceReason(t_after); + return PushTaskAbsence(t_before); + } else if (!IsPresent(t_after)) { + AddPresenceReason(t_before); + return PushTaskAbsence(t_after); + } else { + AddPresenceReason(t_before); + AddPresenceReason(t_after); + return ReportConflict(); + } + } + + if (!IsPresent(t_before) || !IsPresent(t_after)) return true; + + AddPresenceReason(t_before); + AddPresenceReason(t_after); + + AddOtherReason(t_before); + AddOtherReason(t_after); + + return linear2_bounds_->EnqueueLowerOrEqual(expr, rhs, literal_reason_, + integer_reason_); +} + bool SchedulingConstraintHelper::ReportConflict() { ImportOtherReasons(); return integer_trail_->ReportConflict(literal_reason_, integer_reason_); diff --git a/ortools/sat/scheduling_helpers.h b/ortools/sat/scheduling_helpers.h index 2d1daa38768..0082a485828 100644 --- a/ortools/sat/scheduling_helpers.h +++ b/ortools/sat/scheduling_helpers.h @@ -277,6 +277,8 @@ class SchedulingConstraintHelper : public PropagatorInterface { void AddEnergyAfterReason(int t, IntegerValue energy_min, IntegerValue time); void AddEnergyMinInIntervalReason(int t, IntegerValue min, IntegerValue max); + bool TaskIsBeforeOrIsOverlapping(int before, int after); + // Adds the reason why the task "before" must be before task "after", in // the sense that "after" can only start at the same time or later than the // task "before" ends. @@ -321,6 +323,11 @@ class SchedulingConstraintHelper : public PropagatorInterface { ABSL_MUST_USE_RESULT bool PushIntegerLiteralIfTaskPresent(int t, IntegerLiteral lit); + // Push that t_before must end at the same time or before t_after starts. + // This function does the correct thing if t_before or t_after are optional + // and their presence is unknown. Returns false on conflict. + ABSL_MUST_USE_RESULT bool PushTaskOrderWhenPresent(int t_before, int t_after); + absl::Span Starts() const { return starts_; } absl::Span Ends() const { return ends_; } absl::Span Sizes() const { return sizes_; } diff --git a/ortools/sat/solution_crush.cc b/ortools/sat/solution_crush.cc index cad64f12521..e80da657538 100644 --- a/ortools/sat/solution_crush.cc +++ b/ortools/sat/solution_crush.cc @@ -443,6 +443,13 @@ void SolutionCrush::SetReservoirCircuitVars( for (int i = 0; i < active_event_values.size(); ++i) { active_event_value_index[active_event_values[i].index] = i; } + // Set the level vars of inactive events to an arbitrary value. + for (int i = 0; i < num_events; ++i) { + if (active_event_value_index[i] == -1) { + SetVarValue(level_vars[i], min_level); + } + } + for (int i = 0; i < circuit.literals_size(); ++i) { const int head = circuit.heads(i); const int tail = circuit.tails(i); diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index 1eb9590fcca..e6cd92e29c0 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -171,9 +171,12 @@ class SharedSolutionRepository { int num_solutions_to_keep() const { return num_solutions_to_keep_; } + void SetDiversityLimit(int value) { diversity_limit_ = value; } + protected: const std::string name_; const int num_solutions_to_keep_; + int diversity_limit_ = 10; mutable absl::Mutex mutex_; int source_id_ ABSL_GUARDED_BY(mutex_); @@ -212,7 +215,10 @@ class SharedSolutionPool { explicit SharedSolutionPool(const SatParameters& parameters_) : best_solutions_(parameters_.solution_pool_size(), "best_solutions"), alternative_path_(parameters_.alternative_pool_size(), - "alternative_path", /*source_id=*/0) {} + "alternative_path", /*source_id=*/0) { + best_solutions_.SetDiversityLimit( + parameters_.solution_pool_diversity_limit()); + } const SharedSolutionRepository& BestSolutions() const { return best_solutions_; @@ -1170,7 +1176,8 @@ void SharedSolutionRepository::Synchronize( ++num_best; } - if (num_best > num_solutions_to_keep_ && num_solutions_to_keep_ < 10) { + if (num_best > num_solutions_to_keep_ && + num_solutions_to_keep_ <= diversity_limit_) { // We should only be here if a new solution (not in our current set) was // found. It could be one we saw before but forgot about. We put one // first. @@ -1182,8 +1189,9 @@ void SharedSolutionRepository::Synchronize( } } - // We are going to be in O(n^2 * solution_size), so keep n <= 10. - solutions_.resize(std::min(10, num_best)); + // We are going to be in O(n^2 * solution_size + 2^n), + // so keep n <= diversity_limit_. + solutions_.resize(std::min(diversity_limit_, num_best)); // Fill the pairwise distances. const int n = solutions_.size(); @@ -1208,6 +1216,9 @@ void SharedSolutionRepository::Synchronize( // with the rest. // // This way, as we find new solution, the set changes slowly. + // + // TODO(user): When n == num_solutions_to_keep_ + 1, there is + // a faster algo thant 2^n since there is only n possible sets. Fix. const std::vector selected = FindMostDiverseSubset(num_solutions_to_keep_, n, distances_, buffer_, /*always_pick_mask = */ 1); diff --git a/ortools/sat/table_test.cc b/ortools/sat/table_test.cc index 43669185dda..1e94f08789b 100644 --- a/ortools/sat/table_test.cc +++ b/ortools/sat/table_test.cc @@ -119,23 +119,6 @@ TEST(TableConstraintTest, EmptyOrTrivialSemantics) { } )pb"), CpSolverStatus::OPTIMAL); - - // Invalid: not affine - EXPECT_EQ(SolveTextProto(R"pb( - variables { domain: [ 0, 0 ] } - variables { domain: [ 0, 0 ] } - constraints { - table { - values: [ 0 ] - exprs: - [ { - vars: [ 0, 1 ] - coeffs: [ 1, 1 ] - }] - } - } - )pb"), - CpSolverStatus::MODEL_INVALID); } TEST(TableConstraintTest, EnumerationAndEncoding) { diff --git a/ortools/sat/timetable.cc b/ortools/sat/timetable.cc index b8d194297b7..b42989cb01e 100644 --- a/ortools/sat/timetable.cc +++ b/ortools/sat/timetable.cc @@ -19,6 +19,7 @@ #include "absl/log/check.h" #include "absl/types/span.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" @@ -29,10 +30,12 @@ namespace operations_research { namespace sat { -void AddReservoirConstraint(std::vector times, - std::vector deltas, - std::vector presences, int64_t min_level, - int64_t max_level, Model* model) { +void AddReservoirConstraint(absl::Span enforcement_literals, + absl::Span times, + absl::Span deltas, + absl::Span presences, + int64_t min_level, int64_t max_level, + Model* model) { // We only create a side if it can fail. IntegerValue min_possible(0); IntegerValue max_possible(0); @@ -42,51 +45,70 @@ void AddReservoirConstraint(std::vector times, max_possible += std::max(IntegerValue(0), integer_trail->UpperBound(d)); } if (max_possible > max_level) { - model->TakeOwnership(new ReservoirTimeTabling( - times, deltas, presences, IntegerValue(max_level), model)); + model->TakeOwnership( + new ReservoirTimeTabling(enforcement_literals, times, deltas, presences, + IntegerValue(max_level), model)); } if (min_possible < min_level) { - for (AffineExpression& ref : deltas) ref = ref.Negated(); - model->TakeOwnership(new ReservoirTimeTabling( - times, deltas, presences, IntegerValue(-min_level), model)); + std::vector negated_deltas; + for (const AffineExpression& ref : deltas) { + negated_deltas.push_back(ref.Negated()); + } + model->TakeOwnership( + new ReservoirTimeTabling(enforcement_literals, times, negated_deltas, + presences, IntegerValue(-min_level), model)); } } ReservoirTimeTabling::ReservoirTimeTabling( - const std::vector& times, - const std::vector& deltas, - const std::vector& presences, IntegerValue capacity, Model* model) - : times_(times), - deltas_(deltas), - presences_(presences), + absl::Span enforcement_literals, + absl::Span times, + absl::Span deltas, + absl::Span presences, IntegerValue capacity, Model* model) + : enforcement_literals_(enforcement_literals.begin(), + enforcement_literals.end()), + times_(times.begin(), times.end()), + deltas_(deltas.begin(), deltas.end()), + presences_(presences.begin(), presences.end()), capacity_(capacity), assignment_(model->GetOrCreate()->Assignment()), - integer_trail_(model->GetOrCreate()) { + integer_trail_(*model->GetOrCreate()), + enforcement_propagator_(*model->GetOrCreate()) { auto* watcher = model->GetOrCreate(); const int id = watcher->Register(this); const int num_events = times.size(); for (int e = 0; e < num_events; e++) { watcher->WatchLowerBound(deltas_[e], id); - if (integer_trail_->UpperBound(deltas_[e]) > 0) { + if (integer_trail_.UpperBound(deltas_[e]) > 0) { watcher->WatchUpperBound(times_[e].var, id); watcher->WatchLiteral(presences_[e], id); } - if (integer_trail_->LowerBound(deltas_[e]) < 0) { + if (integer_trail_.LowerBound(deltas_[e]) < 0) { watcher->WatchLowerBound(times_[e].var, id); watcher->WatchLiteral(presences_[e].Negated(), id); } } watcher->NotifyThatPropagatorMayNotReachFixedPointInOnePass(id); + enforcement_id_ = + enforcement_propagator_.Register(enforcement_literals, watcher, id); } bool ReservoirTimeTabling::Propagate() { + const EnforcementStatus status = + enforcement_propagator_.DebugStatus(enforcement_id_); + if (status == EnforcementStatus::IS_FALSE || + status == EnforcementStatus::CANNOT_PROPAGATE) { + return true; + } + const int num_events = times_.size(); if (!BuildProfile()) return false; + if (status != EnforcementStatus::IS_ENFORCED) return true; for (int e = 0; e < num_events; e++) { if (assignment_.LiteralIsFalse(presences_[e])) continue; // For positive delta_min, we can maybe increase the min. - const IntegerValue min_d = integer_trail_->LowerBound(deltas_[e]); + const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]); if (min_d > 0 && !TryToIncreaseMin(e)) return false; // For negative delta_min, we can maybe decrease the max. @@ -105,16 +127,16 @@ bool ReservoirTimeTabling::BuildProfile() { const int num_events = times_.size(); profile_.emplace_back(kMinIntegerValue, IntegerValue(0)); // Sentinel. for (int e = 0; e < num_events; e++) { - const IntegerValue min_d = integer_trail_->LowerBound(deltas_[e]); + const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]); if (min_d > 0) { // Only consider present event for positive delta. if (!assignment_.LiteralIsTrue(presences_[e])) continue; - const IntegerValue ub = integer_trail_->UpperBound(times_[e]); + const IntegerValue ub = integer_trail_.UpperBound(times_[e]); profile_.push_back({ub, min_d}); } else if (min_d < 0) { // Only consider non-absent event for negative delta. if (assignment_.LiteralIsFalse(presences_[e])) continue; - profile_.push_back({integer_trail_->LowerBound(times_[e]), min_d}); + profile_.push_back({integer_trail_.LowerBound(times_[e]), min_d}); } } profile_.emplace_back(kMaxIntegerValue, IntegerValue(0)); // Sentinel. @@ -134,11 +156,19 @@ bool ReservoirTimeTabling::BuildProfile() { profile_.resize(last + 1); // Conflict? + const bool is_enforced = enforcement_propagator_.Status(enforcement_id_) == + EnforcementStatus::IS_ENFORCED; for (const ProfileRectangle& rect : profile_) { if (rect.height <= capacity_) continue; FillReasonForProfileAtGivenTime(rect.start); - return integer_trail_->ReportConflict(literal_reason_, integer_reason_); + if (is_enforced) { + return enforcement_propagator_.ReportConflict( + enforcement_id_, literal_reason_, integer_reason_); + } else { + return enforcement_propagator_.PropagateWhenFalse( + enforcement_id_, literal_reason_, integer_reason_); + } } return true; @@ -173,10 +203,10 @@ void ReservoirTimeTabling::FillReasonForProfileAtGivenTime( const int num_events = times_.size(); for (int e = 0; e < num_events; e++) { if (e == event_to_ignore) continue; - const IntegerValue min_d = integer_trail_->LowerBound(deltas_[e]); + const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]); if (min_d > 0) { if (!assignment_.LiteralIsTrue(presences_[e])) continue; - if (integer_trail_->UpperBound(times_[e]) > t) continue; + if (integer_trail_.UpperBound(times_[e]) > t) continue; AddGreaterOrEqual(deltas_[e], min_d, &integer_reason_); AddLowerOrEqual(times_[e], t, &integer_reason_); literal_reason_.push_back(presences_[e].Negated()); @@ -186,7 +216,7 @@ void ReservoirTimeTabling::FillReasonForProfileAtGivenTime( continue; } AddGreaterOrEqual(deltas_[e], min_d, &integer_reason_); - if (min_d < 0 && integer_trail_->LowerBound(times_[e]) > t) { + if (min_d < 0 && integer_trail_.LowerBound(times_[e]) > t) { AddGreaterOrEqual(times_[e], t + 1, &integer_reason_); } } @@ -196,10 +226,10 @@ void ReservoirTimeTabling::FillReasonForProfileAtGivenTime( // Note that a negative event will always be in the profile, even if its // presence is still not settled. bool ReservoirTimeTabling::TryToDecreaseMax(int event) { - const IntegerValue min_d = integer_trail_->LowerBound(deltas_[event]); + const IntegerValue min_d = integer_trail_.LowerBound(deltas_[event]); CHECK_LT(min_d, 0); - const IntegerValue start = integer_trail_->LowerBound(times_[event]); - const IntegerValue end = integer_trail_->UpperBound(times_[event]); + const IntegerValue start = integer_trail_.LowerBound(times_[event]); + const IntegerValue end = integer_trail_.UpperBound(times_[event]); // We already tested for conflict in BuildProfile(). if (start == end) return true; @@ -235,7 +265,8 @@ bool ReservoirTimeTabling::TryToDecreaseMax(int event) { // updated, better be defensive. if (new_end < start) { AddGreaterOrEqual(times_[event], new_end + 1, &integer_reason_); - return integer_trail_->ReportConflict(literal_reason_, integer_reason_); + return enforcement_propagator_.ReportConflict( + enforcement_id_, literal_reason_, integer_reason_); } // First, the task MUST be present, otherwise we have a conflict. @@ -243,20 +274,21 @@ bool ReservoirTimeTabling::TryToDecreaseMax(int event) { // TODO(user): We actually need to look after 'end' to potentially push the // presence in more situation. if (!assignment_.LiteralIsTrue(presences_[event])) { - integer_trail_->EnqueueLiteral(presences_[event], literal_reason_, - integer_reason_); + enforcement_propagator_.EnqueueLiteral(enforcement_id_, presences_[event], + literal_reason_, integer_reason_); } // Push new_end too. Note that we don't need the presence reason. - return integer_trail_->Enqueue(times_[event].LowerOrEqual(new_end), - literal_reason_, integer_reason_); + return enforcement_propagator_.Enqueue(enforcement_id_, + times_[event].LowerOrEqual(new_end), + literal_reason_, integer_reason_); } bool ReservoirTimeTabling::TryToIncreaseMin(int event) { - const IntegerValue min_d = integer_trail_->LowerBound(deltas_[event]); + const IntegerValue min_d = integer_trail_.LowerBound(deltas_[event]); CHECK_GT(min_d, 0); - const IntegerValue start = integer_trail_->LowerBound(times_[event]); - const IntegerValue end = integer_trail_->UpperBound(times_[event]); + const IntegerValue start = integer_trail_.LowerBound(times_[event]); + const IntegerValue end = integer_trail_.UpperBound(times_[event]); // We already tested for conflict in BuildProfile(). if (start == end) return true; @@ -302,9 +334,10 @@ bool ReservoirTimeTabling::TryToIncreaseMin(int event) { // The reason is simply the capacity at new_start - 1; FillReasonForProfileAtGivenTime(new_start - 1, event); AddGreaterOrEqual(deltas_[event], min_d, &integer_reason_); - return integer_trail_->ConditionalEnqueue( - presences_[event], times_[event].GreaterOrEqual(new_start), - &literal_reason_, &integer_reason_); + return enforcement_propagator_.ConditionalEnqueue( + enforcement_id_, presences_[event], + times_[event].GreaterOrEqual(new_start), literal_reason_, + integer_reason_); } TimeTablingPerTask::TimeTablingPerTask(AffineExpression capacity, diff --git a/ortools/sat/timetable.h b/ortools/sat/timetable.h index ba937c08440..08c4f9489d9 100644 --- a/ortools/sat/timetable.h +++ b/ortools/sat/timetable.h @@ -17,6 +17,8 @@ #include #include +#include "absl/types/span.h" +#include "ortools/sat/cp_constraints.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" @@ -33,10 +35,11 @@ namespace sat { // // This instantiate one or more ReservoirTimeTabling class to perform the // propagation. -void AddReservoirConstraint(std::vector times, - std::vector deltas, - std::vector presences, int64_t min_level, - int64_t max_level, Model* model); +void AddReservoirConstraint(absl::Span enforcement_literals, + absl::Span times, + absl::Span deltas, + absl::Span presences, + int64_t min_level, int64_t max_level, Model* model); // The piecewise constant function must be below the given capacity. The initial // function value is zero. Note that a negative capacity will thus be trivially @@ -47,9 +50,10 @@ void AddReservoirConstraint(std::vector times, // full horizon, we could have taken < t with no behavior change. class ReservoirTimeTabling : public PropagatorInterface { public: - ReservoirTimeTabling(const std::vector& times, - const std::vector& deltas, - const std::vector& presences, + ReservoirTimeTabling(absl::Span enforcement_literals, + absl::Span times, + absl::Span deltas, + absl::Span presences, IntegerValue capacity, Model* model); bool Propagate() final; @@ -86,6 +90,7 @@ class ReservoirTimeTabling : public PropagatorInterface { bool TryToDecreaseMax(int event); // Input. + std::vector enforcement_literals_; std::vector times_; std::vector deltas_; std::vector presences_; @@ -93,7 +98,9 @@ class ReservoirTimeTabling : public PropagatorInterface { // Model class. const VariablesAssignment& assignment_; - IntegerTrail* integer_trail_; + const IntegerTrail& integer_trail_; + EnforcementPropagator& enforcement_propagator_; + EnforcementId enforcement_id_; // Temporary data. std::vector literal_reason_; diff --git a/ortools/sat/timetable_test.cc b/ortools/sat/timetable_test.cc index 10733ba5264..9a8cb7d02ad 100644 --- a/ortools/sat/timetable_test.cc +++ b/ortools/sat/timetable_test.cc @@ -406,7 +406,8 @@ TEST(ReservoirTest, FindAllParenthesis) { std::vector all_true(size, true_lit); model.Add(AllDifferentOnBounds(vars)); - AddReservoirConstraint(times, deltas, all_true, 0, size, &model); + AddReservoirConstraint(/*enforcement_literals=*/{}, times, deltas, all_true, + 0, size, &model); absl::btree_map sequence_to_count; int num_solutions_found = 0; @@ -462,7 +463,8 @@ TEST(ReservoirTest, FindAllParenthesisWithOptionality) { } model.Add(AllDifferentOnBounds(vars)); - AddReservoirConstraint(times, deltas, present, 0, size, &model); + AddReservoirConstraint(/*enforcement_literals=*/{}, times, deltas, present, 0, + size, &model); absl::btree_map sequence_to_count; int num_solutions_found = 0; @@ -512,7 +514,8 @@ TEST(ReservoirTest, VariableLevelChange) { const int min_level = 0; const int max_level = 1; - AddReservoirConstraint(times, deltas, all_true, min_level, max_level, &model); + AddReservoirConstraint(/*enforcement_literals=*/{}, times, deltas, all_true, + min_level, max_level, &model); absl::btree_map sequence_to_count; int num_solutions_found = 0; @@ -552,6 +555,26 @@ TEST(ReservoirTest, VariableLevelChange) { EXPECT_EQ(num_solutions_found, 1 << size); } +TEST(ReservoirTimeTablingTest, WithUnassignedEnforcementLiteral) { + Model model; + std::vector times(4); + std::vector deltas(4); + std::vector presences(4); + for (int i = 0; i < 4; ++i) { + times.push_back(AffineExpression(i + 1)); + deltas.push_back(AffineExpression(i + 2)); + presences.push_back(model.GetOrCreate()->GetTrueLiteral()); + } + const Literal b = Literal(model.Add(NewBooleanVariable()), true); + // Always false is enforced (sum(deltas) = 2+3+4+5 > 10). + model.TakeOwnership(new ReservoirTimeTabling({b}, times, deltas, presences, + IntegerValue(10), &model)); + + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsFalse(b)); + EXPECT_EQ(model.GetOrCreate()->num_enqueues(), 0); +} + } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/sat/util.cc b/ortools/sat/util.cc index 8f212eacf83..8e29e93780f 100644 --- a/ortools/sat/util.cc +++ b/ortools/sat/util.cc @@ -129,7 +129,7 @@ void RandomizeDecisionHeuristic(absl::BitGenRef random, namespace { // This will be optimized into one division. I tested that in other places: -// 3/ortools/sat/integer_test.cc;l=1223-1228;bpv=0 +// https://source.corp.google.com/piper///depot/ortools/sat/integer_test.cc;l=1223-1228;bpv=0 // // Note that I am not 100% sure we need the indirection for the optimization // to kick in though, but this seemed safer given our weird r[i ^ 1] inputs. @@ -1014,7 +1014,40 @@ std::vector FindMostDiverseSubset(int k, int n, absl::Span distances, std::vector& buffer, int always_pick_mask) { - CHECK_LE(n, 20); + DCHECK_LE(k, n); + std::vector result; + result.reserve(k); + + if (k == n) { + for (int i = 0; i < n; ++i) result.push_back(i); + return result; + } + + if (k == n - 1) { + // We just exclude the one closer to all the other. + int64_t worse = std::numeric_limits::max(); + int to_exclude = -1; + for (int i = 0; i < n; ++i) { + if ((always_pick_mask >> i) & 1) continue; + + int64_t score = 0; + for (int j = 0; j < n; ++j) { + if (i != j) score += distances[i * n + j]; + } + if (score < worse) { + worse = score; + to_exclude = i; + } + } + + CHECK_NE(to_exclude, -1); + for (int i = 0; i < n; ++i) { + if (i != to_exclude) result.push_back(i); + } + return result; + } + + CHECK_LE(n, 25); const int limit = 1 << n; buffer.assign(limit, 0); int best_mask; @@ -1043,8 +1076,7 @@ std::vector FindMostDiverseSubset(int k, int n, best_mask = mask; } } - std::vector result; - result.reserve(k); + for (int i = 0; i < n; ++i) { if ((best_mask >> i) & 1) { result.push_back(i); diff --git a/ortools/sat/util.h b/ortools/sat/util.h index 030fc254da8..c36e9ffa4a0 100644 --- a/ortools/sat/util.h +++ b/ortools/sat/util.h @@ -394,9 +394,10 @@ int MoveOneUnprocessedLiteralLast( // Selects k out of n such that the sum of pairwise distances is maximal. // distances[i * n + j] = distances[j * n + j] = distances between i and j. // -// This shall only be called with small n, we CHECK_LE(n, 20). -// Complexity is in O(2 ^ n + n_choose_k * n). -// Memory is in O(2 ^ n). +// In the special case k >= n - 1, we use a faster algo. +// +// Otherwise, this shall only be called with small n, we CHECK_LE(n, 25). +// Complexity is in O(2 ^ n + n_choose_k * n). Memory is in O(2 ^ n). // // In case of tie, this will choose deterministically, so one can randomize the // order first to get a random subset. The returned subset will always be diff --git a/ortools/sat/util_test.cc b/ortools/sat/util_test.cc index 76f1dd34076..f9a0c928c4e 100644 --- a/ortools/sat/util_test.cc +++ b/ortools/sat/util_test.cc @@ -1164,46 +1164,47 @@ TEST(DagTopologicalSortIteratorTest, RandomTest) { TEST(FindMostDiverseSubsetTest, Random) { const int k = 4; - const int n = 10; - absl::BitGen random; - std::vector distances(n * n); - std::vector buffer; - for (int i = 0; i < n; ++i) { - for (int j = i + 1; j < n; ++j) { - distances[i * n + j] = distances[j * n + i] = - absl::Uniform(random, 0, 1000); + for (const int n : {4, 5, 10}) { // We test the two special cases. + absl::BitGen random; + std::vector distances(n * n); + std::vector buffer; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + distances[i * n + j] = distances[j * n + i] = + absl::Uniform(random, 0, 1000); + } } - } - const std::vector result = - FindMostDiverseSubset(k, n, distances, buffer); - CHECK(std::is_sorted(result.begin(), result.end())); - int64_t result_value = 0; - for (const int i : result) { - for (const int j : result) { - if (i < j) result_value += distances[i * n + j]; + const std::vector result = + FindMostDiverseSubset(k, n, distances, buffer); + CHECK(std::is_sorted(result.begin(), result.end())); + int64_t result_value = 0; + for (const int i : result) { + for (const int j : result) { + if (i < j) result_value += distances[i * n + j]; + } } - } - int64_t best_seen = 0; - std::vector subset; - const int limit = 1 << n; - for (unsigned int mask = 0; mask < limit; ++mask) { - if (absl::popcount(mask) != k) continue; - subset.clear(); - for (int i = 0; i < n; ++i) { - if ((mask >> i) & 1) subset.push_back(i); - } - int64_t value = 0; - for (const int i : subset) { - for (const int j : subset) { - if (i < j) value += distances[i * n + j]; + int64_t best_seen = 0; + std::vector subset; + const int limit = 1 << n; + for (unsigned int mask = 0; mask < limit; ++mask) { + if (absl::popcount(mask) != k) continue; + subset.clear(); + for (int i = 0; i < n; ++i) { + if ((mask >> i) & 1) subset.push_back(i); } + int64_t value = 0; + for (const int i : subset) { + for (const int j : subset) { + if (i < j) value += distances[i * n + j]; + } + } + ASSERT_LE(value, result_value); + best_seen = std::max(best_seen, value); } - ASSERT_LE(value, result_value); - best_seen = std::max(best_seen, value); + EXPECT_EQ(best_seen, result_value); } - EXPECT_EQ(best_seen, result_value); } TEST(FindMostDiverseSubsetTest, RandomButAlwaysPickZero) { From 6041f1db2c589dd4fd115f6bc2f521a88ebac260 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sun, 31 Aug 2025 14:08:21 +0200 Subject: [PATCH 020/491] [FZ] add global cardinality native support; bypass expansion --- ortools/flatzinc/checker.cc | 76 +- ortools/flatzinc/cp_model_fz_solver.cc | 783 ++++++++++++++---- ortools/flatzinc/mznlib/fzn_all_equal_int.mzn | 2 +- ortools/flatzinc/mznlib/fzn_all_equal_set.mzn | 2 +- .../mznlib/fzn_global_cardinality.mzn | 12 +- .../mznlib/fzn_global_cardinality_closed.mzn | 12 +- .../mznlib/fzn_global_cardinality_low_up.mzn | 14 + .../fzn_global_cardinality_low_up_closed.mzn | 14 + 8 files changed, 669 insertions(+), 246 deletions(-) create mode 100644 ortools/flatzinc/mznlib/fzn_global_cardinality_low_up.mzn create mode 100644 ortools/flatzinc/mznlib/fzn_global_cardinality_low_up_closed.mzn diff --git a/ortools/flatzinc/checker.cc b/ortools/flatzinc/checker.cc index 687c10228b3..69df510757a 100644 --- a/ortools/flatzinc/checker.cc +++ b/ortools/flatzinc/checker.cc @@ -665,92 +665,51 @@ std::vector ComputeGlobalCardinalityCards( return cards; } -bool CheckGlobalCardinality( +bool CheckOrToolsGlobalCardinality( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const std::vector cards = ComputeGlobalCardinalityCards(ct, evaluator); CHECK_EQ(cards.size(), Length(ct.arguments[2])); + const bool is_closed = Eval(ct.arguments[3], evaluator) != 0; for (int i = 0; i < Length(ct.arguments[2]); ++i) { const int64_t card = EvalAt(ct.arguments[2], i, evaluator); if (card != cards[i]) { return false; } } - return true; -} -bool CheckGlobalCardinalityClosed( - const Constraint& ct, const std::function& evaluator, - const std::function(Variable*)>& set_evaluator) { - const std::vector cards = - ComputeGlobalCardinalityCards(ct, evaluator); - CHECK_EQ(cards.size(), Length(ct.arguments[2])); - for (int i = 0; i < Length(ct.arguments[2]); ++i) { - const int64_t card = EvalAt(ct.arguments[2], i, evaluator); - if (card != cards[i]) { - return false; - } - } - int64_t sum_of_cards = 0; - for (int64_t card : cards) { - sum_of_cards += card; - } - return sum_of_cards == Length(ct.arguments[0]); -} - -bool CheckGlobalCardinalityLowUp( - const Constraint& ct, const std::function& evaluator, - const std::function(Variable*)>& set_evaluator) { - const std::vector cards = - ComputeGlobalCardinalityCards(ct, evaluator); - CHECK_EQ(cards.size(), ct.arguments[2].values.size()); - CHECK_EQ(cards.size(), ct.arguments[3].values.size()); - for (int i = 0; i < cards.size(); ++i) { - const int64_t card = cards[i]; - if (card < ct.arguments[2].values[i] || card > ct.arguments[3].values[i]) { - return false; + if (is_closed) { + int64_t sum_of_cards = 0; + for (int64_t card : cards) { + sum_of_cards += card; } + return sum_of_cards == Length(ct.arguments[0]); } return true; } -bool CheckGlobalCardinalityLowUpClosed( +bool CheckOrToolsGlobalCardinalityLowUp( const Constraint& ct, const std::function& evaluator, const std::function(Variable*)>& set_evaluator) { const std::vector cards = ComputeGlobalCardinalityCards(ct, evaluator); CHECK_EQ(cards.size(), ct.arguments[2].values.size()); CHECK_EQ(cards.size(), ct.arguments[3].values.size()); + const bool is_closed = Eval(ct.arguments[4], evaluator) != 0; for (int i = 0; i < cards.size(); ++i) { const int64_t card = cards[i]; if (card < ct.arguments[2].values[i] || card > ct.arguments[3].values[i]) { return false; } } - int64_t sum_of_cards = 0; - for (int64_t card : cards) { - sum_of_cards += card; - } - return sum_of_cards == Length(ct.arguments[0]); -} -bool CheckGlobalCardinalityOld( - const Constraint& ct, const std::function& evaluator, - const std::function(Variable*)>& set_evaluator) { - const int size = Length(ct.arguments[1]); - std::vector cards(size, 0); - for (int i = 0; i < Length(ct.arguments[0]); ++i) { - const int64_t value = EvalAt(ct.arguments[0], i, evaluator); - if (value >= 0 && value < size) { - cards[value]++; - } - } - for (int i = 0; i < size; ++i) { - const int64_t card = EvalAt(ct.arguments[1], i, evaluator); - if (card != cards[i]) { - return false; + if (is_closed) { + int64_t sum_of_cards = 0; + for (int64_t card : cards) { + sum_of_cards += card; } + return sum_of_cards == Length(ct.arguments[0]); } return true; } @@ -1678,11 +1637,6 @@ CallMap CreateCallMap() { m["fzn_diffn"] = CheckDiffn; m["fzn_disjunctive_strict"] = CheckDisjunctiveStrict; m["fzn_disjunctive"] = CheckDisjunctive; - m["global_cardinality_closed"] = CheckGlobalCardinalityClosed; - m["global_cardinality_low_up_closed"] = CheckGlobalCardinalityLowUpClosed; - m["global_cardinality_low_up"] = CheckGlobalCardinalityLowUp; - m["global_cardinality_old"] = CheckGlobalCardinalityOld; - m["global_cardinality"] = CheckGlobalCardinality; m["int_abs"] = CheckIntAbs; m["int_div"] = CheckIntDiv; m["int_eq_imp"] = CheckIntEqImp; @@ -1740,6 +1694,8 @@ CallMap CreateCallMap() { m["ortools_count_eq"] = CheckOrToolsCountEq; m["ortools_cumulative_opt"] = CheckOrToolsCumulativeOpt; m["ortools_disjunctive_strict_opt"] = CheckOrToolsDisjunctiveStrictOpt; + m["ortools_global_cardinality_low_up"] = CheckOrToolsGlobalCardinalityLowUp; + m["ortools_global_cardinality"] = CheckOrToolsGlobalCardinality; m["ortools_inverse"] = CheckOrToolsInverse; m["ortools_lex_less_bool"] = CheckOrToolsLexLessInt; m["ortools_lex_less_int"] = CheckOrToolsLexLessInt; diff --git a/ortools/flatzinc/cp_model_fz_solver.cc b/ortools/flatzinc/cp_model_fz_solver.cc index 507ef423a5e..b82f0a27969 100644 --- a/ortools/flatzinc/cp_model_fz_solver.cc +++ b/ortools/flatzinc/cp_model_fz_solver.cc @@ -18,11 +18,13 @@ #include #include #include +#include #include #include #include #include +#include "absl/algorithm/container.h" #include "absl/container/btree_map.h" #include "absl/container/btree_set.h" #include "absl/container/flat_hash_map.h" @@ -53,7 +55,7 @@ ABSL_FLAG(int64_t, fz_int_max, int64_t{1} << 40, "Default max value for unbounded integer variables."); ABSL_FLAG(bool, force_interleave_search, false, "If true, enable interleaved workers when num_workers is 1."); -ABSL_FLAG(bool, fz_light_full_encoding, false, +ABSL_FLAG(bool, fz_light_encoding, false, "EXPERIMENTAL: Use the at_most_one encoding when fully encoding a " "variable."); @@ -128,17 +130,27 @@ struct CpModelProtoWithMapping { std::shared_ptr LookupSetVarAt(const fz::Argument& argument, int pos); - // Get or create a literal that is equivalent to var == value. + // literal <=> (var op value). int GetOrCreateLiteralForVarEqValue(int var, int64_t value); + void AddVarEqValueLiteral(int var, int64_t value, int literal); + void AddVarGeValueLiteral(int var, int64_t value, int literal); + void AddVarGtValueLiteral(int var, int64_t value, int literal); + void AddVarLeValueLiteral(int var, int64_t value, int literal); + void AddVarLtValueLiteral(int var, int64_t value, int literal); - // Get or create a literal that is equivalent to var1 == var2. + // literal <=> (var1 op var2). int GetOrCreateLiteralForVarEqVar(int var1, int var2); - - // Get or create a literal that is equivalent to var1 < var2. - int GetOrCreateLiteralForVarLtVar(int var1, int var2); - - // Get or create a literal that is equivalent to var1 <= var2. + void AddVarEqVarLiteral(int var1, int var2, int literal); + std::optional GetVarEqVarLiteral(int var1, int var2); + int GetOrCreateLiteralForVarNeVar(int var1, int var2); + std::optional GetVarNeVarLiteral(int var1, int var2); + void AddVarNeVarLiteral(int var1, int var2, int literal); int GetOrCreateLiteralForVarLeVar(int var1, int var2); + std::optional GetVarLeVarLiteral(int var1, int var2); + void AddVarLeVarLiteral(int var1, int var2, int literal); + int GetOrCreateLiteralForVarLtVar(int var1, int var2); + std::optional GetVarLtVarLiteral(int var1, int var2); + void AddVarLtVarLiteral(int var1, int var2, int literal); // Returns the list of literals corresponding to the domain of the variable. absl::flat_hash_map FullyEncode(int var); @@ -193,8 +205,7 @@ struct CpModelProtoWithMapping { const fz::Constraint& fz_ct, ConstraintProto* ct); void FillConstraint(const fz::Constraint& fz_ct, ConstraintProto* ct); - void FillReifOrImpliedConstraint(const fz::Constraint& fz_ct, - ConstraintProto* ct); + void FillReifiedOrImpliedConstraint(const fz::Constraint& fz_ct); void BuildTableFromDomainIntLinEq(const fz::Constraint& fz_ct, ConstraintProto* ct); @@ -226,15 +237,19 @@ struct CpModelProtoWithMapping { absl::flat_hash_map, int> var_eq_var_to_literal; absl::flat_hash_map, int> var_lt_var_to_literal; absl::flat_hash_map, int> var_le_var_to_literal; + absl::flat_hash_map, int> var_le_value_to_literal; absl::flat_hash_map> set_variables; + int num_var_op_value_reif_cached = 0; + int num_var_op_var_reif_cached = 0; + int num_var_op_var_imp_cached = 0; }; int CpModelProtoWithMapping::NewBoolVar() { return NewIntVar(0, 1); } int CpModelProtoWithMapping::NewIntVar(int64_t min_value, int64_t max_value) { const int index = proto.variables_size(); - FillDomainInProto({min_value, max_value}, proto.add_variables()); + FillDomainInProto(min_value, max_value, proto.add_variables()); return index; } @@ -482,6 +497,8 @@ void CpModelProtoWithMapping::AddLexOrdering(absl::Span x, } } +// lit <=> var op value + int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqValue(int var, int64_t value) { CHECK_GE(var, 0); @@ -505,13 +522,54 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqValue(int var, } const int bool_var = NewBoolVar(); - var_eq_value_to_literal[key] = bool_var; + AddVarEqValueLiteral(var, value, bool_var); + return bool_var; +} - AddLinearConstraint({bool_var}, Domain(value), {{var, 1}}); - AddLinearConstraint({NegatedRef(bool_var)}, Domain(value).Complement(), - {{var, 1}}); +void CpModelProtoWithMapping::AddVarEqValueLiteral(int var, int64_t value, + int lit) { + // TODO(user): Special case for Boolean variables. + CHECK_GE(var, 0); + const std::pair key = {var, value}; + const auto it = var_eq_value_to_literal.find(key); + if (it != var_eq_value_to_literal.end()) { + AddLinearConstraint({}, Domain(0), {{lit, 1}, {it->second, -1}}); + ++num_var_op_value_reif_cached; + } else { + AddLinearConstraint({lit}, Domain(value), {{var, 1}}); + AddLinearConstraint({NegatedRef(lit)}, Domain(value).Complement(), + {{var, 1}}); + var_eq_value_to_literal[key] = lit; + } +} - return bool_var; +void CpModelProtoWithMapping::AddVarGeValueLiteral(int var, int64_t value, + int lit) { + AddVarLeValueLiteral(var, value - 1, NegatedRef(lit)); +} + +void CpModelProtoWithMapping::AddVarGtValueLiteral(int var, int64_t value, + int lit) { + AddVarLeValueLiteral(var, value, NegatedRef(lit)); +} + +void CpModelProtoWithMapping::AddVarLtValueLiteral(int var, int64_t value, + int lit) { + AddVarLeValueLiteral(var, value - 1, lit); +} + +void CpModelProtoWithMapping::AddVarLeValueLiteral(int var, int64_t value, + int lit) { + const auto it = var_le_value_to_literal.find({var, value}); + if (it != var_le_value_to_literal.end()) { + AddLinearConstraint({}, Domain(0), {{lit, 1}, {it->second, -1}}); + ++num_var_op_value_reif_cached; + } else { + var_le_value_to_literal[{var, value}] = lit; + const Domain domain(std::numeric_limits::min(), value); + AddLinearConstraint({lit}, domain, {{var, 1}}); + AddLinearConstraint({NegatedRef(lit)}, domain.Complement(), {{var, 1}}); + } } absl::flat_hash_map CpModelProtoWithMapping::FullyEncode( @@ -520,7 +578,7 @@ absl::flat_hash_map CpModelProtoWithMapping::FullyEncode( const Domain domain = ReadDomainFromProto(proto.variables(var)); bool use_normal_encoding = true; - if (absl::GetFlag(FLAGS_fz_light_full_encoding)) { + if (absl::GetFlag(FLAGS_fz_light_encoding)) { use_normal_encoding = false; for (int64_t value : domain.Values()) { if (var_eq_value_to_literal.contains({var, value})) { @@ -549,6 +607,8 @@ absl::flat_hash_map CpModelProtoWithMapping::FullyEncode( return result; } +// literal <=> var1 op var2 + int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqVar(int var1, int var2) { CHECK_NE(var1, kNoVar); CHECK_NE(var2, kNoVar); @@ -559,14 +619,59 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarEqVar(int var1, int var2) { const auto it = var_eq_var_to_literal.find(key); if (it != var_eq_var_to_literal.end()) return it->second; - const int bool_var = NewBoolVar(); + const int lit = NewBoolVar(); + AddVarEqVarLiteral(var1, var2, lit); + return lit; +} - AddLinearConstraint({bool_var}, Domain(0), {{var1, 1}, {var2, -1}}); - AddLinearConstraint({NegatedRef(bool_var)}, Domain(0).Complement(), - {{var1, 1}, {var2, -1}}); +int CpModelProtoWithMapping::GetOrCreateLiteralForVarNeVar(int var1, int var2) { + return NegatedRef(GetOrCreateLiteralForVarEqVar(var1, var2)); +} - var_eq_var_to_literal[key] = bool_var; - return bool_var; +void CpModelProtoWithMapping::AddVarEqVarLiteral(int var1, int var2, int lit) { + // TODO(user): Special case for Boolean variables. + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) { + AddImplication({}, lit); + return; + } + if (var1 > var2) std::swap(var1, var2); + + const std::pair key = {var1, var2}; + const auto it = var_eq_var_to_literal.find(key); + if (it != var_eq_var_to_literal.end()) { + AddLinearConstraint({}, Domain(0), {{lit, 1}, {it->second, -1}}); + ++num_var_op_var_reif_cached; + } else { + AddLinearConstraint({lit}, Domain(0), {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(lit)}, Domain(0).Complement(), + {{var1, 1}, {var2, -1}}); + var_eq_var_to_literal[key] = lit; + } +} + +void CpModelProtoWithMapping::AddVarNeVarLiteral(int var1, int var2, int lit) { + AddVarEqVarLiteral(var1, var2, NegatedRef(lit)); +} + +std::optional CpModelProtoWithMapping::GetVarEqVarLiteral(int var1, + int var2) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) return LookupConstant(1); + if (var1 > var2) std::swap(var1, var2); + const std::pair key = {var1, var2}; + const auto it = var_eq_var_to_literal.find(key); + if (it != var_eq_var_to_literal.end()) return it->second; + return std::nullopt; +} + +std::optional CpModelProtoWithMapping::GetVarNeVarLiteral(int var1, + int var2) { + std::optional lit = GetVarEqVarLiteral(var1, var2); + if (lit.has_value()) return NegatedRef(lit.value()); + return std::nullopt; } int CpModelProtoWithMapping::GetOrCreateLiteralForVarLtVar(int var1, int var2) { @@ -578,33 +683,59 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarLtVar(int var1, int var2) { const auto it = var_lt_var_to_literal.find(key); if (it != var_lt_var_to_literal.end()) return it->second; - const int bool_var = NewBoolVar(); - var_lt_var_to_literal[key] = bool_var; - var_le_var_to_literal[{var2, var1}] = NegatedRef(bool_var); - - if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { - // bool_var => var1 < var2 => !var1 && var2 - BoolArgumentProto* is_lt = - AddEnforcedConstraint(bool_var)->mutable_bool_and(); - is_lt->add_literals(NegatedRef(var1)); - is_lt->add_literals(var2); - - // !bool_var => var1 >= var2 => var1 || !var2 - BoolArgumentProto* is_ge = - AddEnforcedConstraint(NegatedRef(bool_var))->mutable_bool_or(); - is_ge->add_literals(var1); - is_ge->add_literals(NegatedRef(var2)); + const int lit = NewBoolVar(); + AddVarLtVarLiteral(var1, var2, lit); + return lit; +} +void CpModelProtoWithMapping::AddVarLtVarLiteral(int var1, int var2, int lit) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) { + AddImplication({}, NegatedRef(lit)); + return; + } + + const std::pair key = {var1, var2}; + const auto it = var_lt_var_to_literal.find(key); + if (it != var_lt_var_to_literal.end()) { + AddLinearConstraint({}, Domain(0), {{lit, 1}, {it->second, -1}}); + ++num_var_op_var_reif_cached; } else { - AddLinearConstraint({bool_var}, - Domain(std::numeric_limits::min(), -1), - {{var1, 1}, {var2, -1}}); - AddLinearConstraint({NegatedRef(bool_var)}, - Domain(0, std::numeric_limits::max()), - {{var1, 1}, {var2, -1}}); + var_lt_var_to_literal[key] = lit; + var_le_var_to_literal[{var2, var1}] = NegatedRef(lit); + + if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { + // bool_var => var1 < var2 => !var1 && var2 + BoolArgumentProto* is_lt = AddEnforcedConstraint(lit)->mutable_bool_and(); + is_lt->add_literals(NegatedRef(var1)); + is_lt->add_literals(var2); + + // !bool_var => var1 >= var2 => var1 || !var2 + BoolArgumentProto* is_ge = + AddEnforcedConstraint(NegatedRef(lit))->mutable_bool_or(); + is_ge->add_literals(var1); + is_ge->add_literals(NegatedRef(var2)); + } else { + AddLinearConstraint({lit}, + Domain(std::numeric_limits::min(), -1), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(lit)}, + Domain(0, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + } } +} - return bool_var; +std::optional CpModelProtoWithMapping::GetVarLtVarLiteral(int var1, + int var2) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) return LookupConstant(0); + const std::pair key = {var1, var2}; + const auto it = var_lt_var_to_literal.find(key); + if (it != var_lt_var_to_literal.end()) return it->second; + return std::nullopt; } int CpModelProtoWithMapping::GetOrCreateLiteralForVarLeVar(int var1, int var2) { @@ -616,32 +747,58 @@ int CpModelProtoWithMapping::GetOrCreateLiteralForVarLeVar(int var1, int var2) { const auto it = var_le_var_to_literal.find(key); if (it != var_le_var_to_literal.end()) return it->second; - const int bool_var = NewBoolVar(); - var_le_var_to_literal[key] = bool_var; - var_lt_var_to_literal[{var2, var1}] = NegatedRef(bool_var); - - if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { - // bool_var => var1 <= var2 <=> !var1 || var2 - BoolArgumentProto* is_le = - AddEnforcedConstraint(bool_var)->mutable_bool_or(); - is_le->add_literals(NegatedRef(var1)); - is_le->add_literals(var2); - - // !bool_var => var1 > var2 <=> var1 && !var2 - BoolArgumentProto* is_gt = - AddEnforcedConstraint(NegatedRef(bool_var))->mutable_bool_and(); - is_gt->add_literals(var1); - is_gt->add_literals(NegatedRef(var2)); + const int lit = NewBoolVar(); + AddVarLeVarLiteral(var1, var2, lit); + return lit; +} + +void CpModelProtoWithMapping::AddVarLeVarLiteral(int var1, int var2, int lit) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) { + AddImplication({}, lit); + return; + } + + const std::pair key = {var1, var2}; + const auto it = var_le_var_to_literal.find(key); + if (it != var_le_var_to_literal.end()) { + AddLinearConstraint({}, Domain(0), {{lit, 1}, {it->second, -1}}); + ++num_var_op_var_reif_cached; } else { - AddLinearConstraint({bool_var}, - Domain(std::numeric_limits::min(), 0), - {{var1, 1}, {var2, -1}}); - AddLinearConstraint({NegatedRef(bool_var)}, - Domain(1, std::numeric_limits::max()), - {{var1, 1}, {var2, -1}}); + var_le_var_to_literal[key] = lit; + var_lt_var_to_literal[{var2, var1}] = NegatedRef(lit); + + if (VariableIsBoolean(var1) && VariableIsBoolean(var2)) { + // bool_var => var1 <= var2 <=> !var1 || var2 + BoolArgumentProto* is_le = AddEnforcedConstraint(lit)->mutable_bool_or(); + is_le->add_literals(NegatedRef(var1)); + is_le->add_literals(var2); + + // !bool_var => var1 > var2 <=> var1 && !var2 + BoolArgumentProto* is_gt = + AddEnforcedConstraint(NegatedRef(lit))->mutable_bool_and(); + is_gt->add_literals(var1); + is_gt->add_literals(NegatedRef(var2)); + } else { + AddLinearConstraint({lit}, Domain(std::numeric_limits::min(), 0), + {{var1, 1}, {var2, -1}}); + AddLinearConstraint({NegatedRef(lit)}, + Domain(1, std::numeric_limits::max()), + {{var1, 1}, {var2, -1}}); + } } +} - return bool_var; +std::optional CpModelProtoWithMapping::GetVarLeVarLiteral(int var1, + int var2) { + CHECK_NE(var1, kNoVar); + CHECK_NE(var2, kNoVar); + if (var1 == var2) return LookupConstant(1); + const std::pair key = {var1, var2}; + const auto it = var_le_var_to_literal.find(key); + if (it != var_le_var_to_literal.end()) return it->second; + return std::nullopt; } int CpModelProtoWithMapping::GetOrCreateOptionalInterval(VarOrValue start, @@ -868,7 +1025,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, // not(x) => a == b ct->add_enforcement_literal(NegatedRef(x)); auto* const refute = ct->mutable_linear(); - FillDomainInProto(Domain(0), refute); + FillDomainInProto(0, refute); AddTermToLinearConstraint(a, 1, refute); AddTermToLinearConstraint(b, -1, refute); @@ -912,7 +1069,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, FillAMinusBInDomain({0, 0}, fz_ct, ct); } else if (fz_ct.type == "bool_ne" || fz_ct.type == "bool_not") { auto* arg = ct->mutable_linear(); - FillDomainInProto(Domain(1), arg); + FillDomainInProto(1, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[0]), 1, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[1]), 1, arg); } else if (fz_ct.type == "int_ne") { @@ -933,11 +1090,11 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, auto* arg = ct->mutable_linear(); const std::vector vars = LookupVars(fz_ct.arguments[1]); if (fz_ct.arguments[2].IsVariable()) { - FillDomainInProto(Domain(0), arg); + FillDomainInProto(0, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[2]), -1, arg); } else { const int64_t v = fz_ct.arguments[2].Value(); - FillDomainInProto(Domain(v), arg); + FillDomainInProto(v, arg); } for (int i = 0; i < vars.size(); ++i) { AddTermToLinearConstraint(vars[i], fz_ct.arguments[0].values[i], arg); @@ -977,7 +1134,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, } auto* arg = ct->mutable_linear(); - FillDomainInProto(Domain(set_size), arg); + FillDomainInProto(set_size, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[1]), 1, arg); } else if (fz_ct.type == "set_in") { auto* arg = ct->mutable_linear(); @@ -989,9 +1146,8 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, fz_ct.arguments[1].values.end()}), arg); } else if (fz_ct.arguments[1].type == fz::Argument::INT_INTERVAL) { - FillDomainInProto( - Domain(fz_ct.arguments[1].values[0], fz_ct.arguments[1].values[1]), - arg); + FillDomainInProto(fz_ct.arguments[1].values[0], + fz_ct.arguments[1].values[1], arg); } else { LOG(FATAL) << "Wrong format"; } @@ -1047,7 +1203,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, *arg->mutable_target() = LookupExpr(fz_ct.arguments[1]); } else if (fz_ct.type == "int_plus") { auto* arg = ct->mutable_linear(); - FillDomainInProto(Domain(0), arg); + FillDomainInProto(0, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[0]), 1, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[1]), 1, arg); AddTermToLinearConstraint(LookupVar(fz_ct.arguments[2]), -1, arg); @@ -1188,9 +1344,9 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, } if (target.var == kNoVar) { - FillDomainInProto(Domain(target.value - fixed_contributions), arg); + FillDomainInProto(target.value - fixed_contributions, arg); } else { - FillDomainInProto(Domain(-fixed_contributions), arg); + FillDomainInProto(-fixed_contributions, arg); AddTermToLinearConstraint(target.var, -1, arg); } @@ -1207,9 +1363,9 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, const VarOrValue target = LookupVarOrValue(fz_ct.arguments[2]); LinearConstraintProto* arg = ct->mutable_linear(); if (target.var == kNoVar) { - FillDomainInProto(Domain(target.value), arg); + FillDomainInProto(target.value, arg); } else { - FillDomainInProto(Domain(0), arg); + FillDomainInProto(0, arg); AddTermToLinearConstraint(target.var, -1, arg); } for (const VarOrValue& count : counts) { @@ -1252,7 +1408,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, const int literal = proto.variables_size(); { auto* new_var = proto.add_variables(); - FillDomainInProto({0, 1}, new_var); + FillDomainInProto(0, 1, new_var); } // Add the arc. @@ -1608,7 +1764,7 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, const std::vector& x = LookupVars(fz_ct.arguments[1]); LinearConstraintProto* global_cardinality = ct->mutable_linear(); - FillDomainInProto(Domain(x.size()), global_cardinality); + FillDomainInProto(x.size(), global_cardinality); absl::btree_map> value_to_literals; for (int i = 0; i < x.size(); ++i) { @@ -1636,6 +1792,87 @@ void CpModelProtoWithMapping::FillConstraint(const fz::Constraint& fz_ct, is_present_constraint->add_literals(literal); } } + } else if (fz_ct.type == "ortools_global_cardinality") { + const std::vector x = LookupVars(fz_ct.arguments[0]); + CHECK_EQ(fz_ct.arguments[1].type, fz::Argument::INT_LIST); + const std::vector& values = fz_ct.arguments[1].values; + const std::vector cards = LookupVars(fz_ct.arguments[2]); + const bool is_closed = fz_ct.arguments[3].Value() != 0; + + const absl::flat_hash_set all_values(values.begin(), values.end()); + absl::flat_hash_map> value_to_literals; + bool exact_cover = true; + + for (const int x_var : x) { + const absl::flat_hash_map encoding = FullyEncode(x_var); + for (const auto& [value, literal] : encoding) { + if (all_values.contains(value)) { + value_to_literals[value].push_back(literal); + } else if (is_closed) { + AddImplication({}, NegatedRef(literal)); + } else { + exact_cover = false; + } + } + } + + for (int i = 0; i < cards.size(); ++i) { + const int64_t value = values[i]; + const int card = cards[i]; + LinearConstraintProto* lin = + AddLinearConstraint({}, Domain(0), {{card, -1}}); + for (const int literal : value_to_literals[value]) { + AddTermToLinearConstraint(literal, 1, lin); + } + } + + if (exact_cover) { + LinearConstraintProto* cover = AddLinearConstraint({}, Domain(x.size())); + for (const int card : cards) { + AddTermToLinearConstraint(card, 1, cover); + } + } + } else if (fz_ct.type == "ortools_global_cardinality_low_up") { + const std::vector x = LookupVars(fz_ct.arguments[0]); + CHECK_EQ(fz_ct.arguments[1].type, fz::Argument::INT_LIST); + const std::vector& values = fz_ct.arguments[1].values; + absl::Span lbs = fz_ct.arguments[2].values; + absl::Span ubs = fz_ct.arguments[3].values; + const bool is_closed = fz_ct.arguments[4].Value() != 0; + + const absl::flat_hash_set all_values(values.begin(), values.end()); + absl::flat_hash_map> value_to_literals; + bool exact_cover = true; + + for (const int x_var : x) { + const absl::flat_hash_map encoding = FullyEncode(x_var); + for (const auto& [value, literal] : encoding) { + if (all_values.contains(value)) { + value_to_literals[value].push_back(literal); + } else if (is_closed) { + AddImplication({}, NegatedRef(literal)); + } else { + exact_cover = false; + } + } + } + + // Optimization: if sum(lbs) == length(x), then we can reduce ubs to lbs, + // and vice versa. the constraint is redundant. + const int64_t sum_lbs = absl::c_accumulate(lbs, 0); + const int64_t sum_ubs = absl::c_accumulate(ubs, 0); + if (exact_cover && sum_lbs == x.size()) { + ubs = lbs; + } else if (exact_cover && sum_ubs == x.size()) { + lbs = ubs; + } + + for (int i = 0; i < values.size(); ++i) { + LinearConstraintProto* lin = AddLinearConstraint({}, {lbs[i], ubs[i]}); + for (const int literal : value_to_literals[values[i]]) { + AddTermToLinearConstraint(literal, 1, lin); + } + } } else { LOG(FATAL) << " Not supported " << fz_ct.type; } @@ -1780,10 +2017,10 @@ void CpModelProtoWithMapping::ExtractSetConstraint( ConstraintProto* ct = proto.add_constraints(); if (var_or_value.var == kNoVar) { set_var->card_var_index = LookupConstant(var_or_value.value); - FillDomainInProto(Domain(var_or_value.value), ct->mutable_linear()); + FillDomainInProto(var_or_value.value, ct->mutable_linear()); } else { set_var->card_var_index = var_or_value.var; - FillDomainInProto(Domain(0), ct->mutable_linear()); + FillDomainInProto(0, ct->mutable_linear()); AddTermToLinearConstraint(var_or_value.var, -1, ct->mutable_linear()); } for (const int bool_var : set_var->var_indices) { @@ -2076,91 +2313,278 @@ void CpModelProtoWithMapping::ExtractSetConstraint( } } // NOLINT(readability/fn_size) -void CpModelProtoWithMapping::FillReifOrImpliedConstraint( - const fz::Constraint& fz_ct, ConstraintProto* ct) { - // Start by adding a non-reified version of the same constraint. - std::string simplified_type; - if (absl::EndsWith(fz_ct.type, "_reif")) { - // Remove _reif. - simplified_type = fz_ct.type.substr(0, fz_ct.type.size() - 5); - } else if (absl::EndsWith(fz_ct.type, "_imp")) { - // Remove _imp. - simplified_type = fz_ct.type.substr(0, fz_ct.type.size() - 4); - } else { - // Keep name as it is an implicit reified constraint. - simplified_type = fz_ct.type; - } - - // We need a copy to be able to change the type of the constraint. - fz::Constraint copy = fz_ct; - copy.type = simplified_type; - - // Create the CP-SAT constraint. - FillConstraint(copy, ct); - - // In case of reified constraints, the type of the opposite constraint. - std::string negated_type; - - // Fill enforcement_literal and set copy.type to the negated constraint. - if (simplified_type == "array_bool_or") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[1])); - negated_type = "array_bool_or_negated"; - } else if (simplified_type == "array_bool_and") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[1])); - negated_type = "array_bool_and_negated"; - } else if (simplified_type == "set_in") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "set_in_negated"; - } else if (simplified_type == "bool_eq" || simplified_type == "int_eq") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_ne"; - } else if (simplified_type == "bool_ne" || simplified_type == "int_ne") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_eq"; - } else if (simplified_type == "bool_le" || simplified_type == "int_le") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_gt"; - } else if (simplified_type == "bool_lt" || simplified_type == "int_lt") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_ge"; - } else if (simplified_type == "bool_ge" || simplified_type == "int_ge") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_lt"; - } else if (simplified_type == "bool_gt" || simplified_type == "int_gt") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); - negated_type = "int_le"; - } else if (simplified_type == "int_lin_eq") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_ne"; - } else if (simplified_type == "int_lin_ne") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_eq"; - } else if (simplified_type == "int_lin_le") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_gt"; - } else if (simplified_type == "int_lin_ge") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_lt"; - } else if (simplified_type == "int_lin_lt") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_ge"; - } else if (simplified_type == "int_lin_gt") { - ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); - negated_type = "int_lin_le"; +void CpModelProtoWithMapping::FillReifiedOrImpliedConstraint( + const fz::Constraint& fz_ct) { + // Start by processing the constraints that are cached. + if (fz_ct.type == "int_eq_reif" || fz_ct.type == "bool_eq_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + if (left.var == kNoVar) std::swap(left, right); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + CHECK_EQ(right.var, kNoVar); + if (left.value == right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else if (right.var == kNoVar) { + AddVarEqValueLiteral(left.var, right.value, e); + } else { + AddVarEqVarLiteral(left.var, right.var, e); + } + } else if (fz_ct.type == "int_eq_imp" || fz_ct.type == "bool_eq_imp") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + if (left.var == kNoVar) std::swap(left, right); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + CHECK_EQ(right.var, kNoVar); + if (left.value != right.value) { + AddImplication({}, NegatedRef(e)); + } + } else if (right.var == kNoVar) { + AddLinearConstraint({e}, Domain(right.value), {{left.var, 1}}); + } else { + const std::optional lit = GetVarEqVarLiteral(left.var, right.var); + if (lit.has_value()) { + AddImplication({e}, lit.value()); + ++num_var_op_var_imp_cached; + } else { + AddLinearConstraint({e}, Domain(0), {{left.var, 1}, {right.var, -1}}); + } + } + } else if (fz_ct.type == "int_ne_reif" || fz_ct.type == "bool_ne_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + if (left.var == kNoVar) std::swap(left, right); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + CHECK_EQ(right.var, kNoVar); + if (left.value != right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else if (right.var == kNoVar) { + AddVarEqValueLiteral(left.var, right.value, NegatedRef(e)); + } else { + AddVarEqVarLiteral(left.var, right.var, NegatedRef(e)); + } + } else if (fz_ct.type == "int_ne_imp" || fz_ct.type == "bool_ne_imp") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + if (left.var == kNoVar) std::swap(left, right); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + CHECK_EQ(right.var, kNoVar); + if (left.value == right.value) { + AddImplication({}, NegatedRef(e)); + } + } else if (right.var == kNoVar) { + AddLinearConstraint({e}, Domain(right.value).Complement(), + {{left.var, 1}}); + } else { + const std::optional lit = GetVarNeVarLiteral(left.var, right.var); + if (lit.has_value()) { + AddImplication({e}, lit.value()); + ++num_var_op_var_imp_cached; + } else { + AddLinearConstraint({e}, Domain(0).Complement(), + {{left.var, 1}, {right.var, -1}}); + } + } + } else if (fz_ct.type == "int_le_reif" || fz_ct.type == "bool_le_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + if (right.var == kNoVar) { + if (left.value <= right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else { + AddVarGeValueLiteral(right.var, left.value, e); + } + } else if (right.var == kNoVar) { + AddVarLeValueLiteral(left.var, right.value, e); + } else { + AddVarLeVarLiteral(left.var, right.var, e); + } + } else if (fz_ct.type == "int_le_imp" || fz_ct.type == "bool_le_imp") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + if (right.var == kNoVar) { + if (left.value > right.value) { + AddImplication({}, NegatedRef(e)); + } + } else { + AddLinearConstraint({e}, + {left.value, std::numeric_limits::max()}, + {{right.var, 1}}); + } + } else if (right.var == kNoVar) { + AddLinearConstraint({e}, + {std::numeric_limits::min(), right.value}, + {{left.var, 1}}); + } else { + const std::optional lit = GetVarLeVarLiteral(left.var, right.var); + if (lit.has_value()) { + AddImplication({e}, lit.value()); + ++num_var_op_var_imp_cached; + } else { + AddLinearConstraint({e}, {std::numeric_limits::min(), 0}, + {{left.var, 1}, {right.var, -1}}); + } + } + } else if (fz_ct.type == "int_ge_reif" || fz_ct.type == "bool_ge_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + if (right.var == kNoVar) { + if (left.value >= right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else { + AddVarLeValueLiteral(right.var, left.value, e); + } + } else if (right.var == kNoVar) { + AddVarGeValueLiteral(left.var, right.value, e); + } else { + AddVarLeVarLiteral(right.var, left.var, e); + } + } else if (fz_ct.type == "int_lt_reif" || fz_ct.type == "bool_lt_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + if (right.var == kNoVar) { + if (left.value < right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else { + AddVarGtValueLiteral(right.var, left.value, e); + } + } else if (right.var == kNoVar) { + AddVarLtValueLiteral(left.var, right.value, e); + } else { + AddVarLtVarLiteral(left.var, right.var, e); + } + } else if (fz_ct.type == "int_gt_reif" || fz_ct.type == "bool_gt_reif") { + VarOrValue left = LookupVarOrValue(fz_ct.arguments[0]); + VarOrValue right = LookupVarOrValue(fz_ct.arguments[1]); + const int e = LookupVar(fz_ct.arguments[2]); + if (left.var == kNoVar) { + if (right.var == kNoVar) { + if (left.value < right.value) { + AddImplication({}, e); + } else { + AddImplication({}, NegatedRef(e)); + } + } else { + AddVarLtValueLiteral(right.var, left.value, e); + } + } else if (right.var == kNoVar) { + AddVarGtValueLiteral(left.var, right.value, e); + } else { + AddVarLtVarLiteral(right.var, left.var, e); + } } else { - LOG(FATAL) << "Unsupported " << simplified_type; - } + // Start by adding a non-reified version of the same constraint. + ConstraintProto* ct = proto.add_constraints(); + ct->set_name(fz_ct.type); + std::string simplified_type; + if (absl::EndsWith(fz_ct.type, "_reif")) { + // Remove _reif. + simplified_type = fz_ct.type.substr(0, fz_ct.type.size() - 5); + } else if (absl::EndsWith(fz_ct.type, "_imp")) { + // Remove _imp. + simplified_type = fz_ct.type.substr(0, fz_ct.type.size() - 4); + } else { + // Keep name as it is an implicit reified constraint. + simplified_type = fz_ct.type; + } + + // We need a copy to be able to change the type of the constraint. + fz::Constraint copy = fz_ct; + copy.type = simplified_type; + + // Create the CP-SAT constraint. + FillConstraint(copy, ct); + + // In case of reified constraints, the type of the opposite constraint. + std::string negated_type; + + // Fill enforcement_literal and set copy.type to the negated constraint. + if (simplified_type == "array_bool_or") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[1])); + negated_type = "array_bool_or_negated"; + } else if (simplified_type == "array_bool_and") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[1])); + negated_type = "array_bool_and_negated"; + } else if (simplified_type == "set_in") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "set_in_negated"; + } else if (simplified_type == "bool_eq" || simplified_type == "int_eq") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_ne"; + } else if (simplified_type == "bool_ne" || simplified_type == "int_ne") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_eq"; + } else if (simplified_type == "bool_le" || simplified_type == "int_le") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_gt"; + } else if (simplified_type == "bool_lt" || simplified_type == "int_lt") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_ge"; + } else if (simplified_type == "bool_ge" || simplified_type == "int_ge") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_lt"; + } else if (simplified_type == "bool_gt" || simplified_type == "int_gt") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[2])); + negated_type = "int_le"; + } else if (simplified_type == "int_lin_eq") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_ne"; + } else if (simplified_type == "int_lin_ne") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_eq"; + } else if (simplified_type == "int_lin_le") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_gt"; + } else if (simplified_type == "int_lin_ge") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_lt"; + } else if (simplified_type == "int_lin_lt") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_ge"; + } else if (simplified_type == "int_lin_gt") { + ct->add_enforcement_literal(LookupVar(fz_ct.arguments[3])); + negated_type = "int_lin_le"; + } else { + LOG(FATAL) << "Unsupported " << simplified_type; + } - // One way implication. We can stop here. - if (absl::EndsWith(fz_ct.type, "_imp")) return; + // One way implication. We can stop here. + if (absl::EndsWith(fz_ct.type, "_imp")) return; - // Add the other side of the reification because CpModelProto only support - // half reification. - ConstraintProto* negated_ct = - AddEnforcedConstraint(NegatedRef(ct->enforcement_literal(0))); - negated_ct->set_name(fz_ct.type + " (negated)"); - copy.type = negated_type; - FillConstraint(copy, negated_ct); + // Add the other side of the reification because CpModelProto only support + // half reification. + ConstraintProto* negated_ct = + AddEnforcedConstraint(NegatedRef(ct->enforcement_literal(0))); + negated_ct->set_name(fz_ct.type + " (negated)"); + copy.type = negated_type; + FillConstraint(copy, negated_ct); + } } void CpModelProtoWithMapping::TranslateSearchAnnotations( @@ -2491,12 +2915,11 @@ void SolveFzWithCpModelProto(const fz::Model& fz_model, LOG_FIRST_N(WARNING, 1) << " actual domain is [" << -absl::GetFlag(FLAGS_fz_int_max) << ".." << absl::GetFlag(FLAGS_fz_int_max) << "]"; - FillDomainInProto({-absl::GetFlag(FLAGS_fz_int_max), - absl::GetFlag(FLAGS_fz_int_max)}, - var); + FillDomainInProto(-absl::GetFlag(FLAGS_fz_int_max), + absl::GetFlag(FLAGS_fz_int_max), var); } else { - FillDomainInProto( - {fz_var->domain.values[0], fz_var->domain.values[1]}, var); + FillDomainInProto(fz_var->domain.values[0], fz_var->domain.values[1], + var); } } else { FillDomainInProto(Domain::FromValues(fz_var->domain.values), var); @@ -2510,13 +2933,13 @@ void SolveFzWithCpModelProto(const fz::Model& fz_model, if (m.ConstraintContainsSetVariables(*fz_ct)) { m.ExtractSetConstraint(*fz_ct); } else { - ConstraintProto* ct = m.proto.add_constraints(); - ct->set_name(fz_ct->type); if (absl::EndsWith(fz_ct->type, "_reif") || absl::EndsWith(fz_ct->type, "_imp") || fz_ct->type == "array_bool_or" || fz_ct->type == "array_bool_and") { - m.FillReifOrImpliedConstraint(*fz_ct, ct); + m.FillReifiedOrImpliedConstraint(*fz_ct); } else { + ConstraintProto* ct = m.proto.add_constraints(); + ct->set_name(fz_ct->type); m.FillConstraint(*fz_ct, ct); } } @@ -2547,6 +2970,14 @@ void SolveFzWithCpModelProto(const fz::Model& fz_model, // Fill the search order. m.TranslateSearchAnnotations(fz_model.search_annotations(), logger); + // Extra statistics. + SOLVER_LOG(logger, + " - #var_op_value_reif cached: ", m.num_var_op_value_reif_cached); + SOLVER_LOG(logger, + " - #var_op_var_reif cached: ", m.num_var_op_var_reif_cached); + SOLVER_LOG(logger, + " - #var_op_var_imp cached: ", m.num_var_op_var_reif_cached); + if (p.search_all_solutions && !m.proto.has_objective()) { // Enumerate all sat solutions. m.parameters.set_enumerate_all_solutions(true); diff --git a/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn b/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn index 7bae41c2abf..8b604a3a271 100644 --- a/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn +++ b/ortools/flatzinc/mznlib/fzn_all_equal_int.mzn @@ -1,4 +1,4 @@ predicate fzn_all_equal_int(array [int] of var int: x) = if length(x) > 1 then forall (i in index_set(x) diff {min(index_set(x))}) (x[min(index_set(x))] = x[i]) - endif; \ No newline at end of file + endif; diff --git a/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn b/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn index f7f3d78d684..e177e94ef9f 100644 --- a/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn +++ b/ortools/flatzinc/mznlib/fzn_all_equal_set.mzn @@ -1,4 +1,4 @@ predicate fzn_all_equal_set(array [int] of var set of int: x) = if length(x) > 1 then forall (i in index_set(x) diff {min(index_set(x))}) (x[min(index_set(x))] = x[i]) - endif; \ No newline at end of file + endif; diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn index b9865da2b95..a606602f58a 100644 --- a/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality.mzn @@ -1,8 +1,12 @@ +predicate ortools_global_cardinality( + array [int] of var int: x, + array [int] of int: cover, + array [int] of var int: counts, + bool: closed, +); + predicate fzn_global_cardinality( array [int] of var int: x, array [int] of int: cover, array [int] of var int: counts, -) = - forall (i in index_set(cover)) (count(x, cover[i], counts[i])) /\ - % Implied constraint - length(x) >= sum(counts); \ No newline at end of file +) = ortools_global_cardinality(x, cover, counts, false); diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn index 51deabf573e..f40302c731f 100644 --- a/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality_closed.mzn @@ -1,8 +1,12 @@ +predicate ortools_global_cardinality( + array [int] of var int: x, + array [int] of int: cover, + array [int] of var int: counts, + bool: closed, +); + predicate fzn_global_cardinality_closed( array [int] of var int: x, array [int] of int: cover, array [int] of var int: counts, -) = forall (i in index_set(x)) (x[i] in {d | d in cover}) /\ - global_cardinality(x, cover, counts) /\ - sum(counts) = length(x); - \ No newline at end of file +) = ortools_global_cardinality(x, cover, counts, true); diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up.mzn new file mode 100644 index 00000000000..e64c3f4b7be --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up.mzn @@ -0,0 +1,14 @@ +predicate ortools_global_cardinality_low_up( + array [int] of var int: x, + array [int] of int: cover, + array [int] of int: lbound, + array [int] of int: ubound, + bool: closed, +); + +predicate fzn_global_cardinality_low_up( + array [int] of var int: x, + array [int] of int: cover, + array [int] of int: lbound, + array [int] of int: ubound, +) = ortools_global_cardinality_low_up(x, cover, lbound, ubound, false); diff --git a/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up_closed.mzn b/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up_closed.mzn new file mode 100644 index 00000000000..30767565dc5 --- /dev/null +++ b/ortools/flatzinc/mznlib/fzn_global_cardinality_low_up_closed.mzn @@ -0,0 +1,14 @@ +predicate ortools_global_cardinality_low_up( + array [int] of var int: x, + array [int] of int: cover, + array [int] of int: lbound, + array [int] of int: ubound, + bool: closed, +); + +predicate fzn_global_cardinality_low_up_closed( + array [int] of var int: x, + array [int] of int: cover, + array [int] of int: lbound, + array [int] of int: ubound, +) = ortools_global_cardinality_low_up(x, cover, lbound, ubound, true); From 0722c93a753e7815255c7e7cc3d6c6f326d7cba3 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sun, 31 Aug 2025 21:10:49 +0200 Subject: [PATCH 021/491] fix --- ortools/util/proto_tools.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ortools/util/proto_tools.h b/ortools/util/proto_tools.h index 8555bf5ee1b..8d8eb3c15c6 100644 --- a/ortools/util/proto_tools.h +++ b/ortools/util/proto_tools.h @@ -17,6 +17,8 @@ #include #include "absl/base/casts.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" From df258ea5fad786c859ca4554d2e2a12b1299f8c4 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Tue, 2 Sep 2025 16:36:53 +0200 Subject: [PATCH 022/491] bazel: bump deps --- MODULE.bazel | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 63b595a8a73..5766db1dd1b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -32,16 +32,16 @@ bazel_dep(name = "bazel_skylib", version = "1.8.1") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "rules_cc", version = "0.1.4") bazel_dep(name = "rules_go", version = "0.53.0") -bazel_dep(name = "rules_java", version = "8.11.0") +bazel_dep(name = "rules_java", version = "8.12.0") bazel_dep(name = "rules_jvm_external", version = "6.7") bazel_dep(name = "contrib_rules_jvm", version = "0.28.0") bazel_dep(name = "rules_license", version = "1.0.0") bazel_dep(name = "rules_proto", version = "7.1.0") -bazel_dep(name = "rules_python", version = "1.2.0") +bazel_dep(name = "rules_python", version = "1.5.1") # OR-Tools C++ dependencies # keep sorted go/buildifier#keep-sorted -bazel_dep(name = "abseil-cpp", version = "20250512.0") +bazel_dep(name = "abseil-cpp", version = "20250512.1") bazel_dep(name = "bzip2", version = "1.0.8.bcr.2") bazel_dep(name = "eigen", version = "3.4.0.bcr.3") bazel_dep(name = "fuzztest", version = "20250214.0") @@ -51,7 +51,7 @@ bazel_dep(name = "google_benchmark", version = "1.9.2") bazel_dep(name = "googletest", version = "1.17.0") bazel_dep(name = "highs", version = "1.11.0") bazel_dep(name = "protobuf", version = "31.1") -bazel_dep(name = "re2", version = "2024-07-02.bcr.1") +bazel_dep(name = "re2", version = "2025-08-12") bazel_dep(name = "scip", version = "9.2.2") bazel_dep(name = "zlib", version = "1.3.1.bcr.5") From 1f483d5a54001b0812acc2b413a2e12c3defeefa Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 29 Aug 2025 18:37:43 +0200 Subject: [PATCH 023/491] math_opt: export more cpp/ tests --- .bazelrc | 3 + ortools/math_opt/cpp/BUILD.bazel | 257 + ortools/math_opt/cpp/callback_test.cc | 358 + ...ompute_infeasible_subsystem_result_test.cc | 590 ++ ortools/math_opt/cpp/enums_test.cc | 132 + ortools/math_opt/cpp/enums_test.proto | 28 + ortools/math_opt/cpp/formatters_test.cc | 137 + ortools/math_opt/cpp/key_types.h | 6 +- ortools/math_opt/cpp/key_types_test.cc | 165 + .../math_opt/cpp/linear_constraint_test.cc | 205 + ortools/math_opt/cpp/map_filter_test.cc | 331 + ortools/math_opt/cpp/matchers_test.cc | 2032 +++++ ortools/math_opt/cpp/message_callback_test.cc | 415 + .../cpp/model_solve_parameters_test.cc | 742 ++ ortools/math_opt/cpp/objective_test.cc | 233 + ortools/math_opt/cpp/parameters_test.cc | 249 + ortools/math_opt/cpp/solution_test.cc | 504 ++ ortools/math_opt/cpp/solve_arguments_test.cc | 97 + ortools/math_opt/cpp/solve_impl_test.cc | 1865 ++++ ortools/math_opt/cpp/solve_result_test.cc | 932 ++ ortools/math_opt/cpp/solve_test.cc | 1539 ++++ ortools/math_opt/cpp/solver_resources_test.cc | 103 + ortools/math_opt/cpp/statistics_test.cc | 162 + .../streamable_solver_init_arguments_test.cc | 50 + .../cpp/variable_and_expressions_nc_test.cc | 101 + .../cpp/variable_and_expressions_test.cc | 7643 +++++++++++++++++ 26 files changed, 18876 insertions(+), 3 deletions(-) create mode 100644 ortools/math_opt/cpp/callback_test.cc create mode 100644 ortools/math_opt/cpp/compute_infeasible_subsystem_result_test.cc create mode 100644 ortools/math_opt/cpp/enums_test.cc create mode 100644 ortools/math_opt/cpp/enums_test.proto create mode 100644 ortools/math_opt/cpp/formatters_test.cc create mode 100644 ortools/math_opt/cpp/key_types_test.cc create mode 100644 ortools/math_opt/cpp/linear_constraint_test.cc create mode 100644 ortools/math_opt/cpp/map_filter_test.cc create mode 100644 ortools/math_opt/cpp/matchers_test.cc create mode 100644 ortools/math_opt/cpp/message_callback_test.cc create mode 100644 ortools/math_opt/cpp/model_solve_parameters_test.cc create mode 100644 ortools/math_opt/cpp/objective_test.cc create mode 100644 ortools/math_opt/cpp/parameters_test.cc create mode 100644 ortools/math_opt/cpp/solution_test.cc create mode 100644 ortools/math_opt/cpp/solve_arguments_test.cc create mode 100644 ortools/math_opt/cpp/solve_impl_test.cc create mode 100644 ortools/math_opt/cpp/solve_result_test.cc create mode 100644 ortools/math_opt/cpp/solve_test.cc create mode 100644 ortools/math_opt/cpp/solver_resources_test.cc create mode 100644 ortools/math_opt/cpp/statistics_test.cc create mode 100644 ortools/math_opt/cpp/streamable_solver_init_arguments_test.cc create mode 100644 ortools/math_opt/cpp/variable_and_expressions_nc_test.cc create mode 100644 ortools/math_opt/cpp/variable_and_expressions_test.cc diff --git a/.bazelrc b/.bazelrc index c50ba9a2c9f..c407d5cca1d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -52,6 +52,9 @@ test --test_output=errors --test_timeout_filters=-eternal # Put user-specific options in .bazelrc.user try-import %workspace%/.bazelrc.user +# googletest +build --define absl=1 + # asan build:asan --strip=never build:asan --copt -fsanitize=address diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index 99da30313cb..0ecfa61008c 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -186,6 +186,71 @@ cc_library( ], ) +# Same as `variable_and_expressions` but activates the counters of instances of +# LinearExpression. Only for tests. +cc_library( + name = "variable_and_expressions_with_counters", + testonly = 1, + srcs = ["variable_and_expressions.cc"], + hdrs = ["variable_and_expressions.h"], + defines = ["MATH_OPT_USE_EXPRESSION_COUNTERS"], + deps = [ + ":formatters", + ":key_types", + "//ortools/base", + "//ortools/base:intops", + "//ortools/base:map_util", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_item", + "//ortools/math_opt/storage:model_storage_types", + "//ortools/util:fp_roundtrip_conv", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "variable_and_expressions_test", + srcs = ["variable_and_expressions_test.cc"], + deps = [ + ":matchers", + ":variable_and_expressions", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/math_opt/elemental:elements", + "//ortools/math_opt/storage:model_storage", + "//ortools/util:fp_roundtrip_conv", + "//ortools/util:fp_roundtrip_conv_testing", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + ], +) + +# Same as `variable_and_expressions_test` but activates the counters of +# instances of LinearExpression and the associated tests. +cc_test( + name = "variable_and_expressions_with_counters_test", + srcs = ["variable_and_expressions_test.cc"], + defines = ["MATH_OPT_USE_EXPRESSION_COUNTERS"], + deps = [ + ":matchers", + ":variable_and_expressions_with_counters", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/math_opt/elemental:elements", + "//ortools/math_opt/storage:model_storage", + "//ortools/util:fp_roundtrip_conv", + "//ortools/util:fp_roundtrip_conv_testing", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + ], +) + cc_library( name = "objective", srcs = ["objective.cc"], @@ -201,6 +266,20 @@ cc_library( ], ) +cc_test( + name = "objective_test", + srcs = ["objective_test.cc"], + deps = [ + ":objective", + ":variable_and_expressions", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_types", + "@abseil-cpp//absl/strings", + ], +) + cc_library( name = "linear_constraint", hdrs = ["linear_constraint.h"], @@ -217,6 +296,24 @@ cc_library( ], ) +cc_test( + name = "linear_constraint_test", + srcs = ["linear_constraint_test.cc"], + deps = [ + ":linear_constraint", + ":matchers", + ":model", + ":variable_and_expressions", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/base:intops", + "//ortools/math_opt/constraints/util:model_util", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/storage:model_storage_types", + "@abseil-cpp//absl/strings", + ], +) + cc_library( name = "solution", srcs = ["solution.cc"], @@ -245,6 +342,26 @@ cc_library( ], ) +cc_test( + name = "solution_test", + srcs = ["solution_test.cc"], + deps = [ + ":enums_testing", + ":linear_constraint", + ":matchers", + ":math_opt", + ":objective", + ":solution", + ":variable_and_expressions", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/math_opt:solution_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/storage:model_storage", + "@abseil-cpp//absl/status", + ], +) + cc_library( name = "solve_result", srcs = ["solve_result.cc"], @@ -293,6 +410,21 @@ cc_library( ], ) +cc_test( + name = "map_filter_test", + srcs = ["map_filter_test.cc"], + deps = [ + ":linear_constraint", + ":map_filter", + ":model", + ":variable_and_expressions", + "//ortools/base:gmock_main", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/storage:model_storage", + "@abseil-cpp//absl/status", + ], +) + cc_library( name = "callback", srcs = ["callback.cc"], @@ -333,6 +465,20 @@ cc_library( ], ) +cc_test( + name = "key_types_test", + srcs = ["key_types_test.cc"], + deps = [ + ":key_types", + ":variable_and_expressions", + "//ortools/base:gmock_main", + "//ortools/math_opt/storage:model_storage", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status", + "@google_benchmark//:benchmark", + ], +) + cc_library( name = "model_solve_parameters", srcs = ["model_solve_parameters.cc"], @@ -417,6 +563,17 @@ cc_library( ], ) +cc_test( + name = "solve_arguments_test", + srcs = ["solve_arguments_test.cc"], + deps = [ + ":math_opt", + ":solve_arguments", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/status", + ], +) + cc_library( name = "solve", srcs = ["solve.cc"], @@ -454,6 +611,17 @@ cc_library( ], ) +cc_test( + name = "streamable_solver_init_arguments_test", + srcs = ["streamable_solver_init_arguments_test.cc"], + deps = [ + ":streamable_solver_init_arguments", + "//ortools/base:gmock_main", + "//ortools/math_opt:parameters_cc_proto", + "//ortools/math_opt/solvers:gurobi_cc_proto", + ], +) + cc_library( name = "parameters", srcs = ["parameters.cc"], @@ -505,6 +673,22 @@ cc_library( ], ) +cc_test( + name = "matchers_test", + srcs = ["matchers_test.cc"], + deps = [ + ":matchers", + ":math_opt", + ":model", + ":solution", + ":solve_result", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/strings", + ], +) + cc_library( name = "enums", hdrs = ["enums.h"], @@ -515,6 +699,31 @@ cc_library( ], ) +proto_library( + name = "enums_test_proto", + srcs = ["enums_test.proto"], +) + +cc_proto_library( + name = "enums_test_cc_proto", + deps = [":enums_test_proto"], +) + +cc_test( + name = "enums_test", + srcs = ["enums_test.cc"], + deps = [ + ":enums", + ":enums_test_cc_proto", + ":enums_testing", + "//ortools/base:gmock_main", + "//ortools/base:logging", + "//ortools/math_opt/testing:stream", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", + ], +) + cc_library( name = "enums_testing", testonly = True, @@ -541,6 +750,16 @@ cc_library( ], ) +cc_test( + name = "statistics_test", + srcs = ["statistics_test.cc"], + deps = [ + ":model", + ":statistics", + "//ortools/base:gmock_main", + ], +) + cc_library( name = "formatters", hdrs = ["formatters.h"], @@ -585,6 +804,35 @@ cc_library( ], ) +cc_test( + name = "compute_infeasible_subsystem_result_test", + srcs = ["compute_infeasible_subsystem_result_test.cc"], + deps = [ + ":compute_infeasible_subsystem_result", + ":enums", + ":linear_constraint", + ":matchers", + ":model", + ":solve_result", + ":variable_and_expressions", + "//ortools/base:gmock_main", + "//ortools/base:status_macros", + "//ortools/math_opt:infeasible_subsystem_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt/constraints/indicator:indicator_constraint", + "//ortools/math_opt/constraints/quadratic:quadratic_constraint", + "//ortools/math_opt/constraints/second_order_cone:second_order_cone_constraint", + "//ortools/math_opt/constraints/sos:sos1_constraint", + "//ortools/math_opt/constraints/sos:sos2_constraint", + "//ortools/math_opt/storage:model_storage", + "//ortools/math_opt/testing:stream", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + ], +) + cc_library( name = "compute_infeasible_subsystem_arguments", hdrs = ["compute_infeasible_subsystem_arguments.h"], @@ -608,6 +856,15 @@ cc_library( ], ) +cc_test( + name = "solver_resources_test", + srcs = ["solver_resources_test.cc"], + deps = [ + ":solver_resources", + "//ortools/base:gmock_main", + ], +) + cc_library( name = "solve_impl", srcs = ["solve_impl.cc"], diff --git a/ortools/math_opt/cpp/callback_test.cc b/ortools/math_opt/cpp/callback_test.cc new file mode 100644 index 00000000000..1a17138b1a8 --- /dev/null +++ b/ortools/math_opt/cpp/callback_test.cc @@ -0,0 +1,358 @@ +// Copyright 2010-2025 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/callback.h" + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/time/time.h" +#include "google/protobuf/duration.pb.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/protoutil.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/cpp/enums_testing.h" +#include "ortools/math_opt/cpp/map_filter.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research { +namespace math_opt { +namespace { + +constexpr double kInf = std::numeric_limits::infinity(); + +using ::testing::EqualsProto; +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +INSTANTIATE_TYPED_TEST_SUITE_P(CallbackEvent, EnumTest, CallbackEvent); + +TEST(CallbackDataTest, Creation) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Variable y(&storage, storage.AddVariable("y")); + CallbackDataProto proto; + proto.set_event(CALLBACK_EVENT_MIP_NODE); + auto var_values = proto.mutable_primal_solution_vector(); + var_values->add_ids(0); + var_values->add_ids(1); + var_values->add_values(3.0); + var_values->add_values(5.0); + ASSERT_OK_AND_ASSIGN(*proto.mutable_runtime(), + util_time::EncodeGoogleApiProto(absl::Seconds(11))); + proto.mutable_presolve_stats()->set_removed_variables(3); + proto.mutable_simplex_stats()->set_iteration_count(12); + proto.mutable_barrier_stats()->set_primal_objective(10.0); + proto.mutable_mip_stats()->set_explored_nodes(4); + CallbackData cb_data(&storage, proto); + EXPECT_EQ(cb_data.event, CallbackEvent::kMipNode); + ASSERT_TRUE(cb_data.solution.has_value()); + EXPECT_THAT(*cb_data.solution, + UnorderedElementsAre(Pair(x, 3.0), Pair(y, 5.0))); + EXPECT_EQ(cb_data.runtime, absl::Seconds(11)); + EXPECT_THAT(cb_data.presolve_stats, EqualsProto("removed_variables: 3")); + EXPECT_THAT(cb_data.simplex_stats, EqualsProto("iteration_count: 12")); + EXPECT_THAT(cb_data.barrier_stats, EqualsProto("primal_objective: 10.0")); + EXPECT_THAT(cb_data.mip_stats, EqualsProto("explored_nodes: 4")); + + EXPECT_OK(cb_data.CheckModelStorage(&storage)); + const ModelStorage other_storage; + EXPECT_THAT(cb_data.CheckModelStorage(&other_storage), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid variable"))); + + EXPECT_THAT(cb_data.Proto(), IsOkAndHolds(EquivToProto(proto))); +} + +TEST(CallbackDataTest, CreationWithEventConstructor) { + CallbackData cb_data(CallbackEvent::kMipNode, absl::Seconds(10)); + EXPECT_EQ(cb_data.event, CallbackEvent::kMipNode); + EXPECT_EQ(cb_data.runtime, absl::Seconds(10)); +} + +TEST(CallbackDataTest, NoSolution) { + ModelStorage storage; + CallbackDataProto proto; + proto.set_event(CALLBACK_EVENT_MIP_NODE); + proto.mutable_mip_stats()->set_explored_nodes(4); + CallbackData cb_data(&storage, proto); + EXPECT_EQ(cb_data.event, CallbackEvent::kMipNode); + EXPECT_FALSE(cb_data.solution.has_value()); + EXPECT_THAT(cb_data.mip_stats, EqualsProto("explored_nodes: 4")); + + EXPECT_OK(cb_data.CheckModelStorage(&storage)); + const ModelStorage other_storage; + EXPECT_OK(cb_data.CheckModelStorage(&other_storage)); + + EXPECT_THAT(cb_data.Proto(), IsOkAndHolds(EquivToProto(proto))); +} + +TEST(CallbackDataTest, EmptySolution) { + ModelStorage storage; + CallbackDataProto proto; + proto.set_event(CALLBACK_EVENT_MIP_NODE); + proto.mutable_primal_solution_vector(); + proto.mutable_mip_stats()->set_explored_nodes(4); + CallbackData cb_data(&storage, proto); + EXPECT_EQ(cb_data.event, CallbackEvent::kMipNode); + ASSERT_TRUE(cb_data.solution.has_value()); + EXPECT_THAT(*cb_data.solution, IsEmpty()); + EXPECT_THAT(cb_data.mip_stats, EqualsProto("explored_nodes: 4")); + + EXPECT_OK(cb_data.CheckModelStorage(&storage)); + const ModelStorage other_storage; + EXPECT_OK(cb_data.CheckModelStorage(&other_storage)); + + EXPECT_THAT(cb_data.Proto(), IsOkAndHolds(EquivToProto(proto))); +} + +TEST(CallbackDataTest, InvalidRuntime) { + EXPECT_THAT( + CallbackData( + /*event=*/CallbackEvent::kPresolve, + /*runtime=*/absl::InfiniteDuration()) + .Proto(), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("runtime"))); +} + +TEST(CallbackRegistrationTest, ProtoRoundtrip) { + Model model; + model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + // Test with all fields set, and boolean fields to true (non default + // value). Here we assume we use the same function for the mip_node_filter and + // the mip_solution_filter, and thus test the skip_zero_values with one and + // the filter_by_ids for the other. + { + CallbackRegistrationProto registration_proto; + registration_proto.add_request_registration(CALLBACK_EVENT_MIP_SOLUTION); + registration_proto.add_request_registration(CALLBACK_EVENT_MIP_NODE); + registration_proto.mutable_mip_node_filter()->set_skip_zero_values(true); + registration_proto.mutable_mip_solution_filter()->set_filter_by_ids(true); + registration_proto.mutable_mip_solution_filter()->add_filtered_ids(y.id()); + registration_proto.set_add_cuts(true); + registration_proto.set_add_lazy_constraints(true); + + ASSERT_OK_AND_ASSIGN( + const CallbackRegistration registration, + CallbackRegistration::FromProto(model, registration_proto)); + + EXPECT_OK(registration.CheckModelStorage(model.storage())); + EXPECT_THAT(registration.Proto(), EquivToProto(registration_proto)); + } + + // Test with boolean field to false (default value). + { + CallbackRegistrationProto registration_proto; + registration_proto.set_add_cuts(false); + registration_proto.set_add_lazy_constraints(false); + + ASSERT_OK_AND_ASSIGN( + const CallbackRegistration registration, + CallbackRegistration::FromProto(model, registration_proto)); + + EXPECT_OK(registration.CheckModelStorage(model.storage())); + EXPECT_THAT(registration.Proto(), EquivToProto(registration_proto)); + } +} + +TEST(CallbackRegistrationTest, CheckModelStorageMixedModels) { + ModelStorage storage_1; + const Variable x(&storage_1, storage_1.AddVariable("x")); + ModelStorage storage_2; + const Variable y(&storage_2, storage_2.AddVariable("y")); + CallbackRegistration registration; + registration.events = {CallbackEvent::kMipNode, CallbackEvent::kMipSolution}; + registration.mip_node_filter.filtered_keys = {x}; + registration.mip_solution_filter.filtered_keys = {y}; + EXPECT_THAT(registration.CheckModelStorage(&storage_1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(CallbackRegistrationTest, FromProtoBadEvent) { + CallbackRegistrationProto registration_proto; + registration_proto.add_request_registration(CALLBACK_EVENT_UNSPECIFIED); + ASSERT_THAT( + CallbackRegistration::FromProto(Model{}, registration_proto), + StatusIs( + absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("CallbackRegistrationProto.request_registration[0]"), + HasSubstr("CALLBACK_EVENT_UNSPECIFIED")))); +} + +TEST(CallbackRegistrationTest, FromProtoRepeatedEvent) { + CallbackRegistrationProto registration_proto; + registration_proto.add_request_registration(CALLBACK_EVENT_MIP_NODE); + registration_proto.add_request_registration(CALLBACK_EVENT_MIP_NODE); + ASSERT_THAT( + CallbackRegistration::FromProto(Model{}, registration_proto), + StatusIs( + absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("CallbackRegistrationProto.request_registration[1]"), + HasSubstr("mip_node")))); +} + +TEST(CallbackRegistrationTest, FromProtoInvalidMipSolutionFilterId) { + CallbackRegistrationProto registration_proto; + registration_proto.mutable_mip_solution_filter()->set_filter_by_ids(true); + registration_proto.mutable_mip_solution_filter()->add_filtered_ids(1); + ASSERT_THAT(CallbackRegistration::FromProto(Model{}, registration_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("mip_solution_filter"), + HasSubstr("id: 1 not in model")))); +} + +TEST(CallbackRegistrationTest, FromProtoInvalidMipNodeFilterId) { + CallbackRegistrationProto registration_proto; + registration_proto.mutable_mip_node_filter()->set_filter_by_ids(true); + registration_proto.mutable_mip_node_filter()->add_filtered_ids(1); + ASSERT_THAT(CallbackRegistration::FromProto(Model{}, registration_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("mip_node_filter"), + HasSubstr("id: 1 not in model")))); +} + +TEST(CallbackResultTest, ProtoRoundtrip) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + // Here we don't start with the proto, as below we also test + // AddLazyConstraint() and AddUserCut() behavior. We instead start with a + // CallbackResult and build an expected proto. + CallbackResult result; + result.terminate = true; + result.suggested_solutions.push_back({{x, 2.0}, {y, 3.0}}); + result.suggested_solutions.push_back({{y, 5.5}}); + result.AddLazyConstraint(x + y <= 2.0); + result.AddUserCut(y <= 1.0); + result.AddUserCut(-1.0 <= 2 * x + y); + + CallbackResultProto expected_result_proto; + expected_result_proto.set_terminate(true); + { + CallbackResultProto::GeneratedLinearConstraint& cut = + *expected_result_proto.add_cuts(); + cut.set_lower_bound(-kInf); + cut.set_upper_bound(2.0); + cut.set_is_lazy(true); + cut.mutable_linear_expression()->add_ids(0); + cut.mutable_linear_expression()->add_values(1.0); + cut.mutable_linear_expression()->add_ids(1); + cut.mutable_linear_expression()->add_values(1.0); + } + { + CallbackResultProto::GeneratedLinearConstraint& cut = + *expected_result_proto.add_cuts(); + cut.set_lower_bound(-kInf); + cut.set_upper_bound(1.0); + cut.set_is_lazy(false); + cut.mutable_linear_expression()->add_ids(1); + cut.mutable_linear_expression()->add_values(1.0); + } + { + CallbackResultProto::GeneratedLinearConstraint& cut = + *expected_result_proto.add_cuts(); + cut.set_lower_bound(-1.0); + cut.set_upper_bound(kInf); + cut.set_is_lazy(false); + cut.mutable_linear_expression()->add_ids(0); + cut.mutable_linear_expression()->add_values(2.0); + cut.mutable_linear_expression()->add_ids(1); + cut.mutable_linear_expression()->add_values(1.0); + } + { + SparseDoubleVectorProto& suggested_solution = + *expected_result_proto.add_suggested_solutions(); + suggested_solution.add_ids(0); + suggested_solution.add_values(2.0); + suggested_solution.add_ids(1); + suggested_solution.add_values(3.0); + } + { + SparseDoubleVectorProto& suggested_solution = + *expected_result_proto.add_suggested_solutions(); + suggested_solution.add_ids(1); + suggested_solution.add_values(5.5); + } + + EXPECT_OK(result.CheckModelStorage(model.storage())); + EXPECT_THAT(result.Proto(), EqualsProto(expected_result_proto)); + + // Now test the round trip using the expected proto. + { + ASSERT_OK_AND_ASSIGN( + const CallbackResult result_from_proto, + CallbackResult::FromProto(model, expected_result_proto)); + EXPECT_THAT(result_from_proto.Proto(), EqualsProto(expected_result_proto)); + } +} + +TEST(CallbackResultTest, MixedModels) { + ModelStorage storage_1; + const Variable x(&storage_1, storage_1.AddVariable("x")); + ModelStorage storage_2; + const Variable y(&storage_2, storage_2.AddVariable("y")); + CallbackResult result; + result.AddLazyConstraint(x <= 1.0); + result.suggested_solutions.push_back({{y, 1.0}}); + EXPECT_THAT(result.CheckModelStorage(&storage_1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(CallbackResultTest, FromProtoBadCutVariable) { + CallbackResultProto result_proto; + { + CallbackResultProto::GeneratedLinearConstraint& cut = + *result_proto.add_cuts(); + cut.mutable_linear_expression()->add_ids(1); + cut.mutable_linear_expression()->add_values(1.0); + } + EXPECT_THAT( + CallbackResult::FromProto(Model{}, result_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("CallbackResultProto.cuts[0].linear_expression"))); +} + +TEST(CallbackResultTest, FromProtoBadSolutionVariable) { + CallbackResultProto result_proto; + { + SparseDoubleVectorProto& suggested_solution = + *result_proto.add_suggested_solutions(); + suggested_solution.add_ids(1); + suggested_solution.add_values(5.5); + } + EXPECT_THAT( + CallbackResult::FromProto(Model{}, result_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("CallbackResultProto.suggested_solutions[0]"))); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/compute_infeasible_subsystem_result_test.cc b/ortools/math_opt/cpp/compute_infeasible_subsystem_result_test.cc new file mode 100644 index 00000000000..d2c67468fa6 --- /dev/null +++ b/ortools/math_opt/cpp/compute_infeasible_subsystem_result_test.cc @@ -0,0 +1,590 @@ +// Copyright 2010-2025 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/compute_infeasible_subsystem_result.h" + +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/constraints/indicator/indicator_constraint.h" +#include "ortools/math_opt/constraints/quadratic/quadratic_constraint.h" +#include "ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint.h" +#include "ortools/math_opt/constraints/sos/sos1_constraint.h" +#include "ortools/math_opt/constraints/sos/sos2_constraint.h" +#include "ortools/math_opt/cpp/enums.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/solve_result.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/testing/stream.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::ElementsAre; +using ::testing::EqualsProto; +using ::testing::HasSubstr; +using ::testing::Pair; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +MATCHER_P2(ProtoBoundsAre, lower, upper, "") { + return arg.lower() == lower && arg.upper() == upper; +} + +ModelSubsetProto::Bounds BuildBoundsProto(const bool lower, const bool upper) { + ModelSubsetProto::Bounds proto; + proto.set_lower(lower); + proto.set_upper(upper); + return proto; +} + +TEST(ModelSubsetBoundsTest, FromProto) { + EXPECT_EQ(ModelSubset::Bounds::FromProto(ModelSubsetProto::Bounds()), + (ModelSubset::Bounds{.lower = false, .upper = false})); + EXPECT_EQ(ModelSubset::Bounds::FromProto(BuildBoundsProto(false, true)), + (ModelSubset::Bounds{.lower = false, .upper = true})); + EXPECT_EQ(ModelSubset::Bounds::FromProto(BuildBoundsProto(true, false)), + (ModelSubset::Bounds{.lower = true, .upper = false})); + EXPECT_EQ(ModelSubset::Bounds::FromProto(BuildBoundsProto(true, true)), + (ModelSubset::Bounds{.lower = true, .upper = true})); +} + +TEST(ModelSubsetBoundsTest, Proto) { + EXPECT_THAT(ModelSubset::Bounds().Proto(), ProtoBoundsAre(false, false)); + EXPECT_THAT((ModelSubset::Bounds{.lower = false, .upper = true}.Proto()), + ProtoBoundsAre(false, true)); + EXPECT_THAT((ModelSubset::Bounds{.lower = true, .upper = false}.Proto()), + ProtoBoundsAre(true, false)); + EXPECT_THAT((ModelSubset::Bounds{.lower = true, .upper = true}.Proto()), + ProtoBoundsAre(true, true)); +} + +TEST(ModelSubsetBoundsTest, Streaming) { + EXPECT_EQ(StreamToString(ModelSubset::Bounds()), + "{lower: false, upper: false}"); + EXPECT_EQ((StreamToString(ModelSubset::Bounds{.lower = true, .upper = true})), + "{lower: true, upper: true}"); +} + +struct SimpleModel { + SimpleModel() + : var(model.AddVariable("var")), + lin(model.AddLinearConstraint("lin")), + quad(model.AddQuadraticConstraint( + BoundedQuadraticExpression{0.0, 0.0, 0.0}, "quad")), + soc(model.AddSecondOrderConeConstraint({}, 0.0, "soc")), + sos1(model.AddSos1Constraint({}, {}, "sos1")), + sos2(model.AddSos2Constraint({}, {}, "sos2")), + indicator(model.AddIndicatorConstraint( + var, BoundedLinearExpression{0.0, 0.0, 0.0}, false, "indicator")) {} + + ModelSubset SimpleModelSubset() const { + ModelSubset model_subset; + { + const ModelSubset::Bounds bounds{.lower = true, .upper = false}; + model_subset.variable_bounds[var] = bounds; + model_subset.linear_constraints[lin] = bounds; + model_subset.quadratic_constraints[quad] = bounds; + } + model_subset.variable_integrality.insert(var); + model_subset.second_order_cone_constraints.insert(soc); + model_subset.sos1_constraints.insert(sos1); + model_subset.sos2_constraints.insert(sos2); + model_subset.indicator_constraints.insert(indicator); + return model_subset; + } + + ModelSubsetProto SimpleModelSubsetProto() const { + ModelSubsetProto proto; + { + ModelSubsetProto::Bounds bounds; + bounds.set_lower(true); + bounds.set_upper(false); + (*proto.mutable_variable_bounds())[var.id()] = bounds; + proto.add_variable_integrality(var.id()); + (*proto.mutable_linear_constraints())[lin.id()] = bounds; + (*proto.mutable_quadratic_constraints())[quad.id()] = bounds; + } + proto.add_second_order_cone_constraints(soc.id()); + proto.add_sos1_constraints(sos1.id()); + proto.add_sos2_constraints(sos2.id()); + proto.add_indicator_constraints(indicator.id()); + return proto; + } + + Model model; + Variable var; + LinearConstraint lin; + QuadraticConstraint quad; + SecondOrderConeConstraint soc; + Sos1Constraint sos1; + Sos2Constraint sos2; + IndicatorConstraint indicator; +}; + +TEST(ModelSubsetTest, ProtoEmptyOk) { + EXPECT_THAT(ModelSubset().Proto(), EqualsProto(ModelSubsetProto())); +} + +TEST(ModelSubsetTest, ProtoSimpleOk) { + const SimpleModel model; + const ModelSubset model_subset = model.SimpleModelSubset(); + const ModelSubsetProto proto = model_subset.Proto(); + EXPECT_THAT(proto.variable_bounds(), + ElementsAre(Pair(model.var.id(), ProtoBoundsAre(true, false)))); + EXPECT_THAT(proto.variable_integrality(), ElementsAre(model.var.id())); + EXPECT_THAT(proto.linear_constraints(), + ElementsAre(Pair(model.lin.id(), ProtoBoundsAre(true, false)))); + EXPECT_THAT(proto.quadratic_constraints(), + ElementsAre(Pair(model.quad.id(), ProtoBoundsAre(true, false)))); + EXPECT_THAT(proto.second_order_cone_constraints(), + ElementsAre(model.soc.id())); + EXPECT_THAT(proto.sos1_constraints(), ElementsAre(model.sos1.id())); + EXPECT_THAT(proto.sos2_constraints(), ElementsAre(model.sos2.id())); + EXPECT_THAT(proto.indicator_constraints(), ElementsAre(model.indicator.id())); +} + +TEST(ModelSubsetTest, FromProtoEmptyOk) { + ModelStorage storage; + ASSERT_OK_AND_ASSIGN(const ModelSubset subset, + ModelSubset::FromProto(&storage, ModelSubsetProto())); + EXPECT_TRUE(subset.empty()); +} + +TEST(ModelSubsetTest, FromProtoSimpleOk) { + const SimpleModel model; + ASSERT_OK_AND_ASSIGN(const ModelSubset subset, + ModelSubset::FromProto(model.model.storage(), + model.SimpleModelSubsetProto())); + EXPECT_THAT(subset.variable_bounds, + ElementsAre(Pair(model.var, ModelSubset::Bounds{ + .lower = true, .upper = false}))); + EXPECT_THAT(subset.variable_integrality, ElementsAre(model.var)); + EXPECT_THAT(subset.linear_constraints, + ElementsAre(Pair(model.lin, ModelSubset::Bounds{ + .lower = true, .upper = false}))); + EXPECT_THAT( + subset.quadratic_constraints, + ElementsAre(Pair(model.quad, + ModelSubset::Bounds{.lower = true, .upper = false}))); + EXPECT_THAT(subset.sos1_constraints, ElementsAre(model.sos1)); + EXPECT_THAT(subset.sos2_constraints, ElementsAre(model.sos2)); + EXPECT_THAT(subset.indicator_constraints, ElementsAre(model.indicator)); +} + +TEST(ModelSubsetTest, FromProtoInvalidVariableBounds) { + ModelStorage storage; + ModelSubsetProto proto; + (*proto.mutable_variable_bounds())[2] = ModelSubsetProto::Bounds(); + EXPECT_THAT( + ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no variable"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidVariableIntegrality) { + ModelStorage storage; + ModelSubsetProto proto; + proto.add_variable_integrality(2); + EXPECT_THAT( + ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no variable"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidLinearConstraint) { + ModelStorage storage; + ModelSubsetProto proto; + (*proto.mutable_linear_constraints())[2] = ModelSubsetProto::Bounds(); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no linear constraint"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidQuadraticConstraint) { + ModelStorage storage; + ModelSubsetProto proto; + (*proto.mutable_quadratic_constraints())[2] = ModelSubsetProto::Bounds(); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no quadratic constraint"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidSecondOrderConeConstraint) { + ModelStorage storage; + ModelSubsetProto proto; + proto.add_second_order_cone_constraints(2); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no second-order cone constraint"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidSos1Constraint) { + ModelStorage storage; + ModelSubsetProto proto; + proto.add_sos1_constraints(2); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no SOS1 constraint"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidSos2Constraint) { + ModelStorage storage; + ModelSubsetProto proto; + proto.add_sos2_constraints(2); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no SOS2 constraint"))); +} + +TEST(ModelSubsetTest, FromProtoInvalidIndicatorConstraint) { + ModelStorage storage; + ModelSubsetProto proto; + proto.add_indicator_constraints(2); + EXPECT_THAT(ModelSubset::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no indicator constraint"))); +} + +TEST(ModelSubsetTest, CheckModelStorageEmptyOk) { + ModelStorage storage; + EXPECT_OK(ModelSubset().CheckModelStorage(&storage)); +} + +TEST(ModelSubsetTest, CheckModelStorageSimpleOk) { + SimpleModel model; + EXPECT_OK(model.SimpleModelSubset().CheckModelStorage(model.model.storage())); +} + +TEST(ModelSubsetTest, CheckModelStorageBadVariable) { + const SimpleModel model; + ModelSubset subset; + subset.variable_bounds[model.var] = ModelSubset::Bounds{}; + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable_bounds"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadVariableIntegrality) { + const SimpleModel model; + ModelSubset subset; + subset.variable_integrality.insert(model.var); + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable_integrality"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadLinearConstraint) { + const SimpleModel model; + ModelSubset subset; + subset.linear_constraints[model.lin] = ModelSubset::Bounds{}; + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("linear_constraints"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadQuadraticConstraint) { + const SimpleModel model; + ModelSubset subset; + subset.quadratic_constraints[model.quad] = ModelSubset::Bounds{}; + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("quadratic_constraints"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadSecondOrderConeConstraint) { + const SimpleModel model; + ModelSubset subset; + subset.second_order_cone_constraints.insert(model.soc); + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("second_order_cone_constraints"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadSos1Constraint) { + const SimpleModel model; + ModelSubset subset; + subset.sos1_constraints.insert(model.sos1); + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("sos1_constraints"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadSos2Constraint) { + const SimpleModel model; + ModelSubset subset; + subset.sos2_constraints.insert(model.sos2); + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("sos2_constraints"))); +} + +TEST(ModelSubsetTest, CheckModelStorageBadIndicatorConstraint) { + const SimpleModel model; + ModelSubset subset; + subset.indicator_constraints.insert(model.indicator); + const ModelStorage other; + EXPECT_THAT(subset.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("indicator_constraints"))); +} + +TEST(ModelSubsetTest, EmptyEmptyOk) { EXPECT_TRUE(ModelSubset().empty()); } + +TEST(ModelSubsetTest, EmptyWithVariableBounds) { + ModelSubset subset; + subset.variable_bounds[SimpleModel().var] = ModelSubset::Bounds{}; + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithVariableIntegrality) { + ModelSubset subset; + subset.variable_integrality.insert(SimpleModel().var); + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithLinearConstraint) { + ModelSubset subset; + subset.linear_constraints[SimpleModel().lin] = ModelSubset::Bounds{}; + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithQuadraticConstraint) { + ModelSubset subset; + subset.quadratic_constraints[SimpleModel().quad] = ModelSubset::Bounds{}; + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithSocConstraint) { + ModelSubset subset; + subset.second_order_cone_constraints.insert(SimpleModel().soc); + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithSos1Constraint) { + ModelSubset subset; + subset.sos1_constraints.insert(SimpleModel().sos1); + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithSos2Constraint) { + ModelSubset subset; + subset.sos2_constraints.insert(SimpleModel().sos2); + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, EmptyWithIndicatorConstraint) { + ModelSubset subset; + subset.indicator_constraints.insert(SimpleModel().indicator); + EXPECT_FALSE(subset.empty()); +} + +TEST(ModelSubsetTest, ToStringEmptySubset) { + EXPECT_EQ(ModelSubset().ToString(), + R"subset(Model Subset: + Variable bounds: + Variable integrality: + Linear constraints: +)subset"); +} + +TEST(ModelSubsetTest, ToStringSimpleSubset) { + EXPECT_EQ(SimpleModel().SimpleModelSubset().ToString(), + R"subset(Model Subset: + Variable bounds: + var: var ≤ inf + Variable integrality: + var + Linear constraints: + lin: 0 ≤ inf + Quadratic constraints: + quad: 0 ≥ 0 + Second-order cone constraints: + soc: ||{}||₂ ≤ 0 + SOS1 constraints: + sos1: {} is SOS1 + SOS2 constraints: + sos2: {} is SOS2 + Indicator constraints: + indicator: var = 1 ⇒ 0 = 0 +)subset"); +} + +TEST(ModelSubsetTest, ToStringBoundedConstraintWithEmptyBounds) { + Model model; + const Variable x = model.AddVariable("x"); + const ModelSubset subset{.variable_bounds = {{x, ModelSubset::Bounds{}}}}; + EXPECT_EQ(subset.ToString(), + R"subset(Model Subset: + Variable bounds: + Variable integrality: + Linear constraints: +)subset"); +} + +TEST(ModelSubsetTest, ToStringBoundedConstraintWithNonzeroExpressionOffset) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = + model.AddLinearConstraint(BoundedLinearExpression(x + 1, 2, 3), "c"); + const LinearConstraint d = + model.AddLinearConstraint(BoundedLinearExpression(x + 4, 5, 6), "d"); + const ModelSubset subset{ + .linear_constraints = { + {c, ModelSubset::Bounds{.lower = true, .upper = false}}, + {d, ModelSubset::Bounds{.lower = false, .upper = true}}}}; + EXPECT_EQ(subset.ToString(), + R"subset(Model Subset: + Variable bounds: + Variable integrality: + Linear constraints: + c: x ≥ 1 + d: x ≤ 2 +)subset"); +} + +TEST(ModelSubsetTest, Streaming) { + EXPECT_EQ(StreamToString(ModelSubset()), + "{variable_bounds: {}, variable_integrality: {}, " + "linear_constraints: {}, quadratic_constraints: {}, " + "second_order_cone_constraints: {}, sos1_constraints: {}, " + "sos2_constraints: {}, indicator_constraints: {}}"); + EXPECT_EQ( + StreamToString(SimpleModel().SimpleModelSubset()), + "{variable_bounds: {{var, {lower: true, upper: false}}}, " + "variable_integrality: {var}, linear_constraints: {{lin, {lower: true, " + "upper: false}}}, quadratic_constraints: {{quad, {lower: true, upper: " + "false}}}, second_order_cone_constraints: {soc}, sos1_constraints: " + "{sos1}, sos2_constraints: {sos2}, indicator_constraints: {indicator}}"); + // We test only one "map-of-bounds" field (all use the same helper). + { + SimpleModel model; + ModelSubset subset = model.SimpleModelSubset(); + const Variable x = model.model.AddVariable("x"); + subset.variable_bounds.insert({x, {false, true}}); + EXPECT_THAT(StreamToString(subset), + HasSubstr("{variable_bounds: {{var, {lower: true, upper: " + "false}}, {x, {lower: false, upper: true}}}")); + } + // We test only one "set-of-repeated-IDs" field (all use the same helper). + { + SimpleModel model; + ModelSubset subset = model.SimpleModelSubset(); + const Sos1Constraint c = model.model.AddSos1Constraint({}, {}, "c"); + subset.sos1_constraints.insert(c); + EXPECT_THAT(StreamToString(subset), + HasSubstr("sos1_constraints: {sos1, c}")); + } +} + +TEST(ComputeInfeasibleSubsystemResultTest, ProtoEmptyOk) { + ComputeInfeasibleSubsystemResultProto expected; + expected.set_feasibility(FEASIBILITY_STATUS_UNDETERMINED); + EXPECT_THAT(ComputeInfeasibleSubsystemResult{}.Proto(), + EqualsProto(expected)); +} + +TEST(ComputeInfeasibleSubsystemResultTest, ProtoSimpleOk) { + const SimpleModel model; + ComputeInfeasibleSubsystemResult result{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = model.SimpleModelSubset(), + .is_minimal = true}; + ComputeInfeasibleSubsystemResultProto expected; + expected.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE); + expected.set_is_minimal(true); + *expected.mutable_infeasible_subsystem() = model.SimpleModelSubsetProto(); + EXPECT_THAT(result.Proto(), EqualsProto(expected)); +} + +TEST(ComputeInfeasibleSubsystemResultTest, FromProtoTrivialOk) { + ModelStorage storage; + ComputeInfeasibleSubsystemResultProto result_proto; + result_proto.set_feasibility(FEASIBILITY_STATUS_UNDETERMINED); + EXPECT_THAT( + ComputeInfeasibleSubsystemResult::FromProto(&storage, result_proto), + IsOkAndHolds(IsUndetermined())); +} + +TEST(ComputeInfeasibleSubsystemResultTest, FromProtoSimpleOk) { + const SimpleModel model; + ComputeInfeasibleSubsystemResultProto result_proto; + result_proto.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE); + result_proto.set_is_minimal(true); + *result_proto.mutable_infeasible_subsystem() = model.SimpleModelSubsetProto(); + ASSERT_THAT(ComputeInfeasibleSubsystemResult::FromProto(model.model.storage(), + result_proto), + IsOkAndHolds(IsInfeasible(/*expected_is_minimal=*/true, + model.SimpleModelSubset()))); +} + +// Integration test for ValidateInfeasibleSubsystemResultNoModel(). +TEST(ComputeInfeasibleSubsystemResultTest, FromProtoInvalidNoModel) { + ModelStorage storage; + EXPECT_THAT(ComputeInfeasibleSubsystemResult::FromProto( + &storage, ComputeInfeasibleSubsystemResultProto{}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("feasibility must be specified"))); +} + +TEST(ComputeInfeasibleSubsystemResultTest, + FromProtoInvalidInfeasibleSubsystem) { + ModelStorage storage; + ComputeInfeasibleSubsystemResultProto result_proto; + result_proto.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE); + (*result_proto.mutable_infeasible_subsystem()->mutable_variable_bounds())[0]; + EXPECT_THAT( + ComputeInfeasibleSubsystemResult::FromProto(&storage, result_proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no variable"))); +} + +TEST(ComputeInfeasibleSubsystemResultTest, Streaming) { + EXPECT_EQ( + StreamToString(ComputeInfeasibleSubsystemResult()), + "{feasibility: undetermined, infeasible_subsystem: {variable_bounds: {}, " + "variable_integrality: {}, linear_constraints: {}, " + "quadratic_constraints: {}, " + "second_order_cone_constraints: {}, sos1_constraints: {}, " + "sos2_constraints: {}, indicator_constraints: {}}, is_minimal: false}"); +} + +TEST(ComputeInfeasibleSubsystemResultTest, CheckModelStorage) { + const SimpleModel model; + ComputeInfeasibleSubsystemResult result; + result.infeasible_subsystem.variable_bounds[model.var] = + ModelSubset::Bounds{}; + const ModelStorage other; + EXPECT_THAT(result.CheckModelStorage(&other), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable_bounds"))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/enums_test.cc b/ortools/math_opt/cpp/enums_test.cc new file mode 100644 index 00000000000..bdc3b9688a4 --- /dev/null +++ b/ortools/math_opt/cpp/enums_test.cc @@ -0,0 +1,132 @@ +// Copyright 2010-2025 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/enums.h" + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/math_opt/cpp/enums_test.pb.h" +#include "ortools/math_opt/cpp/enums_testing.h" +#include "ortools/math_opt/testing/stream.h" + +namespace operations_research::math_opt { +namespace { + +enum class TestEnum { + kFirstValue = TEST_ENUM_FIRST_VALUE, + kSecondValue = TEST_ENUM_SECOND_VALUE +}; + +} // namespace + +// Template specialization must be in the same namespace. It can't be in the +// anonymous namespace. +MATH_OPT_DEFINE_ENUM(TestEnum, TEST_ENUM_UNSPECIFIED); + +std::optional Enum::ToOptString(TestEnum value) { + switch (value) { + case TestEnum::kFirstValue: + return "first_value"; + case TestEnum::kSecondValue: + return "second_value"; + } + return std::nullopt; +} + +absl::Span Enum::AllValues() { + static constexpr TestEnum kTestEnumValues[] = {TestEnum::kFirstValue, + TestEnum::kSecondValue}; + + return absl::MakeConstSpan(kTestEnumValues); +} + +namespace { + +using ::testing::ElementsAre; +using ::testing::Optional; + +TEST(EnumsTest, EnumToProtoWithOptional) { + std::optional opt_value = TestEnum::kFirstValue; + EXPECT_EQ(EnumToProto(opt_value), TEST_ENUM_FIRST_VALUE); + EXPECT_EQ(EnumToProto(std::nullopt), TEST_ENUM_UNSPECIFIED); +} + +TEST(EnumsTest, EnumToProtoWithValue) { + EXPECT_EQ(EnumToProto(TestEnum::kFirstValue), TEST_ENUM_FIRST_VALUE); + EXPECT_EQ(EnumToProto(TestEnum::kSecondValue), TEST_ENUM_SECOND_VALUE); +} + +TEST(EnumsTest, FromProto) { + EXPECT_THAT(EnumFromProto(TEST_ENUM_FIRST_VALUE), + Optional(TestEnum::kFirstValue)); + EXPECT_THAT(EnumFromProto(TEST_ENUM_SECOND_VALUE), + Optional(TestEnum::kSecondValue)); + EXPECT_EQ(EnumFromProto(TEST_ENUM_UNSPECIFIED), std::nullopt); +} + +TEST(EnumsTest, EnumToOptString) { + EXPECT_THAT(EnumToOptString(TestEnum::kFirstValue), + Optional(absl::string_view("first_value"))); + EXPECT_THAT(EnumToOptString(TestEnum::kSecondValue), + Optional(absl::string_view("second_value"))); + EXPECT_EQ(EnumToOptString(static_cast(-15)), std::nullopt); +} + +TEST(EnumsTest, EnumToString) { + EXPECT_THAT(EnumToString(TestEnum::kFirstValue), "first_value"); + EXPECT_THAT(EnumToString(TestEnum::kSecondValue), "second_value"); +} + +TEST(EnumsDeathTest, EnumToString) { + EXPECT_DEATH_IF_SUPPORTED(EnumToString(static_cast(-15)), + "invalid value: -15"); +} + +TEST(EnumsTest, EnumFromString) { + EXPECT_THAT(EnumFromString("first_value"), + Optional(TestEnum::kFirstValue)); + EXPECT_THAT(EnumFromString("second_value"), + Optional(TestEnum::kSecondValue)); + EXPECT_EQ(EnumFromString("unknown"), std::nullopt); +} + +TEST(EnumsTest, AllValues) { + EXPECT_THAT(Enum::AllValues(), + ElementsAre(TestEnum::kFirstValue, TestEnum::kSecondValue)); +} + +TEST(EnumsTest, Stream) { + EXPECT_EQ(StreamToString(TestEnum::kFirstValue), "first_value"); + EXPECT_EQ(StreamToString(static_cast(-15)), ""); +} + +TEST(EnumsTest, OptStream) { + EXPECT_EQ(StreamToString(std::make_optional(TestEnum::kFirstValue)), + "first_value"); + EXPECT_EQ(StreamToString(std::make_optional(static_cast(-15))), + ""); + EXPECT_EQ(StreamToString(std::optional()), ""); +} + +INSTANTIATE_TYPED_TEST_SUITE_P(TestEnum, EnumTest, TestEnum); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/enums_test.proto b/ortools/math_opt/cpp/enums_test.proto new file mode 100644 index 00000000000..d8c60cf4a7f --- /dev/null +++ b/ortools/math_opt/cpp/enums_test.proto @@ -0,0 +1,28 @@ +// Copyright 2010-2025 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. + +syntax = "proto3"; + +package operations_research.math_opt; + +// Proto enum used in the unit tests of enums.h. +enum TestEnumProto { + TEST_ENUM_UNSPECIFIED = 0; + + TEST_ENUM_FIRST_VALUE = 1; + + // We intentionally have a gap in values to test this case. + reserved 2; + + TEST_ENUM_SECOND_VALUE = 3; +} diff --git a/ortools/math_opt/cpp/formatters_test.cc b/ortools/math_opt/cpp/formatters_test.cc new file mode 100644 index 00000000000..c347f620cd4 --- /dev/null +++ b/ortools/math_opt/cpp/formatters_test.cc @@ -0,0 +1,137 @@ +// Copyright 2010-2025 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/formatters.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/math_opt/testing/stream.h" +#include "ortools/util/fp_roundtrip_conv_testing.h" +#include "util/gtl/extend/debug_printing.h" +#include "util/gtl/extend/equality.h" +#include "util/gtl/extend/extend.h" + +namespace operations_research::math_opt { + +namespace { + +struct FormattersTestCase + : gtl::Extend::With { + const std::string test_name; + const double x; + const bool is_first; + const std::string expected_leading_coefficient; + const std::string expected_constant_term; +}; + +using FormattersTest = testing::TestWithParam; + +TEST_P(FormattersTest, LeadingCoefficientFormatter) { + const FormattersTestCase& test_case = GetParam(); + EXPECT_EQ(StreamToString( + LeadingCoefficientFormatter(test_case.x, test_case.is_first)), + test_case.expected_leading_coefficient); +} + +TEST_P(FormattersTest, ConstantFormatter) { + const FormattersTestCase& test_case = GetParam(); + EXPECT_EQ(StreamToString(ConstantFormatter(test_case.x, test_case.is_first)), + test_case.expected_constant_term); +} + +INSTANTIATE_TEST_SUITE_P( + FormattersTests, FormattersTest, + testing::ValuesIn( + {{.test_name = "positive", + .x = 1.2, + .is_first = false, + .expected_leading_coefficient = " + 1.2*", + .expected_constant_term = " + 1.2"}, + {.test_name = "positive_first", + .x = 1.2, + .is_first = true, + .expected_leading_coefficient = "1.2*", + .expected_constant_term = "1.2"}, + {.test_name = "negative", + .x = -4, + .is_first = false, + .expected_leading_coefficient = " - 4*", + .expected_constant_term = " - 4"}, + {.test_name = "negative_first", + .x = -4.35, + .is_first = true, + .expected_leading_coefficient = "-4.35*", + .expected_constant_term = "-4.35"}, + {.test_name = "one", + .x = 1, + .is_first = false, + .expected_leading_coefficient = " + ", + .expected_constant_term = " + 1"}, + {.test_name = "one_first", + .x = 1, + .is_first = true, + .expected_leading_coefficient = "", + .expected_constant_term = "1"}, + {.test_name = "minus_one", + .x = -1, + .is_first = false, + .expected_leading_coefficient = " - ", + .expected_constant_term = " - 1"}, + {.test_name = "minus_one_first", + .x = -1, + .is_first = true, + .expected_leading_coefficient = "-", + .expected_constant_term = "-1"}, + {.test_name = "zero", + .x = 0, + .is_first = false, + .expected_leading_coefficient = " + 0*", + .expected_constant_term = ""}, + {.test_name = "zero_first", + .x = 0, + .is_first = true, + .expected_leading_coefficient = "0*", + .expected_constant_term = "0"}, + {.test_name = "nan", + .x = std::nan(""), + .is_first = false, + .expected_leading_coefficient = " + nan*", + .expected_constant_term = " + nan"}, + {.test_name = "nan_first", + .x = std::nan(""), + .is_first = true, + .expected_leading_coefficient = "nan*", + .expected_constant_term = "nan"}, + {.test_name = "fp_roundtrip", + .x = kRoundTripTestNumber, + .is_first = false, + .expected_leading_coefficient = + absl::StrCat(" + ", kRoundTripTestNumberStr, "*"), + .expected_constant_term = absl::StrCat(" + ", + kRoundTripTestNumberStr)}, + {.test_name = "fp_roundtrip_true", + .x = kRoundTripTestNumber, + .is_first = true, + .expected_leading_coefficient = absl::StrCat(kRoundTripTestNumberStr, + "*"), + .expected_constant_term = std::string(kRoundTripTestNumberStr)}}), + [](const testing::TestParamInfo& info) { + return info.param.test_name; + }); + +} // namespace + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/key_types.h b/ortools/math_opt/cpp/key_types.h index df72faad6af..5ceec1dd80d 100644 --- a/ortools/math_opt/cpp/key_types.h +++ b/ortools/math_opt/cpp/key_types.h @@ -149,17 +149,17 @@ std::vector Values(const Map& map, namespace internal { // The CHECK message to use when a KeyType::storage() is nullptr. -inline constexpr const char kKeyHasNullModelStorage[] = +inline constexpr absl::string_view kKeyHasNullModelStorage = "The input key has null .storage()."; // The CHECK message to use when two KeyType with different storage() are used // in the same collection. -inline constexpr const char kObjectsFromOtherModelStorage[] = +inline constexpr absl::string_view kObjectsFromOtherModelStorage = "The input objects belongs to another model."; // The Status message to use when an input KeyType is from an unexpected // storage(). -inline constexpr const char kInputFromInvalidModelStorage[] = +inline constexpr absl::string_view kInputFromInvalidModelStorage = "the input does not belong to the same model"; // Returns a failure when the input pointer is not nullptr and points to a diff --git a/ortools/math_opt/cpp/key_types_test.cc b/ortools/math_opt/cpp/key_types_test.cc new file mode 100644 index 00000000000..7b79d247ed0 --- /dev/null +++ b/ortools/math_opt/cpp/key_types_test.cc @@ -0,0 +1,165 @@ +// Copyright 2010-2025 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/key_types.h" + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research::math_opt::internal { +namespace { + +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::status::StatusIs; + +TEST(CheckModelStorageTest, NullExpected) { + ModelStorage model; + // The compiler will prevent us from passing nullptr to a function expecting + // a `ModelStorageCPtr`. So we launder the `nullptr` to test the behavior of + // `CheckModelStorage()` in that case. + ModelStorage* laundered_nullptr = nullptr; + benchmark::DoNotOptimize(laundered_nullptr); + EXPECT_THAT( + CheckModelStorage(/*storage=*/nullptr, + /*expected_storage=*/laundered_nullptr), + StatusIs(absl::StatusCode::kInternal, HasSubstr("expected_storage"))); + EXPECT_THAT( + CheckModelStorage(/*storage=*/&model, + /*expected_storage=*/laundered_nullptr), + StatusIs(absl::StatusCode::kInternal, HasSubstr("expected_storage"))); +} + +TEST(CheckModelStorageTest, SingleModel) { + ModelStorage model; + EXPECT_OK(CheckModelStorage(/*storage=*/nullptr, + /*expected_storage=*/&model)); + EXPECT_OK(CheckModelStorage(/*storage=*/&model, + /*expected_storage=*/&model)); +} + +TEST(CheckModelStorageTest, TwoModels) { + ModelStorage model_a; + ModelStorage model_b; + EXPECT_THAT(CheckModelStorage(/*storage=*/&model_a, + /*expected_storage=*/&model_b), + StatusIs(absl::StatusCode::kInvalidArgument, + kInputFromInvalidModelStorage)); +} + +TEST(SortedKeysTest, EmptyFlatHashMap) { + const absl::flat_hash_map vmap; + EXPECT_THAT(SortedKeys(vmap), IsEmpty()); +} + +TEST(SortedKeysTest, MultipleModelsFlatHashMap) { + // Here we use std::make_unique<> instead of using the stack as using the + // stack may for the order of the pointers on each model. + const auto model_1 = std::make_unique(); + const Variable first1(model_1.get(), model_1->AddVariable("first1")); + const Variable second1(model_1.get(), model_1->AddVariable("second1")); + const Variable third1(model_1.get(), model_1->AddVariable("third1")); + const Variable fourth1(model_1.get(), model_1->AddVariable("fourth1")); + + const auto model_2 = std::make_unique(); + const Variable first2(model_2.get(), model_2->AddVariable("first2")); + const Variable second2(model_2.get(), model_2->AddVariable("second2")); + const Variable third2(model_2.get(), model_2->AddVariable("third2")); + const Variable fourth2(model_2.get(), model_2->AddVariable("fourth2")); + + const absl::flat_hash_map vmap = { + {second1, 3.0}, {third1, 5.0}, {second2, -3.5}, {fourth1, 0.0}, + {third2, 8.0}, {first2, 1.25}, {fourth2, 12.0}, {first1, -5.0}, + }; + + // The sort order between models depends on the pointer values. + if (model_1 < model_2) { + EXPECT_THAT(SortedKeys(vmap), + ElementsAre(first1, second1, third1, fourth1, first2, second2, + third2, fourth2)); + } else { + EXPECT_THAT(SortedKeys(vmap), + ElementsAre(first2, second2, third2, fourth2, first1, second1, + third1, fourth1)); + } +} + +TEST(SortedElementsTest, EmptyFlatHashSet) { + const absl::flat_hash_set vset; + EXPECT_THAT(SortedElements(vset), IsEmpty()); +} + +TEST(SortedElementsTest, MultipleModelsFlatHashSet) { + // Here we use std::make_unique<> instead of using the stack as using the + // stack may for the order of the pointers on each model. + const auto model_1 = std::make_unique(); + const Variable first1(model_1.get(), model_1->AddVariable("first1")); + const Variable second1(model_1.get(), model_1->AddVariable("second1")); + const Variable third1(model_1.get(), model_1->AddVariable("third1")); + const Variable fourth1(model_1.get(), model_1->AddVariable("fourth1")); + + const auto model_2 = std::make_unique(); + const Variable first2(model_2.get(), model_2->AddVariable("first2")); + const Variable second2(model_2.get(), model_2->AddVariable("second2")); + const Variable third2(model_2.get(), model_2->AddVariable("third2")); + const Variable fourth2(model_2.get(), model_2->AddVariable("fourth2")); + + const absl::flat_hash_set vset = {second1, third1, second2, fourth1, + third2, first2, fourth2, first1}; + + // The sort order between models depends on the pointer values. + if (model_1 < model_2) { + EXPECT_THAT(SortedElements(vset), + ElementsAre(first1, second1, third1, fourth1, first2, second2, + third2, fourth2)); + } else { + EXPECT_THAT(SortedElements(vset), + ElementsAre(first2, second2, third2, fourth2, first1, second1, + third1, fourth1)); + } +} + +TEST(ValuesTest, MultipleModelsFlatHashMap) { + ModelStorage model_1; + const Variable first1(&model_1, model_1.AddVariable("first1")); + const Variable second1(&model_1, model_1.AddVariable("second1")); + const Variable third1(&model_1, model_1.AddVariable("third1")); + const Variable fourth1(&model_1, model_1.AddVariable("fourth1")); + + ModelStorage model_2; + const Variable first2(&model_2, model_2.AddVariable("first2")); + const Variable second2(&model_2, model_2.AddVariable("second2")); + const Variable third2(&model_2, model_2.AddVariable("third2")); + const Variable fourth2(&model_2, model_2.AddVariable("fourth2")); + + const absl::flat_hash_map vmap = { + {second1, 3.0}, {third1, 5.0}, {second2, -3.5}, {fourth1, 0.0}, + {third2, 8.0}, {first2, 1.25}, {fourth2, 12.0}, {first1, -5.0}, + }; + + const std::vector vars = {second2, first1, fourth2}; + EXPECT_THAT(Values(vmap, vars), ElementsAre(-3.5, -5.0, 12.0)); +} + +} // namespace +} // namespace operations_research::math_opt::internal diff --git a/ortools/math_opt/cpp/linear_constraint_test.cc b/ortools/math_opt/cpp/linear_constraint_test.cc new file mode 100644 index 00000000000..df493fd476b --- /dev/null +++ b/ortools/math_opt/cpp/linear_constraint_test.cc @@ -0,0 +1,205 @@ +// Copyright 2010-2025 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/linear_constraint.h" + +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_int.h" +#include "ortools/math_opt/constraints/util/model_util.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/model_storage_types.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +constexpr double kInf = std::numeric_limits::infinity(); + +TEST(LinearConstraintTest, OutputStreaming) { + ModelStorage storage; + const LinearConstraint c(&storage, storage.AddLinearConstraint("c")); + const LinearConstraint anonymous(&storage, storage.AddLinearConstraint()); + + auto to_string = [](LinearConstraint c) { + std::ostringstream oss; + oss << c; + return oss.str(); + }; + + EXPECT_EQ(to_string(c), "c"); + EXPECT_EQ(to_string(anonymous), + absl::StrCat("__lin_con#", anonymous.id(), "__")); +} + +TEST(LinearConstraintTest, Accessors) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Variable y(&storage, storage.AddVariable("y")); + { + const LinearConstraintId c_id = storage.AddLinearConstraint( + /*lower_bound=*/-kInf, /*upper_bound=*/1.5, "upper_bounded"); + const LinearConstraint c(&storage, c_id); + storage.set_linear_constraint_coefficient(c_id, x.typed_id(), 1.0); + storage.set_linear_constraint_coefficient(c_id, y.typed_id(), 1.0); + + EXPECT_EQ(c.name(), "upper_bounded"); + EXPECT_EQ(c.id(), c_id.value()); + EXPECT_EQ(c.typed_id(), c_id); + EXPECT_EQ(c.lower_bound(), -kInf); + EXPECT_EQ(c.upper_bound(), 1.5); + + EXPECT_DOUBLE_EQ(c.coefficient(x), 1.0); + EXPECT_DOUBLE_EQ(c.coefficient(y), 1.0); + + EXPECT_TRUE(c.is_coefficient_nonzero(x)); + EXPECT_TRUE(c.is_coefficient_nonzero(y)); + } + { + const LinearConstraintId c_id = storage.AddLinearConstraint( + /*lower_bound=*/0.5, /*upper_bound=*/kInf, "lower_bounded"); + const LinearConstraint c(&storage, c_id); + storage.set_linear_constraint_coefficient(c_id, y.typed_id(), 2.0); + + EXPECT_EQ(c.name(), "lower_bounded"); + EXPECT_EQ(c.id(), 1); + EXPECT_EQ(c.lower_bound(), 0.5); + EXPECT_EQ(c.upper_bound(), kInf); + + EXPECT_DOUBLE_EQ(c.coefficient(x), 0.0); + EXPECT_DOUBLE_EQ(c.coefficient(y), 2.0); + + EXPECT_FALSE(c.is_coefficient_nonzero(x)); + EXPECT_TRUE(c.is_coefficient_nonzero(y)); + } +} + +TEST(LinearConstraintTest, AsBoundedExpression) { + // We don't use the IsNearlyEquivalent(BoundedLinearExpression) matcher in + // tests as we want to test that the offset of + // BoundedLinearExpression.expression is indeed 0 in returned values. + + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + { + const LinearConstraint c = model.AddLinearConstraint( + /*lower_bound=*/-kInf, /*upper_bound=*/1.5, "upper_bounded"); + model.set_coefficient(c, x, 1.0); + model.set_coefficient(c, y, 2.0); + + const BoundedLinearExpression c_bounded_expr = + c.AsBoundedLinearExpression(); + EXPECT_EQ(c_bounded_expr.lower_bound, -kInf); + EXPECT_EQ(c_bounded_expr.upper_bound, 1.5); + EXPECT_THAT(c_bounded_expr.expression, IsIdentical(x + 2.0 * y)); + } + { + const LinearConstraint c = model.AddLinearConstraint( + /*lower_bound=*/0.5, /*upper_bound=*/kInf, "lower_bounded"); + model.set_coefficient(c, y, 2.0); + + const BoundedLinearExpression c_bounded_expr = + c.AsBoundedLinearExpression(); + EXPECT_EQ(c_bounded_expr.lower_bound, 0.5); + EXPECT_EQ(c_bounded_expr.upper_bound, kInf); + // TODO(b/290378193): remove the `+ 0.0` when the IsIdentical() overload + // issue is fixed (see bug for details). + EXPECT_THAT(c_bounded_expr.expression, IsIdentical(2.0 * y + 0.0)); + } + { + const LinearConstraint c = + model.AddLinearConstraint(3.5 <= 5.0 * x + 2.0 * y + 1.0 <= 4.25); + const BoundedLinearExpression c_bounded_expr = + c.AsBoundedLinearExpression(); + EXPECT_EQ(c_bounded_expr.lower_bound, 3.5 - 1.0); + EXPECT_EQ(c_bounded_expr.upper_bound, 4.25 - 1.0); + EXPECT_THAT(c_bounded_expr.expression, IsIdentical(5.0 * x + 2.0 * y)); + } +} + +TEST(LinearConstraintTest, ToString) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + + const LinearConstraint c = + model.AddLinearConstraint(1.0 <= -2 * x + 3 * y + 4.0 <= 5.0); + EXPECT_EQ(c.ToString(), "-3 ≤ -2*x + 3*y ≤ 1"); + + model.DeleteVariable(x); + EXPECT_EQ(c.ToString(), "-3 ≤ 3*y ≤ 1"); +} + +TEST(LinearConstraintTest, Equality) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Variable y(&storage, storage.AddVariable("y")); + + const LinearConstraint c(&storage, storage.AddLinearConstraint( + /*lower_bound=*/-kInf, + /*upper_bound=*/1.5, "upper_bounded")); + storage.set_linear_constraint_coefficient(c.typed_id(), x.typed_id(), 1.0); + storage.set_linear_constraint_coefficient(c.typed_id(), y.typed_id(), 1.0); + + const LinearConstraint d( + &storage, + storage.AddLinearConstraint(/*lower_bound=*/0.5, + /*upper_bound=*/kInf, "lower_bounded")); + storage.set_linear_constraint_coefficient(d.typed_id(), y.typed_id(), 2.0); + + // `d2` is another `LinearConstraint` that points the same constraint in the + // indexed storage. It should compares == to `d`. + const LinearConstraint d2(d.storage(), d.typed_id()); + + // `e` has identical data as `d`. It should not compares equal to `d` tough. + const LinearConstraint e( + &storage, + storage.AddLinearConstraint(/*lower_bound=*/0.5, + /*upper_bound=*/kInf, "lower_bounded")); + storage.set_linear_constraint_coefficient(d.typed_id(), y.typed_id(), 2.0); + + EXPECT_TRUE(c == c); + EXPECT_FALSE(c == d); + EXPECT_TRUE(d == d2); + EXPECT_FALSE(d == e); + EXPECT_FALSE(c != c); + EXPECT_TRUE(c != d); + EXPECT_FALSE(d != d2); + EXPECT_TRUE(d != e); +} + +TEST(LinearConstraintTest, NameAfterDeletion) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint(2 * x >= 1, "c"); + ASSERT_EQ(c.name(), "c"); + + model.DeleteLinearConstraint(c); + EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/map_filter_test.cc b/ortools/math_opt/cpp/map_filter_test.cc new file mode 100644 index 00000000000..1a8e1d7a3dc --- /dev/null +++ b/ortools/math_opt/cpp/map_filter_test.cc @@ -0,0 +1,331 @@ +// Copyright 2010-2025 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/map_filter.h" + +#include +#include + +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Optional; +using ::testing::UnorderedElementsAre; +using ::testing::status::StatusIs; + +TEST(MapFilterTest, VariableDefault) { + const ModelStorage model; + const MapFilter filter; + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_EQ(filter.filtered_keys, std::nullopt); + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto("")); +} + +TEST(MapFilterTest, LinearConstraintDefault) { + const ModelStorage model; + const MapFilter filter; + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_EQ(filter.filtered_keys, std::nullopt); + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto("")); +} + +TEST(MapFilterTest, SkipZeros) { + const ModelStorage model; + + // Setting skip_zero_values to true. + MapFilter filter; + filter.skip_zero_values = true; + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto(R"pb(skip_zero_values: true)pb")); + + // Setting skip_zero_values to false. + filter.skip_zero_values = false; + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto(R"pb(skip_zero_values: false)pb")); +} + +TEST(MapFilterTest, SetEmptyFilteredKeys) { + const ModelStorage model; + MapFilter filter; + filter.filtered_keys.emplace(); + + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto(R"pb(filter_by_ids: true)pb")); +} + +TEST(MapFilterTest, SetFilteredKeysWithInitializerList) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const Variable c(&model, model.AddVariable("c")); + + MapFilter filter; + filter.filtered_keys = {a, c}; + + EXPECT_OK(filter.CheckModelStorage(&model)); + SparseVectorFilterProto expected; + expected.set_filter_by_ids(true); + expected.add_filtered_ids(a.id()); + expected.add_filtered_ids(c.id()); + EXPECT_THAT(filter.Proto(), EquivToProto(expected)); +} + +TEST(MapFilterTest, SetFilteredKeysWithVector) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const Variable c(&model, model.AddVariable("c")); + + MapFilter filter; + const std::vector vars = {a, c}; + filter.filtered_keys.emplace(vars.begin(), vars.end()); + + EXPECT_OK(filter.CheckModelStorage(&model)); + SparseVectorFilterProto expected; + expected.set_filter_by_ids(true); + expected.add_filtered_ids(a.id()); + expected.add_filtered_ids(c.id()); + EXPECT_THAT(filter.Proto(), EquivToProto(expected)); +} + +TEST(MapFilterTest, CheckModelStorageWithMixedModels) { + ModelStorage model_1; + const Variable a(&model_1, model_1.AddVariable("a")); + const Variable b(&model_1, model_1.AddVariable("b")); + + ModelStorage model_2; + const Variable c(&model_2, model_2.AddVariable("c")); + + MapFilter filter; + const std::vector vars = {a, c}; + filter.filtered_keys = {a, c}; + + EXPECT_THAT(filter.CheckModelStorage(&model_1), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(MapFilterTest, UnsetNonEmptyFilteredKeys) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const Variable c(&model, model.AddVariable("c")); + + MapFilter filter; + filter.filtered_keys = {a, b, c}; + filter.filtered_keys.reset(); + + EXPECT_OK(filter.CheckModelStorage(&model)); + EXPECT_THAT(filter.Proto(), EquivToProto("")); +} + +TEST(MakeSkipAllFilterTest, Variables) { + const auto filter = MakeSkipAllFilter(); + EXPECT_THAT(filter.Proto(), EquivToProto(R"pb(filter_by_ids: true)pb")); +} + +TEST(MakeSkipZerosFilterTest, Variables) { + const auto filter = MakeSkipZerosFilter(); + EXPECT_THAT(filter.Proto(), EquivToProto(R"pb(skip_zero_values: true)pb")); +} + +TEST(MakeKeepKeysFilterTest, Vector) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const Variable c(&model, model.AddVariable("c")); + + const std::vector vars = {a, c}; + const auto filter = MakeKeepKeysFilter(vars); + + SparseVectorFilterProto expected; + expected.set_filter_by_ids(true); + expected.add_filtered_ids(a.id()); + expected.add_filtered_ids(c.id()); + + EXPECT_THAT(filter.Proto(), EquivToProto(expected)); +} + +TEST(MakeKeepKeysFilterTest, InitializerList) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const Variable c(&model, model.AddVariable("c")); + + const auto filter = MakeKeepKeysFilter({a, c}); + + SparseVectorFilterProto expected; + expected.set_filter_by_ids(true); + expected.add_filtered_ids(a.id()); + expected.add_filtered_ids(c.id()); + + EXPECT_THAT(filter.Proto(), EquivToProto(expected)); +} + +TEST(VariableFilterFromProtoTest, EmptyFilter) { + Model model; + SparseVectorFilterProto proto; + ASSERT_OK_AND_ASSIGN(MapFilter filter, + VariableFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_EQ(filter.filtered_keys, std::nullopt); +} + +TEST(VariableFilterFromProtoTest, NonemptyFilter) { + Model model; + Variable x = model.AddBinaryVariable(); + model.AddBinaryVariable(); + Variable z = model.AddBinaryVariable(); + SparseVectorFilterProto proto; + proto.set_skip_zero_values(true); + proto.set_filter_by_ids(true); + proto.add_filtered_ids(0); + proto.add_filtered_ids(2); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + VariableFilterFromProto(model, proto)); + EXPECT_TRUE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(UnorderedElementsAre(x, z))); +} + +TEST(VariableFilterFromProtoTest, FilterByIdsEmpty) { + Model model; + model.AddBinaryVariable(); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + VariableFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(IsEmpty())); +} + +TEST(VariableFilterFromProtoTest, BadFilterIdFails) { + Model model; + model.AddBinaryVariable(); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + proto.add_filtered_ids(3); + EXPECT_THAT(VariableFilterFromProto(model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("3"))); +} + +TEST(LinearConstraintFilterFromProtoTest, EmptyFilter) { + Model model; + SparseVectorFilterProto proto; + ASSERT_OK_AND_ASSIGN(MapFilter filter, + LinearConstraintFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_EQ(filter.filtered_keys, std::nullopt); +} + +TEST(LinearConstraintFilterFromProtoTest, NonemptyFilter) { + Model model; + LinearConstraint c = model.AddLinearConstraint(); + model.AddLinearConstraint(); + LinearConstraint e = model.AddLinearConstraint(); + SparseVectorFilterProto proto; + proto.set_skip_zero_values(true); + proto.set_filter_by_ids(true); + proto.add_filtered_ids(0); + proto.add_filtered_ids(2); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + LinearConstraintFilterFromProto(model, proto)); + EXPECT_TRUE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(UnorderedElementsAre(c, e))); +} + +TEST(LinearConstraintFilterFromProtoTest, FilterByIdsEmpty) { + Model model; + model.AddLinearConstraint(); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + LinearConstraintFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(IsEmpty())); +} + +TEST(LinearConstraintFilterFromProtoTest, BadFilterIdFails) { + Model model; + model.AddLinearConstraint(); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + proto.add_filtered_ids(3); + EXPECT_THAT(LinearConstraintFilterFromProto(model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("3"))); +} + +TEST(QuadraticConstraintFilterFromProtoTest, EmptyFilter) { + Model model; + SparseVectorFilterProto proto; + ASSERT_OK_AND_ASSIGN(MapFilter filter, + QuadraticConstraintFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_EQ(filter.filtered_keys, std::nullopt); +} + +TEST(QuadraticConstraintFilterFromProtoTest, NonemptyFilter) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + QuadraticConstraint c = model.AddQuadraticConstraint(x * x <= 0.0); + model.AddQuadraticConstraint(x * x <= 0.0); + QuadraticConstraint e = model.AddQuadraticConstraint(x * x <= 0.0); + SparseVectorFilterProto proto; + proto.set_skip_zero_values(true); + proto.set_filter_by_ids(true); + proto.add_filtered_ids(0); + proto.add_filtered_ids(2); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + QuadraticConstraintFilterFromProto(model, proto)); + EXPECT_TRUE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(UnorderedElementsAre(c, e))); +} + +TEST(QuadraticConstraintFilterFromProtoTest, FilterByIdsEmpty) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + model.AddQuadraticConstraint(x * x <= 0.0); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + ASSERT_OK_AND_ASSIGN(MapFilter filter, + QuadraticConstraintFilterFromProto(model, proto)); + EXPECT_FALSE(filter.skip_zero_values); + EXPECT_THAT(filter.filtered_keys, Optional(IsEmpty())); +} + +TEST(QuadraticConstraintFilterFromProtoTest, BadFilterIdFails) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + model.AddQuadraticConstraint(x * x <= 0.0); + SparseVectorFilterProto proto; + proto.set_filter_by_ids(true); + proto.add_filtered_ids(3); + EXPECT_THAT(QuadraticConstraintFilterFromProto(model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("3"))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/matchers_test.cc b/ortools/math_opt/cpp/matchers_test.cc new file mode 100644 index 00000000000..cfef8a8e2d5 --- /dev/null +++ b/ortools/math_opt/cpp/matchers_test.cc @@ -0,0 +1,2032 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/cpp/matchers.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_replace.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/solution.h" +#include "ortools/math_opt/cpp/solve_result.h" + +namespace operations_research::math_opt { +namespace { +constexpr double kInf = std::numeric_limits::infinity(); + +using ::testing::HasSubstr; +using ::testing::Not; +using ::testing::StrEq; +constexpr double kNaN = std::numeric_limits::quiet_NaN(); + +TEST(ObjectiveBoundsNear, Is) { + const ObjectiveBounds bounds = {.primal_bound = 1.0, .dual_bound = 2.0}; + EXPECT_THAT(bounds, ObjectiveBoundsNear(bounds)); +} + +TEST(ObjectiveBoundsNear, IsNotPrimalDifferent) { + const ObjectiveBounds expected = {.primal_bound = 1.0, .dual_bound = 2.0}; + const ObjectiveBounds actual = {.primal_bound = 1.1, .dual_bound = 2.0}; + EXPECT_THAT(actual, Not(ObjectiveBoundsNear(expected))); + EXPECT_THAT(actual, ObjectiveBoundsNear(expected, /*tolerance*/ 0.2)); +} + +TEST(ObjectiveBoundsNear, IsNotDualDifferent) { + const ObjectiveBounds expected = {.primal_bound = 1.0, .dual_bound = 2.0}; + const ObjectiveBounds actual = {.primal_bound = 1.0, .dual_bound = 2.1}; + EXPECT_THAT(actual, Not(ObjectiveBoundsNear(expected))); + EXPECT_THAT(actual, ObjectiveBoundsNear(expected, /*tolerance*/ 0.2)); +} + +TEST(ProblemStatusIs, IsEqual) { + const ProblemStatus status = { + .primal_status = FeasibilityStatus::kUndetermined, + .dual_status = FeasibilityStatus::kInfeasible, + .primal_or_dual_infeasible = false}; + EXPECT_THAT(status, ProblemStatusIs(status)); +} + +TEST(ProblemStatusIs, IsNotPrimalDifferent) { + const ProblemStatus expected = { + .primal_status = FeasibilityStatus::kUndetermined, + .dual_status = FeasibilityStatus::kInfeasible, + .primal_or_dual_infeasible = false}; + const ProblemStatus actual = {.primal_status = FeasibilityStatus::kFeasible, + .dual_status = FeasibilityStatus::kInfeasible, + .primal_or_dual_infeasible = false}; + EXPECT_THAT(actual, Not(ProblemStatusIs(expected))); +} + +TEST(ProblemStatusIs, IsNotDualDifferent) { + const ProblemStatus expected = { + .primal_status = FeasibilityStatus::kFeasible, + .dual_status = FeasibilityStatus::kUndetermined, + .primal_or_dual_infeasible = false}; + const ProblemStatus actual = {.primal_status = FeasibilityStatus::kFeasible, + .dual_status = FeasibilityStatus::kInfeasible, + .primal_or_dual_infeasible = false}; + EXPECT_THAT(actual, Not(ProblemStatusIs(expected))); +} + +TEST(ProblemStatusIs, IsNotPrimalOrDualInfeasibleDifferent) { + const ProblemStatus expected = { + .primal_status = FeasibilityStatus::kUndetermined, + .dual_status = FeasibilityStatus::kUndetermined, + .primal_or_dual_infeasible = true}; + const ProblemStatus actual = { + .primal_status = FeasibilityStatus::kUndetermined, + .dual_status = FeasibilityStatus::kUndetermined, + .primal_or_dual_infeasible = false}; + EXPECT_THAT(actual, Not(ProblemStatusIs(expected))); +} + +TEST(ApproximateMapMatcherTest, VariableIsNear) { + Model model; + const Variable w = model.AddBinaryVariable("w"); + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + const VariableMap actual = {{x, 2.0}, {y, 4.1}, {z, -2.5}}; + EXPECT_THAT(actual, IsNear(actual)); + EXPECT_THAT(actual, IsNear({{x, 2 + 1e-8}, {y, 4.1}, {z, -2.5}})); + EXPECT_THAT(actual, Not(IsNear({{x, 2 + 1e-3}, {y, 4.1}, {z, -2.5}}))); + EXPECT_THAT(actual, Not(IsNear({{w, 1}, {z, -2.5}}))); + EXPECT_THAT(actual, Not(IsNear({{z, -2.5}}))); +} + +TEST(ApproximateMapMatcherTest, VariableIsNearlySubsetOf) { + Model model; + const Variable w = model.AddBinaryVariable("w"); + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + const VariableMap actual = {{x, 2.0}, {y, 4.1}, {z, -2.5}}; + EXPECT_THAT(actual, IsNearlySubsetOf(actual)); + EXPECT_THAT(actual, IsNearlySubsetOf({{y, 4.1}, {z, -2.5}})); + EXPECT_THAT(actual, Not(IsNearlySubsetOf({{w, 1}, {y, 4.1}, {z, -2.5}}))); + EXPECT_THAT(actual, Not(IsNearlySubsetOf({{y, 4.4}, {z, -2.5}}))); +} + +TEST(ApproximateMapMatcherTest, QuadraticConstraintIsNearAndIsNearlySubsetOf) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const QuadraticConstraint c = model.AddQuadraticConstraint(x * x <= 0, "c"); + const QuadraticConstraint d = model.AddQuadraticConstraint(x * x <= 0, "d"); + const QuadraticConstraint e = model.AddQuadraticConstraint(x * x <= 0, "e"); + + const absl::flat_hash_map actual = {{c, 2}, + {e, 5}}; + EXPECT_THAT(actual, IsNearlySubsetOf(actual)); + EXPECT_THAT(actual, IsNear(actual)); + EXPECT_THAT(actual, IsNear({{c, 2 + 1e-8}, {e, 5}})); + EXPECT_THAT(actual, Not(IsNear({{e, 5}}))); + EXPECT_THAT(actual, Not(IsNear({{c, 2 + 1e-2}, {e, 5}}))); + EXPECT_THAT(actual, Not(IsNear({{d, 5}}))); + EXPECT_THAT(actual, IsNearlySubsetOf({{e, 5}})); +} + +TEST(ApproximateMapMatcherTest, LinearConstraintIsNearAndIsNearlySubsetOf) { + Model model; + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + const LinearConstraint e = model.AddLinearConstraint("e"); + + const LinearConstraintMap actual = {{c, 2}, {e, 5}}; + EXPECT_THAT(actual, IsNearlySubsetOf(actual)); + EXPECT_THAT(actual, IsNear(actual)); + EXPECT_THAT(actual, IsNear({{c, 2 + 1e-8}, {e, 5}})); + EXPECT_THAT(actual, Not(IsNear({{e, 5}}))); + EXPECT_THAT(actual, Not(IsNear({{c, 2 + 1e-2}, {e, 5}}))); + EXPECT_THAT(actual, Not(IsNear({{d, 5}}))); + EXPECT_THAT(actual, IsNearlySubsetOf({{e, 5}})); +} + +TEST(LinearExpressionMatcherTest, IsIdentical) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + const LinearExpression actual({{x, 1}, {y, 3}}, 4); + EXPECT_THAT(actual, IsIdentical(LinearExpression({{x, 1}, {y, 3}}, 4))); + EXPECT_THAT(actual, Not(IsIdentical(LinearExpression({{x, 1}}, 4)))); + EXPECT_THAT(actual, Not(IsIdentical(LinearExpression({{x, 1}, {y, 3}}, 5)))); + EXPECT_THAT(actual, + Not(IsIdentical(LinearExpression({{x, 1}, {y, 3}, {z, 1}}, 4)))); + EXPECT_THAT(actual, Not(IsIdentical(LinearExpression( + {{x, std::nextafter(1, 2)}, {y, 3}}, 4)))); + + Model other_model; + const Variable other_x = Variable(other_model.storage(), x.typed_id()); + EXPECT_THAT(LinearExpression({{x, 1}}, 1), + Not(IsIdentical(LinearExpression({{other_x, 1}}, 1)))); + + // Same as actual, but with a structural zero term. + const LinearExpression other({{x, 1}, {y, 3}, {z, 0}}, 4); + EXPECT_THAT(other, Not(IsIdentical(actual))); + EXPECT_THAT(other, + IsIdentical(LinearExpression({{x, 1}, {y, 3}, {z, 0}}, 4))); +} + +TEST(LinearExpressionMatcherDeathTest, IsIdenticalWithNaNs) { + Model model; + const Variable x = model.AddBinaryVariable(); + + EXPECT_DEATH(IsIdentical(LinearExpression({}, kNaN)), "Illegal NaN"); + EXPECT_DEATH(IsIdentical(LinearExpression({{x, kNaN}}, 0)), "Illegal NaN"); +} + +TEST(LinearExpressionMatcherTest, IsIdenticalMatchedAgainstNaNs) { + Model model; + const Variable x = model.AddBinaryVariable(); + + EXPECT_THAT(LinearExpression(kNaN), Not(IsIdentical(LinearExpression(0)))); + EXPECT_THAT(LinearExpression({{x, kNaN}}, 0), + Not(IsIdentical(LinearExpression({{x, 1}}, 0)))); +} + +TEST(LinearExpressionMatcherTest, LinearExpressionIsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const LinearExpression actual({{x, 1.0}, {y, 3.0}}, 4.0); + EXPECT_THAT(actual, LinearExpressionIsNear( + LinearExpression({{x, 1.0}, {y, 3.0}}, 4.0))); + EXPECT_THAT(actual, Not(LinearExpressionIsNear( + LinearExpression({{x, 1.0}, {y, 3.0}}, 4.2), + /*tolerance=*/0.1))); + EXPECT_THAT(actual, Not(LinearExpressionIsNear( + LinearExpression({{x, 1.2}, {y, 3.0}}, 4.0), + /*tolerance=*/0.1))); + EXPECT_THAT(actual, LinearExpressionIsNear( + LinearExpression({{x, 1.0}, {y, 3.0}}, 4.2), + /*tolerance=*/0.3)); + EXPECT_THAT(actual, LinearExpressionIsNear( + LinearExpression({{x, 1.2}, {y, 3.0}}, 4.0), + /*tolerance=*/0.3)); +} + +TEST(BoundedLinearExpressionMatcherTest, ToleranceEachComponent) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const BoundedLinearExpression actual(x + 3.0 * y + 4.0, -2.0, 5.0); + constexpr double esp_error = 0.1; + const BoundedLinearExpression esp_lb_error(x + 3.0 * y + 4.0, + -2.0 + esp_error, 5.0); + const BoundedLinearExpression esp_ub_error(x + 3.0 * y + 4.0, -2.0, + 5.0 + esp_error); + const BoundedLinearExpression esp_term_error(x + (3.0 + esp_error) * y + 4.0, + -2.0, 5.0); + const BoundedLinearExpression esp_offset_error(x + 3.0 * y + 4.0, + -2.0 + esp_error, 5.0); + EXPECT_THAT(actual, Not(IsNearlyEquivalent(esp_lb_error, esp_error / 2))); + EXPECT_THAT(actual, Not(IsNearlyEquivalent(esp_lb_error, esp_error / 2))); + EXPECT_THAT(actual, Not(IsNearlyEquivalent(esp_lb_error, esp_error / 2))); + EXPECT_THAT(actual, Not(IsNearlyEquivalent(esp_lb_error, esp_error / 2))); + + EXPECT_THAT(actual, IsNearlyEquivalent(esp_lb_error, esp_error * 2)); + EXPECT_THAT(actual, IsNearlyEquivalent(esp_lb_error, esp_error * 2)); + EXPECT_THAT(actual, IsNearlyEquivalent(esp_lb_error, esp_error * 2)); + EXPECT_THAT(actual, IsNearlyEquivalent(esp_lb_error, esp_error * 2)); +} + +TEST(BoundedLinearExpressionMatcherTest, IsNearAddAndScale) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const BoundedLinearExpression actual(x + 3.0 * y + 4.0, -2.0, 5.0); + const BoundedLinearExpression negated(-x + -3.0 * y - 4.0, -5.0, 2.0); + const BoundedLinearExpression add_scale(x + 3.0 * y + 5.0, -1.0, 6.0); + const BoundedLinearExpression negated_add_scale(-x + -3.0 * y - 3.0, -4.0, + 3.0); + + EXPECT_THAT(actual, IsNearlyEquivalent(actual, 0.0)); + EXPECT_THAT(actual, IsNearlyEquivalent(negated, 1e-10)); + EXPECT_THAT(actual, IsNearlyEquivalent(add_scale, 1e-10)); + EXPECT_THAT(actual, IsNearlyEquivalent(negated_add_scale, 1e-10)); +} + +TEST(QuadraticExpressionMatcherTest, IsIdentical) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + const QuadraticExpression actual({{x, x, 1}, {x, y, 3}}, {{z, 4}}, 5); + EXPECT_THAT(actual, IsIdentical(QuadraticExpression({{x, x, 1}, {x, y, 3}}, + {{z, 4}}, 5))); + EXPECT_THAT(actual, + Not(IsIdentical(QuadraticExpression({{x, x, 1}}, {{z, 4}}, 5)))); + EXPECT_THAT( + actual, + Not(IsIdentical(QuadraticExpression({{x, x, 1}, {x, y, 3}}, {}, 5)))); + EXPECT_THAT(actual, Not(IsIdentical(QuadraticExpression( + {{x, x, 1}, {x, y, 3}}, {{z, 4}}, 6)))); + EXPECT_THAT(actual, Not(IsIdentical(QuadraticExpression( + {{x, x, 1}, {x, y, 3}, {x, z, 6}}, {{z, 4}}, 5)))); + EXPECT_THAT(actual, Not(IsIdentical(QuadraticExpression( + {{x, x, 1}, {x, y, 3}}, {{z, 4}, {x, 6}}, 5)))); + EXPECT_THAT(actual, + Not(IsIdentical(QuadraticExpression( + {{x, x, std::nextafter(1, 2)}, {x, y, 3}}, {{z, 4}}, 5)))); + + Model other_model; + const Variable other_x = Variable(other_model.storage(), x.typed_id()); + EXPECT_THAT( + QuadraticExpression({{x, x, 1}}, {}, 1), + Not(IsIdentical(QuadraticExpression({{other_x, other_x, 1}}, {}, 1)))); + + // Same as actual, but with structural zero terms. + const QuadraticExpression other({{x, x, 1}, {x, y, 3}, {x, z, 0}}, + {{z, 4}, {y, 0}}, 5); + EXPECT_THAT(other, Not(IsIdentical(actual))); + EXPECT_THAT(other, + IsIdentical(QuadraticExpression({{x, x, 1}, {x, y, 3}, {x, z, 0}}, + {{z, 4}, {y, 0}}, 5))); +} + +TEST(QuadraticExpressionMatcherDeathTest, IsIdenticalWithNaNs) { + Model model; + const Variable x = model.AddBinaryVariable(); + + EXPECT_DEATH(IsIdentical(QuadraticExpression({}, {}, kNaN)), "Illegal NaN"); + EXPECT_DEATH(IsIdentical(QuadraticExpression({}, {{x, kNaN}}, 0)), + "Illegal NaN"); + EXPECT_DEATH(IsIdentical(QuadraticExpression({{x, x, kNaN}}, {}, 0)), + "Illegal NaN"); +} + +TEST(QuadraticExpressionMatcherTest, IsIdenticalMatchedAgainstNaNs) { + Model model; + const Variable x = model.AddBinaryVariable(); + + EXPECT_THAT(QuadraticExpression(kNaN), + Not(IsIdentical(QuadraticExpression(0)))); + EXPECT_THAT(QuadraticExpression({}, {{x, kNaN}}, 0), + Not(IsIdentical(QuadraticExpression({}, {{x, 1}}, 0)))); + EXPECT_THAT(QuadraticExpression({{x, x, kNaN}}, {}, 0), + Not(IsIdentical(QuadraticExpression({{x, x, 1}}, {}, 0)))); +} + +TEST(PrimalSolutionMatcherTest, IsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + const PrimalSolution expected = { + .variable_values = {{x, 2.0}, {y, 4.1}, {z, -2.5}}, + .objective_value = 2.0, + .feasibility_status = SolutionStatus::kFeasible}; + PrimalSolution expected_no_status = expected; + expected_no_status.feasibility_status = SolutionStatus::kUndetermined; + + { + const PrimalSolution actual = expected; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + PrimalSolution actual = expected; + actual.variable_values[x] += 1e-8; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + PrimalSolution actual = expected; + actual.variable_values[x] += 4; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + PrimalSolution actual = expected; + actual.variable_values.erase(x); + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + PrimalSolution actual = expected; + actual.objective_value += 5.0; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + PrimalSolution actual = expected; + actual.feasibility_status = SolutionStatus::kInfeasible; + EXPECT_THAT(actual, Not(IsNear(expected))); + EXPECT_THAT(actual, Not(IsNear(expected_no_status))); + actual.feasibility_status = SolutionStatus::kUndetermined; + } +} + +TEST(DualSolutionMatcherTest, IsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + + const QuadraticConstraint e = model.AddQuadraticConstraint(x * x <= 0.0, "e"); + const QuadraticConstraint f = model.AddQuadraticConstraint(x * x <= 0.0, "f"); + + const DualSolution expected = { + .dual_values = {{c, 1.0}, {d, 3.1}}, + .quadratic_dual_values = {{e, 5.0}, {f, 6.1}}, + .reduced_costs = {{x, 2.0}, {y, 4.1}}, + .objective_value = 2.0, + .feasibility_status = SolutionStatus::kFeasible}; + DualSolution expected_no_obj = expected; + expected_no_obj.objective_value = std::nullopt; + DualSolution expected_no_status = expected; + expected_no_status.feasibility_status = SolutionStatus::kUndetermined; + + { + const DualSolution actual = expected; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + DualSolution actual = expected; + actual.reduced_costs[x] += 1e-8; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + DualSolution actual = expected; + actual.reduced_costs[x] += 4; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + DualSolution actual = expected; + actual.dual_values.erase(c); + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + DualSolution actual = expected; + actual.quadratic_dual_values.erase(e); + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + DualSolution actual = expected; + *actual.objective_value += 5.0; + EXPECT_THAT(actual, Not(IsNear(expected))); + EXPECT_THAT(actual, Not(IsNear(expected_no_obj))); + } + + { + DualSolution actual = expected; + actual.feasibility_status = SolutionStatus::kInfeasible; + EXPECT_THAT(actual, Not(IsNear(expected))); + EXPECT_THAT(actual, Not(IsNear(expected_no_status))); + } +} + +TEST(BasisTest, BasisIsOrCompatibleTest) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + + Basis b1 = {.constraint_status = {{c, BasisStatus::kBasic}, + {d, BasisStatus::kAtUpperBound}}, + .variable_status = {{x, BasisStatus::kAtLowerBound}, + {y, BasisStatus::kBasic}}, + .basic_dual_feasibility = SolutionStatus::kFeasible}; + + EXPECT_THAT(b1, BasisIs(b1)); + + { + Basis b2 = b1; + b2.constraint_status[d] = BasisStatus::kAtLowerBound; + EXPECT_THAT(b1, Not(BasisIs(b2))); + } + + { + Basis b3 = b1; + b3.variable_status[x] = BasisStatus::kBasic; + EXPECT_THAT(b1, Not(BasisIs(b3))); + } + + { + Basis b4 = b1; + b4.variable_status.clear(); + EXPECT_THAT(b1, Not(BasisIs(b4))); + } + + { + Basis b5 = b1; + b5.basic_dual_feasibility = SolutionStatus::kUndetermined; + EXPECT_THAT(b1, Not(BasisIs(b5))); + } +} + +TEST(SolutionTest, IsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + + const Solution expected = { + .primal_solution = + PrimalSolution{.variable_values = {{x, 2.0}, {y, 4.1}}, + .objective_value = 2.0, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 1.0}, {d, 3.1}}, + .reduced_costs = {{x, 2.0}, {y, 4.1}}, + .objective_value = 2.0, + .feasibility_status = SolutionStatus::kFeasible}, + .basis = Basis{.constraint_status = {{c, BasisStatus::kBasic}, + {d, BasisStatus::kAtUpperBound}}, + .variable_status = {{x, BasisStatus::kAtLowerBound}, + {y, BasisStatus::kBasic}}, + .basic_dual_feasibility = SolutionStatus::kFeasible}}; + + { + const Solution actual = expected; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + Solution actual = expected; + actual.primal_solution->objective_value += 5.0; + *actual.dual_solution->objective_value += 5.0; + actual.basis->variable_status[x] = BasisStatus::kBasic; + EXPECT_THAT(actual, Not(IsNear(expected))); + + const SolutionMatcherOptions check_nothing{ + .check_primal = false, .check_dual = false, .check_basis = false}; + EXPECT_THAT(actual, IsNear(expected, check_nothing)); + } + + { + Solution actual = expected; + actual.primal_solution->feasibility_status = SolutionStatus::kUndetermined; + actual.dual_solution->feasibility_status = SolutionStatus::kUndetermined; + actual.basis->basic_dual_feasibility = SolutionStatus::kUndetermined; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + const SolutionMatcherOptions skip_primal{.check_primal = false}; + Solution actual = expected; + actual.primal_solution->objective_value += 5.0; + EXPECT_THAT(actual, IsNear(expected, skip_primal)); + } + + { + const SolutionMatcherOptions skip_dual{.check_dual = false}; + Solution actual = expected; + *actual.dual_solution->objective_value += 5.0; + EXPECT_THAT(actual, IsNear(expected, skip_dual)); + } + + { + const SolutionMatcherOptions skip_basis{.check_basis = false}; + Solution actual = expected; + actual.basis->variable_status[x] = BasisStatus::kBasic; + EXPECT_THAT(actual, IsNear(expected, skip_basis)); + } +} + +TEST(PrimalRayMatcherTest, PrimalRayIsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + PrimalRay actual = {.variable_values = {{x, 2.0}, {y, 4.1}, {z, -2.5}}}; + EXPECT_THAT(actual, IsNear(actual)); + { + PrimalRay expected = { + .variable_values = {{x, 2.0 + 1e-8}, {y, 4.1}, {z, -2.5}}}; + EXPECT_THAT(actual, IsNear(expected)); + } + { + PrimalRay expected = {.variable_values = {{x, 4.0}, {y, 8.2}, {z, -5.0}}}; + EXPECT_THAT(actual, IsNear(expected)); + } + { + PrimalRay expected = {.variable_values = {{x, 1.0}, {y, 2.05}, {z, -1.25}}}; + EXPECT_THAT(actual, IsNear(expected)); + } + { + PrimalRay expected = { + .variable_values = {{x, 4.0}, {y, 8.2 + 1e-8}, {z, -5.0}}}; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + PrimalRay expected = {.variable_values = {{x, 2.1}, {y, 4.1}, {z, -2.5}}}; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + { + PrimalRay expected = {.variable_values = {{x, 4.0}, {y, 8.5}, {z, -5.0}}}; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + { + PrimalRay expected = {.variable_values = {{x, 0}, {y, 0}, {z, 0}}}; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + { + PrimalRay expected; + EXPECT_THAT(actual, Not(IsNear(expected))); + } +} + +TEST(DualRayMatcherTest, DualRayIsNear) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + + DualRay actual = {.dual_values = {{c, 1.0}, {d, 3.1}}, + .reduced_costs = {{x, 2.0}, {y, 4.1}}}; + + { + DualRay expected = actual; + EXPECT_THAT(actual, IsNear(expected)); + } + { + DualRay expected = {.dual_values = {{c, 2.0}, {d, 6.2}}, + .reduced_costs = {{x, 4.0}, {y, 8.2}}}; + EXPECT_THAT(actual, IsNear(expected)); + } + + { + DualRay expected = {.dual_values = {{c, 3.0}, {d, 9.3}}, + .reduced_costs = {{x, 4.0}, {y, 8.2}}}; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + DualRay expected = actual; + expected.reduced_costs[x] += 3.0; + EXPECT_THAT(actual, Not(IsNear(expected))); + } + + { + DualRay expected = actual; + expected.dual_values.erase(c); + EXPECT_THAT(actual, Not(IsNear(expected))); + } + { + DualRay expected; + EXPECT_THAT(actual, Not(IsNear(expected))); + } +} + +TEST(LimitIs, Is) { + const Termination actual_feasible = + Termination::Feasible(/*is_maximize=*/false, Limit::kTime, + /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0, "full string"); + EXPECT_THAT(actual_feasible, LimitIs(Limit::kTime, HasSubstr("full"))); + EXPECT_THAT(actual_feasible, LimitIs(Limit::kTime)); + const Termination actual_no_solution = Termination::NoSolutionFound( + /*is_maximize=*/false, Limit::kTime, + /*optional_dual_objective=*/10.0, "full string"); + EXPECT_THAT(actual_no_solution, LimitIs(Limit::kTime, HasSubstr("full"))); + EXPECT_THAT(actual_no_solution, LimitIs(Limit::kTime)); +} + +TEST(LimitIs, IsNotLimit) { + const Termination actual_feasible = + Termination::Feasible(/*is_maximize=*/false, Limit::kIteration, + /*finite_primal_objective=*/20.0); + EXPECT_THAT(actual_feasible, Not(LimitIs(Limit::kTime))); + const Termination actual_no_solution = + Termination::NoSolutionFound(/*is_maximize=*/false, Limit::kIteration); + EXPECT_THAT(actual_no_solution, Not(LimitIs(Limit::kTime))); +} + +TEST(LimitIs, IsNotDetail) { + const Termination actual_feasible = + Termination::Feasible(/*is_maximize=*/false, Limit::kIteration, + /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0, "string"); + EXPECT_THAT(actual_feasible, + Not(LimitIs(Limit::kIteration, HasSubstr("full")))); +} + +TEST(LimitIs, IsNotReason) { + const Termination actual = Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible); + EXPECT_THAT(actual, Not(LimitIs(Limit::kTime))); +} + +TEST(TerminationIsIgnoreDetailTest, NoLimitEqual) { + const Termination actual = Termination::Optimal(/*objective_value=*/10.0); + EXPECT_THAT(actual, TerminationIsIgnoreDetail(actual)); +} + +TEST(TerminationIsIgnoreDetailTest, BadLimitNotEqual) { + const Termination actual = Termination::Optimal(/*objective_value=*/10.0); + Termination bad_expected = actual; + bad_expected.limit = Limit::kTime; + EXPECT_THAT(actual, Not(TerminationIsIgnoreDetail(bad_expected))); +} + +TEST(TerminationIsIgnoreDetailTest, DetailIgnored) { + const Termination actual = + Termination::Optimal(/*objective_value=*/10.0, "cat"); + const Termination expected = + Termination::Optimal(/*objective_value=*/10.0, "dog"); + EXPECT_THAT(actual, TerminationIsIgnoreDetail(expected)); +} + +TEST(TerminationIsIgnoreDetailTest, ExpectedHasLimit) { + const Termination actual = + Termination::Feasible(/*is_maximize=*/false, Limit::kTime, + /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0); + EXPECT_THAT(actual, TerminationIsIgnoreDetail(actual)); +} + +TEST(TerminationIsIgnoreDetailTest, ExpectedHasWrongLimit) { + const Termination actual = + Termination::Feasible(/*is_maximize=*/false, Limit::kTime, + /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0); + EXPECT_THAT(actual, Not(TerminationIsIgnoreDetail(Termination::Feasible( + /*is_maximize=*/false, Limit::kIteration, + /*finite_primal_objective=*/20.0)))); +} + +TEST(TerminationIsIgnoreDetailTest, ExpectedHasLimitDetailIgnored) { + const Termination actual = + Termination::Feasible(/*is_maximize=*/false, Limit::kTime, + /*finite_primal_objective=*/20.0); + EXPECT_THAT(actual, TerminationIsIgnoreDetail(Termination::Feasible( + /*is_maximize=*/false, Limit::kTime, + /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0, "mouse"))); +} + +TEST(ReasonIs, Is) { + Termination actual = Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible); + EXPECT_THAT(actual, ReasonIs(TerminationReason::kInfeasible)); +} + +TEST(ReasonIs, IsNot) { + Termination actual = Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible); + EXPECT_THAT(actual, Not(ReasonIs(TerminationReason::kUnbounded))); +} + +TEST(ReasonIsOptimal, IsOptimal) { + Termination actual = Termination::Optimal(/*objective_value=*/10.0); + EXPECT_THAT(actual, ReasonIsOptimal()); +} + +TEST(ReasonIsOptimal, NotOptimal) { + Termination actual = Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible); + EXPECT_THAT(actual, Not(ReasonIsOptimal())); +} + +TEST(TerminationIsOptimal, NotOptimalReason) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + Termination actual = + Termination::Optimal(primal_objective_value, dual_objective_value); + actual.reason = TerminationReason::kInfeasible; + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); +} + +TEST(TerminationIsOptimal, NotPrimalFeasible) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + Termination actual = + Termination::Optimal(primal_objective_value, dual_objective_value); + actual.problem_status.primal_status = FeasibilityStatus::kInfeasible; + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); +} + +TEST(TerminationIsOptimal, NotDualFeasible) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + Termination actual = + Termination::Optimal(primal_objective_value, dual_objective_value); + actual.problem_status.dual_status = FeasibilityStatus::kInfeasible; + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); +} + +TEST(TerminationIsOptimal, NotFalsePrimalOrDualInfeasible) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + Termination actual = + Termination::Optimal(primal_objective_value, dual_objective_value); + actual.problem_status.primal_or_dual_infeasible = true; + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); +} + +TEST(TerminationIsOptimal, WrongPrimalBound) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + const Termination actual = Termination::Optimal( + /*primal_objective_value=*/10.1, dual_objective_value); + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); + EXPECT_THAT(actual, + TerminationIsOptimal(primal_objective_value, dual_objective_value, + /*tolerance=*/0.2)); +} + +TEST(TerminationIsOptimal, WrongDualBound) { + const double primal_objective_value = 10.0; + const double dual_objective_value = 20.0; + const Termination actual = Termination::Optimal( + primal_objective_value, /*dual_objective_value=*/20.1); + EXPECT_THAT(actual, Not(TerminationIsOptimal(primal_objective_value, + dual_objective_value))); + EXPECT_THAT(actual, + TerminationIsOptimal(primal_objective_value, dual_objective_value, + /*tolerance=*/0.2)); +} + +TEST(TerminationIsOptimal, Optimal) { + const double primal_objective_value = 10.0; + const Termination actual = + Termination::Optimal(primal_objective_value, + /*dual_objective_value=*/primal_objective_value + + kMatcherDefaultTolerance / 2.0); + EXPECT_THAT(actual, TerminationIsOptimal(primal_objective_value)); +} + +TEST(IsOptimalTest, IsOptimal) { + // Assuming maximization. + // TODO(b/309658404): consider changing to finite dual bound. + SolveResult actual{Termination::Optimal(/*primal_objective_value=*/20.0, + /*dual_objective_value=*/kInf)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .objective_value = 10.0, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, IsOptimal()); +} + +TEST(IsOptimalTest, NotOptimal) { + SolveResult actual{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + EXPECT_THAT(actual, Not(IsOptimal())); +} + +TEST(IsOptimalTest, CheckObjective) { + // Assuming maximization. + // TODO(b/309658404): consider changing to finite dual bound. + SolveResult actual{Termination::Optimal(/*primal_objective_value=*/50.0, + /*dual_objective_value=*/kInf)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .objective_value = 42.0, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, IsOptimal(42.0)); + EXPECT_THAT(actual, Not(IsOptimal(35))); +} + +TEST(IsOptimalTest, CheckObjectiveMissingSolution) { + // Assuming maximization. + // TODO(b/309658404): consider changing to finite dual bound. + SolveResult actual{Termination::Optimal(/*primal_objective_value=*/50.0, + /*dual_objective_value=*/kInf)}; + EXPECT_THAT(actual, Not(IsOptimal(42.0))); +} + +TEST(IsOptimalTest, CheckObjectiveWrongObjectiveForSolution) { + // Assuming maximization. + // TODO(b/309658404): consider changing to finite dual bound. + SolveResult actual{Termination::Optimal(/*primal_objective_value=*/42.0, + /*dual_objective_value=*/kInf)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .objective_value = 35.0, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, Not(IsOptimal(42.0))); +} + +TEST(TerminatesWithTest, Expected) { + SolveResult actual{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + EXPECT_THAT(actual, TerminatesWith(TerminationReason::kInfeasible)); +} + +TEST(TerminatesWithTest, WrongReason) { + SolveResult actual{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + EXPECT_THAT(actual, Not(TerminatesWith(TerminationReason::kUnbounded))); +} + +TEST(TerminatesWithOneOfTest, ExpectedInList) { + SolveResult actual{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + EXPECT_THAT(actual, TerminatesWithOneOf({TerminationReason::kUnbounded, + TerminationReason::kInfeasible})); +} + +TEST(TerminatesWithOneOfTest, ExpectedNotInList) { + SolveResult actual{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + EXPECT_THAT(actual, Not(TerminatesWithOneOf({TerminationReason::kUnbounded, + TerminationReason::kOptimal}))); +} + +TEST(TerminatesWithLimitTest, Expected) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kTime, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, TerminatesWithLimit(Limit::kTime)); + EXPECT_THAT(feasible, Not(TerminatesWithLimit(Limit::kIteration))); + + SolveResult no_solution_found{ + Termination::NoSolutionFound(/*is_maximize=*/false, Limit::kTime)}; + EXPECT_THAT(no_solution_found, TerminatesWithLimit(Limit::kTime)); + EXPECT_THAT(no_solution_found, Not(TerminatesWithLimit(Limit::kIteration))); +} + +TEST(TerminatesWithLimitTest, AllowUndetermined) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kUndetermined, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, TerminatesWithLimit(Limit::kTime, + /*allow_limit_undetermined=*/true)); + EXPECT_THAT(feasible, Not(TerminatesWithLimit(Limit::kTime))); + + SolveResult no_solution_found{Termination::NoSolutionFound( + /*is_maximize=*/false, Limit::kUndetermined)}; + EXPECT_THAT(no_solution_found, + TerminatesWithLimit(Limit::kTime, + /*allow_limit_undetermined=*/true)); + EXPECT_THAT(no_solution_found, Not(TerminatesWithLimit(Limit::kTime))); +} + +TEST(TerminatesWithReasonFeasibleTest, Expected) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kTime, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, TerminatesWithReasonFeasible(Limit::kTime)); + EXPECT_THAT(feasible, Not(TerminatesWithReasonFeasible(Limit::kIteration))); + + SolveResult no_solution_found{ + Termination::NoSolutionFound(/*is_maximize=*/false, Limit::kTime)}; + EXPECT_THAT(no_solution_found, + Not(TerminatesWithReasonFeasible(Limit::kTime))); +} + +TEST(TerminatesWithReasonFeasibleTest, AllowUndetermined) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kUndetermined, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, + TerminatesWithReasonFeasible(Limit::kTime, + /*allow_limit_undetermined=*/true)); + EXPECT_THAT(feasible, Not(TerminatesWithReasonFeasible(Limit::kTime))); + + SolveResult no_solution_found{Termination::NoSolutionFound( + /*is_maximize=*/false, Limit::kUndetermined)}; + EXPECT_THAT(no_solution_found, Not(TerminatesWithReasonFeasible( + Limit::kTime, + /*allow_limit_undetermined=*/true))); +} + +TEST(TerminatesWithReasonNoSolutionFoundTest, Expected) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kTime, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, Not(TerminatesWithReasonNoSolutionFound(Limit::kTime))); + + SolveResult no_solution_found{ + Termination::NoSolutionFound(/*is_maximize=*/false, Limit::kTime)}; + EXPECT_THAT(no_solution_found, + TerminatesWithReasonNoSolutionFound(Limit::kTime)); + EXPECT_THAT(no_solution_found, + Not(TerminatesWithReasonNoSolutionFound(Limit::kIteration))); +} + +TEST(TTerminatesWithReasonNoSolutionFoundTest, AllowUndetermined) { + SolveResult feasible{Termination::Feasible(/*is_maximize=*/false, + Limit::kUndetermined, + /*finite_primal_objective=*/20.0)}; + EXPECT_THAT(feasible, Not(TerminatesWithReasonNoSolutionFound( + Limit::kTime, + /*allow_limit_undetermined=*/true))); + + SolveResult no_solution_found{Termination::NoSolutionFound( + /*is_maximize=*/false, Limit::kUndetermined)}; + EXPECT_THAT(no_solution_found, TerminatesWithReasonNoSolutionFound( + Limit::kTime, + /*allow_limit_undetermined=*/true)); + EXPECT_THAT(no_solution_found, + Not(TerminatesWithReasonNoSolutionFound(Limit::kTime))); +} + +TEST(HasSolution, NoSolution) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + EXPECT_THAT(actual, Not(HasSolution(PrimalSolution{ + .variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}))); +} + +TEST(HasSolution, HasSolution) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 2.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 0.0}}, + .objective_value = 10, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, -1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kInfeasible}}); + EXPECT_THAT(actual, HasSolution(PrimalSolution{ + .variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible})); + EXPECT_THAT(actual, Not(HasSolution(PrimalSolution{ + .variable_values = {{x, 1.0}}, + .objective_value = 32, + .feasibility_status = SolutionStatus::kFeasible}))); +} + +TEST(HasDualSolution, NoSolution) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + DualSolution expected; + expected.dual_values[c] = 1; + expected.reduced_costs[x] = 3; + expected.objective_value = 5; + EXPECT_THAT(actual, Not(HasDualSolution(DualSolution{ + .dual_values = {{c, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}))); +} + +TEST(HasBestDualSolutionTest, HasSolution) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + actual.solutions.push_back( + Solution{.dual_solution = DualSolution{ + .dual_values = {{c, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back( + Solution{.dual_solution = DualSolution{ + .dual_values = {{c, 2.0}}, + .reduced_costs = {{x, 1}}, + .objective_value = 12, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, HasDualSolution(*actual.solutions[0].dual_solution)); + EXPECT_THAT(actual, HasDualSolution(*actual.solutions[1].dual_solution)); + { + DualSolution expected = *actual.solutions[0].dual_solution; + expected.feasibility_status = SolutionStatus::kInfeasible; + EXPECT_THAT(actual, Not(HasDualSolution(expected))); + } + + { + DualSolution expected = *actual.solutions[0].dual_solution; + expected.feasibility_status = SolutionStatus::kUndetermined; + EXPECT_THAT(actual, Not(HasDualSolution(expected))); + } +} + +TEST(IsOptimalWithSolution, IsOptimalCorrectlyCalled) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, Not(IsOptimalWithSolution(43, {{x, 1.0}}, + /*tolerance=*/0.1))); + EXPECT_THAT(actual, IsOptimalWithSolution(43, {{x, 1.0}}, + /*tolerance=*/10)); + EXPECT_THAT(actual, IsOptimalWithSolution(42, {{x, 1.0}})); +} + +TEST(IsOptimalWithSolution, HasSolutionCorrectlyCalled) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 1.0}, {y, 0.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 0.0}, {y, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, IsOptimalWithSolution(42, {{x, 1.0}, {y, 0.0}})); + EXPECT_THAT(actual, IsOptimalWithSolution(42, {{x, 0.0}, {y, 1.0}})); + EXPECT_THAT(actual, Not(IsOptimalWithSolution(42, {{x, 0.0}, {y, 2.0}}, + /*tolerance=*/0.1))); + EXPECT_THAT(actual, IsOptimalWithSolution(42, {{x, 0.0}, {y, 2.0}}, + /*tolerance=*/10)); + actual.solutions[1].primal_solution->feasibility_status = + SolutionStatus::kInfeasible; + EXPECT_THAT(actual, Not(IsOptimalWithSolution(42, {{x, 0.0}, {y, 1.0}}))); +} + +TEST(IsOptimalWithDualSolution, IsOptimalCorrectlyCalled) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, Not(IsOptimalWithDualSolution(43, {{c, 3.0}}, {{x, 5}}, + + /*tolerance=*/0.1))); + EXPECT_THAT(actual, IsOptimalWithDualSolution(43, {{c, 3.0}}, {{x, 5}}, + /*tolerance=*/10)); + EXPECT_THAT(actual, IsOptimalWithDualSolution(42, {{c, 3.0}}, {{x, 5}})); +} + +TEST(IsOptimalWithDualSolution, HasDualSolutionCorrectlyCalled) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 1.0}, {d, 0.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 0.0}, {d, 1.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, + IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, {{x, 5}})); + EXPECT_THAT(actual, + IsOptimalWithDualSolution(42, {{c, 0.0}, {d, 1.0}}, {{x, 5}})); + EXPECT_THAT(actual, + Not(IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, {{x, 6}}, + /*tolerance=*/0.1))); + EXPECT_THAT(actual, IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, + {{x, 6}}, /*tolerance=*/10)); +} + +TEST(IsOptimalWithDualSolutionQc, IsOptimalCorrectlyCalled) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + const QuadraticConstraint d = model.AddQuadraticConstraint(x * x <= 1, "c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 3.0}}, + .quadratic_dual_values = {{d, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, Not(IsOptimalWithDualSolution(43, {{c, 3.0}}, {{d, 3.0}}, + {{x, 5}}, + + /*tolerance=*/0.1))); + EXPECT_THAT(actual, + IsOptimalWithDualSolution(43, {{c, 3.0}}, {{d, 3.0}}, {{x, 5}}, + /*tolerance=*/10)); + EXPECT_THAT(actual, + IsOptimalWithDualSolution(42, {{c, 3.0}}, {{d, 3.0}}, {{x, 5}})); +} + +TEST(IsOptimalWithDualSolutionQc, HasDualSolutionCorrectlyCalled) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + const LinearConstraint d = model.AddLinearConstraint("d"); + const QuadraticConstraint e = model.AddQuadraticConstraint(x * x <= 1, "e"); + SolveResult actual{Termination::Optimal(/*objective_value=*/42.0)}; + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 1.0}, {d, 0.0}}, + .quadratic_dual_values = {{e, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + actual.solutions.push_back(Solution{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 1.0}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 0.0}, {d, 1.0}}, + .quadratic_dual_values = {{e, 3.0}}, + .reduced_costs = {{x, 5}}, + .objective_value = 42, + .feasibility_status = SolutionStatus::kFeasible}}); + EXPECT_THAT(actual, IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, + {{e, 3.0}}, {{x, 5}})); + EXPECT_THAT(actual, IsOptimalWithDualSolution(42, {{c, 0.0}, {d, 1.0}}, + {{e, 3.0}}, {{x, 5}})); + EXPECT_THAT(actual, Not(IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, + {{e, 4.0}}, {{x, 5}}, + /*tolerance=*/0.1))); + EXPECT_THAT(actual, + IsOptimalWithDualSolution(42, {{c, 1.0}, {d, 0.0}}, {{e, 4.0}}, + {{x, 5}}, /*tolerance=*/10)); +} + +TEST(HasPrimalRayTest, NoRay) { + Model model; + const Variable x = model.AddVariable("x"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + EXPECT_THAT(actual, Not(HasPrimalRay({{x, 1.0}}))); + PrimalRay expected = {.variable_values = {{x, 1.0}}}; + EXPECT_THAT(actual, Not(HasPrimalRay(expected))); +} + +TEST(HasPrimalRayTest, HasRay) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + PrimalRay& first_ray = actual.primal_rays.emplace_back(); + first_ray.variable_values = {{x, 1}, {y, 0}}; + PrimalRay& second_ray = actual.primal_rays.emplace_back(); + second_ray.variable_values = {{x, 1}, {y, 2}}; + EXPECT_THAT(actual, HasPrimalRay({{x, 1}, {y, 0}})); + EXPECT_THAT(actual, HasPrimalRay({{x, 2}, {y, 0}})); + EXPECT_THAT(actual, HasPrimalRay({{x, 1}, {y, 2}})); + EXPECT_THAT(actual, HasPrimalRay(actual.primal_rays[0])); + EXPECT_THAT(actual, HasPrimalRay(actual.primal_rays[1])); + EXPECT_THAT(actual, Not(HasPrimalRay({{x, 0}, {y, 1}}))); + PrimalRay bad_ray; + bad_ray.variable_values = {{x, 0}, {y, 1}}; + EXPECT_THAT(actual, Not(HasPrimalRay(bad_ray))); +} + +TEST(HasDualRayTest, NoRay) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + DualRay expected = {.dual_values = {{c, 2}}, .reduced_costs = {{x, 1}}}; + EXPECT_THAT(actual, Not(HasDualRay(expected))); +} + +TEST(HasDualRayTest, HasRay) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult actual{Termination::Optimal(/*objective_value=*/10.0)}; + DualRay& first_ray = actual.dual_rays.emplace_back(); + first_ray.dual_values[c] = 1; + first_ray.reduced_costs[x] = 2; + + DualRay& second_ray = actual.dual_rays.emplace_back(); + second_ray.dual_values[c] = 3; + second_ray.reduced_costs[x] = 1; + + DualRay bad_ray; + second_ray.dual_values[c] = -3; + second_ray.reduced_costs[x] = -3; + + EXPECT_THAT(actual, HasDualRay(actual.dual_rays[0])); + EXPECT_THAT(actual, HasDualRay(actual.dual_rays[1])); + EXPECT_THAT(actual, Not(HasDualRay(bad_ray))); +} + +TEST(ResultsConsistentTest, SimpleOptimal) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + SolveResult expected{Termination::Optimal(/*objective_value=*/10.0)}; + expected.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 1}, {y, 0}}, .objective_value = 3}}); + + { + SolveResult actual = expected; + EXPECT_THAT(actual, IsConsistentWith(expected)); + } + + { + SolveResult small_error = expected; + small_error.solutions[0].primal_solution->variable_values[x] += 1e-7; + EXPECT_THAT(small_error, IsConsistentWith(expected, {.tolerance = 1e-6})); + EXPECT_THAT(small_error, + Not(IsConsistentWith(expected, {.tolerance = 1e-8}))); + } + + { + SolveResult extra_solution = expected; + PrimalSolution solution2; + solution2.variable_values = {{x, 0}, {y, 1}}; + solution2.objective_value = 2; + extra_solution.solutions.push_back(Solution{.primal_solution = solution2}); + EXPECT_THAT(extra_solution, + IsConsistentWith(expected, {.first_solution_only = true})); + EXPECT_THAT(extra_solution, Not(IsConsistentWith( + expected, {.first_solution_only = false}))); + } +} + +TEST(ResultsConsistentTest, MultipleSolutions) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + + SolveResult expected{Termination::Optimal(/*objective_value=*/10.0)}; + expected.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 1}, {y, 0}}, .objective_value = 3}}); + expected.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 0}, {y, 1}}, .objective_value = 2}}); + expected.solutions.push_back( + Solution{.primal_solution = PrimalSolution{ + .variable_values = {{x, 0}, {y, 0}}, .objective_value = 0}}); + { + SolveResult actual = expected; + EXPECT_THAT(actual, + IsConsistentWith(expected, {.first_solution_only = false})); + EXPECT_THAT(actual, + IsConsistentWith(expected, {.first_solution_only = true})); + } + + { + SolveResult alternate{Termination::Optimal(/*objective_value=*/10.0)}; + alternate.solutions.push_back(Solution{ + .primal_solution = PrimalSolution{.variable_values = {{x, 1}, {y, 0}}, + .objective_value = 3}}); + alternate.solutions.push_back(Solution{ + .primal_solution = PrimalSolution{.variable_values = {{x, 1}, {y, 1}}, + .objective_value = -1}}); + EXPECT_THAT(alternate, Not(IsConsistentWith( + expected, {.first_solution_only = false}))); + EXPECT_THAT(alternate, + IsConsistentWith(expected, {.first_solution_only = true})); + } +} + +TEST(ResultsConsistentTest, DualSolutionAndBasis) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + const Variable y = model.AddContinuousVariable(0, 1, "y"); + const LinearConstraint c = model.AddLinearConstraint("c"); + + SolveResult expected{Termination::Optimal(/*objective_value=*/10.0)}; + expected.solutions.push_back(Solution{ + .primal_solution = PrimalSolution{.variable_values = {{x, 1}, {y, 0}}, + .objective_value = 3}, + .dual_solution = DualSolution{.dual_values = {{c, 1.0}}, + .reduced_costs = {{x, 0.0}, {y, 1.0}}, + .objective_value = 3}, + .basis = Basis{.constraint_status = {{c, BasisStatus::kBasic}}, + .variable_status = {{x, BasisStatus::kAtUpperBound}, + {y, BasisStatus::kAtLowerBound}}}}); + + { + SolveResult actual = expected; + EXPECT_THAT(actual, IsConsistentWith(expected)); + EXPECT_THAT(actual, IsConsistentWith(expected, {.check_dual = false})); + EXPECT_THAT(actual, IsConsistentWith(expected, {.check_basis = true})); + } + + { + SolveResult dual_missing = expected; + dual_missing.solutions[0].dual_solution.reset(); + EXPECT_THAT(dual_missing, Not(IsConsistentWith(expected))); + EXPECT_THAT(dual_missing, + IsConsistentWith(expected, {.check_dual = false})); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith(dual_missing))); + EXPECT_THAT(expected, + IsConsistentWith(dual_missing, {.check_dual = false})); + } + + { + SolveResult basis_missing = expected; + basis_missing.solutions[0].basis.reset(); + EXPECT_THAT(basis_missing, + Not(IsConsistentWith(expected, {.check_basis = true}))); + EXPECT_THAT(basis_missing, IsConsistentWith(expected)); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, + Not(IsConsistentWith(basis_missing, {.check_basis = true}))); + EXPECT_THAT(expected, IsConsistentWith(basis_missing)); + } + + { + SolveResult extra_solution = expected; + extra_solution.solutions.push_back(Solution{ + .dual_solution = DualSolution{.dual_values = {{c, 1.0}}, + .reduced_costs = {{x, 1.0}, {y, 0.0}}, + .objective_value = 4}}); + EXPECT_THAT(extra_solution, IsConsistentWith(expected)); + EXPECT_THAT(extra_solution, Not(IsConsistentWith( + expected, {.first_solution_only = false}))); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, IsConsistentWith(extra_solution)); + EXPECT_THAT(expected, Not(IsConsistentWith( + extra_solution, {.first_solution_only = false}))); + } +} + +TEST(ResultsConsistentTest, Unbounded) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + const Variable y = model.AddContinuousVariable(0, 1, "y"); + + SolveResult expected{Termination::Unbounded(/*is_maximize=*/false)}; + expected.primal_rays.push_back({.variable_values = {{x, 1}, {y, 0}}}); + + { + SolveResult actual = expected; + EXPECT_THAT(actual, IsConsistentWith(expected)); + EXPECT_THAT(actual, IsConsistentWith(expected, {.check_rays = false})); + EXPECT_THAT(actual, + IsConsistentWith(expected, {.inf_or_unb_soft_match = false})); + } + + { + SolveResult dual_infeasible = expected; + dual_infeasible.termination.reason = + TerminationReason::kInfeasibleOrUnbounded; + EXPECT_THAT(dual_infeasible, IsConsistentWith(expected)); + EXPECT_THAT( + dual_infeasible, + Not(IsConsistentWith(expected, {.inf_or_unb_soft_match = false}))); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, IsConsistentWith(dual_infeasible)); + EXPECT_THAT(expected, + Not(IsConsistentWith(dual_infeasible, + {.inf_or_unb_soft_match = false}))); + } + + { + SolveResult with_primal = expected; + with_primal.solutions.push_back(Solution{ + .primal_solution = PrimalSolution{.variable_values = {{x, 1}, {y, 0}}, + .objective_value = 3.0}}); + EXPECT_THAT(with_primal, + Not(IsConsistentWith( + expected, {.check_solutions_if_inf_or_unbounded = true}))); + EXPECT_THAT(with_primal, IsConsistentWith(expected)); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith( + with_primal, + {.check_solutions_if_inf_or_unbounded = true}))); + EXPECT_THAT(expected, IsConsistentWith(with_primal)); + } + + { + SolveResult ray_missing = expected; + ray_missing.primal_rays.clear(); + EXPECT_THAT(ray_missing, Not(IsConsistentWith(expected))); + EXPECT_THAT(ray_missing, IsConsistentWith(expected, {.check_rays = false})); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith(ray_missing))); + EXPECT_THAT(expected, IsConsistentWith(ray_missing, {.check_rays = false})); + } + + { + SolveResult wrong_ray = expected; + wrong_ray.primal_rays[0].variable_values[y] = 1; + EXPECT_THAT(wrong_ray, Not(IsConsistentWith(expected))); + EXPECT_THAT(wrong_ray, IsConsistentWith(expected, {.check_rays = false})); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith(wrong_ray))); + EXPECT_THAT(expected, IsConsistentWith(wrong_ray, {.check_rays = false})); + } +} + +TEST(ResultsConsistentTest, UnboundedMultipleRays) { + Model model; + std::vector xs; + for (int i = 0; i < 4; ++i) { + xs.push_back(model.AddContinuousVariable(0, 1, absl::StrCat("x_", i))); + } + + SolveResult first{Termination::Unbounded(/*is_maximize=*/false)}; + SolveResult second{Termination::Unbounded(/*is_maximize=*/false)}; + + for (int i = 0; i < 4; ++i) { + PrimalRay ray; + for (int j = 0; j < 4; ++j) { + ray.variable_values[xs[j]] = j == i ? 1 : 0; + } + if (i < 3) { + first.primal_rays.push_back(ray); + } + if (i > 0) { + second.primal_rays.push_back(ray); + } + } + + EXPECT_THAT(first, IsConsistentWith(second)); + EXPECT_THAT(first, + Not(IsConsistentWith(second, {.first_solution_only = false}))); + // Reverse first and second, the result should match + EXPECT_THAT(second, IsConsistentWith(first)); + EXPECT_THAT(second, + Not(IsConsistentWith(first, {.first_solution_only = false}))); + + EXPECT_THAT(first, IsConsistentWith(first, {.first_solution_only = false})); + EXPECT_THAT(second, IsConsistentWith(second, {.first_solution_only = false})); +} + +TEST(ResultsConsistentTest, Infeasible) { + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + + SolveResult expected{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + expected.dual_rays.push_back( + {.dual_values = {{c, 1}}, .reduced_costs = {{x, 0}}}); + + { + SolveResult actual = expected; + EXPECT_THAT(actual, IsConsistentWith(expected)); + EXPECT_THAT(actual, IsConsistentWith(expected, {.check_rays = false})); + EXPECT_THAT(actual, + IsConsistentWith(expected, {.inf_or_unb_soft_match = false})); + } + + { + SolveResult dual_infeasible = expected; + dual_infeasible.termination.reason = + TerminationReason::kInfeasibleOrUnbounded; + EXPECT_THAT(dual_infeasible, IsConsistentWith(expected)); + EXPECT_THAT( + dual_infeasible, + Not(IsConsistentWith(expected, {.inf_or_unb_soft_match = false}))); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, IsConsistentWith(dual_infeasible)); + EXPECT_THAT(expected, + Not(IsConsistentWith(dual_infeasible, + {.inf_or_unb_soft_match = false}))); + } + + { + SolveResult with_dual = expected; + with_dual.solutions.push_back( + Solution{.dual_solution = DualSolution{.dual_values = {{c, 1}}, + .reduced_costs = {{x, 1}}, + .objective_value = 3.0}}); + EXPECT_THAT(with_dual, + Not(IsConsistentWith( + expected, {.check_solutions_if_inf_or_unbounded = true}))); + EXPECT_THAT(with_dual, IsConsistentWith(expected)); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, + Not(IsConsistentWith( + with_dual, {.check_solutions_if_inf_or_unbounded = true}))); + EXPECT_THAT(expected, IsConsistentWith(with_dual)); + } + + { + SolveResult ray_missing = expected; + ray_missing.dual_rays.clear(); + EXPECT_THAT(ray_missing, Not(IsConsistentWith(expected))); + EXPECT_THAT(ray_missing, IsConsistentWith(expected, {.check_rays = false})); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith(ray_missing))); + EXPECT_THAT(expected, IsConsistentWith(ray_missing, {.check_rays = false})); + } + + { + SolveResult wrong_ray = expected; + wrong_ray.dual_rays[0].reduced_costs[x] = 1; + EXPECT_THAT(wrong_ray, Not(IsConsistentWith(expected))); + EXPECT_THAT(wrong_ray, IsConsistentWith(expected, {.check_rays = false})); + // Flip the roles of actual and expected, should still hold. + EXPECT_THAT(expected, Not(IsConsistentWith(wrong_ray))); + EXPECT_THAT(expected, IsConsistentWith(wrong_ray, {.check_rays = false})); + } +} + +TEST(ResultsConsistentTest, InfeasibleMultipleRays) { + Model model; + std::vector xs; + for (int i = 0; i < 4; ++i) { + xs.push_back(model.AddContinuousVariable(0, 1, absl::StrCat("x_", i))); + } + + SolveResult first{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + SolveResult second{Termination::Infeasible( + /*is_maximize=*/false, + /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)}; + + for (int i = 0; i < 4; ++i) { + DualRay ray; + for (int j = 0; j < 4; ++j) { + ray.reduced_costs[xs[j]] = j == i ? 1 : 0; + } + if (i < 3) { + first.dual_rays.push_back(ray); + } + if (i > 0) { + second.dual_rays.push_back(ray); + } + } + + EXPECT_THAT(first, IsConsistentWith(second)); + EXPECT_THAT(first, + Not(IsConsistentWith(second, {.first_solution_only = false}))); + // Reverse first and second, the result should match + EXPECT_THAT(second, IsConsistentWith(first)); + EXPECT_THAT(second, + Not(IsConsistentWith(first, {.first_solution_only = false}))); + + EXPECT_THAT(first, IsConsistentWith(first, {.first_solution_only = false})); + EXPECT_THAT(second, IsConsistentWith(second, {.first_solution_only = false})); +} + +TEST(PrintToTest, SmallIdMap) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + VariableMap vars = {{x, 1.0}, {y, 0.0}}; + std::ostringstream oss; + PrintTo(vars, &oss); + EXPECT_EQ(oss.str(), "{{x, 1}, {y, 0}}"); +} + +TEST(PrintToTest, LargeIdMap) { + Model model; + std::vector xs; + VariableMap vars; + for (int i = 0; i < 100; ++i) { + xs.push_back(model.AddVariable(absl::StrCat("x", i))); + vars[xs.back()] = i; + } + std::ostringstream oss; + PrintTo(vars, &oss); + EXPECT_EQ(oss.str(), + "{{x0, 0}, {x1, 1}, {x2, 2}, {x3, 3}, {x4, 4}, {x5, 5}, {x6, 6}, " + "{x7, 7}, {x8, 8}, {x9, 9}, ...(size=100)}"); +} + +TEST(PrintToTest, BasisIdMap) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + VariableMap vars = {{x, BasisStatus::kAtLowerBound}, + {y, BasisStatus::kAtUpperBound}}; + std::ostringstream oss; + PrintTo(vars, &oss); + EXPECT_EQ(oss.str(), "{{x, at_lower_bound}, {y, at_upper_bound}}"); +} + +TEST(PrintToTest, PrimalSolution) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + PrimalSolution solution; + solution.variable_values = {{x, 1.0}, {y, 0.0}}; + solution.objective_value = 12; + solution.feasibility_status = SolutionStatus::kFeasible; + std::ostringstream oss; + PrintTo(solution, &oss); + EXPECT_EQ(oss.str(), + "{variable_values: {{x, 1}, {y, 0}}, objective_value: 12, " + "feasibility_status: feasible}"); +} + +TEST(PrintToTest, PrimalRay) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + PrimalRay ray; + ray.variable_values = {{x, 1.0}, {y, 0.0}}; + std::ostringstream oss; + PrintTo(ray, &oss); + EXPECT_EQ(oss.str(), "{variable_values: {{x, 1}, {y, 0}}}"); +} + +TEST(PrintToTest, DualSolution) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const LinearConstraint c = model.AddLinearConstraint("c"); + const QuadraticConstraint d = model.AddQuadraticConstraint(x * x <= 0.0, "d"); + DualSolution solution; + solution.reduced_costs = {{x, 1.0}, {y, 0.0}}; + solution.dual_values = {{c, 2.0}}; + solution.quadratic_dual_values = {{d, 3.0}}; + solution.feasibility_status = SolutionStatus::kInfeasible; + std::ostringstream oss; + PrintTo(solution, &oss); + EXPECT_EQ(oss.str(), + "{dual_values: {{c, 2}}, quadratic_dual_values: {{d, 3}}, " + "reduced_costs: {{x, 1}, {y, 0}}, objective_value: (nullopt), " + "feasibility_status: infeasible}"); +} + +TEST(PrintToTest, DualRay) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const LinearConstraint c = model.AddLinearConstraint("c"); + DualRay ray; + ray.reduced_costs = {{x, 1.0}, {y, 0.0}}; + ray.dual_values = {{c, 2.0}}; + std::ostringstream oss; + PrintTo(ray, &oss); + EXPECT_EQ(oss.str(), + "{dual_values: {{c, 2}}, reduced_costs: {{x, 1}, {y, 0}}}"); +} + +TEST(PrintToTest, Basis) { + Model model; + const Variable x = model.AddVariable("x"); + const Variable y = model.AddVariable("y"); + const LinearConstraint c = model.AddLinearConstraint("c"); + Basis basis; + basis.variable_status = {{x, BasisStatus::kAtUpperBound}, + {y, BasisStatus::kAtLowerBound}}; + basis.constraint_status = {{c, BasisStatus::kAtLowerBound}}; + basis.basic_dual_feasibility = SolutionStatus::kUndetermined; + std::ostringstream oss; + PrintTo(basis, &oss); + EXPECT_EQ(oss.str(), + "{variable_status: {{x, at_upper_bound}, {y, " + "at_lower_bound}}, " + "constraint_status: {{c, at_lower_bound}}, " + "basic_dual_feasibility: (undetermined)}"); +} + +TEST(PrintToTest, SolveResult) { + Model model; + const Variable x = model.AddVariable("x"); + const LinearConstraint c = model.AddLinearConstraint("c"); + SolveResult result{Termination::Feasible( + /*is_maximize=*/false, Limit::kTime, /*finite_primal_objective=*/20.0, + /*optional_dual_objective=*/10.0, "hit \"3\" seconds")}; + result.solve_stats.node_count = 2; + result.solve_stats.simplex_iterations = 0; + result.solve_stats.barrier_iterations = 0; + result.solve_stats.first_order_iterations = 0; + + PrimalSolution primal; + primal.objective_value = 20.0; + primal.variable_values = {{x, 1}}; + primal.feasibility_status = SolutionStatus::kFeasible; + DualSolution dual; + dual.reduced_costs = {{x, 1.0}}; + dual.dual_values = {{c, 2.0}}; + dual.objective_value = 10.0; + dual.feasibility_status = SolutionStatus::kFeasible; + Basis basis; + basis.variable_status = {{x, BasisStatus::kAtUpperBound}}; + basis.constraint_status = {{c, BasisStatus::kAtLowerBound}}; + basis.basic_dual_feasibility = SolutionStatus::kFeasible; + result.solutions.push_back(Solution{.primal_solution = std::move(primal), + .dual_solution = std::move(dual), + .basis = std::move(basis)}); + + PrimalRay primal_ray; + primal_ray.variable_values = {{x, 2}}; + result.primal_rays.push_back(std::move(primal_ray)); + + DualRay dual_ray; + dual_ray.reduced_costs = {{x, 4.0}}; + dual_ray.dual_values = {{c, 5.0}}; + result.dual_rays.push_back(std::move(dual_ray)); + + std::ostringstream oss; + PrintTo(result, &oss); +} + +TEST(IsFeasibleTest, Feasible) { + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kFeasible, + .is_minimal = false, + }), + IsFeasible()); + + // True .is_minimal should not match. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kFeasible, + .is_minimal = true, + }), + Not(IsFeasible())); + + // A non-empty .infeasible_subsystem should not match. + Model model; + const Variable x = model.AddIntegerVariable( + /*lower_bound=*/0.0, /*upper_bound=*/5.0, "x"); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kFeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = true, + }), + Not(IsFeasible())); +} + +TEST(IsFeasibleTest, Undetermined) { + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kUndetermined, + .is_minimal = false, + }), + Not(IsFeasible())); +} + +TEST(IsFeasibleTest, Infeasible) { + Model model; + const Variable x = model.AddIntegerVariable( + /*lower_bound=*/0.0, /*upper_bound=*/5.0, "x"); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = true, + }), + Not(IsFeasible())); +} + +TEST(IsUndeterminedTest, Undetermined) { + // The value of .is_minimal should be ignored. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kUndetermined, + .is_minimal = false, + }), + IsUndetermined()); + + // True .is_minimal should not match. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kUndetermined, + .is_minimal = true, + }), + Not(IsUndetermined())); + + // A non-empty .infeasible_subsystem should not match. + Model model; + const Variable x = model.AddIntegerVariable( + /*lower_bound=*/0.0, /*upper_bound=*/5.0, "x"); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kUndetermined, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = true, + }), + Not(IsUndetermined())); +} + +TEST(IsUndeterminedTest, Feasible) { + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kFeasible, + .is_minimal = false, + }), + Not(IsUndetermined())); +} + +TEST(IsUndeterminedTest, Infeasible) { + Model model; + const Variable x = model.AddIntegerVariable( + /*lower_bound=*/0.0, /*upper_bound=*/5.0, "x"); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = true, + }), + Not(IsUndetermined())); +} + +TEST(IsInfeasibleTest, Infeasible) { + Model model; + const Variable x = model.AddIntegerVariable( + /*lower_bound=*/0.0, /*upper_bound=*/5.0, "x"); + + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = true, + }), + IsInfeasible()); + + // Same with .is_minimal = false. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = false, + }), + IsInfeasible()); + + // Empty .infeasible_subsystem should not match. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .is_minimal = true, + }), + Not(IsInfeasible())); + + // Testing .expected_is_minimal. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = {.variable_integrality = {x}}, + .is_minimal = false, + }), + IsInfeasible(/*expected_is_minimal=*/false)); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = {.variable_integrality = {x}}, + .is_minimal = false, + }), + Not(IsInfeasible(/*expected_is_minimal=*/true))); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = {.variable_integrality = {x}}, + .is_minimal = true, + }), + Not(IsInfeasible(/*expected_is_minimal=*/false))); + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = {.variable_integrality = {x}}, + .is_minimal = true, + }), + IsInfeasible(/*expected_is_minimal=*/true)); + + // Testing .expected_infeasible_subsystem. + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kInfeasible, + .infeasible_subsystem = + { + .variable_integrality = {x}, + }, + .is_minimal = false, + }), + IsInfeasible(/*expected_is_minimal=*/std::nullopt, + /*expected_infeasible_subsystem=*/ModelSubset{ + .variable_integrality = {x}, + })); +} + +TEST(IsInfeasibleTest, Feasible) { + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kFeasible, + .is_minimal = false, + }), + Not(IsInfeasible())); +} + +TEST(IsInfeasibleTest, Undetermined) { + EXPECT_THAT((ComputeInfeasibleSubsystemResult{ + .feasibility = FeasibilityStatus::kUndetermined, + .is_minimal = false, + }), + Not(IsInfeasible())); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/message_callback_test.cc b/ortools/math_opt/cpp/message_callback_test.cc new file mode 100644 index 00000000000..cf5b240b155 --- /dev/null +++ b/ortools/math_opt/cpp/message_callback_test.cc @@ -0,0 +1,415 @@ +// Copyright 2010-2025 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/message_callback.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/status/statusor.h" +#include "google/protobuf/repeated_ptr_field.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/core/solver_interface_mock.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "testing/base/public/mock-log.h" + +namespace operations_research::math_opt { +namespace { + +using ::base_logging::INFO; +using ::testing::_; +using ::testing::ByMove; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::EquivToProto; +using ::testing::InSequence; +using ::testing::kDoNotCaptureLogsYet; +using ::testing::Ne; +using ::testing::Return; +using ::testing::ScopedMockLog; + +constexpr double kInf = std::numeric_limits::infinity(); + +// Basic LP model: +// +// a and b are continuous variable +// +// minimize a - b +// s.t. 0 <= a +// 0 <= b <= 3 +struct BasicLp { + BasicLp(); + + // Returns the expected optimal result for this model. Only put the given set + // of variables in the result (to test filters). + SolveResultProto OptimalResult( + const absl::flat_hash_set& vars) const; + + Model model; + const Variable a; + const Variable b; +}; + +BasicLp::BasicLp() + : a(model.AddVariable(0.0, kInf, false, "a")), + b(model.AddVariable(0.0, 3.0, false, "b")) {} + +SolveResultProto BasicLp::OptimalResult( + const absl::flat_hash_set& vars) const { + SolveResultProto result; + result.mutable_termination()->set_reason(TERMINATION_REASON_OPTIMAL); + result.mutable_solve_stats()->mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_solve_stats()->mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + PrimalSolutionProto* const solution = + result.add_solutions()->mutable_primal_solution(); + solution->set_objective_value(0.0); + solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + if (vars.contains(a)) { + solution->mutable_variable_values()->add_ids(a.id()); + solution->mutable_variable_values()->add_values(0.0); + } + if (vars.contains(b)) { + solution->mutable_variable_values()->add_ids(b.id()); + solution->mutable_variable_values()->add_values(3.0); + } + return result; +} + +TEST(PrinterMessageCallbackTest, StringStream) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + std::ostringstream oss; + const SolveArguments args = {.message_callback = + PrinterMessageCallback(oss, "logs| ")}; + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_EQ(oss.str(), "logs| line 1\nlogs| line 2\nlogs| line 3\n"); +} + +TEST(InfoLoggerMessageCallbackTest, Logging) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + std::ostringstream oss; + const SolveArguments args = {.message_callback = + InfoLoggerMessageCallback("logs| ")}; + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + ScopedMockLog log(kDoNotCaptureLogsYet); + + // Expected file path for testing logs. + const std::string test_path = "ortools/math_opt/cpp/message_callback_test.cc"; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 1")); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 2")); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 3")); + } + + log.StartCapturingLogs(); + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); +} + +// In this test we set the `-v` flag to enable verbose logging. +TEST(VLoggerMessageCallbackTest, VisibleLog) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + std::ostringstream oss; + const SolveArguments args = {.message_callback = + VLoggerMessageCallback(1, "logs| ")}; + + // Flags local to each test, see go/gunitprimer#invoking-the-tests. + absl::SetFlag(&FLAGS_v, true); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + ScopedMockLog log(kDoNotCaptureLogsYet); + + // Expected file path for testing logs. + const std::string test_path = "ortools/math_opt/cpp/message_callback_test.cc"; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 1")); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 2")); + EXPECT_CALL(log, Log(INFO, test_path, "logs| line 3")); + } + + log.StartCapturingLogs(); + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); +} + +// In this test we set the `-v` flag to false to disable verbose logging. +TEST(VLoggerMessageCallbackTest, InvisibleLog) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + std::ostringstream oss; + const SolveArguments args = {.message_callback = + VLoggerMessageCallback(1, "logs| ")}; + + // Flags local to each test, see go/gunitprimer#invoking-the-tests. + absl::SetFlag(&FLAGS_v, false); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + ScopedMockLog log(kDoNotCaptureLogsYet); + + // Expected file path for testing logs. + const std::string test_path = "ortools/math_opt/cpp/message_callback_test.cc"; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + log.StartCapturingLogs(); + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); +} + +TEST(VectorMessageCallbackTest, Basic) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + std::vector messages = {"initial content 1", + "initial content 2"}; + const SolveArguments args = {.message_callback = + VectorMessageCallback(&messages)}; + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(messages, ElementsAre("initial content 1", "initial content 2", + "line 1", "line 2", "line 3")); +} + +TEST(RepeatedPtrFieldMessageCallbackTest, Basic) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + google::protobuf::RepeatedPtrField messages; + messages.Add("initial content 1"); + messages.Add("initial content 2"); + const SolveArguments args = {.message_callback = + RepeatedPtrFieldMessageCallback(&messages)}; + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback, + const SolveInterrupter*) -> absl::StatusOr { + message_cb({"line 1", "line 2"}); + message_cb({"line 3"}); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(messages, ElementsAre("initial content 1", "initial content 2", + "line 1", "line 2", "line 3")); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/model_solve_parameters_test.cc b/ortools/math_opt/cpp/model_solve_parameters_test.cc new file mode 100644 index 00000000000..dfca7165b50 --- /dev/null +++ b/ortools/math_opt/cpp/model_solve_parameters_test.cc @@ -0,0 +1,742 @@ +// Copyright 2010-2025 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/model_solve_parameters.h" + +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/time/time.h" +#include "google/protobuf/duration.pb.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/map_util.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/solution.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "util/tuple/struct.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +// Define the value of MapFilter.storage() we want in the test for a filter. +// +// In below tests, we cover all possible combinations of filters with same or +// different value for storage() (either for death testing of regular testing). +// To have total coverage, we only need to test with three models max since we +// have three filters. +// +// The cases to test are the one in the set {null, 1, 2, 3}^3. The cases with +// different non null models are the one expected to crash, the others (using +// one or more times the same model) are expected to pass. +enum FilterModel { + // The filter has no model (no referencing any variable or constraint). + kNullModel, + // The filter points the first test model. + kModel1, + // The filter points the second test model. + kModel2, + // The filter points the third test model. + kModel3, +}; + +const std::array kAllFilterModels = {kNullModel, kModel1, + kModel2, kModel3}; + +// A combination of filter models to test, one model for each filter in +// ModelSolveParameters. +struct FilterModelsCombination { + TUPLE_DEFINE_STRUCT(FilterModelsCombination, (ostream, ctor), + (FilterModel, variable_values_filter_model), + (FilterModel, dual_values_filter_model), + (FilterModel, reduced_costs_filter_model)); + + // Returns a non nullopt value if and only if all filters have either the same + // model or a null model. If all filter have a null model, kNullModel is + // returned. + std::optional common_storage() const; + + // Returns true if the combination is expected to pass (i.e. it has a common + // model), false if it is expected to die (i.e. it references at least two + // different models). + bool ok() const; +}; + +std::optional FilterModelsCombination::common_storage() const { + const std::vector models = {variable_values_filter_model, + dual_values_filter_model, + reduced_costs_filter_model}; + FilterModel ret = kNullModel; + for (const auto model : models) { + if (model != kNullModel) { + if (ret == kNullModel) { + ret = model; + } else if (model != ret) { + return std::nullopt; + } + } + } + return ret; +} + +bool FilterModelsCombination::ok() const { + return common_storage().has_value(); +} + +// Returns all possible model combinations. +std::vector AllCombinations() { + std::vector ret; + for (const FilterModel primal_vars_model : kAllFilterModels) { + for (const FilterModel dual_cstrs_model : kAllFilterModels) { + for (const FilterModel dual_vars_model : kAllFilterModels) { + ret.emplace_back(primal_vars_model, dual_cstrs_model, dual_vars_model); + } + } + } + return ret; +} + +// Test fixture for testing filter models combinations. +class FilterModelsCombinationTest : public ::testing::Test { + public: + FilterModelsCombinationTest() + : a_1_(&model_1_, model_1_.AddVariable("a_1")), + cstr_1_(&model_1_, model_1_.AddLinearConstraint("cstr_1")), + a_2_(&model_2_, model_2_.AddVariable("a_2")), + cstr_2_(&model_2_, model_2_.AddLinearConstraint("cstr_2")), + a_3_(&model_3_, model_3_.AddVariable("a_3")), + cstr_3_(&model_3_, model_3_.AddLinearConstraint("cstr_3")) {} + + protected: + // Returns a filter for the given model. The input model_to_key is expected to + // contain a value for each FilterModel but kNullModel. + template + MapFilter MakeMapFilter( + const FilterModel model, + const absl::flat_hash_map& model_to_key) { + MapFilter ret; + + if (model == kNullModel) { + return ret; + } + + ret.filtered_keys = {gtl::FindOrDie(model_to_key, model)}; + return ret; + } + + ModelSolveParameters MakeParameters( + const FilterModelsCombination& combination) { + const absl::flat_hash_map vars = { + {kModel1, a_1_}, + {kModel2, a_2_}, + {kModel3, a_3_}, + }; + const absl::flat_hash_map cstrs = { + {kModel1, cstr_1_}, + {kModel2, cstr_2_}, + {kModel3, cstr_3_}, + }; + ModelSolveParameters params; + params.variable_values_filter = + MakeMapFilter(combination.variable_values_filter_model, vars); + params.dual_values_filter = + MakeMapFilter(combination.dual_values_filter_model, cstrs); + params.reduced_costs_filter = + MakeMapFilter(combination.reduced_costs_filter_model, vars); + return params; + } + + ModelStorage model_1_; + const Variable a_1_; + const LinearConstraint cstr_1_; + ModelStorage model_2_; + const Variable a_2_; + const LinearConstraint cstr_2_; + ModelStorage model_3_; + const Variable a_3_; + const LinearConstraint cstr_3_; +}; + +using FilterModelsCombinationDeathTest = FilterModelsCombinationTest; + +TEST(ModelSolveParametersTest, Default) { + ModelStorage model; + const ModelSolveParameters params; + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(""))); + EXPECT_OK(params.CheckModelStorage(&model)); +} + +TEST(ModelSolveParametersTest, SetFiltersSameModel) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + const Variable b(&model, model.AddVariable("b")); + const LinearConstraint cstr(&model, model.AddLinearConstraint("cstr")); + + ModelSolveParameters params; + params.variable_values_filter = MakeKeepKeysFilter({a}); + params.dual_values_filter = MakeKeepKeysFilter({cstr}); + params.reduced_costs_filter = MakeKeepKeysFilter({b}); + + ModelSolveParametersProto expected; + expected.mutable_variable_values_filter()->set_filter_by_ids(true); + expected.mutable_variable_values_filter()->add_filtered_ids(a.id()); + expected.mutable_dual_values_filter()->set_filter_by_ids(true); + expected.mutable_dual_values_filter()->add_filtered_ids(cstr.id()); + expected.mutable_reduced_costs_filter()->set_filter_by_ids(true); + expected.mutable_reduced_costs_filter()->add_filtered_ids(b.id()); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); + EXPECT_OK(params.CheckModelStorage(&model)); +} + +TEST_F(FilterModelsCombinationTest, ValidCombinations) { + const absl::flat_hash_map models = { + {kNullModel, nullptr}, + {kModel1, &model_1_}, + {kModel2, &model_2_}, + {kModel3, &model_3_}, + }; + for (const FilterModelsCombination& combination : AllCombinations()) { + if (!combination.ok()) { + continue; + } + + SCOPED_TRACE(combination); + const ModelSolveParameters params = MakeParameters(combination); + + const std::optional model = combination.common_storage(); + ASSERT_TRUE(model.has_value()); + const NullableModelStorageCPtr expected_model = + gtl::FindOrDie(models, *model); + if (expected_model != nullptr) { + EXPECT_OK(params.CheckModelStorage(expected_model)); + } else { + EXPECT_OK(params.CheckModelStorage(&model_1_)); + EXPECT_OK(params.CheckModelStorage(&model_2_)); + EXPECT_OK(params.CheckModelStorage(&model_3_)); + } + } +} + +TEST_F(FilterModelsCombinationTest, InvalidCombinations) { + for (const FilterModelsCombination& combination : AllCombinations()) { + if (combination.ok()) { + continue; + } + + SCOPED_TRACE(combination); + const ModelSolveParameters params = MakeParameters(combination); + EXPECT_THAT(params.CheckModelStorage(&model_1_), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(params.CheckModelStorage(&model_2_), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(params.CheckModelStorage(&model_3_), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + } +} + +TEST(ModelSolveParametersTest, OnlyPrimalVariables) { + ModelStorage model; + const ModelSolveParameters params = + ModelSolveParameters::OnlyPrimalVariables(); + + EXPECT_THAT(params.Proto(), + IsOkAndHolds(EquivToProto( + R"pb(dual_values_filter { filter_by_ids: true } + quadratic_dual_values_filter { filter_by_ids: true } + reduced_costs_filter { filter_by_ids: true })pb"))); + EXPECT_OK(params.CheckModelStorage(&model)); +} + +TEST(ModelSolveParametersTest, OnlySomePrimalVariablesInitializerList) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + + const ModelSolveParameters params = + ModelSolveParameters::OnlySomePrimalVariables({a}); + + ModelSolveParametersProto expected; + expected.mutable_variable_values_filter()->set_filter_by_ids(true); + expected.mutable_variable_values_filter()->add_filtered_ids(a.id()); + expected.mutable_dual_values_filter()->set_filter_by_ids(true); + expected.mutable_quadratic_dual_values_filter()->set_filter_by_ids(true); + expected.mutable_reduced_costs_filter()->set_filter_by_ids(true); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); + EXPECT_OK(params.CheckModelStorage(&model)); +} + +TEST(ModelSolveParametersTest, OnlySomePrimalVariablesVector) { + ModelStorage model; + const Variable a(&model, model.AddVariable("a")); + + const std::vector vars = {a}; + const ModelSolveParameters params = + ModelSolveParameters::OnlySomePrimalVariables(vars); + + ModelSolveParametersProto expected; + expected.mutable_variable_values_filter()->set_filter_by_ids(true); + expected.mutable_variable_values_filter()->add_filtered_ids(a.id()); + expected.mutable_dual_values_filter()->set_filter_by_ids(true); + expected.mutable_quadratic_dual_values_filter()->set_filter_by_ids(true); + expected.mutable_reduced_costs_filter()->set_filter_by_ids(true); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); + EXPECT_OK(params.CheckModelStorage(&model)); +} + +TEST(ModelSolveParametersTest, BasisStart) { + ModelStorage model; + const Variable x1(&model, model.AddVariable("x1")); + const Variable x2(&model, model.AddVariable("x2")); + const LinearConstraint c1(&model, model.AddLinearConstraint("c1")); + const LinearConstraint c2(&model, model.AddLinearConstraint("c2")); + ModelSolveParameters params; + auto& initial_basis = params.initial_basis.emplace(); + initial_basis.variable_status.insert( + {{x1, BasisStatus::kAtUpperBound}, {x2, BasisStatus::kBasic}}); + initial_basis.constraint_status.insert( + {{c1, BasisStatus::kAtLowerBound}, {c2, BasisStatus::kBasic}}); + + EXPECT_OK(params.CheckModelStorage(&model)); + + ModelSolveParametersProto expected; + expected.mutable_initial_basis()->mutable_constraint_status()->add_ids( + c1.id()); + expected.mutable_initial_basis()->mutable_constraint_status()->add_ids( + c2.id()); + expected.mutable_initial_basis()->mutable_constraint_status()->add_values( + BASIS_STATUS_AT_LOWER_BOUND); + expected.mutable_initial_basis()->mutable_constraint_status()->add_values( + BASIS_STATUS_BASIC); + expected.mutable_initial_basis()->mutable_variable_status()->add_ids(x1.id()); + expected.mutable_initial_basis()->mutable_variable_status()->add_ids(x2.id()); + expected.mutable_initial_basis()->mutable_variable_status()->add_values( + BASIS_STATUS_AT_UPPER_BOUND); + expected.mutable_initial_basis()->mutable_variable_status()->add_values( + BASIS_STATUS_BASIC); + expected.mutable_initial_basis()->set_basic_dual_feasibility( + SolutionStatusProto::SOLUTION_STATUS_UNSPECIFIED); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); +} + +TEST(ModelSolveParametersTest, FilterAndBasisDifferentModels) { + ModelStorage model_a; + const Variable a_x(&model_a, model_a.AddVariable("x")); + ModelStorage model_b; + const Variable b_x(&model_b, model_b.AddVariable("x")); + + const ModelSolveParameters params = { + .variable_values_filter = {.filtered_keys = {{a_x}}}, + .initial_basis = Basis{.variable_status = {{b_x, BasisStatus::kFree}}}, + }; + + EXPECT_THAT(params.CheckModelStorage(&model_a), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(params.CheckModelStorage(&model_b), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(ModelSolveParametersTest, SolutionHint) { + ModelStorage model; + const Variable x1(&model, model.AddVariable("x1")); + const Variable x2(&model, model.AddVariable("x2")); + const Variable x3(&model, model.AddVariable("x3")); + const LinearConstraint c1(&model, model.AddLinearConstraint("c1")); + ModelSolveParameters params; + + ModelSolveParameters::SolutionHint first_hint; + first_hint.variable_values.emplace(x1, 1.0); + first_hint.variable_values.emplace(x3, 0.0); + first_hint.dual_values.emplace(c1, 5.25); + params.solution_hints.emplace_back(first_hint); + ModelSolveParameters::SolutionHint second_hint; + second_hint.variable_values.emplace(x1, 1.0); + second_hint.variable_values.emplace(x2, 0.0); + params.solution_hints.emplace_back(second_hint); + + EXPECT_OK(params.CheckModelStorage(&model)); + EXPECT_OK(first_hint.CheckModelStorage(&model)); + EXPECT_OK(second_hint.CheckModelStorage(&model)); + + ModelSolveParametersProto expected; + SolutionHintProto& first_expected_hint = *expected.add_solution_hints(); + first_expected_hint.mutable_variable_values()->add_ids(x1.id()); + first_expected_hint.mutable_variable_values()->add_values(1.0); + first_expected_hint.mutable_variable_values()->add_ids(x3.id()); + first_expected_hint.mutable_variable_values()->add_values(0.0); + first_expected_hint.mutable_dual_values()->add_ids(c1.id()); + first_expected_hint.mutable_dual_values()->add_values(5.25); + SolutionHintProto& second_expected_hint = *expected.add_solution_hints(); + second_expected_hint.mutable_variable_values()->add_ids(x1.id()); + second_expected_hint.mutable_variable_values()->add_values(1.0); + second_expected_hint.mutable_variable_values()->add_ids(x2.id()); + second_expected_hint.mutable_variable_values()->add_values(0.0); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); + EXPECT_THAT(first_hint.Proto(), EquivToProto(first_expected_hint)); + EXPECT_THAT(second_hint.Proto(), EquivToProto(second_expected_hint)); +} + +TEST(ModelSolveParametersTest, FilterAndHintDifferentModels) { + ModelStorage model_a; + const Variable a_x(&model_a, model_a.AddVariable("x")); + ModelStorage model_b; + const Variable b_x(&model_b, model_b.AddVariable("x")); + + const ModelSolveParameters params = { + .variable_values_filter = {.filtered_keys = {{a_x}}}, + .solution_hints = {{.variable_values = {{b_x, 1.0}}}}, + }; + + EXPECT_THAT(params.CheckModelStorage(&model_a), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(params.CheckModelStorage(&model_b), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(ModelSolveParametersTest, BranchingPriority) { + ModelStorage model; + const Variable x1(&model, model.AddVariable("x1")); + const Variable x2(&model, model.AddVariable("x2")); + const Variable x3(&model, model.AddVariable("x3")); + ModelSolveParameters params; + + params.branching_priorities.emplace(x1, 2); + params.branching_priorities.emplace(x3, 1); + + ModelSolveParametersProto expected; + expected.mutable_branching_priorities()->add_ids(x1.id()); + expected.mutable_branching_priorities()->add_values(2); + expected.mutable_branching_priorities()->add_ids(x3.id()); + expected.mutable_branching_priorities()->add_values(1); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); +} + +TEST(ModelSolveParametersTest, BranchingPriorityOtherModel) { + Model model; + Model other; + const Variable x = other.AddVariable("x"); + const ModelSolveParameters params{.branching_priorities = {{x, 2}}}; + EXPECT_THAT(params.CheckModelStorage(model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(ModelSolveParametersTest, ObjectiveParameters) { + ModelStorage model; + const auto primary = Objective::Primary(&model); + const auto secondary = Objective::Auxiliary(&model, AuxiliaryObjectiveId(2)); + ModelSolveParameters params; + params.objective_parameters[primary] + .objective_degradation_absolute_tolerance = 3.0; + params.objective_parameters[primary] + .objective_degradation_relative_tolerance = 4.0; + params.objective_parameters[primary].time_limit = absl::Seconds(10); + params.objective_parameters[secondary] + .objective_degradation_absolute_tolerance = 5.0; + params.objective_parameters[secondary] + .objective_degradation_relative_tolerance = 6.0; + params.objective_parameters[secondary].time_limit = absl::Seconds(20); + + ModelSolveParametersProto expected; + expected.mutable_primary_objective_parameters() + ->set_objective_degradation_absolute_tolerance(3.0); + expected.mutable_primary_objective_parameters() + ->set_objective_degradation_relative_tolerance(4.0); + expected.mutable_primary_objective_parameters() + ->mutable_time_limit() + ->set_seconds(10); + (*expected.mutable_auxiliary_objective_parameters())[2] + .set_objective_degradation_absolute_tolerance(5.0); + (*expected.mutable_auxiliary_objective_parameters())[2] + .set_objective_degradation_relative_tolerance(6.0); + expected.mutable_auxiliary_objective_parameters() + ->at(2) + .mutable_time_limit() + ->set_seconds(20); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); +} + +TEST(ModelSolveParametersTest, ObjectiveParametersOtherModel) { + Model model; + Model other; + const auto o = other.primary_objective(); + const ModelSolveParameters params{.objective_parameters = {{o, {}}}}; + EXPECT_THAT(params.CheckModelStorage(model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(ModelSolveParametersTest, LazyLinearConstraints) { + Model model; + const LinearConstraint c = model.AddLinearConstraint("c"); + model.AddLinearConstraint("d"); + const LinearConstraint e = model.AddLinearConstraint("e"); + ModelSolveParameters params{.lazy_linear_constraints = {c, e}}; + + ModelSolveParametersProto expected; + expected.add_lazy_linear_constraint_ids(c.id()); + expected.add_lazy_linear_constraint_ids(e.id()); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); +} + +TEST(ModelSolveParametersTest, LazyLinearConstraintsOtherModel) { + Model model; + Model other; + const LinearConstraint c = other.AddLinearConstraint("c"); + const ModelSolveParameters params{.lazy_linear_constraints = {c}}; + EXPECT_THAT(params.CheckModelStorage(model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(ModelSolveParametersTest, FromProtoEmptyRoundTrip) { + const Model model; + const ModelSolveParametersProto proto; + + ASSERT_OK_AND_ASSIGN(const auto params, + ModelSolveParameters::FromProto(model, proto)); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(proto))); +} + +TEST(ModelSolveParametersTest, FromProtoFullRoundTrip) { + Model model; + const Variable x = model.AddVariable(); + model.AddVariable(); + model.AddLinearConstraint(); + model.AddLinearConstraint(); + model.AddQuadraticConstraint(x * x <= 0.0); + model.AddQuadraticConstraint(x * x <= 0.0); + model.AddAuxiliaryObjective(2); + model.AddAuxiliaryObjective(3); + + ModelSolveParametersProto proto; + proto.mutable_variable_values_filter()->set_filter_by_ids(true); + proto.mutable_variable_values_filter()->add_filtered_ids(1); + + proto.mutable_dual_values_filter()->set_filter_by_ids(true); + proto.mutable_dual_values_filter()->add_filtered_ids(0); + + proto.mutable_quadratic_dual_values_filter()->set_filter_by_ids(true); + proto.mutable_quadratic_dual_values_filter()->add_filtered_ids(0); + + proto.mutable_reduced_costs_filter()->set_skip_zero_values(true); + + proto.mutable_initial_basis()->set_basic_dual_feasibility( + SolutionStatusProto::SOLUTION_STATUS_FEASIBLE); + auto& basis_vars = *proto.mutable_initial_basis()->mutable_variable_status(); + basis_vars.add_ids(0); + basis_vars.add_ids(1); + basis_vars.add_values(BasisStatusProto::BASIS_STATUS_BASIC); + basis_vars.add_values(BasisStatusProto::BASIS_STATUS_BASIC); + + auto& basis_cons = + *proto.mutable_initial_basis()->mutable_constraint_status(); + basis_cons.add_ids(0); + basis_cons.add_ids(1); + basis_cons.add_values(BasisStatusProto::BASIS_STATUS_AT_LOWER_BOUND); + basis_cons.add_values(BasisStatusProto::BASIS_STATUS_AT_UPPER_BOUND); + + SolutionHintProto& hint = *proto.add_solution_hints(); + hint.mutable_variable_values()->add_ids(0); + hint.mutable_variable_values()->add_ids(1); + hint.mutable_variable_values()->add_values(10.0); + hint.mutable_variable_values()->add_values(20.0); + + proto.mutable_branching_priorities()->add_ids(1); + proto.mutable_branching_priorities()->add_values(3); + + proto.mutable_primary_objective_parameters() + ->set_objective_degradation_absolute_tolerance(0.5); + proto.mutable_primary_objective_parameters() + ->mutable_time_limit() + ->set_seconds(10); + + (*proto.mutable_auxiliary_objective_parameters())[1] + .set_objective_degradation_relative_tolerance(0.2); + (*proto.mutable_auxiliary_objective_parameters())[1] + .mutable_time_limit() + ->set_seconds(20); + + ASSERT_OK_AND_ASSIGN(const auto params, + ModelSolveParameters::FromProto(model, proto)); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(proto))); +} + +TEST(ModelSolveParametersTest, FromProtoInvalidAuxObj) { + const Model model; + ModelSolveParametersProto proto; + (*proto.mutable_auxiliary_objective_parameters())[1] + .set_objective_degradation_absolute_tolerance(0.5); + + EXPECT_THAT(ModelSolveParameters::FromProto(model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("auxiliary_objective_parameters"))); +} + +TEST(ModelSolveParametersTest, FromProtoInvalidLazyConstraintIdsIsError) { + const Model model; + ModelSolveParametersProto proto; + proto.add_lazy_linear_constraint_ids(2); + + EXPECT_THAT(ModelSolveParameters::FromProto(model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("lazy_linear_constraint"))); +} + +TEST(ObjectiveParametersTest, Proto) { + ModelSolveParameters::ObjectiveParameters params; + params.objective_degradation_absolute_tolerance = 3.0; + params.objective_degradation_relative_tolerance = 4.0; + params.time_limit = absl::Seconds(10); + + ObjectiveParametersProto expected; + expected.set_objective_degradation_absolute_tolerance(3.0); + expected.set_objective_degradation_relative_tolerance(4.0); + expected.mutable_time_limit()->set_seconds(10); + EXPECT_THAT(params.Proto(), IsOkAndHolds(EquivToProto(expected))); +} + +TEST(ObjectiveParametersTest, FromProtoFull) { + ObjectiveParametersProto proto; + proto.set_objective_degradation_absolute_tolerance(3.0); + proto.set_objective_degradation_relative_tolerance(4.0); + proto.mutable_time_limit()->set_seconds(10); + + ASSERT_OK_AND_ASSIGN( + const auto params, + ModelSolveParameters::ObjectiveParameters::FromProto(proto)); + EXPECT_THAT(params.objective_degradation_absolute_tolerance, + testing::Optional(3.0)); + EXPECT_THAT(params.objective_degradation_relative_tolerance, + testing::Optional(4.0)); + EXPECT_THAT(params.time_limit, absl::Seconds(10)); +} + +TEST(ObjectiveParametersTest, FromProtoEmpty) { + ObjectiveParametersProto proto; + ASSERT_OK_AND_ASSIGN( + const auto params, + ModelSolveParameters::ObjectiveParameters::FromProto(proto)); + EXPECT_EQ(params.objective_degradation_absolute_tolerance, std::nullopt); + EXPECT_EQ(params.objective_degradation_relative_tolerance, std::nullopt); + EXPECT_EQ(params.time_limit, absl::InfiniteDuration()); +} + +TEST(SolutionHintTest, HintMixedModels) { + ModelStorage model_a; + const Variable a_x(&model_a, model_a.AddVariable("x")); + ModelStorage model_b; + const LinearConstraint b_c(&model_b, model_b.AddLinearConstraint("c")); + + const ModelSolveParameters::SolutionHint hint = { + .variable_values = {{a_x, 1.0}}, + .dual_values = {{b_c, 3.2}}, + }; + + EXPECT_THAT(hint.CheckModelStorage(&model_a), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(hint.CheckModelStorage(&model_b), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(SolutionHintTest, FromValidProto) { + Model model; + const Variable x1 = model.AddVariable("x1"); + model.AddVariable("x2"); + const Variable x3 = model.AddVariable("x3"); + model.AddLinearConstraint("c1"); + const LinearConstraint c2 = model.AddLinearConstraint("c2"); + + SolutionHintProto hint_proto; + hint_proto.mutable_variable_values()->add_ids(x1.id()); + hint_proto.mutable_variable_values()->add_values(1.0); + hint_proto.mutable_variable_values()->add_ids(x3.id()); + hint_proto.mutable_variable_values()->add_values(0.0); + hint_proto.mutable_dual_values()->add_ids(c2.id()); + hint_proto.mutable_dual_values()->add_values(-1.0); + + ASSERT_OK_AND_ASSIGN( + const ModelSolveParameters::SolutionHint hint, + ModelSolveParameters::SolutionHint::FromProto(model, hint_proto)); + EXPECT_THAT(hint.variable_values, + UnorderedElementsAre(Pair(x1, 1.0), Pair(x3, 0.0))); + EXPECT_THAT(hint.dual_values, UnorderedElementsAre(Pair(c2, -1.0))); +} + +TEST(SolutionHintTest, FromProtoInvalidVariableValues) { + // This test only tests one failing case. It relies on the fact that we use + // VariableValuesFromProto() which is already properly unit tested. + Model model; + const Variable x1 = model.AddVariable("x1"); + model.AddVariable("x2"); + model.AddVariable("x3"); + + SolutionHintProto hint_proto; + hint_proto.mutable_variable_values()->add_ids(x1.id()); + hint_proto.mutable_variable_values()->add_values(1.0); + // We use an index that does not exist in the model. + hint_proto.mutable_variable_values()->add_ids(model.next_variable_id()); + hint_proto.mutable_variable_values()->add_values(0.0); + + EXPECT_THAT(ModelSolveParameters::SolutionHint::FromProto(model, hint_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable_values"))); +} + +TEST(SolutionHintTest, FromProtoInvalidDualValues) { + // This test only tests one failing case. It relies on the fact that we use + // LinearConstraintValuesFromProto() which is already properly unit tested. + Model model; + const LinearConstraint c1 = model.AddLinearConstraint("c1"); + model.AddLinearConstraint("c2"); + model.AddLinearConstraint("c3"); + + SolutionHintProto hint_proto; + hint_proto.mutable_dual_values()->add_ids(c1.id()); + hint_proto.mutable_dual_values()->add_values(1.0); + // We use an index that does not exist in the model. + hint_proto.mutable_dual_values()->add_ids(model.next_linear_constraint_id()); + hint_proto.mutable_dual_values()->add_values(0.0); + + EXPECT_THAT( + ModelSolveParameters::SolutionHint::FromProto(model, hint_proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("dual_values"))); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/objective_test.cc b/ortools/math_opt/cpp/objective_test.cc new file mode 100644 index 00000000000..e850ce11c93 --- /dev/null +++ b/ortools/math_opt/cpp/objective_test.cc @@ -0,0 +1,233 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/cpp/objective.h" + +#include +#include + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/storage/model_storage_types.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +TEST(ObjectiveTest, Accessors) { + ModelStorage storage(/*model_name=*/"", /*primary_objective_name=*/"primary"); + const Variable x(&storage, storage.AddVariable("x")); + const Variable y(&storage, storage.AddVariable("y")); + + const Objective primary = Objective::Primary(&storage); + storage.set_objective_priority(primary.typed_id(), 1); + storage.set_objective_offset(primary.typed_id(), 2.0); + storage.set_linear_objective_coefficient(primary.typed_id(), x.typed_id(), + 3.0); + const Objective secondary = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + storage.set_maximize(secondary.typed_id()); + storage.set_quadratic_objective_coefficient(secondary.typed_id(), + x.typed_id(), y.typed_id(), 4.0); + + EXPECT_EQ(primary.id(), std::nullopt); + EXPECT_EQ(secondary.id(), 0); + + EXPECT_EQ(primary.typed_id(), kPrimaryObjectiveId); + EXPECT_EQ(secondary.typed_id(), AuxiliaryObjectiveId(0)); + + EXPECT_EQ(primary.storage(), &storage); + EXPECT_EQ(secondary.storage(), &storage); + + EXPECT_TRUE(primary.is_primary()); + EXPECT_FALSE(secondary.is_primary()); + + EXPECT_FALSE(primary.maximize()); + EXPECT_TRUE(secondary.maximize()); + + EXPECT_EQ(primary.priority(), 1); + EXPECT_EQ(secondary.priority(), 12); + + EXPECT_EQ(primary.name(), "primary"); + EXPECT_EQ(secondary.name(), "secondary"); + + EXPECT_EQ(primary.offset(), 2.0); + EXPECT_EQ(secondary.offset(), 0.0); + EXPECT_EQ(primary.num_linear_terms(), 1); + EXPECT_EQ(secondary.num_linear_terms(), 0); + EXPECT_EQ(primary.num_quadratic_terms(), 0); + EXPECT_EQ(secondary.num_quadratic_terms(), 1); + + EXPECT_EQ(primary.coefficient(x), 3.0); + EXPECT_EQ(secondary.coefficient(x), 0.0); + + EXPECT_EQ(primary.coefficient(x, y), 0.0); + EXPECT_EQ(secondary.coefficient(x, y), 4.0); + + EXPECT_TRUE(primary.is_coefficient_nonzero(x)); + EXPECT_FALSE(secondary.is_coefficient_nonzero(x)); + + EXPECT_FALSE(primary.is_coefficient_nonzero(x, y)); + EXPECT_TRUE(secondary.is_coefficient_nonzero(x, y)); +} + +TEST(ObjectiveTest, NameAfterDeletion) { + ModelStorage storage; + const Objective o = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + + ASSERT_EQ(o.name(), "secondary"); + + storage.DeleteAuxiliaryObjective(o.typed_id().value()); + EXPECT_EQ(o.name(), kDeletedObjectiveDefaultDescription); +} + +TEST(ObjectiveTest, Equality) { + ModelStorage storage(/*model_name=*/"", /*primary_objective_name=*/"primary"); + const Variable x(&storage, storage.AddVariable("x")); + const Variable y(&storage, storage.AddVariable("y")); + + const Objective c = Objective::Primary(&storage); + const Objective d = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + + // `d2` is another `Objective` that points the same auxiliary objective in the + // indexed storage. It should compares == to `d`. + const Objective d2 = Objective::Auxiliary(d.storage(), d.typed_id().value()); + + // `e` has identical data as `d`. It should not compares equal to `d`, though. + const Objective e = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + + EXPECT_TRUE(c == c); + EXPECT_FALSE(c == d); + EXPECT_TRUE(d == d2); + EXPECT_FALSE(d == e); + EXPECT_FALSE(c != c); + EXPECT_TRUE(c != d); + EXPECT_FALSE(d != d2); + EXPECT_TRUE(d != e); +} + +TEST(ObjectiveTest, AsLinearExpression) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Objective o = Objective::Primary(&storage); + storage.set_objective_offset(o.typed_id(), 1.0); + storage.set_linear_objective_coefficient(o.typed_id(), x.typed_id(), 2.0); + + const LinearExpression o_expr = o.AsLinearExpression(); + EXPECT_EQ(o_expr.offset(), 1.0); + EXPECT_THAT(o_expr.terms(), UnorderedElementsAre(Pair(x, 2.0))); +} + +TEST(ObjectiveDeathTest, QuadraticObjectiveAsLinearExpression) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Objective o = + Objective::Auxiliary(&storage, storage.AddAuxiliaryObjective(12)); + storage.set_quadratic_objective_coefficient(o.typed_id(), x.typed_id(), + x.typed_id(), 1.0); + EXPECT_DEATH(o.AsLinearExpression(), "quadratic"); +} + +TEST(ObjectiveTest, AsQuadraticExpression) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Objective o = Objective::Primary(&storage); + storage.set_objective_offset(o.typed_id(), 1.0); + storage.set_linear_objective_coefficient(o.typed_id(), x.typed_id(), 2.0); + storage.set_quadratic_objective_coefficient(o.typed_id(), x.typed_id(), + x.typed_id(), 3.0); + + const QuadraticExpression o_expr = o.AsQuadraticExpression(); + EXPECT_EQ(o_expr.offset(), 1.0); + EXPECT_THAT(o_expr.linear_terms(), UnorderedElementsAre(Pair(x, 2.0))); + EXPECT_THAT(o_expr.quadratic_terms(), + UnorderedElementsAre(Pair(QuadraticTermKey(x, x), 3.0))); +} + +TEST(ObjectiveTest, ToString) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Objective o = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + storage.set_objective_offset(o.typed_id(), 1.0); + storage.set_linear_objective_coefficient(o.typed_id(), x.typed_id(), 2.0); + storage.set_quadratic_objective_coefficient(o.typed_id(), x.typed_id(), + x.typed_id(), 3.0); + + EXPECT_EQ(o.ToString(), "3*x² + 2*x + 1"); + + storage.DeleteAuxiliaryObjective(o.typed_id().value()); + EXPECT_EQ(o.ToString(), kDeletedObjectiveDefaultDescription); +} + +TEST(ObjectiveTest, OutputStreaming) { + ModelStorage storage(/*model_name=*/"", /*primary_objective_name=*/"primary"); + const Variable x(&storage, storage.AddVariable("x")); + const Objective primary = Objective::Primary(&storage); + const Objective secondary = Objective::Auxiliary( + &storage, storage.AddAuxiliaryObjective(12, "secondary")); + + auto to_string = [](Objective c) { + std::ostringstream oss; + oss << c; + return oss.str(); + }; + + EXPECT_EQ(to_string(primary), "primary"); + EXPECT_EQ(to_string(secondary), "secondary"); +} + +TEST(ObjectiveTest, OutputStreamingEmptyName) { + ModelStorage storage; + const Variable x(&storage, storage.AddVariable("x")); + const Objective primary = Objective::Primary(&storage); + const Objective secondary = + Objective::Auxiliary(&storage, storage.AddAuxiliaryObjective(12)); + + auto to_string = [](Objective c) { + std::ostringstream oss; + oss << c; + return oss.str(); + }; + + EXPECT_EQ(to_string(primary), "__primary_obj__"); + EXPECT_EQ(to_string(secondary), + absl::StrCat("__aux_obj#", secondary.id().value(), "__")); +} + +TEST(ObjectiveDeathTest, CoefficientDifferentModel) { + ModelStorage storage_a; + ModelStorage storage_b; + + const Variable x_a = Variable(&storage_a, storage_a.AddVariable("x")); + const Variable y_b = Variable(&storage_b, storage_b.AddVariable("y")); + const Objective o_b = Objective::Primary(&storage_b); + + EXPECT_DEATH(o_b.coefficient(x_a), "another model"); + EXPECT_DEATH(o_b.coefficient(x_a, y_b), "another model"); + EXPECT_DEATH(o_b.coefficient(y_b, x_a), "another model"); + EXPECT_DEATH(o_b.is_coefficient_nonzero(x_a), "another model"); + EXPECT_DEATH(o_b.is_coefficient_nonzero(x_a, y_b), "another model"); + EXPECT_DEATH(o_b.is_coefficient_nonzero(y_b, x_a), "another model"); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/parameters_test.cc b/ortools/math_opt/cpp/parameters_test.cc new file mode 100644 index 00000000000..bba022fb0fa --- /dev/null +++ b/ortools/math_opt/cpp/parameters_test.cc @@ -0,0 +1,249 @@ +// Copyright 2010-2025 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/parameters.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "google/protobuf/duration.pb.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/linked_hash_map.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/enums_testing.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/solvers/glpk.pb.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::ElementsAre; +using ::testing::EqualsProto; +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::Optional; +using ::testing::Pair; +using ::testing::status::StatusIs; + +INSTANTIATE_TYPED_TEST_SUITE_P(SolverType, EnumTest, SolverType); +INSTANTIATE_TYPED_TEST_SUITE_P(LPAlgorithm, EnumTest, LPAlgorithm); +INSTANTIATE_TYPED_TEST_SUITE_P(Emphasis, EnumTest, Emphasis); + +TEST(SolverTypeTest, Flags) { + { + SolverType value = SolverType::kGlop; + std::string error; + EXPECT_TRUE(AbslParseFlag("glpk", &value, &error)); + EXPECT_EQ(value, SolverType::kGlpk); + EXPECT_EQ(error, ""); + + EXPECT_EQ(AbslUnparseFlag(value), "glpk"); + } + { + SolverType value = SolverType::kGlpk; + std::string error; + EXPECT_FALSE(AbslParseFlag("unknown", &value, &error)); + EXPECT_NE(error, ""); + } +} + +TEST(LPAlgorithmTest, Flags) { + { + LPAlgorithm value = LPAlgorithm::kPrimalSimplex; + std::string error; + EXPECT_TRUE(AbslParseFlag("primal_simplex", &value, &error)); + EXPECT_EQ(value, LPAlgorithm::kPrimalSimplex); + EXPECT_EQ(error, ""); + + EXPECT_EQ(AbslUnparseFlag(value), "primal_simplex"); + } + { + LPAlgorithm value = LPAlgorithm::kPrimalSimplex; + std::string error; + EXPECT_FALSE(AbslParseFlag("unknown", &value, &error)); + EXPECT_NE(error, ""); + } +} + +TEST(EmphasisTest, Flags) { + { + Emphasis value = Emphasis::kHigh; + std::string error; + EXPECT_TRUE(AbslParseFlag("high", &value, &error)); + EXPECT_EQ(value, Emphasis::kHigh); + EXPECT_EQ(error, ""); + + EXPECT_EQ(AbslUnparseFlag(value), "high"); + } + { + Emphasis value = Emphasis::kHigh; + std::string error; + EXPECT_FALSE(AbslParseFlag("unknown", &value, &error)); + EXPECT_NE(error, ""); + } +} + +TEST(GurobiParameters, Empty) { + GurobiParameters gurobi; + EXPECT_TRUE(gurobi.empty()); + gurobi.param_values["x"] = "dog"; + EXPECT_FALSE(gurobi.empty()); +} + +TEST(GurobiParameters, ProtoRoundTrip) { + GurobiParameters gurobi; + gurobi.param_values["x"] = "dog"; + gurobi.param_values["ab"] = "7"; + const GurobiParametersProto proto = gurobi.Proto(); + EXPECT_THAT(proto, EquivToProto(R"pb(parameters: + [ { name: "x" value: "dog" } + , { name: "ab" value: "7" }])pb")); + GurobiParameters end = GurobiParameters::FromProto(proto); + EXPECT_THAT(end.param_values, ElementsAre(Pair("x", "dog"), Pair("ab", "7"))); +} + +TEST(GurobiParameters, EmptyProtoRoundTrip) { + GurobiParameters gurobi; + const GurobiParametersProto proto = gurobi.Proto(); + EXPECT_THAT(proto, EquivToProto(GurobiParametersProto())); + GurobiParameters end = GurobiParameters::FromProto(proto); + EXPECT_THAT(end.param_values, ::testing::IsEmpty()); +} + +TEST(GlpkParameters, ProtoRoundTrip) { + // Test with `optional bool` set to true. + { + GlpkParametersProto proto; + proto.set_compute_unbound_rays_if_possible(true); + EXPECT_THAT(GlpkParameters::FromProto(proto).Proto(), EqualsProto(proto)); + } + // Test with `optional bool` set to false. + { + GlpkParametersProto proto; + proto.set_compute_unbound_rays_if_possible(false); + EXPECT_THAT(GlpkParameters::FromProto(proto).Proto(), EqualsProto(proto)); + } +} + +TEST(GlpkParameters, EmptyProtoRoundTrip) { + const GlpkParametersProto empty; + EXPECT_THAT(GlpkParameters::FromProto(empty).Proto(), EqualsProto(empty)); +} + +TEST(SolveParameters, EmptyProtoRoundTrip) { + const SolveParametersProto start; + ASSERT_OK_AND_ASSIGN(SolveParameters cpp_params, + SolveParameters::FromProto(start)); + const SolveParametersProto end = cpp_params.Proto(); + EXPECT_THAT(end, EquivToProto(start)); +} + +TEST(SolveParameters, CommonParamsRoundTrip) { + SolveParameters start; + start.enable_output = true; + start.time_limit = absl::Seconds(1); + start.iteration_limit = 7; + start.node_limit = 3; + start.relative_gap_tolerance = 1.0; + start.absolute_gap_tolerance = 0.1; + start.cutoff_limit = 50.1; + start.objective_limit = 51.1; + start.best_bound_limit = 52.1; + start.solution_limit = 17; + start.threads = 3; + start.random_seed = 12; + start.solution_pool_size = 44; + start.lp_algorithm = LPAlgorithm::kDualSimplex; + start.presolve = Emphasis::kMedium; + start.cuts = Emphasis::kOff; + start.scaling = Emphasis::kLow; + start.heuristics = Emphasis::kVeryHigh; + const SolveParametersProto proto = start.Proto(); + EXPECT_THAT(proto, EquivToProto(R"pb( + enable_output: true + time_limit: { seconds: 1 } + iteration_limit: 7 + node_limit: 3 + relative_gap_tolerance: 1.0 + absolute_gap_tolerance: 0.1 + cutoff_limit: 50.1 + objective_limit: 51.1 + best_bound_limit: 52.1 + solution_limit: 17 + threads: 3 + random_seed: 12 + solution_pool_size: 44 + lp_algorithm: LP_ALGORITHM_DUAL_SIMPLEX + presolve: EMPHASIS_MEDIUM + cuts: EMPHASIS_OFF + scaling: EMPHASIS_LOW + heuristics: EMPHASIS_VERY_HIGH + )pb")); + ASSERT_OK_AND_ASSIGN(const SolveParameters end, + SolveParameters::FromProto(proto)); + EXPECT_TRUE(end.enable_output); + EXPECT_EQ(end.time_limit, absl::Seconds(1)); + EXPECT_THAT(end.iteration_limit, Optional(7)); + EXPECT_THAT(end.node_limit, Optional(3)); + EXPECT_THAT(end.relative_gap_tolerance, Optional(1.0)); + EXPECT_THAT(end.absolute_gap_tolerance, Optional(0.1)); + EXPECT_THAT(end.cutoff_limit, Optional(50.1)); + EXPECT_THAT(end.objective_limit, Optional(51.1)); + EXPECT_THAT(end.best_bound_limit, Optional(52.1)); + EXPECT_THAT(end.solution_limit, Optional(17)); + EXPECT_THAT(end.threads, Optional(3)); + EXPECT_THAT(end.random_seed, Optional(12)); + EXPECT_THAT(end.solution_pool_size, Optional(44)); + EXPECT_THAT(end.lp_algorithm, Optional(LPAlgorithm::kDualSimplex)); + EXPECT_THAT(end.presolve, Optional(Emphasis::kMedium)); + EXPECT_THAT(end.cuts, Optional(Emphasis::kOff)); + EXPECT_THAT(end.scaling, Optional(Emphasis::kLow)); + EXPECT_THAT(end.heuristics, Optional(Emphasis::kVeryHigh)); +} + +TEST(SolveParameters, SolverSpecificParamsRoundTrip) { + SolveParametersProto start; + start.set_random_seed(7); + start.mutable_gscip()->set_num_solutions(12); + auto* p = start.mutable_gurobi()->add_parameters(); + p->set_name("x"); + p->set_value("7"); + start.mutable_glop()->set_random_seed(45); + start.mutable_cp_sat()->set_num_workers(50); + start.mutable_osqp()->set_alpha(1.2); + start.mutable_glpk()->set_compute_unbound_rays_if_possible(true); + (*start.mutable_highs()->mutable_int_options())["test_param"] = 3; + ASSERT_OK_AND_ASSIGN(SolveParameters cpp_params, + SolveParameters::FromProto(start)); + const SolveParametersProto end = cpp_params.Proto(); + EXPECT_THAT(end, EquivToProto(start)); +} + +TEST(SolveParameters, FromProtoBadTimeLimit) { + SolveParametersProto proto; + proto.mutable_time_limit()->set_seconds(1); + proto.mutable_time_limit()->set_nanos(-1); + EXPECT_THAT( + SolveParameters::FromProto(proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("time_limit"))); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solution_test.cc b/ortools/math_opt/cpp/solution_test.cc new file mode 100644 index 00000000000..092eeca9005 --- /dev/null +++ b/ortools/math_opt/cpp/solution_test.cc @@ -0,0 +1,504 @@ +// Copyright 2010-2025 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/solution.h" + +#include + +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/enums_testing.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/objective.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +INSTANTIATE_TYPED_TEST_SUITE_P(SolutionStatus, EnumTest, SolutionStatus); + +TEST(PrimalSolutionTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(2)); + + PrimalSolutionProto proto; + proto.set_objective_value(9.0); + (*proto.mutable_auxiliary_objective_values())[o.id().value()] = 3.0; + proto.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + for (int i : {0, 1}) { + proto.mutable_variable_values()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + proto.mutable_variable_values()->add_values(v); + } + + PrimalSolution expected = {.variable_values = {{x, 2.0}, {y, 1.0}}, + .objective_value = 9.0, + .auxiliary_objective_values = {{o, 3.0}}, + .feasibility_status = SolutionStatus::kInfeasible}; + + // Test one way + EXPECT_THAT(PrimalSolution::FromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); + // Test round trip + EXPECT_THAT(PrimalSolution::FromProto(&model, expected.Proto()), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); +} + +TEST(PrimalSolutionTest, InvalidVariableValues) { + ModelStorage model; + model.AddVariable("x"); + + PrimalSolutionProto proto; + proto.set_objective_value(9.0); + proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + proto.mutable_variable_values()->add_ids(0); + + EXPECT_THAT(PrimalSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid variable_values"))); +} + +TEST(PrimalSolutionTest, InvalidAuxiliaryObjectiveValues) { + ModelStorage model; + + PrimalSolutionProto proto; + (*proto.mutable_auxiliary_objective_values())[0] = 3.0; + + EXPECT_THAT(PrimalSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid auxiliary_objective_values"))); +} + +TEST(PrimalSolutionTest, FeasibilityStatusUnspecified) { + ModelStorage model; + PrimalSolutionProto proto; + EXPECT_THAT(PrimalSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("feasibility_status"))); +} + +TEST(PrimalSolutionTest, GetObjectiveValue) { + ModelStorage model; + const Objective p = Objective::Primary(&model); + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(1)); + PrimalSolution solution{.objective_value = 1.0, + .auxiliary_objective_values = {{o, 2.0}}}; + EXPECT_EQ(solution.get_objective_value(p), 1.0); + EXPECT_EQ(solution.get_objective_value(o), 2.0); +} + +TEST(PrimalSolutionDeathTest, GetObjectiveValueWrongModel) { + ModelStorage model_a; + const Variable v_a(&model_a, model_a.AddVariable("v")); + const Objective o_a = + Objective::Auxiliary(&model_a, model_a.AddAuxiliaryObjective(1)); + PrimalSolution solution{.objective_value = 1.0, + .auxiliary_objective_values = {{o_a, 2.0}}}; + + ModelStorage model_b; + const Objective p_b = Objective::Primary(&model_b); + const Objective o_b = + Objective::Auxiliary(&model_b, model_b.AddAuxiliaryObjective(1)); + // This is a documented corner case where we don't CHECK the model. + EXPECT_EQ(solution.get_objective_value(p_b), 1.0); + EXPECT_DEATH(solution.get_objective_value(o_b), ""); + + solution.variable_values.insert({v_a, 3.0}); + EXPECT_DEATH(solution.get_objective_value(p_b), ""); + EXPECT_DEATH(solution.get_objective_value(o_b), ""); +} + +TEST(PrimalRayTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + + PrimalRayProto proto; + for (int i : {0, 1}) { + proto.mutable_variable_values()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + proto.mutable_variable_values()->add_values(v); + } + + const PrimalRay expected = {.variable_values = {{x, 2.0}, {y, 1.0}}}; + + // Test one way + EXPECT_THAT(PrimalRay::FromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); + // Test round trip + EXPECT_THAT(PrimalRay::FromProto(&model, expected.Proto()), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); +} + +TEST(PrimalRayTest, InvalidVariableValues) { + ModelStorage model; + model.AddVariable("x"); + + PrimalRayProto proto; + proto.mutable_variable_values()->add_ids(0); + + EXPECT_THAT(PrimalRay::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid variable_values"))); +} + +TEST(DualSolutionTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + const LinearConstraint c(&model, model.AddLinearConstraint("c")); + const LinearConstraint d(&model, model.AddLinearConstraint("d")); + const QuadraticConstraint e( + &model, model.AddAtomicConstraint(QuadraticConstraintData{.name = "e"})); + const QuadraticConstraint f( + &model, model.AddAtomicConstraint(QuadraticConstraintData{.name = "f"})); + + DualSolutionProto proto; + proto.set_objective_value(9.0); + proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + for (int i : {0, 1}) { + proto.mutable_reduced_costs()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + proto.mutable_reduced_costs()->add_values(v); + } + for (int i : {0, 1}) { + proto.mutable_dual_values()->add_ids(i); + } + for (double v : {3.0, 4.0}) { + proto.mutable_dual_values()->add_values(v); + } + for (int i : {0, 1}) { + proto.mutable_quadratic_dual_values()->add_ids(i); + } + for (double v : {5.0, 6.0}) { + proto.mutable_quadratic_dual_values()->add_values(v); + } + + const DualSolution expected = { + .dual_values = {{c, 3.0}, {d, 4.0}}, + .quadratic_dual_values = {{e, 5.0}, {f, 6.0}}, + .reduced_costs = {{x, 2.0}, {y, 1.0}}, + .objective_value = 9.0, + .feasibility_status = SolutionStatus::kFeasible}; + + // Test one way + EXPECT_THAT(DualSolution::FromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); + // Test round trip + EXPECT_THAT(DualSolution::FromProto(&model, expected.Proto()), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); +} + +TEST(DualSolutionTest, InvalidDualValues) { + ModelStorage model; + model.AddLinearConstraint("c"); + + DualSolutionProto proto; + proto.set_objective_value(9.0); + proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + proto.mutable_dual_values()->add_ids(0); + + EXPECT_THAT(DualSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid dual_values"))); +} + +TEST(DualSolutionTest, InvalidReducedCosts) { + ModelStorage model; + model.AddVariable("x"); + + DualSolutionProto proto; + proto.set_objective_value(9.0); + proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + proto.mutable_reduced_costs()->add_ids(0); + + EXPECT_THAT(DualSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid reduced_costs"))); +} + +TEST(DualSolutionTest, InvalidQuadraticDualValues) { + ModelStorage model; + const QuadraticConstraint c( + &model, model.AddAtomicConstraint(QuadraticConstraintData{.name = "c"})); + + DualSolutionProto proto; + proto.set_objective_value(9.0); + proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + proto.mutable_quadratic_dual_values()->add_ids(0); + + EXPECT_THAT(DualSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid quadratic_dual_values"))); +} + +TEST(DualSolutionTest, FeasibilityStatusUnspecified) { + ModelStorage model; + DualSolutionProto proto; + EXPECT_THAT(DualSolution::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("feasibility_status"))); +} + +TEST(DualRayTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + const LinearConstraint c(&model, model.AddLinearConstraint("c")); + const LinearConstraint d(&model, model.AddLinearConstraint("d")); + + DualRayProto proto; + for (int i : {0, 1}) { + proto.mutable_reduced_costs()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + proto.mutable_reduced_costs()->add_values(v); + } + for (int i : {0, 1}) { + proto.mutable_dual_values()->add_ids(i); + } + for (double v : {3.0, 4.0}) { + proto.mutable_dual_values()->add_values(v); + } + + const DualRay expected = {.dual_values = {{c, 3.0}, {d, 4.0}}, + .reduced_costs = {{x, 2.0}, {y, 1.0}}}; + + // Test one way + EXPECT_THAT(DualRay::FromProto(&model, proto), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); + // Test round trip + EXPECT_THAT(DualRay::FromProto(&model, expected.Proto()), + IsOkAndHolds(IsNear(expected, /*tolerance=*/0))); +} + +TEST(DualRayTest, InvalidDualValues) { + ModelStorage model; + model.AddLinearConstraint("c"); + + DualRayProto proto; + proto.mutable_dual_values()->add_ids(0); + + EXPECT_THAT(DualRay::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid dual_values"))); +} + +TEST(DualRayTest, InvalidReducedCosts) { + ModelStorage model; + model.AddVariable("x"); + + DualRayProto proto; + proto.mutable_reduced_costs()->add_ids(0); + + EXPECT_THAT(DualRay::FromProto(&model, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid reduced_costs"))); +} + +TEST(BasisTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + const LinearConstraint c(&model, model.AddLinearConstraint("c")); + const LinearConstraint d(&model, model.AddLinearConstraint("d")); + + BasisProto proto; + for (int i : {0, 1}) { + proto.mutable_variable_status()->add_ids(i); + } + proto.mutable_variable_status()->add_values(BASIS_STATUS_AT_UPPER_BOUND); + proto.mutable_variable_status()->add_values(BASIS_STATUS_FREE); + for (int i : {0, 1}) { + proto.mutable_constraint_status()->add_ids(i); + } + proto.mutable_constraint_status()->add_values(BASIS_STATUS_AT_LOWER_BOUND); + proto.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); + proto.set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE); + + const Basis expected = {.constraint_status = {{c, BasisStatus::kAtLowerBound}, + {d, BasisStatus::kBasic}}, + .variable_status = {{x, BasisStatus::kAtUpperBound}, + {y, BasisStatus::kFree}}, + .basic_dual_feasibility = SolutionStatus::kFeasible}; + + EXPECT_OK(expected.CheckModelStorage(&model)); + + // Test one way + EXPECT_THAT(Basis::FromProto(&model, proto), IsOkAndHolds(BasisIs(expected))); + // Test round trip + EXPECT_THAT(Basis::FromProto(&model, expected.Proto()), + IsOkAndHolds(BasisIs(expected))); +} + +TEST(BasisTest, VariablesAndConstraintsDifferentModels) { + ModelStorage model_a; + const Variable a_x(&model_a, model_a.AddVariable("x")); + ModelStorage model_b; + const LinearConstraint b_c(&model_b, model_b.AddLinearConstraint("c")); + + const Basis basis = {.constraint_status = {{b_c, BasisStatus::kAtLowerBound}}, + .variable_status = {{a_x, BasisStatus::kAtUpperBound}}}; + + EXPECT_THAT(basis.CheckModelStorage(&model_a), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + EXPECT_THAT(basis.CheckModelStorage(&model_b), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(BasisTest, FromProtoUnspecifiedBasicDualFeasibility) { + ModelStorage storage; + BasisProto proto; + ASSERT_OK_AND_ASSIGN(const Basis basis, Basis::FromProto(&storage, proto)); + EXPECT_THAT(basis.variable_status, IsEmpty()); + EXPECT_THAT(basis.constraint_status, IsEmpty()); + EXPECT_EQ(basis.basic_dual_feasibility, std::nullopt); +} + +TEST(SolutionTest, ProtoRoundTripTest) { + ModelStorage model; + const Variable x(&model, model.AddVariable("x")); + const Variable y(&model, model.AddVariable("y")); + const LinearConstraint c(&model, model.AddLinearConstraint("c")); + const LinearConstraint d(&model, model.AddLinearConstraint("d")); + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(2)); + + SolutionProto proto; + + PrimalSolutionProto& primal_proto = *proto.mutable_primal_solution(); + primal_proto.set_objective_value(9.0); + (*primal_proto.mutable_auxiliary_objective_values())[o.id().value()] = 2.0; + primal_proto.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + for (int i : {0, 1}) { + primal_proto.mutable_variable_values()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + primal_proto.mutable_variable_values()->add_values(v); + } + + DualSolutionProto& dual_proto = *proto.mutable_dual_solution(); + dual_proto.set_objective_value(9.0); + dual_proto.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + for (int i : {0, 1}) { + dual_proto.mutable_reduced_costs()->add_ids(i); + } + for (double v : {2.0, 1.0}) { + dual_proto.mutable_reduced_costs()->add_values(v); + } + for (int i : {0, 1}) { + dual_proto.mutable_dual_values()->add_ids(i); + } + for (double v : {3.0, 4.0}) { + dual_proto.mutable_dual_values()->add_values(v); + } + + BasisProto& basis_proto = *proto.mutable_basis(); + for (int i : {0, 1}) { + basis_proto.mutable_variable_status()->add_ids(i); + } + basis_proto.mutable_variable_status()->add_values( + BASIS_STATUS_AT_UPPER_BOUND); + basis_proto.mutable_variable_status()->add_values(BASIS_STATUS_FREE); + for (int i : {0, 1}) { + basis_proto.mutable_constraint_status()->add_ids(i); + } + basis_proto.mutable_constraint_status()->add_values( + BASIS_STATUS_AT_LOWER_BOUND); + basis_proto.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); + basis_proto.set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE); + + const Solution expected{ + .primal_solution = + PrimalSolution{.variable_values = {{x, 2.0}, {y, 1.0}}, + .objective_value = 9.0, + .auxiliary_objective_values = {{o, 2.0}}, + .feasibility_status = SolutionStatus::kInfeasible}, + .dual_solution = + DualSolution{.dual_values = {{c, 3.0}, {d, 4.0}}, + .reduced_costs = {{x, 2.0}, {y, 1.0}}, + .objective_value = 9.0, + .feasibility_status = SolutionStatus::kFeasible}, + .basis = Basis{.constraint_status = {{c, BasisStatus::kAtLowerBound}, + {d, BasisStatus::kBasic}}, + .variable_status = {{x, BasisStatus::kAtUpperBound}, + {y, BasisStatus::kFree}}, + .basic_dual_feasibility = SolutionStatus::kFeasible}}; + + // Test one way + EXPECT_THAT( + Solution::FromProto(&model, proto), + IsOkAndHolds(IsNear(expected, SolutionMatcherOptions{.tolerance = 0.0}))); + // Test round trip + EXPECT_THAT( + Solution::FromProto(&model, expected.Proto()), + IsOkAndHolds(IsNear(expected, SolutionMatcherOptions{.tolerance = 0.0}))); +} + +TEST(SolutionTest, FromProtoInvalidPrimalSolution) { + ModelStorage storage; + storage.AddVariable("x"); + SolutionProto proto; + proto.mutable_primal_solution()->mutable_variable_values()->add_ids(0); + proto.mutable_primal_solution()->set_feasibility_status( + SOLUTION_STATUS_FEASIBLE); + EXPECT_THAT(Solution::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("primal_solution"))); +} + +TEST(SolutionTest, FromProtoInvalidDualSolution) { + ModelStorage storage; + storage.AddLinearConstraint("c"); + SolutionProto proto; + proto.mutable_dual_solution()->mutable_dual_values()->add_ids(0); + proto.mutable_dual_solution()->set_feasibility_status( + SOLUTION_STATUS_FEASIBLE); + EXPECT_THAT( + Solution::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("dual_solution"))); +} + +TEST(SolutionTest, FromProtoInvalidBasis) { + ModelStorage storage; + storage.AddVariable("c"); + SolutionProto proto; + proto.mutable_basis()->mutable_variable_status()->add_ids(0); + EXPECT_THAT(Solution::FromProto(&storage, proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("basis"))); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solve_arguments_test.cc b/ortools/math_opt/cpp/solve_arguments_test.cc new file mode 100644 index 00000000000..0451cd38723 --- /dev/null +++ b/ortools/math_opt/cpp/solve_arguments_test.cc @@ -0,0 +1,97 @@ +// Copyright 2010-2025 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/solve_arguments.h" + +#include +#include + +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::HasSubstr; +using ::testing::status::StatusIs; + +TEST(CheckModelStorageAndCallbackTest, CorrectModelAndCallback) { + Model model; + const Variable x = model.AddVariable("x"); + + const SolveArguments args = { + .model_parameters = ModelSolveParameters::OnlySomePrimalVariables({x}), + .callback_registration = + { + .events = {CallbackEvent::kMipSolution}, + .mip_solution_filter = MakeKeepKeysFilter({x}), + }, + .callback = [](const CallbackData&) { return CallbackResult{}; }, + }; + + EXPECT_OK(args.CheckModelStorageAndCallback(model.storage())); +} + +TEST(CheckModelStorageAndCallbackTest, WrongModelInModelParameters) { + Model model; + const Variable x = model.AddVariable("x"); + + const SolveArguments args = { + .model_parameters = ModelSolveParameters::OnlySomePrimalVariables({x}), + }; + + Model other_model; + EXPECT_THAT(args.CheckModelStorageAndCallback(other_model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("model_parameters"))); +} + +TEST(CheckModelStorageAndCallbackTest, WrongModelInCallbackRegistration) { + Model model; + const Variable x = model.AddVariable("x"); + + const SolveArguments args = { + .callback_registration = + { + .events = {CallbackEvent::kMipSolution}, + .mip_solution_filter = MakeKeepKeysFilter({x}), + }, + .callback = [](const CallbackData&) { return CallbackResult{}; }, + }; + + Model other_model; + EXPECT_THAT(args.CheckModelStorageAndCallback(other_model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("callback_registration"), + HasSubstr("mip_solution_filter")))); +} + +TEST(CheckModelStorageAndCallbackTest, NoCallbackWithRegisteredEvents) { + Model model; + + const SolveArguments args = { + .callback_registration = + { + .events = {CallbackEvent::kMipSolution}, + }, + }; + + EXPECT_THAT( + args.CheckModelStorageAndCallback(model.storage()), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no callback"))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/solve_impl_test.cc b/ortools/math_opt/cpp/solve_impl_test.cc new file mode 100644 index 00000000000..12f264ddc7b --- /dev/null +++ b/ortools/math_opt/cpp/solve_impl_test.cc @@ -0,0 +1,1865 @@ +// Copyright 2010-2025 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/solve_impl.h" + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/functional/any_invocable.h" +#include "absl/log/die_if_null.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/base_solver.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/sparse_collection_matchers.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/math_opt.h" +#include "ortools/math_opt/cpp/model.h" +#include "ortools/math_opt/cpp/update_result.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research::math_opt::internal { +namespace { + +using ::testing::_; +using ::testing::ByMove; +using ::testing::Eq; +using ::testing::EquivToProto; +using ::testing::Field; +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Mock; +using ::testing::Ne; +using ::testing::Optional; +using ::testing::Pair; +using ::testing::Return; +using ::testing::UnorderedElementsAre; +using ::testing::status::StatusIs; + +class BaseSolverMock : public BaseSolver { + public: + MOCK_METHOD(absl::StatusOr, Solve, + (const SolveArgs& arguments), (override)); + + MOCK_METHOD(absl::StatusOr, + ComputeInfeasibleSubsystem, + (const ComputeInfeasibleSubsystemArgs& arguments), (override)); + + MOCK_METHOD(absl::StatusOr, Update, (ModelUpdateProto model_update), + (override)); +}; + +// Mock for BaseSolverFactory. +using BaseSolverFactoryMock = + testing::MockFunction>( + SolverTypeProto solver_type, const ModelProto& model, + SolveInterrupter* local_canceller)>; + +// Delegate all calls to another instance of BaseSolver. +// +// This is used as a return value in BaseSolverFactoryMock as: +// * this function needs to return a unique_ptr +// * but we want to be able to use a BaseSolverMock from the stack. +// +// Thus we can simply mock BaseSolverFactoryMock with +// BaseSolverMock solver; +// +// EXPECT_CALL(factory_mock, +// Call(EquivToProto(basic_lp.model.ExportModel()), _, _)) +// .WillOnce(Return(ByMove( +// std::make_unique(&solver)))); +// +// and add other EXPECT_CALL(solver, ...) on the instance that exists on stack. +class DelegatingBaseSolver : public BaseSolver { + public: + // Wraps the input solver interface, delegating calls to it. The optional + // destructor_cb callback will be called in ~DelegatingBaseSolver(). + explicit DelegatingBaseSolver( + BaseSolver* const solver, + absl::AnyInvocable destructor_cb = nullptr) + : solver_(ABSL_DIE_IF_NULL(solver)), + destructor_cb_(std::move(destructor_cb)) {} + + ~DelegatingBaseSolver() override { + if (destructor_cb_ != nullptr) { + destructor_cb_(); + } + } + + absl::StatusOr Solve(const SolveArgs& arguments) override { + return solver_->Solve(arguments); + } + + absl::StatusOr + ComputeInfeasibleSubsystem( + const ComputeInfeasibleSubsystemArgs& arguments) override { + return solver_->ComputeInfeasibleSubsystem(arguments); + } + + absl::StatusOr Update(ModelUpdateProto model_update) override { + return solver_->Update(std::move(model_update)); + } + + private: + BaseSolver* const solver_; + absl::AnyInvocable destructor_cb_; +}; + +// Returns a matcher that matches the fields of SolveArgs according to the +// provided matchers. +// +// Note that we have to use template parameters for function/data pointers +// fields as we can't use testing::Matcher or +// testing::Matcher. +template +testing::Matcher SolveArgsAre( + testing::Matcher parameters, + testing::Matcher model_parameters, + MessageCallbackMatcher message_callback, + testing::Matcher callback_registration, + CallbackMatcher user_cb, SolveInterrupterPtrMatcher interrupter) { + return AllOf( + Field("parameters", &BaseSolver::SolveArgs::parameters, parameters), + Field("model_parameters", &BaseSolver::SolveArgs::model_parameters, + model_parameters), + Field("message_callback", &BaseSolver::SolveArgs::message_callback, + message_callback), + Field("callback_registration", + &BaseSolver::SolveArgs::callback_registration, + callback_registration), + Field("user_cb", &BaseSolver::SolveArgs::user_cb, user_cb), + Field("interrupter", &BaseSolver::SolveArgs::interrupter, interrupter)); +} + +// Returns a matcher that matches the fields of ComputeInfeasibleSubsystemArgs +// according to the provided matchers. +// +// Note that we have to use template parameters for function/data pointers +// fields as we can't use testing::Matcher or +// testing::Matcher. +template +testing::Matcher +ComputeInfeasibleSubsystemArgsAre( + testing::Matcher parameters, + MessageCallbackMatcher message_callback, + SolveInterrupterPtrMatcher interrupter) { + return AllOf( + Field("parameters", + &BaseSolver::ComputeInfeasibleSubsystemArgs::parameters, + parameters), + Field("message_callback", + &BaseSolver::ComputeInfeasibleSubsystemArgs::message_callback, + message_callback), + Field("interrupter", + &BaseSolver::ComputeInfeasibleSubsystemArgs::interrupter, + interrupter)); +} + +constexpr double kInf = std::numeric_limits::infinity(); + +// Basic LP model: +// +// a and b are continuous variable +// +// minimize a - b +// s.t. 0 <= a +// 0 <= b <= 3 +struct BasicLp { + BasicLp(); + + // Sets the upper bound of variable b to 2.0 and returns the corresponding + // update. + std::optional UpdateUpperBoundOfB(); + + // Returns the expected optimal result for this model. Only put the given set + // of variables in the result (to test filters). When `after_update` is true, + // returns the optimal result after UpdateUpperBoundOfB() has been called. + SolveResultProto OptimalResult(const absl::flat_hash_set& vars, + bool after_update = false) const; + + Model model; + const Variable a; + const Variable b; +}; + +BasicLp::BasicLp() + : a(model.AddVariable(0.0, kInf, false, "a")), + b(model.AddVariable(0.0, 3.0, false, "b")) {} + +std::optional BasicLp::UpdateUpperBoundOfB() { + const std::unique_ptr tracker = model.NewUpdateTracker(); + model.set_upper_bound(b, 2.0); + return tracker->ExportModelUpdate().value(); +} + +SolveResultProto BasicLp::OptimalResult( + const absl::flat_hash_set& vars, bool after_update) const { + SolveResultProto result; + result.mutable_termination()->set_reason(TERMINATION_REASON_OPTIMAL); + result.mutable_solve_stats()->mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_solve_stats()->mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + PrimalSolutionProto* const solution = + result.add_solutions()->mutable_primal_solution(); + solution->set_objective_value(0.0); + solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + if (vars.contains(a)) { + solution->mutable_variable_values()->add_ids(a.id()); + solution->mutable_variable_values()->add_values(0.0); + } + if (vars.contains(b)) { + solution->mutable_variable_values()->add_ids(b.id()); + solution->mutable_variable_values()->add_values(after_update ? 2.0 : 3.0); + } + return result; +} + +// Basic infeasible LP model: +// +// minimize 0 +// s.t. x <= -1 (linear constraint) +// 0 <= x <= 1 (bounds) +struct BasicInfeasibleLp { + BasicInfeasibleLp() + : x(model.AddContinuousVariable(0.0, 1.0, "x")), + c(model.AddLinearConstraint(x <= -1.0, "c")) {} + + ComputeInfeasibleSubsystemResultProto InfeasibleResult() const { + ComputeInfeasibleSubsystemResultProto result; + result.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE); + (*result.mutable_infeasible_subsystem()->mutable_variable_bounds())[0] + .set_lower(true); + (*result.mutable_infeasible_subsystem()->mutable_variable_bounds())[0] + .set_upper(false); + (*result.mutable_infeasible_subsystem()->mutable_linear_constraints())[0] + .set_lower(false); + (*result.mutable_infeasible_subsystem()->mutable_linear_constraints())[0] + .set_upper(true); + result.set_is_minimal(true); + return result; + } + + // Sets the upper bound of constraint c to -2.0 and returns the corresponding + // update. + std::optional UpdateUpperBoundOfC() { + const std::unique_ptr tracker = model.NewUpdateTracker(); + model.set_upper_bound(c, -2.0); + return tracker->ExportModelUpdate().value(); + } + + Model model; + const Variable x; + const LinearConstraint c; +}; + +// Test calling Solve() without any callback. +TEST(SolveImplTest, SuccessfulSolveNoCallback) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolveInterrupter interrupter; + args.interrupter = &interrupter; + + args.message_callback = [](absl::Span) {}; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), Ne(nullptr))) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(SolveArgsAre( + EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(&interrupter)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, + /*remove_names=*/false)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling Solve() with a callback. +TEST(SolveImplTest, SuccessfulSolveWithCallback) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + args.user_cb(cb_data); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL( + factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(SolveArgsAre( + EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr)))) + .WillOnce(fake_solve); + } + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, /*remove_names=*/false)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +TEST(SolveImplTest, RemoveNamesSendsNoNames) { + Model model; + model.AddBinaryVariable("x"); + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + SolveResultProto fake_result; + *fake_result.mutable_termination() = + NoSolutionFoundTerminationProto(/*is_maximize=*/false, LIMIT_TIME); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(expected_model), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL(solver, Solve(SolveArgsAre(_, _, _, _, _, _))) + .WillOnce(Return(fake_result)); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + SolveImpl(factory_mock.AsStdFunction(), model, SolverType::kGlop, {}, + /*user_canceller=*/nullptr, /*remove_names=*/true)); +} + +// Test calling Solve() with a solver that fails to returns the SolverInterface +// for a given model. +TEST(SolveImplTest, FailingSolveInstantiation) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("instantiation failed")))); + + ASSERT_THAT(SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, + SolverType::kGlop, args, + /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInternal, "instantiation failed")); +} + +// Test calling Solve() with a solver that returns an error on +// SolverInterface::Solve(). +TEST(SolveImplTest, FailingSolve) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL( + factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(SolveArgsAre( + EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(absl::InternalError("solve failed"))); + } + + ASSERT_THAT( + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +TEST(SolveImplTest, NullCallback) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT( + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no callback was provided"))); +} + +TEST(SolveImplTest, WrongModelInModelParameters) { + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({other_basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT( + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(SolveImplTest, WrongModelInCallbackRegistration) { + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.callback_registration.mip_solution_filter = + MakeKeepKeysFilter({other_basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT( + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + args, /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(SolveImplTest, WrongModelInCallbackResult) { + // We repeat the same test but either return a valid result or an error in + // fake_solve. + for (const bool return_an_error : {false, true}) { + SCOPED_TRACE(return_an_error ? "with fake_solve returning an error" + : "with fake_solve returning a result"); + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + // Will be set to the provided local_canceller in the factory. + SolveInterrupter* provided_local_canceller = nullptr; + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + CallbackResultProto result = args.user_cb(cb_data); + // Errors in callback should result in early termination. + EXPECT_TRUE(result.terminate()); + // Errors in callback should trigger the cancellation. + EXPECT_TRUE(provided_local_canceller->IsInterrupted()); + // The returned value should be ignored. + if (return_an_error) { + return absl::CancelledError("solver has been cancelled"); + } + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL( + factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce([&](const SolverTypeProto solver_type, + const ModelProto& model, + SolveInterrupter* const local_canceller) { + provided_local_canceller = local_canceller; + return std::make_unique(&solver); + }); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(SolveArgsAre( + EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr)))) + .WillOnce(fake_solve); + } + + args.callback = [&](const CallbackData& callback_data) { + CallbackResult result; + // We use the wrong model here. + result.AddLazyConstraint(other_basic_lp.a + other_basic_lp.b <= 3); + return result; + }; + + EXPECT_THAT(SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, + SolverType::kGlop, args, /*user_canceller=*/nullptr, + /*remove_names=*/false), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); + } +} + +TEST(SolveImplTest, UserCancellation) { + BasicLp basic_lp; + + // Will be set to the provided local_canceller in the factory. + SolveInterrupter* provided_local_canceller = nullptr; + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + // The solver should have been cancelled before its Solve() is called. + EXPECT_TRUE(provided_local_canceller->IsInterrupted()); + return absl::CancelledError("solver has been cancelled"); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, _, Ne(nullptr))) + .WillOnce([&](const SolverTypeProto solver_type, + const ModelProto& model, + SolveInterrupter* const local_canceller) { + provided_local_canceller = local_canceller; + return std::make_unique(&solver); + }); + + EXPECT_CALL(solver, Solve(_)).WillOnce(fake_solve); + } + + SolveInterrupter user_canceller; + user_canceller.Interrupt(); + + ASSERT_THAT( + SolveImpl(factory_mock.AsStdFunction(), basic_lp.model, SolverType::kGlop, + {}, /*user_canceller=*/&user_canceller, + /*remove_names=*/false), + StatusIs(absl::StatusCode::kCancelled)); +} + +TEST(ComputeInfeasibleSubsystemImplTest, SuccessfulCall) { + BasicInfeasibleLp lp; + + ComputeInfeasibleSubsystemArguments args; + args.parameters.enable_output = true; + + SolveInterrupter interrupter; + args.interrupter = &interrupter; + + args.message_callback = [](absl::Span) {}; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL(solver, + ComputeInfeasibleSubsystem(ComputeInfeasibleSubsystemArgsAre( + EquivToProto(args.parameters.Proto()), Ne(nullptr), + Eq(&interrupter)))) + .WillOnce(Return(lp.InfeasibleResult())); + } + + ASSERT_OK_AND_ASSIGN( + const ComputeInfeasibleSubsystemResult result, + ComputeInfeasibleSubsystemImpl( + factory_mock.AsStdFunction(), lp.model, SolverType::kGlop, args, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + EXPECT_EQ(result.feasibility, FeasibilityStatus::kInfeasible); +} + +TEST(ComputeInfeasibleSubsystemImplTest, FailingSolve) { + BasicInfeasibleLp lp; + + ComputeInfeasibleSubsystemArguments args; + args.parameters.enable_output = true; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL( + solver, + ComputeInfeasibleSubsystem(ComputeInfeasibleSubsystemArgsAre( + EquivToProto(args.parameters.Proto()), Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(absl::InternalError("infeasible subsystem failed"))); + } + + ASSERT_THAT( + ComputeInfeasibleSubsystemImpl( + factory_mock.AsStdFunction(), lp.model, SolverType::kGlop, args, + /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInternal, "infeasible subsystem failed")); +} + +TEST(ComputeInfeasibleSubsystemImplTest, UserCancellation) { + BasicLp basic_lp; + + // Will be set to the provided local_canceller in the factory. + SolveInterrupter* provided_local_canceller = nullptr; + + const auto fake_solve = + [&](const BaseSolver::ComputeInfeasibleSubsystemArgs& args) + -> absl::StatusOr { + // The solver should have been cancelled before its + // ComputeInfeasibleSubsystem() is called. + EXPECT_TRUE(provided_local_canceller->IsInterrupted()); + return absl::CancelledError("solver has been cancelled"); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), Ne(nullptr))) + .WillOnce([&](const SolverTypeProto solver_type, + const ModelProto& model, + SolveInterrupter* const local_canceller) { + provided_local_canceller = local_canceller; + return std::make_unique(&solver); + }); + + EXPECT_CALL(solver, ComputeInfeasibleSubsystem(_)).WillOnce(fake_solve); + } + + SolveInterrupter user_canceller; + user_canceller.Interrupt(); + + ASSERT_THAT(ComputeInfeasibleSubsystemImpl( + factory_mock.AsStdFunction(), basic_lp.model, + SolverType::kGlop, {}, /*user_canceller=*/&user_canceller, + /*remove_names=*/false), + StatusIs(absl::StatusCode::kCancelled)); +} + +TEST(IncrementalSolverImplTest, NullModel) { + BaseSolverFactoryMock factory_mock; + + EXPECT_THAT(IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), nullptr, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("model"))); +} + +TEST(IncrementalSolverImplTest, SolverType) { + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + BasicLp basic_lp; + + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), + Ne(nullptr))) + .WillOnce( + Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr incremental_solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + EXPECT_EQ(incremental_solver->solver_type(), SolverType::kGlop); +} + +// Test calling IncrementalSolver without any callback with a succeeding +// non-empty update. +TEST(IncrementalSolverImplTest, IncrementalSolveNoCallback) { + BasicLp basic_lp; + + BaseSolverMock solver_interface; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolveInterrupter interrupter; + args_1.interrupter = &interrupter; + + BaseSolverFactoryMock factory_mock; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(&interrupter)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(true)); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}, + /*after_update=*/true))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 2.0))); +} + +TEST(IncrementalSolverImplTest, RemoveNamesSendsNoNamesOnModel) { + Model model; + model.AddBinaryVariable("x"); + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(expected_model), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + EXPECT_OK(IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/true)); +} + +TEST(IncrementalSolverImplTest, RemoveNamesSendsNoNamesOnModelUpdate) { + Model model; + + SolveArguments args; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, _, _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + IncrementalSolverImpl::New(factory_mock.AsStdFunction(), + &model, SolverType::kGlop, + /*user_canceller=*/nullptr, + /*remove_names=*/true)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + model.AddBinaryVariable("x"); + + ModelUpdateProto expected_update; + expected_update.mutable_new_variables()->add_ids(0); + expected_update.mutable_new_variables()->add_lower_bounds(0.0); + expected_update.mutable_new_variables()->add_upper_bounds(1.0); + expected_update.mutable_new_variables()->add_integers(true); + + EXPECT_CALL(solver_interface, Update(EquivToProto(expected_update))) + .WillOnce(Return(true)); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); +} + +TEST(IncrementalSolverImplTest, RemoveNamesOnFullModelAfterUpdateFails) { + Model model; + + SolveArguments args; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, _, _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + IncrementalSolverImpl::New(factory_mock.AsStdFunction(), + &model, SolverType::kGlop, + /*user_canceller=*/nullptr, + /*remove_names=*/true)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + model.AddBinaryVariable("x"); + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + EXPECT_CALL(solver_interface, Update(_)).WillOnce(Return(false)); + BaseSolverMock solver_interface2; + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(expected_model), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface2)))); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_FALSE(update_result.did_update); +} + +// Test calling IncrementalSolver without any callback with an empty update. +TEST(IncrementalSolverImplTest, IncrementalSolveWithEmptyUpdate) { + BasicLp basic_lp; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}))); + } + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 3.0))); +} + +// Test calling IncrementalSolver without any callback and with a failing +// update; thus resulting in the re-creation of the solver instead. +TEST(IncrementalSolverImplTest, IncrementalSolveWithFailedUpdate) { + BasicLp basic_lp; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_1; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver_1)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_1, Solve(SolveArgsAre( + EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + BaseSolverMock solver_2; + + { + InSequence s; + + EXPECT_CALL(solver_1, Update(EquivToProto(*update))) + .WillOnce(Return(false)); + + EXPECT_CALL( + factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver_2)))); + } + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_FALSE(update_result.did_update); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + Mock::VerifyAndClearExpectations(&solver_2); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL(solver_2, Solve(SolveArgsAre( + EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}, + /*after_update=*/true))); + + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 2.0))); +} + +// Test calling IncrementalSolver without any callback and with an impossible +// update, i.e. an update that contains an unsupported feature. +TEST(IncrementalSolverImplTest, IncrementalSolveWithImpossibleUpdate) { + BasicLp basic_lp; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_1; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce( + Return(ByMove(std::make_unique(&solver_1)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_1, Solve(SolveArgsAre( + EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + { + InSequence s; + + // The solver will refuse the update with the unsupported feature. + EXPECT_CALL(solver_1, Update(EquivToProto(*update))) + .WillOnce(Return(false)); + + // The solver factory will fail for the same reason. + EXPECT_CALL( + factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("*unsupported model*")))); + } + + ASSERT_THAT(solver->Update(), + StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*unsupported model*"), + HasSubstr("solver re-creation failed")))); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + // Next calls should fail and not crash. Note that since we failed recreating + // a new solver we still will use solver_1; and this solver will return an + // error. + + EXPECT_CALL(solver_1, Update(_)) + .WillOnce( + Return(ByMove(absl::InvalidArgumentError("previous call failed")))); + + basic_lp.model.set_lower_bound(basic_lp.a, -3.0); + EXPECT_THAT(solver->Update(), StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("update failed"))); +} + +// Test calling IncrementalSolver with a callback. We don't test calling +// Update() here since only the Solve() function takes a callback. +TEST(IncrementalSolverImplTest, SuccessfulSolveWithCallback) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + args.user_cb(cb_data); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL( + factory_mock, + + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr)))) + .WillOnce(fake_solve); + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN(const SolveResult result, + solver->SolveWithoutUpdate(args)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling IncrementalSolver with a solver that fails to returns the +// SolverInterface for a given model. +TEST(IncrementalSolverImplTest, FailingSolverInstantiation) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("instantiation failed")))); + + ASSERT_THAT(IncrementalSolverImpl::New(factory_mock.AsStdFunction(), + &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, + /*remove_names=*/false), + StatusIs(absl::StatusCode::kInternal, "instantiation failed")); +} + +// Test calling IncrementalSolver with a solver that returns an error on +// SolverInterface::Solve(). +TEST(IncrementalSolverImplTest, FailingSolver) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(absl::InternalError("solve failed"))); + + ASSERT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +// Test calling IncrementalSolver with a solver that returns an error on +// SolverInterface::Update(). +TEST(IncrementalSolverImplTest, FailingSolverUpdate) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(absl::InternalError("*update failure*"))); + + ASSERT_THAT(solver->Update(), StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*update failure*"), + HasSubstr("update failed")))); +} + +// Test calling IncrementalSolver::Solve() with a callback and a non trivial +// update. +TEST(IncrementalSolverImplTest, UpdateAndSolve) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + args.user_cb(cb_data); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL( + factory_mock, + + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + // Update the model before calling Solve(). + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + { + InSequence s; + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(true)); + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr)))) + .WillOnce(fake_solve); + } + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling IncrementalSolver::Solve() with a solver that returns an error +// on SolverInterface::Solve(). +TEST(IncrementalSolverImplTest, UpdateAndSolveWithFailingSolver) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(absl::InternalError("solve failed"))); + + ASSERT_THAT(solver->Solve(args), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +// Test calling IncrementalSolver::Solve() with a solver that returns an error +// on SolverInterface::Update(). +TEST(IncrementalSolverImplTest, UpdateAndSolveWithFailingSolverUpdate) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(absl::InternalError("*update failure*"))); + + ASSERT_THAT(solver->Solve({}), StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*update failure*"), + HasSubstr("update failed")))); +} + +TEST(IncrementalSolverImplTest, NullCallback) { + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no callback was provided"))); +} + +TEST(IncrementalSolverImplTest, WrongModelInModelParameters) { + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({other_basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverImplTest, WrongModelInCallbackRegistration) { + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.callback_registration.mip_solution_filter = + MakeKeepKeysFilter({other_basic_lp.a}); + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + EXPECT_CALL( + factory_mock, + + Call(SOLVER_TYPE_GLOP, EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverImplTest, WrongModelInCallbackResult) { + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = [&](const BaseSolver::SolveArgs& args) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + args.user_cb(cb_data); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver_interface; + + args.callback = [&](const CallbackData& callback_data) { + CallbackResult result; + // We use the wrong model here. + result.AddLazyConstraint(other_basic_lp.a + other_basic_lp.b <= 3); + return result; + }; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, + EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + IncrementalSolverImpl::New( + factory_mock.AsStdFunction(), &basic_lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL( + solver_interface, + Solve(SolveArgsAre(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr)))) + .WillOnce(fake_solve); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverImplTest, ComputeInfeasibleSubsystem) { + BasicInfeasibleLp lp; + + BaseSolverMock solver_interface; + + // The first computation. + ComputeInfeasibleSubsystemArguments args_1; + args_1.parameters.enable_output = true; + + SolveInterrupter interrupter; + args_1.interrupter = &interrupter; + + BaseSolverFactoryMock factory_mock; + EXPECT_CALL(factory_mock, + Call(SOLVER_TYPE_GLOP, EquivToProto(lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + IncrementalSolverImpl::New(factory_mock.AsStdFunction(), + &lp.model, SolverType::kGlop, + /*user_canceller=*/nullptr, + /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_CALL(solver_interface, + ComputeInfeasibleSubsystem(ComputeInfeasibleSubsystemArgsAre( + EquivToProto(args_1.parameters.Proto()), Eq(nullptr), + Eq(&interrupter)))) + .WillOnce(Return(lp.InfeasibleResult())); + + { + ASSERT_OK_AND_ASSIGN( + const ComputeInfeasibleSubsystemResult result, + solver->ComputeInfeasibleSubsystemWithoutUpdate(args_1)); + EXPECT_EQ(result.feasibility, FeasibilityStatus::kInfeasible); + } + + // Second computation with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = lp.UpdateUpperBoundOfC(); + ASSERT_TRUE(update); + + ComputeInfeasibleSubsystemArguments args_2; + args_2.parameters.enable_output = true; + + { + InSequence s; + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(true)); + + EXPECT_CALL( + solver_interface, + ComputeInfeasibleSubsystem(ComputeInfeasibleSubsystemArgsAre( + EquivToProto(args_2.parameters.Proto()), Eq(nullptr), Eq(nullptr)))) + .WillOnce(Return(lp.InfeasibleResult())); + } + + ASSERT_OK_AND_ASSIGN(const ComputeInfeasibleSubsystemResult result, + solver->ComputeInfeasibleSubsystem(args_2)); + EXPECT_EQ(result.feasibility, FeasibilityStatus::kInfeasible); +} + +TEST(IncrementalSolverImplTest, UserCancellation) { + BasicLp basic_lp; + + // Will be set to the provided local_canceller in the factory. + SolveInterrupter* provided_local_canceller = nullptr; + + BaseSolverFactoryMock factory_mock; + BaseSolverMock solver; + + EXPECT_CALL(factory_mock, Call(SOLVER_TYPE_GLOP, _, Ne(nullptr))) + .WillOnce([&](const SolverTypeProto solver_type, const ModelProto& model, + SolveInterrupter* const local_canceller) { + provided_local_canceller = local_canceller; + return std::make_unique(&solver); + }); + + SolveInterrupter user_canceller; + + ASSERT_OK_AND_ASSIGN( + const std::unique_ptr incremental_solver, + IncrementalSolverImpl::New(factory_mock.AsStdFunction(), &basic_lp.model, + SolverType::kGlop, + /*user_canceller=*/&user_canceller, + /*remove_names=*/false)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver); + + ASSERT_NE(provided_local_canceller, nullptr); + + // Since user_canceller has not been cancelled yet the local canceller should + // still be untriggered. + EXPECT_FALSE(provided_local_canceller->IsInterrupted()); + + // Triggering the user canceller should trigger the local canceller. + user_canceller.Interrupt(); + EXPECT_TRUE(provided_local_canceller->IsInterrupted()); +} + +} // namespace +} // namespace operations_research::math_opt::internal diff --git a/ortools/math_opt/cpp/solve_result_test.cc b/ortools/math_opt/cpp/solve_result_test.cc new file mode 100644 index 00000000000..94528b4ed37 --- /dev/null +++ b/ortools/math_opt/cpp/solve_result_test.cc @@ -0,0 +1,932 @@ +// Copyright 2010-2025 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/solve_result.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/protoutil.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/cpp/enums_testing.h" +#include "ortools/math_opt/cpp/linear_constraint.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/objective.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/math_opt/testing/stream.h" +#include "ortools/pdlp/solve_log.pb.h" +#include "ortools/util/fp_roundtrip_conv_testing.h" + +namespace operations_research { +namespace math_opt { +namespace { +constexpr double kInf = std::numeric_limits::infinity(); + +using ::testing::AllOf; +using ::testing::HasSubstr; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +INSTANTIATE_TYPED_TEST_SUITE_P(TerminationReason, EnumTest, TerminationReason); +INSTANTIATE_TYPED_TEST_SUITE_P(Limit, EnumTest, Limit); +INSTANTIATE_TYPED_TEST_SUITE_P(FeasibilityStatus, EnumTest, FeasibilityStatus); + +TEST(ObjectiveBounds, StringTest) { + const ObjectiveBounds objective_bounds = {.primal_bound = 1.0, + .dual_bound = 2.0}; + std::string expected = "{primal_bound: 1, dual_bound: 2}"; + EXPECT_EQ(objective_bounds.ToString(), expected); + EXPECT_EQ(StreamToString(objective_bounds), expected); +} + +TEST(ObjectiveBounds, FloatingPointRoundTripPrimalBound) { + const ObjectiveBounds objective_bounds = { + .primal_bound = kRoundTripTestNumber, .dual_bound = kInf}; + EXPECT_THAT( + objective_bounds.ToString(), + HasSubstr(absl::StrCat("primal_bound: ", kRoundTripTestNumberStr))); +} + +TEST(ObjectiveBounds, FloatingPointRoundTripDualBound) { + const ObjectiveBounds objective_bounds = {.primal_bound = -kInf, + .dual_bound = kRoundTripTestNumber}; + EXPECT_THAT(objective_bounds.ToString(), + HasSubstr(absl::StrCat("dual_bound: ", kRoundTripTestNumberStr))); +} + +TEST(ObjectiveBounds, ProtoRoundTripTest) { + const ObjectiveBounds objective_bounds = {.primal_bound = 10, + .dual_bound = 20}; + const ObjectiveBounds actual = + ObjectiveBounds::FromProto(objective_bounds.Proto()); + EXPECT_EQ(actual.primal_bound, 10); + EXPECT_EQ(actual.dual_bound, 20); +} + +TEST(ObjectiveBounds, MakeTrivial) { + EXPECT_THAT(ObjectiveBounds::MaximizeMakeTrivial(), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = -kInf, .dual_bound = kInf})); + EXPECT_THAT(ObjectiveBounds::MinimizeMakeTrivial(), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = kInf, .dual_bound = -kInf})); +} + +TEST(ObjectiveBounds, MakeTrivialIsMaximize) { + EXPECT_THAT(ObjectiveBounds::MakeTrivial(/*is_maximize=*/true), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = -kInf, .dual_bound = kInf})); + EXPECT_THAT(ObjectiveBounds::MakeTrivial(/*is_maximize=*/false), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = kInf, .dual_bound = -kInf})); +} + +TEST(ObjectiveBounds, MakeUnbounded) { + EXPECT_THAT(ObjectiveBounds::MaximizeMakeUnbounded(), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = kInf, .dual_bound = kInf})); + EXPECT_THAT(ObjectiveBounds::MinimizeMakeUnbounded(), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = -kInf, .dual_bound = -kInf})); +} + +TEST(ObjectiveBounds, MakeUnboundedIsMaximize) { + EXPECT_THAT(ObjectiveBounds::MakeUnbounded(/*is_maximize=*/true), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = kInf, .dual_bound = kInf})); + EXPECT_THAT(ObjectiveBounds::MakeUnbounded(/*is_maximize=*/false), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = -kInf, .dual_bound = -kInf})); +} + +TEST(ObjectiveBounds, MakeOptimal) { + EXPECT_THAT(ObjectiveBounds::MakeOptimal(/*objective_value=*/10.0), + ObjectiveBoundsNear( + ObjectiveBounds{.primal_bound = 10.0, .dual_bound = 10.0})); +} + +TEST(ProblemStatusTest, ProtoToProblemStatus) { + ProblemStatusProto proto; + proto.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + proto.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); + proto.set_primal_or_dual_infeasible(true); + + ASSERT_OK_AND_ASSIGN(const ProblemStatus actual_status, + ProblemStatus::FromProto(proto)); + EXPECT_EQ(actual_status.primal_status, FeasibilityStatus::kUndetermined); + EXPECT_EQ(actual_status.dual_status, FeasibilityStatus::kFeasible); + EXPECT_EQ(actual_status.primal_or_dual_infeasible, true); +} + +TEST(ProblemStatusTest, ProblemStatusToProto) { + const ProblemStatus problem_status = { + .primal_status = FeasibilityStatus::kUndetermined, + .dual_status = FeasibilityStatus::kFeasible, + .primal_or_dual_infeasible = true}; + const ProblemStatusProto proto = problem_status.Proto(); + + EXPECT_EQ(proto.primal_status(), FEASIBILITY_STATUS_UNDETERMINED); + EXPECT_EQ(proto.dual_status(), FEASIBILITY_STATUS_FEASIBLE); + EXPECT_EQ(proto.primal_or_dual_infeasible(), true); +} + +TEST(ProblemStatusTest, FromProtoInvalidInputDualStatus) { + ProblemStatusProto proto; + proto.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + EXPECT_THAT( + ProblemStatus::FromProto(proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("dual_status"))); +} + +TEST(ProblemStatusTest, FromProtoInvalidInputPrimalStatus) { + ProblemStatusProto proto; + proto.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); + EXPECT_THAT( + ProblemStatus::FromProto(proto), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("primal_status"))); +} + +TEST(ProblemStatusTest, StringTest) { + const ProblemStatus problem_status = { + .primal_status = FeasibilityStatus::kInfeasible, + .dual_status = FeasibilityStatus::kFeasible, + .primal_or_dual_infeasible = false}; + + std::string expected = + "{primal_status: infeasible, dual_status: feasible, " + "primal_or_dual_infeasible: false}"; + EXPECT_EQ(problem_status.ToString(), expected); + EXPECT_EQ(StreamToString(problem_status), expected); +} + +TEST(SolveStats, ProtoRoundTripTest) { + const SolveStats solve_stats = {.solve_time = absl::Seconds(1), + .simplex_iterations = 1, + .barrier_iterations = 2, + .first_order_iterations = 3, + .node_count = 4}; + ASSERT_OK_AND_ASSIGN(const SolveStatsProto proto, solve_stats.Proto()); + ASSERT_OK_AND_ASSIGN(const SolveStats actual, SolveStats::FromProto(proto)); + EXPECT_EQ(actual.solve_time, absl::Seconds(1)); + EXPECT_EQ(actual.simplex_iterations, 1); + EXPECT_EQ(actual.barrier_iterations, 2); + EXPECT_EQ(actual.first_order_iterations, 3); + EXPECT_EQ(actual.node_count, 4); +} + +TEST(SolveStats, ToProtoInvalidSolveTime) { + const SolveStats solve_stats = {.solve_time = absl::InfiniteDuration()}; + EXPECT_THAT(solve_stats.Proto(), + StatusIs(absl::StatusCode::kInvalidArgument, + AllOf(HasSubstr("solve_time"), HasSubstr("finite")))); +} + +TEST(SolveStats, FromProtoInvalidSolveTime) { + SolveStatsProto stats; + stats.mutable_solve_time()->set_nanos(-1); + stats.mutable_solve_time()->set_seconds(1); + EXPECT_THAT( + SolveStats::FromProto(stats), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("solve_time"))); +} + +TEST(SolveStats, StringTest) { + const SolveStats solve_stats = {.solve_time = absl::Seconds(1), + .simplex_iterations = 1, + .barrier_iterations = 2, + .first_order_iterations = 3, + .node_count = 4}; + + std::string expected = + "{solve_time: 1s, simplex_iterations: 1, " + "barrier_iterations: 2, first_order_iterations: 3, node_count: 4}"; + EXPECT_EQ(solve_stats.ToString(), expected); + EXPECT_EQ(StreamToString(solve_stats), expected); +} + +TEST(TerminationTest, ProtoRoundTripTestForFeasible) { + const Termination termination = Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0, 20.0, "hit time limit"); + ASSERT_OK_AND_ASSIGN(const Termination actual, + Termination::FromProto(termination.Proto())); + EXPECT_EQ(actual.reason, TerminationReason::kFeasible); + EXPECT_EQ(actual.limit, Limit::kTime); + EXPECT_EQ(actual.detail, "hit time limit"); + EXPECT_EQ(actual.problem_status.primal_status, FeasibilityStatus::kFeasible); + EXPECT_EQ(actual.problem_status.dual_status, FeasibilityStatus::kFeasible); + EXPECT_FALSE(actual.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(actual.objective_bounds, + ObjectiveBoundsNear({.primal_bound = 10.0, .dual_bound = 20.0})); +} + +TEST(TerminationTest, InvalidUnspecifiedTerminationReason) { + TerminationProto termination; + EXPECT_THAT( + Termination::FromProto(termination), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("reason"))); +} + +TEST(TerminationTest, OptimalTwoArgument) { + const Termination termination = Termination::Optimal(10.0, 20.0, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kOptimal); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear({.primal_bound = 10.0, .dual_bound = 20.0})); +} + +TEST(TerminationTest, OptimalOneArgument) { + const Termination termination = Termination::Optimal(10.0, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kOptimal); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear({.primal_bound = 10.0, .dual_bound = 10.0})); +} + +class TerminationMinMaxTest : public ::testing::TestWithParam {}; +INSTANTIATE_TEST_SUITE_P( + TerminationMinMaxTests, TerminationMinMaxTest, testing::Values(true, false), + [](const testing::TestParamInfo& info) { + return info.param ? "max" : "min"; + }); + +TEST_P(TerminationMinMaxTest, InfeasibleDualNotFeasible) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeTrivial(is_maximize); + const Termination termination = Termination::Infeasible( + is_maximize, FeasibilityStatus::kUndetermined, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kInfeasible); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kInfeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kUndetermined); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, InfeasibleDualFeasible) { + const bool is_maximize = GetParam(); + const double bound = is_maximize ? -kInf : kInf; + const ObjectiveBounds expected_bounds = {.primal_bound = bound, + .dual_bound = bound}; + const Termination termination = Termination::Infeasible( + is_maximize, FeasibilityStatus::kFeasible, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kInfeasible); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kInfeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, InfeasibleOrUnboundedDualNotInfeasible) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeTrivial(is_maximize); + const Termination termination = Termination::InfeasibleOrUnbounded( + is_maximize, FeasibilityStatus::kUndetermined, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kInfeasibleOrUnbounded); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kUndetermined); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kUndetermined); + EXPECT_TRUE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, InfeasibleOrUnboundedDualInfeasible) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeTrivial(is_maximize); + const Termination termination = Termination::InfeasibleOrUnbounded( + is_maximize, FeasibilityStatus::kInfeasible, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kInfeasibleOrUnbounded); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kUndetermined); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kInfeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, Unbounded) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeUnbounded(is_maximize); + const Termination termination = Termination::Unbounded(is_maximize, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kUnbounded); + EXPECT_EQ(termination.limit, std::nullopt); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kInfeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, NoSolutionFoundDualFeasible) { + const bool is_maximize = GetParam(); + const double dual_bound = 20.0; + ObjectiveBounds expected_bounds = ObjectiveBounds::MakeTrivial(is_maximize); + expected_bounds.dual_bound = dual_bound; + const Termination termination = Termination::NoSolutionFound( + is_maximize, Limit::kTime, dual_bound, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kNoSolutionFound); + EXPECT_EQ(termination.limit, Limit::kTime); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kUndetermined); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, NoSolutionFoundDualNotfeasible) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeTrivial(is_maximize); + const Termination termination = Termination::NoSolutionFound( + is_maximize, Limit::kTime, std::nullopt, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kNoSolutionFound); + EXPECT_EQ(termination.limit, Limit::kTime); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kUndetermined); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kUndetermined); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, FeasibleDualFeasible) { + const bool is_maximize = GetParam(); + const double primal_bound = is_maximize ? 10.0 : 20.0; + ObjectiveBounds expected_bounds = ObjectiveBounds::MakeTrivial(is_maximize); + expected_bounds.primal_bound = primal_bound; + + const Termination termination = Termination::Feasible( + is_maximize, Limit::kTime, primal_bound, std::nullopt, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kFeasible); + EXPECT_EQ(termination.limit, Limit::kTime); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kUndetermined); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, Cutoff) { + const bool is_maximize = GetParam(); + const ObjectiveBounds expected_bounds = + ObjectiveBounds::MakeTrivial(is_maximize); + const Termination termination = Termination::Cutoff(is_maximize, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kNoSolutionFound); + EXPECT_EQ(termination.limit, Limit::kCutoff); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kUndetermined); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kUndetermined); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST_P(TerminationMinMaxTest, FeasibleDualNotFeasible) { + const bool is_maximize = GetParam(); + const double primal_bound = is_maximize ? 10.0 : 20.0; + const double dual_bound = is_maximize ? 20.0 : 10.0; + const ObjectiveBounds expected_bounds = {.primal_bound = primal_bound, + .dual_bound = dual_bound}; + const Termination termination = Termination::Feasible( + is_maximize, Limit::kTime, primal_bound, dual_bound, "detail"); + EXPECT_EQ(termination.reason, TerminationReason::kFeasible); + EXPECT_EQ(termination.limit, Limit::kTime); + EXPECT_EQ(termination.detail, "detail"); + EXPECT_EQ(termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE(termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(termination.objective_bounds, + ObjectiveBoundsNear(expected_bounds)); +} + +TEST(TerminationTest, StringTestWithLimit) { + const Termination termination = Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0, 20.0, "hit time limit"); + std::string expected = + "{reason: feasible, limit: time, detail: \"hit time limit\", " + "problem_status: " + "{primal_status: feasible, dual_status: feasible, " + "primal_or_dual_infeasible: false}, objective_bounds: {primal_bound: " + "10, " + "dual_bound: 20}}"; + EXPECT_EQ(termination.ToString(), expected); + EXPECT_EQ(StreamToString(termination), expected); + EXPECT_EQ(absl::StrFormat("%v", termination), expected); +} + +TEST(TerminationTest, StringTestNoLimit) { + const Termination termination = Termination::Optimal(10.0, "good answer"); + std::string expected = + "{reason: optimal, detail: \"good answer\", problem_status: " + "{primal_status: " + "feasible, dual_status: feasible, primal_or_dual_infeasible: false}, " + "objective_bounds: {primal_bound: 10, dual_bound: 10}}"; + EXPECT_EQ(termination.ToString(), expected); + EXPECT_EQ(StreamToString(termination), expected); + EXPECT_EQ(absl::StrFormat("%v", termination), expected); +} + +TEST(TerminationTest, StringTestNoDetail) { + const Termination termination = Termination::Optimal(10.0); + std::string expected = + "{reason: optimal, problem_status: {primal_status: feasible, " + "dual_status: feasible, primal_or_dual_infeasible: false}, " + "objective_bounds: {primal_bound: 10, dual_bound: 10}}"; + EXPECT_EQ(termination.ToString(), expected); + EXPECT_EQ(StreamToString(termination), expected); + EXPECT_EQ(absl::StrFormat("%v", termination), expected); +} + +TEST(TerminationTest, StringTestDetailEscaping) { + const Termination termination = + Termination::Optimal(10.0, "hit \"3\" seconds"); + std::string expected = + "{reason: optimal, detail: \"hit \\\"3\\\" seconds\", problem_status: " + "{primal_status: feasible, " + "dual_status: feasible, primal_or_dual_infeasible: false}, " + "objective_bounds: {primal_bound: 10, dual_bound: 10}}"; + EXPECT_EQ(termination.ToString(), expected); + EXPECT_EQ(StreamToString(termination), expected); + EXPECT_EQ(absl::StrFormat("%v", termination), expected); +} + +TEST(TerminationTest, LimitReached) { + const Termination termination_optimal = Termination::Optimal(10.0); + EXPECT_FALSE(termination_optimal.limit_reached()); + const Termination termination_feasible = Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0); + EXPECT_TRUE(termination_feasible.limit_reached()); + const Termination termination_nosolution = Termination::NoSolutionFound( + /*is_maximize=*/true, Limit::kTime); + EXPECT_TRUE(termination_nosolution.limit_reached()); +} + +TEST(TerminationTest, EnsureReasonIs) { + EXPECT_OK(Termination(/*is_maximize=*/true, TerminationReason::kImprecise) + .EnsureReasonIs(TerminationReason::kImprecise)); + EXPECT_THAT(Termination(/*is_maximize=*/true, TerminationReason::kFeasible) + .EnsureReasonIs(TerminationReason::kOptimal), + StatusIs(absl::StatusCode::kInternal, + HasSubstr("expected termination reason 'optimal' but " + "got {reason: feasible"))); +} + +TEST(TerminationTest, EnsureReasonIsAnyOf) { + EXPECT_OK(Termination(/*is_maximize=*/true, TerminationReason::kImprecise) + .EnsureReasonIsAnyOf({TerminationReason::kImprecise, + TerminationReason::kInfeasible})); + EXPECT_THAT(Termination(/*is_maximize=*/true, TerminationReason::kInfeasible) + .EnsureReasonIsAnyOf({TerminationReason::kOptimal, + TerminationReason::kFeasible}), + StatusIs(absl::StatusCode::kInternal, + HasSubstr("expected termination reason in {'optimal', " + "'feasible'} but got {reason: infeasible"))); +} + +TEST(TerminationTest, EnsureIsOptimal) { + EXPECT_OK(Termination::Optimal(10.0).EnsureIsOptimal()); + EXPECT_THAT(Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0) + .EnsureIsOptimal(), + StatusIs(absl::StatusCode::kInternal, + HasSubstr("expected termination reason 'optimal' but " + "got {reason: feasible"))); +} + +TEST(TerminationTest, IsOptimal) { + EXPECT_TRUE(Termination::Optimal(10.0).IsOptimal()); + EXPECT_FALSE(Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0) + .IsOptimal()); +} + +TEST(TerminationTest, EnsureIsOptimalOrFeasible) { + EXPECT_OK(Termination::Optimal(10.0).EnsureIsOptimalOrFeasible()); + EXPECT_OK(Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0) + .EnsureIsOptimalOrFeasible()); + EXPECT_THAT(Termination::Infeasible( + /*is_maximize=*/true, FeasibilityStatus::kInfeasible) + .EnsureIsOptimalOrFeasible(), + StatusIs(absl::StatusCode::kInternal, + HasSubstr("expected termination reason in {'optimal', " + "'feasible'} but got {reason: infeasible"))); +} + +TEST(TerminationTest, IsOptimalOrFeasible) { + EXPECT_TRUE(Termination::Optimal(10.0).IsOptimalOrFeasible()); + EXPECT_TRUE(Termination::Feasible( + /*is_maximize=*/true, Limit::kTime, 10.0) + .IsOptimalOrFeasible()); + EXPECT_FALSE(Termination::Infeasible( + /*is_maximize=*/true, FeasibilityStatus::kInfeasible) + .IsOptimalOrFeasible()); +} + +SolveResultProto MinimalSolveResultProto() { + SolveResultProto proto; + *proto.mutable_termination() = OptimalTerminationProto(10.0, 10.0); + return proto; +} + +SolveResultMatcherOptions StrictCheck() { + return {.tolerance = 0, + .first_solution_only = false, + .check_dual = true, + .check_rays = true, + .check_solutions_if_inf_or_unbounded = true, + .check_basis = true, + .inf_or_unb_soft_match = false}; +} + +TEST(SolveResultTest, MinimalSolveResultRoundTrips) { + ModelStorage model; + SolveResult solve_result(Termination::Optimal(10.0)); + + // Half trip + EXPECT_THAT(SolveResult::FromProto(&model, MinimalSolveResultProto()), + IsOkAndHolds(IsConsistentWith(solve_result, StrictCheck()))); + + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, solve_result.Proto()); + // Full trip + EXPECT_THAT(SolveResult::FromProto(&model, proto), + IsOkAndHolds(IsConsistentWith(solve_result, StrictCheck()))); +} + +TEST(SolveResultTest, ProblemStatusInStatsButNotTermination) { + ModelStorage model; + SolveResultProto proto = MinimalSolveResultProto(); + *proto.mutable_solve_stats()->mutable_problem_status() = + proto.termination().problem_status(); + proto.mutable_termination()->clear_problem_status(); + + ASSERT_OK_AND_ASSIGN(const SolveResult solve_result, + SolveResult::FromProto(&model, proto)); + EXPECT_EQ(solve_result.termination.problem_status.primal_status, + FeasibilityStatus::kFeasible); + EXPECT_EQ(solve_result.termination.problem_status.dual_status, + FeasibilityStatus::kFeasible); + EXPECT_FALSE( + solve_result.termination.problem_status.primal_or_dual_infeasible); + EXPECT_THAT(SolveResult::FromProto(&model, proto), + IsOkAndHolds(IsConsistentWith(solve_result, StrictCheck()))); +} + +TEST(SolveResultTest, ObjectiveBoundsInStatsButNotTermination) { + ModelStorage model; + SolveResultProto proto = MinimalSolveResultProto(); + proto.mutable_solve_stats()->set_best_primal_bound(10.0); + proto.mutable_solve_stats()->set_best_dual_bound(20.0); + proto.mutable_termination()->clear_objective_bounds(); + + ASSERT_OK_AND_ASSIGN(const SolveResult solve_result, + SolveResult::FromProto(&model, proto)); + EXPECT_DOUBLE_EQ(solve_result.termination.objective_bounds.primal_bound, + 10.0); + EXPECT_DOUBLE_EQ(solve_result.termination.objective_bounds.dual_bound, 20.0); + EXPECT_THAT(SolveResult::FromProto(&model, proto), + IsOkAndHolds(IsConsistentWith(solve_result, StrictCheck()))); +} + +TEST(SolveResultTest, SolveTimeRoundTrip) { + ModelStorage model; + const absl::Duration time = absl::Seconds(10); + SolveResultProto result_proto = MinimalSolveResultProto(); + ASSERT_OK(util_time::EncodeGoogleApiProto( + time, result_proto.mutable_solve_stats()->mutable_solve_time())); + ASSERT_OK_AND_ASSIGN(const auto result, + SolveResult::FromProto(&model, result_proto)); + EXPECT_EQ(result.solve_time(), time); +} + +TEST(SolveResultTest, ProtoInvalidSolveStats) { + SolveResult solve_result(Termination::Optimal(10.0)); + solve_result.solve_stats.solve_time = absl::InfiniteDuration(); + EXPECT_THAT(solve_result.Proto(), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(SolveResultTest, FromProtoInvalidTermination) { + ModelStorage model; + SolveResultProto result = MinimalSolveResultProto(); + result.clear_termination(); + EXPECT_THAT(SolveResult::FromProto(&model, result), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(SolveResultTest, FromProtoInvalidStats) { + ModelStorage model; + SolveResultProto result = MinimalSolveResultProto(); + result.mutable_solve_stats()->mutable_solve_time()->set_nanos(-1); + result.mutable_solve_stats()->mutable_solve_time()->set_seconds(1); + EXPECT_THAT(SolveResult::FromProto(&model, result), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(SolveResultTest, FromProtoInvalidSolution) { + ModelStorage model; + SolveResultProto result = MinimalSolveResultProto(); + // default primal solution is invalid, no + result.add_solutions() + ->mutable_primal_solution() + ->mutable_variable_values() + ->add_values(0); + EXPECT_THAT(SolveResult::FromProto(&model, result), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(SolveResultTest, FromProtoInvalidPrimalRay) { + ModelStorage model; + SolveResultProto result = MinimalSolveResultProto(); + // default primal solution is invalid, no + result.add_primal_rays()->mutable_variable_values()->add_values(0); + EXPECT_THAT(SolveResult::FromProto(&model, result), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(SolveResultTest, FromProtoInvalidDualRay) { + ModelStorage model; + SolveResultProto result = MinimalSolveResultProto(); + // default primal solution is invalid, no + result.add_dual_rays()->mutable_reduced_costs()->add_values(0); + EXPECT_THAT(SolveResult::FromProto(&model, result), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +class SolveResultModelTest : public testing::Test { + protected: + SolveResultModelTest() + : x_(&storage_, storage_.AddVariable("x")), + c_(&storage_, storage_.AddLinearConstraint("c")) {} + + ModelStorage storage_; + Variable x_; + LinearConstraint c_; +}; + +TEST_F(SolveResultModelTest, RoundTripSolutions) { + SolveResult result(Termination::Optimal(10.0)); + Solution s1; + s1.primal_solution = {.variable_values = {{x_, 1.0}}, + .objective_value = 1.0, + .feasibility_status = SolutionStatus::kFeasible}; + result.solutions.push_back(s1); + Solution s2; + s2.primal_solution = {.variable_values = {{x_, 0.0}}, + .objective_value = 0.0, + .feasibility_status = SolutionStatus::kFeasible}; + + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, result.Proto()); + EXPECT_THAT(SolveResult::FromProto(&storage_, proto), + IsOkAndHolds(IsConsistentWith(result, StrictCheck()))); +} + +TEST_F(SolveResultModelTest, RoundTripPrimalRays) { + SolveResult result(Termination::Unbounded(/*is_maximize=*/true)); + result.primal_rays.push_back({.variable_values = {{x_, 1.0}}}); + result.primal_rays.push_back({.variable_values = {{x_, 0.0}}}); + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, result.Proto()); + EXPECT_THAT(SolveResult::FromProto(&storage_, proto), + IsOkAndHolds(IsConsistentWith(result, StrictCheck()))); +} + +TEST_F(SolveResultModelTest, RoundTripDualRays) { + SolveResult result(Termination::Infeasible(/*is_maximize=*/true, + FeasibilityStatus::kInfeasible)); + result.dual_rays.push_back( + {.dual_values = {{c_, 2.0}}, .reduced_costs = {{x_, 1.0}}}); + result.dual_rays.push_back( + {.dual_values = {{c_, 3.0}}, .reduced_costs = {{x_, 0.0}}}); + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, result.Proto()); + EXPECT_THAT(SolveResult::FromProto(&storage_, proto), + IsOkAndHolds(IsConsistentWith(result, StrictCheck()))); +} + +TEST(SolveResultTest, RoundTripGscipOutput) { + ModelStorage model; + SolveResult solve_result(Termination::Optimal(10.0)); + solve_result.gscip_solver_specific_output.set_status_detail("gscip_detail"); + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, solve_result.Proto()); + ASSERT_OK_AND_ASSIGN(const SolveResult round_trip, + SolveResult::FromProto(&model, proto)); + + // Full trip + EXPECT_THAT(round_trip, IsConsistentWith(solve_result, StrictCheck())); + // Proto matcher is not portable + EXPECT_EQ(round_trip.gscip_solver_specific_output.status_detail(), + "gscip_detail"); +} + +TEST(SolveResultTest, RoundTripPdlpOutput) { + ModelStorage model; + SolveResult solve_result(Termination::Optimal(10.0)); + solve_result.pdlp_solver_specific_output.mutable_convergence_information() + ->set_corrected_dual_objective(2.0); + + ASSERT_OK_AND_ASSIGN(const SolveResultProto proto, solve_result.Proto()); + ASSERT_OK_AND_ASSIGN(const SolveResult round_trip, + SolveResult::FromProto(&model, proto)); + + // Full trip + EXPECT_THAT(round_trip, IsConsistentWith(solve_result, StrictCheck())); + EXPECT_NEAR(round_trip.pdlp_solver_specific_output.convergence_information() + .corrected_dual_objective(), + 2.0, 1e-6); +} + +TEST(SolveResultTest, StringTestOptimal) { + SolveResult solve_result(Termination::Optimal(10.0)); + + // One solution. + solve_result.solutions.emplace_back(); + EXPECT_EQ(StreamToString(solve_result), + "{termination: {reason: optimal, problem_status: {primal_status: " + "feasible, dual_status: feasible, primal_or_dual_infeasible: " + "false}, objective_bounds: {primal_bound: 10, dual_bound: 10}}, " + "solve_stats: {solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [1 available], primal_rays: [], dual_rays: []}"); + + // Two solutions. + solve_result.solutions.emplace_back(); + EXPECT_EQ(StreamToString(solve_result), + "{termination: {reason: optimal, problem_status: {primal_status: " + "feasible, dual_status: feasible, primal_or_dual_infeasible: " + "false}, objective_bounds: {primal_bound: 10, dual_bound: 10}}, " + "solve_stats: {solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [2 available], primal_rays: [], dual_rays: []}"); +} + +TEST(SolveResultTest, StringTestUnbounded) { + SolveResult solve_result(Termination::Unbounded(/*is_maximize=*/true)); + + // One ray. + solve_result.primal_rays.emplace_back(); + EXPECT_EQ(StreamToString(solve_result), + "{termination: {reason: unbounded, problem_status: {primal_status: " + "feasible, dual_status: infeasible, primal_or_dual_infeasible: " + "false}, objective_bounds: {primal_bound: inf, dual_bound: inf}}, " + "solve_stats: {solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [], primal_rays: [1 available], dual_rays: []}"); + + // Two rays. + solve_result.primal_rays.emplace_back(); + EXPECT_EQ(StreamToString(solve_result), + "{termination: {reason: unbounded, problem_status: {primal_status: " + "feasible, dual_status: infeasible, primal_or_dual_infeasible: " + "false}, objective_bounds: {primal_bound: inf, dual_bound: inf}}, " + "solve_stats: {solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [], primal_rays: [2 available], dual_rays: []}"); +} + +TEST(SolveResultTest, StringTestInfeasible) { + SolveResult solve_result(Termination::Infeasible( + /*is_maximize=*/true, FeasibilityStatus::kFeasible)); + + // One ray. + solve_result.dual_rays.emplace_back(); + EXPECT_EQ( + StreamToString(solve_result), + "{termination: {reason: infeasible, problem_status: {primal_status: " + "infeasible, dual_status: feasible, primal_or_dual_infeasible: false}, " + "objective_bounds: {primal_bound: -inf, dual_bound: -inf}}, " + "solve_stats: " + "{solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [], primal_rays: [], dual_rays: [1 available]}"); + + // Two rays. + solve_result.dual_rays.emplace_back(); + EXPECT_EQ( + StreamToString(solve_result), + "{termination: {reason: infeasible, problem_status: {primal_status: " + "infeasible, dual_status: feasible, primal_or_dual_infeasible: false}, " + "objective_bounds: {primal_bound: -inf, dual_bound: -inf}}, " + "solve_stats: " + "{solve_time: 0, " + "simplex_iterations: 0, " + "barrier_iterations: 0, first_order_iterations: 0, node_count: 0}, " + "solutions: [], primal_rays: [], dual_rays: [2 available]}"); +} + +TEST(SolveResultTest, BestPrimalSolution) { + SolveResult result(Termination::Optimal(3.0)); + result.solutions.emplace_back().primal_solution.emplace() = { + .objective_value = 3.0, .feasibility_status = SolutionStatus::kFeasible}; + result.solutions.emplace_back().primal_solution.emplace() = { + .objective_value = 4.0, .feasibility_status = SolutionStatus::kFeasible}; + EXPECT_EQ(result.best_primal_solution().objective_value, 3.0); + ASSERT_TRUE(result.has_primal_feasible_solution()); + EXPECT_EQ(result.objective_value(), 3.0); + EXPECT_EQ(result.primal_bound(), 3.0); + EXPECT_EQ(result.dual_bound(), 3.0); + EXPECT_EQ(result.best_objective_bound(), 3.0); +} + +TEST(SolveResultTest, PrimalAndDualBounds) { + SolveResult result(Termination::Feasible( + /*is_maximize=*/true, Limit::kIteration, /*finite_primal_objective=*/10.0, + /*optional_dual_objective=*/20.0)); + EXPECT_EQ(result.primal_bound(), 10.0); + EXPECT_EQ(result.dual_bound(), 20.0); + EXPECT_EQ(result.best_objective_bound(), 20.0); +} + +TEST(SolveResultTest, ObjectiveValue) { + ModelStorage model; + const Objective p = Objective::Primary(&model); + const Objective o = + Objective::Auxiliary(&model, model.AddAuxiliaryObjective(1)); + SolveResult result(Termination::Optimal(10.0)); + result.solutions.emplace_back().primal_solution.emplace() = { + .objective_value = 3.0, + .auxiliary_objective_values = {{o, 4.0}}, + .feasibility_status = SolutionStatus::kFeasible}; + result.solutions.emplace_back().primal_solution.emplace() = { + .objective_value = 5.0, + .auxiliary_objective_values = {{o, 6.0}}, + .feasibility_status = SolutionStatus::kFeasible}; + EXPECT_EQ(result.objective_value(), 3.0); + EXPECT_EQ(result.objective_value(p), 3.0); + EXPECT_EQ(result.objective_value(o), 4.0); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solve_test.cc b/ortools/math_opt/cpp/solve_test.cc new file mode 100644 index 00000000000..fc591aade62 --- /dev/null +++ b/ortools/math_opt/cpp/solve_test.cc @@ -0,0 +1,1539 @@ +// Copyright 2010-2025 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/solve.h" + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/core/solver_interface_mock.h" +#include "ortools/math_opt/core/sparse_collection_matchers.h" +#include "ortools/math_opt/cpp/callback.h" +#include "ortools/math_opt/cpp/compute_infeasible_subsystem_result.h" +#include "ortools/math_opt/cpp/enums.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::_; +using ::testing::ByMove; +using ::testing::Eq; +using ::testing::EquivToProto; +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Mock; +using ::testing::Ne; +using ::testing::Optional; +using ::testing::Pair; +using ::testing::Return; +using ::testing::UnorderedElementsAre; +using ::testing::status::StatusIs; + +constexpr double kInf = std::numeric_limits::infinity(); + +// Basic LP model: +// +// a and b are continuous variable +// +// minimize a - b +// s.t. 0 <= a +// 0 <= b <= 3 +struct BasicLp { + BasicLp(); + + // Sets the upper bound of variable b to 2.0 and returns the corresponding + // update. + std::optional UpdateUpperBoundOfB(); + + // Returns the expected optimal result for this model. Only put the given set + // of variables in the result (to test filters). When `after_update` is true, + // returns the optimal result after UpdateUpperBoundOfB() has been called. + SolveResultProto OptimalResult(const absl::flat_hash_set& vars, + bool after_update = false) const; + + Model model; + const Variable a; + const Variable b; +}; + +BasicLp::BasicLp() + : a(model.AddVariable(0.0, kInf, false, "a")), + b(model.AddVariable(0.0, 3.0, false, "b")) {} + +std::optional BasicLp::UpdateUpperBoundOfB() { + const std::unique_ptr tracker = model.NewUpdateTracker(); + model.set_upper_bound(b, 2.0); + return tracker->ExportModelUpdate().value(); +} + +SolveResultProto BasicLp::OptimalResult( + const absl::flat_hash_set& vars, bool after_update) const { + SolveResultProto result; + result.mutable_termination()->set_reason(TERMINATION_REASON_OPTIMAL); + result.mutable_solve_stats()->mutable_problem_status()->set_primal_status( + FEASIBILITY_STATUS_FEASIBLE); + result.mutable_solve_stats()->mutable_problem_status()->set_dual_status( + FEASIBILITY_STATUS_FEASIBLE); + PrimalSolutionProto* const solution = + result.add_solutions()->mutable_primal_solution(); + solution->set_objective_value(0.0); + solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + if (vars.contains(a)) { + solution->mutable_variable_values()->add_ids(a.id()); + solution->mutable_variable_values()->add_values(0.0); + } + if (vars.contains(b)) { + solution->mutable_variable_values()->add_ids(b.id()); + solution->mutable_variable_values()->add_values(after_update ? 2.0 : 3.0); + } + return result; +} + +// Test calling Solve() without any callback. +TEST(SolveTest, SuccessfulSolveNoCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolveInterrupter interrupter; + args.interrupter = &interrupter; + + args.message_callback = [](absl::Span) {}; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Ne(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(&interrupter))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling Solve() with a callback. +TEST(SolveTest, SuccessfulSolveWithCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback cb, + const SolveInterrupter* const interrupter) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + ASSIGN_OR_RETURN(const CallbackResultProto result, cb(cb_data)); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(basic_lp.model, EnumFromProto(registration.solver_type()).value(), + args)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +TEST(SolveTest, RemoveNamesSendsNoNames) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + Model model; + model.AddBinaryVariable("x"); + + SolveArguments args; + SolverInitArguments init_args; + init_args.remove_names = true; + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + SolveResultProto fake_result; + *fake_result.mutable_termination() = + NoSolutionFoundTerminationProto(/*is_maximize=*/false, LIMIT_TIME); + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, Call(EquivToProto(expected_model), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL(solver, Solve(_, _, _, _, _, _)).WillOnce(Return(fake_result)); + } + + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(model, EnumFromProto(registration.solver_type()).value(), args, + init_args)); +} + +// Test calling Solve() with a solver that fails to returns the SolverInterface +// for a given model. +TEST(SolveTest, FailingSolveInstantiation) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("instantiation failed")))); + + ASSERT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInternal, "instantiation failed")); +} + +// Test calling Solve() with a solver that returns an error on +// SolverInterface::Solve(). +TEST(SolveTest, FailingSolve) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(absl::InternalError("solve failed"))); + } + + ASSERT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +TEST(SolveTest, NullCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + SolverInterfaceMock solver; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no callback was provided"))); +} + +TEST(SolveTest, WrongModelInModelParameters) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({other_basic_lp.a}); + + SolverInterfaceMock solver; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(SolveTest, WrongModelInCallbackRegistration) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.callback_registration.mip_solution_filter = + MakeKeepKeysFilter({other_basic_lp.a}); + + SolverInterfaceMock solver; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(SolveTest, WrongModelInCallbackResult) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback cb, + const SolveInterrupter* const interrupter) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + ASSIGN_OR_RETURN(const CallbackResultProto result, cb(cb_data)); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, + + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver, Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + args.callback = [&](const CallbackData& callback_data) { + CallbackResult result; + // We use the wrong model here. + result.AddLazyConstraint(other_basic_lp.a + other_basic_lp.b <= 3); + return result; + }; + + EXPECT_THAT(Solve(basic_lp.model, + EnumFromProto(registration.solver_type()).value(), args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverTest, NullModel) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + EXPECT_THAT(NewIncrementalSolver( + nullptr, EnumFromProto(registration.solver_type()).value()), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("model"))); +} + +TEST(IncrementalSolverTest, SolverType) { + BasicLp basic_lp; + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, SolverType::kGlop)); + EXPECT_EQ(solver->solver_type(), SolverType::kGlop); +} + +// Test calling IncrementalSolver without any callback with a succeeding +// non-empty update. +TEST(IncrementalSolverTest, IncrementalSolveNoCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolverInterfaceMock solver_interface; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolveInterrupter interrupter; + args_1.interrupter = &interrupter; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(&interrupter))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(true)); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}, + /*after_update=*/true))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 2.0))); +} + +TEST(IncrementalSolverTest, RemoveNamesSendsNoNamesOnModel) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + Model model; + model.AddBinaryVariable("x"); + + SolverInitArguments init_args; + init_args.remove_names = true; + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + SolverInterfaceMock solver_interface; + EXPECT_CALL(factory_mock, Call(EquivToProto(expected_model), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + EXPECT_OK(NewIncrementalSolver( + &model, EnumFromProto(registration.solver_type()).value(), init_args)); +} + +TEST(IncrementalSolverTest, RemoveNamesSendsNoNamesOnModelUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + Model model; + + SolveArguments args; + SolverInitArguments init_args; + init_args.remove_names = true; + + SolverInterfaceMock solver_interface; + EXPECT_CALL(factory_mock, Call(_, _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&model, + EnumFromProto(registration.solver_type()).value(), + init_args)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + model.AddBinaryVariable("x"); + + ModelUpdateProto expected_update; + expected_update.mutable_new_variables()->add_ids(0); + expected_update.mutable_new_variables()->add_lower_bounds(0.0); + expected_update.mutable_new_variables()->add_upper_bounds(1.0); + expected_update.mutable_new_variables()->add_integers(true); + + EXPECT_CALL(solver_interface, Update(EquivToProto(expected_update))) + .WillOnce(Return(true)); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); +} + +TEST(IncrementalSolverTest, RemoveNamesOnFullModelAfterUpdateFails) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + Model model; + + SolveArguments args; + SolverInitArguments init_args; + init_args.remove_names = true; + + SolverInterfaceMock solver_interface; + EXPECT_CALL(factory_mock, Call(_, _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&model, + EnumFromProto(registration.solver_type()).value(), + init_args)); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + model.AddBinaryVariable("x"); + + ModelProto expected_model; + expected_model.mutable_variables()->add_ids(0); + expected_model.mutable_variables()->add_lower_bounds(0.0); + expected_model.mutable_variables()->add_upper_bounds(1.0); + expected_model.mutable_variables()->add_integers(true); + + EXPECT_CALL(solver_interface, Update(_)).WillOnce(Return(false)); + SolverInterfaceMock solver_interface2; + EXPECT_CALL(factory_mock, Call(EquivToProto(expected_model), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface2)))); + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_FALSE(update_result.did_update); +} + +// Test calling IncrementalSolver without any callback with an empty update. +TEST(IncrementalSolverTest, IncrementalSolveWithEmptyUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolverInterfaceMock solver_interface; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}))); + } + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_TRUE(update_result.did_update); + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 3.0))); +} + +// Test calling IncrementalSolver without any callback and with a failing +// update; thus resulting in the re-creation of the solver instead. +// +// This also tests that at any given time only one instance of Solver +// exists. This is important for Gubori as only one instance can exist at any +// given time when using a single-use license. +TEST(IncrementalSolverTest, IncrementalSolveWithFailedUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolverInterfaceMock solver_1; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + // The current number of instances of solver. + int num_instances = 0; + const auto constructor_cb = [&]() { + ++num_instances; + // We only want one instance at most at any given time. + EXPECT_LE(num_instances, 1); + }; + const auto destructor_cb = [&]() { + ASSERT_GE(num_instances, 1); + --num_instances; + }; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce([&](const ModelProto&, const SolverInterface::InitArgs&) { + constructor_cb(); + return std::make_unique(&solver_1, destructor_cb); + }); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_1, + Solve(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Second solve with update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + SolverInterfaceMock solver_2; + + { + InSequence s; + + EXPECT_CALL(solver_1, Update(EquivToProto(*update))) + .WillOnce(Return(false)); + + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce([&](const ModelProto&, const SolverInterface::InitArgs&) { + constructor_cb(); + return std::make_unique(&solver_2, destructor_cb); + }); + } + + ASSERT_OK_AND_ASSIGN(const UpdateResult update_result, solver->Update()); + EXPECT_FALSE(update_result.did_update); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + Mock::VerifyAndClearExpectations(&solver_2); + + { + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_2, + args_2.model_parameters.Proto()); + EXPECT_CALL(solver_2, + Solve(EquivToProto(args_2.parameters.Proto()), + EquivToProto(model_parameters_2), Eq(nullptr), + EquivToProto(args_2.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a, basic_lp.b}, + /*after_update=*/true))); + } + + ASSERT_OK_AND_ASSIGN(const SolveResult result_2, + solver->SolveWithoutUpdate(args_2)); + + EXPECT_EQ(result_2.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT( + result_2.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0), Pair(basic_lp.b, 2.0))); +} + +// Test calling IncrementalSolver without any callback and with an impossible +// update, i.e. an update that contains an unsupported feature. +TEST(IncrementalSolverTest, IncrementalSolveWithImpossibleUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolverInterfaceMock solver_1; + + // The first solve. + SolveArguments args_1; + args_1.parameters.enable_output = true; + args_1.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver_1)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters_1, + args_1.model_parameters.Proto()); + EXPECT_CALL(solver_1, + Solve(EquivToProto(args_1.parameters.Proto()), + EquivToProto(model_parameters_1), Eq(nullptr), + EquivToProto(args_1.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(basic_lp.OptimalResult({basic_lp.a}))); + + ASSERT_OK_AND_ASSIGN(const SolveResult result_1, + solver->SolveWithoutUpdate(args_1)); + + EXPECT_EQ(result_1.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result_1.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); + + // Update. + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_1); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + SolveArguments args_2; + args_2.parameters.enable_output = true; + + { + InSequence s; + + // The solver will refuse the update with the unsupported feature. + EXPECT_CALL(solver_1, Update(EquivToProto(*update))) + .WillOnce(Return(false)); + + // The solver factory will fail for the same reason. + EXPECT_CALL(factory_mock, + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("*unsupported model*")))); + } + + ASSERT_THAT(solver->Update(), + StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*unsupported model*"), + HasSubstr("solver re-creation failed")))); + + // Next calls should fail and not crash. + basic_lp.model.set_lower_bound(basic_lp.a, -3.0); + EXPECT_THAT(solver->Update(), StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Update() failed"))); + EXPECT_THAT(solver->SolveWithoutUpdate(), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Update() failed"))); +} + +// Test calling IncrementalSolver with a callback. We don't test calling +// Update() here since only the Solve() function takes a callback. +TEST(IncrementalSolverTest, SuccessfulSolveWithCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback cb, + const SolveInterrupter* const interrupter) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + ASSIGN_OR_RETURN(const CallbackResultProto result, cb(cb_data)); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, + + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN(const SolveResult result, + solver->SolveWithoutUpdate(args)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling IncrementalSolver with a solver that fails to returns the +// SolverInterface for a given model. +TEST(IncrementalSolverTest, FailingSolverInstantiation) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver_interface; + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(absl::InternalError("instantiation failed")))); + + ASSERT_THAT( + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value()), + StatusIs(absl::StatusCode::kInternal, "instantiation failed")); +} + +// Test calling IncrementalSolver with a solver that returns an error on +// SolverInterface::Solve(). +TEST(IncrementalSolverTest, FailingSolver) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(absl::InternalError("solve failed"))); + + ASSERT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +// Test calling IncrementalSolver with a solver that returns an error on +// SolverInterface::Update(). +TEST(IncrementalSolverTest, FailingSolverUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(absl::InternalError("*update failure*"))); + + ASSERT_THAT(solver->Update(), StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*update failure*"), + HasSubstr("update failed")))); +} + +// Test calling IncrementalSolver::Solve() with a callback and a non trivial +// update. +TEST(IncrementalSolverTest, UpdateAndSolve) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback cb, + const SolveInterrupter* const interrupter) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + ASSIGN_OR_RETURN(const CallbackResultProto result, cb(cb_data)); + return basic_lp.OptimalResult({basic_lp.a}); + }; + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, + + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + // Update the model before calling Solve(). + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + { + InSequence s; + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(true)); + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + } + + int callback_called_count = 0; + args.callback = [&](const CallbackData& callback_data) { + ++callback_called_count; + CallbackResult result; + result.AddLazyConstraint(basic_lp.a + basic_lp.b <= 3); + return result; + }; + ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args)); + + EXPECT_EQ(callback_called_count, 1); + EXPECT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_THAT(result.variable_values(), + UnorderedElementsAre(Pair(basic_lp.a, 0.0))); +} + +// Test calling IncrementalSolver::Solve() with a solver that returns an error +// on SolverInterface::Solve(). +TEST(IncrementalSolverTest, UpdateAndSolveWithFailingSolver) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Eq(nullptr), Eq(nullptr))) + .WillOnce(Return(absl::InternalError("solve failed"))); + + ASSERT_THAT(solver->Solve(args), + StatusIs(absl::StatusCode::kInternal, "solve failed")); +} + +// Test calling IncrementalSolver::Solve() with a solver that returns an error +// on SolverInterface::Update(). +TEST(IncrementalSolverTest, UpdateAndSolveWithFailingSolverUpdate) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + const std::optional update = basic_lp.UpdateUpperBoundOfB(); + ASSERT_TRUE(update); + + EXPECT_CALL(solver_interface, Update(EquivToProto(*update))) + .WillOnce(Return(absl::InternalError("*update failure*"))); + + ASSERT_THAT(solver->Solve(), StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("*update failure*"), + HasSubstr("update failed")))); +} + +TEST(IncrementalSolverTest, NullCallback) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({basic_lp.a}); + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("no callback was provided"))); +} + +TEST(IncrementalSolverTest, WrongModelInModelParameters) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.model_parameters = + ModelSolveParameters::OnlySomePrimalVariables({other_basic_lp.a}); + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverTest, WrongModelInCallbackRegistration) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + // Here we use the wrong variable. + args.callback_registration.mip_solution_filter = + MakeKeepKeysFilter({other_basic_lp.a}); + + SolverInterfaceMock solver_interface; + + EXPECT_CALL(factory_mock, + + Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +TEST(IncrementalSolverTest, WrongModelInCallbackResult) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicLp basic_lp; + BasicLp other_basic_lp; + + SolveArguments args; + args.parameters.enable_output = true; + + args.callback_registration.add_lazy_constraints = true; + args.callback_registration.events.insert(CallbackEvent::kMipSolution); + + const auto fake_solve = + [&](const SolveParametersProto&, const ModelSolveParametersProto&, + const MessageCallback message_cb, const CallbackRegistrationProto&, + SolverInterface::Callback cb, + const SolveInterrupter* const interrupter) + -> absl::StatusOr { + CallbackDataProto cb_data; + cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION); + *cb_data.mutable_primal_solution_vector() = MakeSparseDoubleVector( + {{basic_lp.a.id(), 1.0}, {basic_lp.b.id(), 0.0}}); + ASSIGN_OR_RETURN(const CallbackResultProto result, cb(cb_data)); + return basic_lp.OptimalResult({basic_lp.a, basic_lp.b}); + }; + + SolverInterfaceMock solver_interface; + + args.callback = [&](const CallbackData& callback_data) { + CallbackResult result; + // We use the wrong model here. + result.AddLazyConstraint(other_basic_lp.a + other_basic_lp.b <= 3); + return result; + }; + + EXPECT_CALL(factory_mock, Call(EquivToProto(basic_lp.model.ExportModel()), _)) + .WillOnce(Return( + ByMove(std::make_unique(&solver_interface)))); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + NewIncrementalSolver(&basic_lp.model, + EnumFromProto(registration.solver_type()).value())); + + Mock::VerifyAndClearExpectations(&factory_mock); + Mock::VerifyAndClearExpectations(&solver_interface); + + ASSERT_OK_AND_ASSIGN(const ModelSolveParametersProto model_parameters, + args.model_parameters.Proto()); + EXPECT_CALL(solver_interface, + Solve(EquivToProto(args.parameters.Proto()), + EquivToProto(model_parameters), Eq(nullptr), + EquivToProto(args.callback_registration.Proto()), + Ne(nullptr), Eq(nullptr))) + .WillOnce(fake_solve); + + EXPECT_THAT(solver->SolveWithoutUpdate(args), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(internal::kInputFromInvalidModelStorage))); +} + +// Basic infeasible LP model: +// +// minimize 0 +// s.t. x <= -1 (linear constraint) +// 0 <= x <= 1 (bounds) +struct BasicInfeasibleLp { + BasicInfeasibleLp() + : x(model.AddContinuousVariable(0.0, 1.0, "x")), + c(model.AddLinearConstraint(x <= -1.0, "c")) {} + + ComputeInfeasibleSubsystemResultProto InfeasibleResult() const { + ComputeInfeasibleSubsystemResultProto result; + result.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE); + (*result.mutable_infeasible_subsystem()->mutable_variable_bounds())[0] + .set_lower(true); + (*result.mutable_infeasible_subsystem()->mutable_variable_bounds())[0] + .set_upper(false); + (*result.mutable_infeasible_subsystem()->mutable_linear_constraints())[0] + .set_lower(false); + (*result.mutable_infeasible_subsystem()->mutable_linear_constraints())[0] + .set_upper(true); + result.set_is_minimal(true); + return result; + } + + Model model; + const Variable x; + const LinearConstraint c; +}; + +TEST(ComputeInfeasibleSubsystemTest, SuccessfulCall) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicInfeasibleLp lp; + + ComputeInfeasibleSubsystemArguments args; + args.parameters.enable_output = true; + + SolveInterrupter interrupter; + args.interrupter = &interrupter; + + args.message_callback = [](absl::Span) {}; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, Call(EquivToProto(lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL(solver, ComputeInfeasibleSubsystem( + EquivToProto(args.parameters.Proto()), Ne(nullptr), + Eq(&interrupter))) + .WillOnce(Return(lp.InfeasibleResult())); + } + + ASSERT_OK_AND_ASSIGN( + const ComputeInfeasibleSubsystemResult result, + ComputeInfeasibleSubsystem( + lp.model, EnumFromProto(registration.solver_type()).value(), args)); + + EXPECT_EQ(result.feasibility, FeasibilityStatus::kInfeasible); +} + +TEST(ComputeInfeasibleSubsystemTest, FailingSolve) { + SolverInterfaceFactoryMock factory_mock; + SolverFactoryRegistration registration(factory_mock.AsStdFunction()); + + BasicInfeasibleLp lp; + + ComputeInfeasibleSubsystemArguments args; + args.parameters.enable_output = true; + + SolverInterfaceMock solver; + { + InSequence s; + + EXPECT_CALL(factory_mock, Call(EquivToProto(lp.model.ExportModel()), _)) + .WillOnce(Return(ByMove(std::make_unique(&solver)))); + + EXPECT_CALL(solver, ComputeInfeasibleSubsystem( + EquivToProto(args.parameters.Proto()), Eq(nullptr), + Eq(nullptr))) + .WillOnce(Return(absl::InternalError("infeasible subsystem failed"))); + } + + ASSERT_THAT( + ComputeInfeasibleSubsystem( + lp.model, EnumFromProto(registration.solver_type()).value(), args, + {}), + StatusIs(absl::StatusCode::kInternal, "infeasible subsystem failed")); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solver_resources_test.cc b/ortools/math_opt/cpp/solver_resources_test.cc new file mode 100644 index 00000000000..08615404f4c --- /dev/null +++ b/ortools/math_opt/cpp/solver_resources_test.cc @@ -0,0 +1,103 @@ +// Copyright 2010-2025 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_resources.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::EqualsProto; +using ::testing::HasSubstr; +using ::testing::Optional; + +TEST(SolverResourcesTest, Proto) { + { + const SolverResources empty; + EXPECT_THAT(empty.Proto(), EqualsProto(SolverResourcesProto{})); + } + { + const SolverResources resources = {.cpu = 3.5}; + SolverResourcesProto expected; + expected.set_cpu(3.5); + EXPECT_THAT(resources.Proto(), EqualsProto(expected)); + } + { + const SolverResources resources = {.ram = 70.0 * 1024.0 * 1024.0}; + SolverResourcesProto expected; + expected.set_ram(70.0 * 1024.0 * 1024.0); + EXPECT_THAT(resources.Proto(), EqualsProto(expected)); + } +} + +TEST(SolverResourcesTest, FromProto) { + { + ASSERT_OK_AND_ASSIGN(const SolverResources resources, + SolverResources::FromProto({})); + EXPECT_EQ(resources.cpu, std::nullopt); + } + { + SolverResourcesProto resources_proto; + resources_proto.set_cpu(3.5); + ASSERT_OK_AND_ASSIGN(const SolverResources resources, + SolverResources::FromProto(resources_proto)); + EXPECT_THAT(resources.cpu, Optional(3.5)); + } + { + SolverResourcesProto resources_proto; + resources_proto.set_ram(70.0 * 1024.0 * 1024.0); + ASSERT_OK_AND_ASSIGN(const SolverResources resources, + SolverResources::FromProto(resources_proto)); + EXPECT_THAT(resources.ram, Optional(70.0 * 1024.0 * 1024.0)); + } +} + +TEST(SolverResourcesTest, AbslParseFlag) { + // Empty value. + { + SolverResources resources; + std::string error; + EXPECT_TRUE(AbslParseFlag("", &resources, &error)); + EXPECT_EQ(resources.cpu, std::nullopt); + EXPECT_EQ(error, ""); + } + // Test valid SolverResources::cpu. + { + SolverResources resources; + std::string error; + EXPECT_TRUE(AbslParseFlag("cpu:3.5", &resources, &error)); + EXPECT_THAT(resources.cpu, Optional(3.5)); + EXPECT_EQ(error, ""); + } + // Test invalid SolverResources::cpu. + { + SolverResources resources; + std::string error; + EXPECT_FALSE(AbslParseFlag("cpu:a", &resources, &error)); + EXPECT_EQ(resources.cpu, std::nullopt); + EXPECT_THAT(error, HasSubstr("double")); + } +} + +TEST(SolverResourcesTest, AbslUnparseFlag) { + EXPECT_EQ(AbslUnparseFlag({}), ""); + EXPECT_EQ(AbslUnparseFlag({.cpu = 3.5}), "cpu: 3.5"); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/statistics_test.cc b/ortools/math_opt/cpp/statistics_test.cc new file mode 100644 index 00000000000..3cffb64a017 --- /dev/null +++ b/ortools/math_opt/cpp/statistics_test.cc @@ -0,0 +1,162 @@ +// Copyright 2010-2025 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/statistics.h" + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/model.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::Optional; +using ::testing::Pair; + +constexpr double kInf = std::numeric_limits::infinity(); + +std::string PrintToString(const ModelRanges& ranges) { + std::ostringstream oss; + oss << ranges; + return oss.str(); +} + +TEST(ModelRangesTest, Printing) { + EXPECT_EQ(PrintToString(ModelRanges{}), + "Objective terms : no finite values\n" + "Variable bounds : no finite values\n" + "Linear constraints bounds : no finite values\n" + "Linear constraints coeffs : no finite values"); + + EXPECT_EQ( + PrintToString(ModelRanges{ + .objective_terms = std::make_pair(2.12345e-99, 1.12345e3), + .variable_bounds = std::make_pair(9.12345e-2, 1.12345e2), + .linear_constraint_bounds = std::make_pair(2.12345e6, 5.12345e99), + .linear_constraint_coefficients = std::make_pair(0.0, 0.0), + }), + "Objective terms : [2.12e-99 , 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]"); + + EXPECT_EQ( + PrintToString(ModelRanges{ + .objective_terms = std::make_pair(2.12345e-1, 1.12345e3), + .variable_bounds = std::make_pair(9.12345e-2, 1.12345e2), + .linear_constraint_bounds = std::make_pair(2.12345e6, 5.12345e99), + .linear_constraint_coefficients = std::make_pair(0.0, 1.0e100), + }), + "Objective terms : [2.12e-01 , 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 1.00e+100]"); + + EXPECT_EQ( + PrintToString(ModelRanges{ + .objective_terms = std::make_pair(2.12345e-100, 1.12345e3), + .variable_bounds = std::make_pair(9.12345e-2, 1.12345e2), + .linear_constraint_bounds = std::make_pair(2.12345e6, 5.12345e99), + .linear_constraint_coefficients = std::make_pair(0.0, 0.0), + }), + "Objective terms : [2.12e-100, 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]"); + + EXPECT_EQ( + PrintToString(ModelRanges{ + .objective_terms = std::make_pair(2.12345e-100, 1.12345e3), + .variable_bounds = std::make_pair(9.12345e-2, 1.12345e2), + .linear_constraint_bounds = std::make_pair(2.12345e6, 5.12345e99), + .linear_constraint_coefficients = std::make_pair(0.0, 1.0e100), + }), + "Objective terms : [2.12e-100, 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 1.00e+100]"); +} + +TEST(ModelRangesTest, PrintingResetFlags) { + const ModelRanges ranges = { + .objective_terms = std::make_pair(2.12345e-100, 1.12345e3), + .variable_bounds = std::make_pair(9.12345e-2, 1.12345e2), + .linear_constraint_bounds = std::make_pair(2.12345e6, 5.12345e99), + .linear_constraint_coefficients = std::make_pair(0.0, 1.0e100), + }; + + std::ostringstream oss; + oss << ranges << '\n' << 1.23456789; + + EXPECT_EQ(oss.str(), + "Objective terms : [2.12e-100, 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 1.00e+100]\n" + "1.23457"); +} + +TEST(ComputeModelRangesTest, Empty) { + const ModelRanges ranges = ComputeModelRanges(Model("model")); + EXPECT_EQ(ranges.objective_terms, std::nullopt); + EXPECT_EQ(ranges.variable_bounds, std::nullopt); + EXPECT_EQ(ranges.linear_constraint_bounds, std::nullopt); + EXPECT_EQ(ranges.linear_constraint_coefficients, std::nullopt); +} + +TEST(ComputeModelRangesTest, OnlyZeroAndInfiniteValues) { + Model model("model"); + model.AddContinuousVariable(/*lower_bound=*/0.0, /*upper_bound=*/kInf); + model.AddContinuousVariable(/*lower_bound=*/-kInf, /*upper_bound=*/0.0); + model.AddContinuousVariable(/*lower_bound=*/-kInf, /*upper_bound=*/kInf); + model.AddLinearConstraint(/*lower_bound=*/0.0, /*upper_bound=*/kInf); + model.AddLinearConstraint(/*lower_bound=*/-kInf, /*upper_bound=*/0.0); + model.AddLinearConstraint(/*lower_bound=*/-kInf, /*upper_bound=*/kInf); + + const ModelRanges ranges = ComputeModelRanges(model); + EXPECT_EQ(ranges.objective_terms, std::nullopt); + EXPECT_EQ(ranges.variable_bounds, std::nullopt); + EXPECT_EQ(ranges.linear_constraint_bounds, std::nullopt); + EXPECT_EQ(ranges.linear_constraint_coefficients, std::nullopt); +} + +TEST(ComputeModelRangesTest, MixedValues) { + Model model("model"); + const Variable x = model.AddContinuousVariable(/*lower_bound=*/0.0, + /*upper_bound=*/0.0, "x"); + const Variable y = model.AddContinuousVariable(/*lower_bound=*/-kInf, + /*upper_bound=*/1e-3, "y"); + const Variable z = model.AddContinuousVariable(/*lower_bound=*/-3e2, + /*upper_bound=*/kInf, "z"); + model.Minimize(-5.0e4 * x + 1.0e-6 * z * x + 3.0 * y); + model.AddLinearConstraint(0.0 <= 1.25e-3 * y - 4.5e3 * x, "c"); + model.AddLinearConstraint(/*lower_bound=*/-kInf, /*upper_bound=*/3e4); + model.AddLinearConstraint(-1e-5 <= 2.5e-3 * y <= 0.0); + + const ModelRanges ranges = ComputeModelRanges(model); + EXPECT_THAT(ranges.objective_terms, Optional(Pair(1.0e-6, 5.0e4))); + EXPECT_THAT(ranges.variable_bounds, Optional(Pair(1e-3, 3e2))); + EXPECT_THAT(ranges.linear_constraint_bounds, Optional(Pair(1e-5, 3e4))); + EXPECT_THAT(ranges.linear_constraint_coefficients, + Optional(Pair(1.25e-3, 4.5e3))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/streamable_solver_init_arguments_test.cc b/ortools/math_opt/cpp/streamable_solver_init_arguments_test.cc new file mode 100644 index 00000000000..060f5cb2990 --- /dev/null +++ b/ortools/math_opt/cpp/streamable_solver_init_arguments_test.cc @@ -0,0 +1,50 @@ +// Copyright 2010-2025 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/streamable_solver_init_arguments.h" + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/solvers/gurobi.pb.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::EqualsProto; + +TEST(StreamableInitArgumentsTest, Empty) { + const SolverInitializerProto args_proto; + ASSERT_OK_AND_ASSIGN(const StreamableSolverInitArguments args, + StreamableSolverInitArguments::FromProto(args_proto)); + EXPECT_THAT(args.Proto(), EqualsProto(args_proto)); +} + +TEST(StreamableGurobiInitArgumentsTest, ISV) { + SolverInitializerProto args_proto; + GurobiInitializerProto::ISVKey& isv_key_proto = + *args_proto.mutable_gurobi()->mutable_isv_key(); + isv_key_proto.set_name("the name"); + isv_key_proto.set_application_name("the application"); + isv_key_proto.set_expiration(15); + isv_key_proto.set_key("the key"); + + ASSERT_OK_AND_ASSIGN(const StreamableSolverInitArguments args, + StreamableSolverInitArguments::FromProto(args_proto)); + EXPECT_THAT(args.Proto(), EqualsProto(args_proto)); +} + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/variable_and_expressions_nc_test.cc b/ortools/math_opt/cpp/variable_and_expressions_nc_test.cc new file mode 100644 index 00000000000..6330c9a4c27 --- /dev/null +++ b/ortools/math_opt/cpp/variable_and_expressions_nc_test.cc @@ -0,0 +1,101 @@ +// Copyright 2010-2025 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 is a non-compilation test: go/cc_clang_verify_test. + +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/variable_and_expressions.h" + +namespace operations_research::math_opt { +namespace { + +// This is kind of obvious, but here as a baseline. +void CannotMutateTermsThroughConstReference() { + ModelStorage storage; + LinearExpression expr; + for (const auto& [var, coeff] : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'coeff'{{.*}}}} + coeff += 1.0; + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + var = Variable(&storage, storage.AddVariable("foo")); + } + for (const auto& term : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'term'{{.*}}}} + term.second += 1.0; + // And of course, one can't mutate the key. + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + term.first = Variable(&storage, storage.AddVariable("foo")); + } + for (const auto [var, coeff] : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'coeff'{{.*}}}} + coeff += 1.0; + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + var = Variable(&storage, storage.AddVariable("foo")); + } + for (const auto term : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'term'{{.*}}}} + term.second += 1.0; + // And of course, one can't mutate the key. + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + term.first = Variable(&storage, storage.AddVariable("foo")); + } +} + +// People might think that they can mutate the coefficients through a non-const +// reference. This is not supported. +void CannotMutateTermsThroughReference() { + ModelStorage storage; + LinearExpression expr; + for (auto& term : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'term'{{.*}}}} + term.second += 1.0; + // And of course, one can't mutate the key. + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + term.first = Variable(&storage, storage.AddVariable("foo")); + } +} +void CannotMutateTermsThroughReferenceBinding() { + ModelStorage storage; + LinearExpression expr; + for (auto& [var, coeff] : expr.terms()) { + // expected-error-re@+1 {{{{.*}}cannot assign to variable 'coeff'{{.*}}}} + coeff += 1.0; + // And of course, one can't mutate the key. + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + var = Variable(&storage, storage.AddVariable("foo")); + } +} + +// People might also think that the keys are proxy object that allow mutating +// the coefficients through a non-const value. This is not supported either. +void CannotMutateTermsThroughValue() { + ModelStorage storage; + LinearExpression expr; + for (auto term : expr.terms()) { + term.second += 1.0; + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + term.first = Variable(&storage, storage.AddVariable("foo")); + } +} +void CannotMutateTermsThroughValueBinding() { + ModelStorage storage; + LinearExpression expr; + for (auto [var, coeff] : expr.terms()) { + coeff += 1.0; + // expected-error-re@+1 {{{{.*}}no viable overloaded '='{{.*}}}} + var = Variable(&storage, storage.AddVariable("foo")); + } +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/variable_and_expressions_test.cc b/ortools/math_opt/cpp/variable_and_expressions_test.cc new file mode 100644 index 00000000000..7cb818374fa --- /dev/null +++ b/ortools/math_opt/cpp/variable_and_expressions_test.cc @@ -0,0 +1,7643 @@ +// Copyright 2010-2025 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/variable_and_expressions.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/elemental/elements.h" +#include "ortools/math_opt/storage/model_storage.h" +#include "ortools/util/fp_roundtrip_conv.h" +#include "ortools/util/fp_roundtrip_conv_testing.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::ContainerEq; +using ::testing::ExplainMatchResult; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Pair; +using ::testing::PrintToString; +using ::testing::UnorderedElementsAre; + +using internal::kObjectsFromOtherModelStorage; + +constexpr double kInf = std::numeric_limits::infinity(); +constexpr double kNaN = std::numeric_limits::quiet_NaN(); + +// Struct used as parameter in BoundedLinearExpressionEquiv matcher. +struct LinearTerms { + LinearTerms() = default; + LinearTerms(std::initializer_list terms) : terms(terms) {} + std::vector terms; +}; + +std::ostream& operator<<(std::ostream& ostr, const LinearTerms& terms) { + if (terms.terms.empty()) { + ostr << "0"; + return ostr; + } + + bool first = true; + for (const auto& term : terms.terms) { + if (first) { + first = false; + } else { + ostr << " + "; + } + ostr << term.coefficient << "*" << term.variable; + } + return ostr; +} + +// Return true if the two BoundedLinearExpression are equivalent. This is the +// case when they have the same bounds (after removing the offset) and the same +// coefficients. But this is also the case when they exchange their bounds and +// change all the signs. +// +// For example these bounded expressions are equivalent: +// 3 <= 2 * x - y + 2 <= 5 +// 4 <= 2 * x - y + 3 <= 6 +// -5 <= -2 * x + y - 2 <= -3 +// +// If one number is NaN the bounded expression will also be considered +// different. +// +// Usage: +// EXPECT_THAT(-2 <= 3 * x + 2 * y <= 4, +// BoundedLinearExpressionEquiv(-2, LinearTerms({{x, 3}, {y, 2}}), +// 4)); +MATCHER_P3(BoundedLinearExpressionEquiv, lower_bound, terms, upper_bound, + absl::StrFormat("%s equivalent to %s <= %s <= %s", + negation ? "isn't" : "is", + PrintToString(lower_bound), PrintToString(terms), + PrintToString(upper_bound))) { + // We detect if we need to switch and negate bounds, and also negate terms. + const bool negation = arg.lower_bound_minus_offset() != lower_bound; + { + const double expected_lower_bound_minus_offset = + negation ? -upper_bound : lower_bound; + // We use the `!(x == y)` trick here so that NaN are seen as + // errors. Comparison with NaN is always false, hence the negation of + // equality will be true is at least one operand is NaN. + if (!(arg.lower_bound_minus_offset() == + expected_lower_bound_minus_offset)) { + *result_listener << "lower_bound - offset = " + << arg.lower_bound_minus_offset() + << " != " << expected_lower_bound_minus_offset; + return false; + } + } + { + const double expected_upper_bound_minus_offset = + negation ? -lower_bound : upper_bound; + // We use the `!(x == y)` trick here so that NaN are seen as + // errors. Comparison with NaN is always false, hence the negation of + // equality will be true is at least one operand is NaN. + if (!(arg.upper_bound_minus_offset() == + expected_upper_bound_minus_offset)) { + *result_listener << "upper_bound - offset = " + << arg.upper_bound_minus_offset() + << " != " << expected_upper_bound_minus_offset; + return false; + } + } + VariableMap expected_terms; + for (const auto& term : terms.terms) { + expected_terms.emplace(term.variable, + negation ? -term.coefficient : term.coefficient); + } + return ExplainMatchResult(ContainerEq(expected_terms), arg.expression.terms(), + result_listener); +} + +TEST(BoundedLinearExpressionMatcher, EmptyExpressions) { + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -kInf, kInf), + BoundedLinearExpressionEquiv(-kInf, LinearTerms(), kInf)); + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -3.0, 5.0), + BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0)); + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -5.0, 3.0), + BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0)); + + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), kNaN, 5.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -3.0, kNaN), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), kNaN, kNaN), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); + + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -2.0, 5.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); + EXPECT_THAT(BoundedLinearExpression(LinearExpression(), -3.0, 6.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); +} + +TEST(BoundedLinearExpressionMatcher, OffsetOnlyExpressions) { + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, 3.0), -kInf, kInf), + BoundedLinearExpressionEquiv(-kInf, LinearTerms(), kInf)); + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, 4.0), -3.0, 5.0), + BoundedLinearExpressionEquiv(-7.0, LinearTerms(), 1.0)); + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, -4.0), -5.0, 3.0), + BoundedLinearExpressionEquiv(-7.0, LinearTerms(), 1.0)); + + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, kNaN), -3.0, 5.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); + + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, 1.0), -3.0, 5.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms(), 5.0))); +} + +TEST(BoundedLinearExpressionMatcher, OffsetAndTermsExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, 1.0}, {b, -2.0}}, 3.0), -kInf, kInf), + BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 1.0}, {b, -2.0}}), kInf)); + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, 1.0}, {b, -2.0}}, 4.0), -3.0, 5.0), + BoundedLinearExpressionEquiv( + -7.0, LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0)); + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, -1.0}, {b, 2.0}}, -4.0), -5.0, 3.0), + BoundedLinearExpressionEquiv( + -7.0, LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0)); + + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, kNaN}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 1.0}, {b, -2.0}}), 5.0))); + + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, 1.0}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, -1.0}, {b, -2.0}}), 5.0))); + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, 1.0}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 1.0}, {b, -2.0}, {c, 3.0}}), 5.0))); + EXPECT_THAT( + BoundedLinearExpression(LinearExpression({{a, 1.0}, {b, -2.0}}, 0.0), + -3.0, 5.0), + Not(BoundedLinearExpressionEquiv(-3.0, LinearTerms({{a, 1.0}}), 5.0))); + EXPECT_THAT(BoundedLinearExpression( + LinearExpression({{a, -1.0}, {b, 2.0}}, -4.0), -5.0, 3.0), + Not(BoundedLinearExpressionEquiv(-7.0, LinearTerms(), 1.0))); + EXPECT_THAT(BoundedLinearExpression(LinearExpression({}, -4.0), -5.0, 3.0), + Not(BoundedLinearExpressionEquiv( + -7.0, LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0))); +} + +// Struct used as parameter in BoundedLinearExpressionEquiv matcher. +struct QuadraticTerms { + QuadraticTerms() = default; + QuadraticTerms(std::initializer_list terms) : terms(terms) {} + std::vector terms; +}; + +std::ostream& operator<<(std::ostream& ostr, const QuadraticTerms& terms) { + if (terms.terms.empty()) { + ostr << "0"; + return ostr; + } + + bool first = true; + for (const auto& term : terms.terms) { + if (first) { + first = false; + } else { + ostr << " + "; + } + ostr << term.coefficient() << "*" << term.first_variable(); + if (term.first_variable() == term.second_variable()) { + ostr << "²"; + } else { + ostr << "*" << term.second_variable(); + } + } + return ostr; +} + +// Return true if the two BoundedQuadraticExpression are equivalent. This is the +// case when they have the same bounds (after removing the offset) and the same +// coefficients. But this is also the case when they exchange their bounds and +// change all the signs. +// +// For example these bounded expressions are equivalent: +// 3 <= 2 * x * y - y + 2 <= 5 +// 4 <= 2 * x * y - y + 3 <= 6 +// -5 <= -2 * x * y + y - 2 <= -3 +// +// If one number is NaN the bounded expression will also be considered +// different. +// +// Usage: +// EXPECT_THAT(-2 <= 3 * x * y + 2 * y <= 4, +// BoundedQuadraticExpressionEquiv(-2, QuadraticTerms({{x, y, +// 3}}), LinearTerms({{x, 3}, {y, 2}}), 4)); +MATCHER_P4(BoundedQuadraticExpressionEquiv, lower_bound, quadratic_terms, + linear_terms, upper_bound, + absl::StrFormat("%s equivalent to %s <= %s + %s <= %s", + negation ? "isn't" : "is", + PrintToString(lower_bound), + PrintToString(quadratic_terms), + PrintToString(linear_terms), + PrintToString(upper_bound))) { + // We detect if we need to switch and negate bounds, and also negate terms. + const bool negation = arg.lower_bound_minus_offset() != lower_bound; + { + const double expected_lower_bound_minus_offset = + negation ? -upper_bound : lower_bound; + // We use the `!(x == y)` trick here so that NaN are seen as + // errors. Comparison with NaN is always false, hence the negation of + // equality will be true is at least one operand is NaN. + if (!(arg.lower_bound_minus_offset() == + expected_lower_bound_minus_offset)) { + *result_listener << "lower_bound - offset = " + << arg.lower_bound_minus_offset() + << " != " << expected_lower_bound_minus_offset; + return false; + } + } + { + const double expected_upper_bound_minus_offset = + negation ? -lower_bound : upper_bound; + // We use the `!(x == y)` trick here so that NaN are seen as + // errors. Comparison with NaN is always false, hence the negation of + // equality will be true is at least one operand is NaN. + if (!(arg.upper_bound_minus_offset() == + expected_upper_bound_minus_offset)) { + *result_listener << "upper_bound - offset = " + << arg.upper_bound_minus_offset() + << " != " << expected_upper_bound_minus_offset; + return false; + } + } + VariableMap expected_linear_terms; + for (const auto& term : linear_terms.terms) { + expected_linear_terms.insert( + {term.variable, negation ? -term.coefficient : term.coefficient}); + } + QuadraticTermMap expected_quadratic_terms; + for (const auto& term : quadratic_terms.terms) { + expected_quadratic_terms.insert( + {term.GetKey(), negation ? -term.coefficient() : term.coefficient()}); + } + return ExplainMatchResult(ContainerEq(expected_linear_terms), + arg.expression.linear_terms(), result_listener) & + ExplainMatchResult(ContainerEq(expected_quadratic_terms), + arg.expression.quadratic_terms(), result_listener); +} + +TEST(BoundedQuadraticExpressionMatcher, EmptyExpressions) { + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -kInf, kInf), + BoundedQuadraticExpressionEquiv(-kInf, QuadraticTerms(), + LinearTerms(), kInf)); + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -3.0, 5.0), + BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0)); + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -5.0, 3.0), + BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0)); + + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), kNaN, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0))); + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -3.0, kNaN), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0))); + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), kNaN, kNaN), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0))); + + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -2.0, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0))); + EXPECT_THAT(BoundedQuadraticExpression(QuadraticExpression(), -3.0, 6.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms(), 5.0))); +} + +TEST(BoundedQuadraticExpressionMatcher, OffsetOnlyExpressions) { + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, 3.0), -kInf, kInf), + BoundedQuadraticExpressionEquiv(-kInf, QuadraticTerms(), LinearTerms(), + kInf)); + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, 4.0), -3.0, 5.0), + BoundedQuadraticExpressionEquiv(-7.0, QuadraticTerms(), LinearTerms(), + 1.0)); + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, -4.0), -5.0, 3.0), + BoundedQuadraticExpressionEquiv(-7.0, QuadraticTerms(), LinearTerms(), + 1.0)); + + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, kNaN), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), LinearTerms(), + 5.0))); + + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, 1.0), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), LinearTerms(), + 5.0))); +} + +TEST(BoundedQuadraticExpressionMatcher, OffsetAndLinearTermsOnlyExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1.0}, {b, -2.0}}, 3.0), -kInf, kInf), + BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms(), LinearTerms({{a, 1.0}, {b, -2.0}}), kInf)); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1.0}, {b, -2.0}}, 4.0), -3.0, 5.0), + BoundedQuadraticExpressionEquiv(-7.0, QuadraticTerms(), + LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0)); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, -1.0}, {b, 2.0}}, -4.0), -5.0, 3.0), + BoundedQuadraticExpressionEquiv(-7.0, QuadraticTerms(), + LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0)); + + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, kNaN}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms(), LinearTerms({{a, 1.0}, {b, -2.0}}), 5.0))); + + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1.0}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms(), LinearTerms({{a, -1.0}, {b, -2.0}}), 5.0))); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1.0}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms(), LinearTerms({{a, 1.0}, {b, -2.0}, {c, 3.0}}), + 5.0))); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1.0}, {b, -2.0}}, 0.0), -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms(), + LinearTerms({{a, 1.0}}), 5.0))); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({}, {{a, -1.0}, {b, 2.0}}, -4.0), -5.0, 3.0), + Not(BoundedQuadraticExpressionEquiv(-7.0, QuadraticTerms(), LinearTerms(), + 1.0))); + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, -4.0), -5.0, 3.0), + Not(BoundedQuadraticExpressionEquiv( + -7.0, QuadraticTerms(), LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0))); +} + +TEST(BoundedQuadraticExpressionMatcher, OffsetAndTermsExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, + {{a, 1.0}, {b, -2.0}}, 3.0), + -kInf, kInf), + BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}}), + LinearTerms({{a, 1.0}, {b, -2.0}}), kInf)); + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, + {{a, 1.0}, {b, -2.0}}, 4.0), + -3.0, 5.0), + BoundedQuadraticExpressionEquiv( + -7.0, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}}), + LinearTerms({{a, 1.0}, {b, -2.0}}), 1.0)); + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, + {{a, -1.0}, {b, 2.0}}, -4.0), + -5.0, 3.0), + BoundedQuadraticExpressionEquiv( + -1.0, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}}), + LinearTerms({{a, -1.0}, {b, 2.0}}), 7.0)); + + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, kNaN}, {a, b, 5.0}}, + {{a, 1.0}, {b, -2.0}}, 0.0), + -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}}), + LinearTerms({{a, 1.0}, {b, -2.0}}), 5.0))); + + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, + {{a, 1.0}, {b, -2.0}}, 0.0), + -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms({{a, a, -4.0}, {a, b, 5.0}}), + LinearTerms({{a, 1.0}, {b, -2.0}}), 5.0))); + EXPECT_THAT(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, + {{a, 1.0}, {b, -2.0}}, 0.0), + -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv( + -3.0, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}, {a, c, 6.0}}), + LinearTerms({{a, 1.0}, {b, -2.0}, {c, 3.0}}), 5.0))); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, {{a, 1.0}, {b, -2.0}}, + 0.0), + -3.0, 5.0), + Not(BoundedQuadraticExpressionEquiv(-3.0, QuadraticTerms({{a, a, 4.0}}), + LinearTerms({{a, 1.0}}), 5.0))); + EXPECT_THAT( + BoundedQuadraticExpression( + QuadraticExpression({{a, a, 4.0}, {a, b, 5.0}}, {{a, -1.0}, {b, 2.0}}, + -4.0), + -5.0, 3.0), + Not(BoundedQuadraticExpressionEquiv( + -7.0, QuadraticTerms(), LinearTerms({{a, -1.0}, {b, 2.0}}), 1.0))); + EXPECT_THAT( + BoundedQuadraticExpression(QuadraticExpression({}, {}, -4.0), -5.0, 3.0), + Not(BoundedQuadraticExpressionEquiv( + -7.0, QuadraticTerms({{a, a, 4.0}, {a, b, 5.0}}), LinearTerms(), + 1.0))); +} + +TEST(Variable, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable()); + + auto to_string = [](Variable v) { + std::ostringstream oss; + oss << v; + return oss.str(); + }; + + EXPECT_EQ(to_string(a), "a"); + EXPECT_EQ(to_string(b), absl::StrCat("__var#", b.id(), "__")); +} + +TEST(Variable, Accessors) { + ModelStorage storage; + { + const VariableId v_id = + storage.AddVariable(/*lower_bound=*/-kInf, /*upper_bound=*/kInf, + /*is_integer=*/false, "continuous"); + const Variable v(&storage, v_id); + EXPECT_EQ(v.name(), "continuous"); + EXPECT_EQ(v.id(), v_id.value()); + EXPECT_EQ(v.typed_id(), v_id); + EXPECT_EQ(v.lower_bound(), -kInf); + EXPECT_EQ(v.upper_bound(), kInf); + EXPECT_FALSE(v.is_integer()); + } + { + const VariableId v_id = + storage.AddVariable(/*lower_bound=*/3.0, /*upper_bound=*/5.0, + /*is_integer=*/true, "integer"); + const Variable v(&storage, v_id); + EXPECT_EQ(v.name(), "integer"); + EXPECT_EQ(v.id(), v_id.value()); + EXPECT_EQ(v.typed_id(), v_id); + EXPECT_EQ(v.lower_bound(), 3.0); + EXPECT_EQ(v.upper_bound(), 5.0); + EXPECT_TRUE(v.is_integer()); + } +} + +TEST(Variable, NameAfterDeletion) { + Model model; + const Variable x = model.AddVariable("x"); + ASSERT_EQ(x.name(), "x"); + + model.DeleteVariable(x); + EXPECT_EQ(x.name(), "[variable deleted from model]"); +} + +TEST(VariablesEqualityTest, SameModelAndVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable a_copy = a; + + // First test with EXPECT_ macros. These works even if the `operator bool` + // conversion is explicit. + EXPECT_EQ(a, a); + EXPECT_EQ(a, a_copy); + + // Then test with writing `==` directly. + EXPECT_TRUE(a == a); + EXPECT_TRUE(a == a_copy); + + // And the operator `!=`. + EXPECT_FALSE(a != a); + EXPECT_FALSE(a != a_copy); +} + +TEST(VariablesEqualityTest, SameModelTwoVariables) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with EXPECT_NE macro. It works even if the `operator bool` + // conversion is explicit. + EXPECT_NE(a, b); + + // Then test with writing `==` directly to test the implicit cast. + EXPECT_FALSE(a == b); + + // Same for `!=`. + EXPECT_TRUE(a != b); +} + +TEST(VariablesEqualityTest, DifferentModelsSameVariable) { + // Create two variables with the same name and index but in two different + // models. + ModelStorage model_a; + const Variable a_a(&model_a, model_a.AddVariable("a")); + ModelStorage model_b; + const Variable b_a(&model_b, model_b.AddVariable("a")); + + // First test with EXPECT_NE macro. It works even if the `operator bool` + // conversion is explicit. + EXPECT_NE(a_a, b_a); + + // Then test with writing `==` directly to test the implicit cast. + EXPECT_FALSE(a_a == b_a); + + // Same for `!=`. + EXPECT_TRUE(a_a != b_a); +} + +TEST(VariablesEqualityTest, VariablesAsKeysInMap) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // Test using the variables as keys. + absl::flat_hash_map map; + map.emplace(a, 1); + map[b] = 2; + + EXPECT_EQ(map.at(a), 1); + EXPECT_EQ(map.at(b), 2); +} + +TEST(LinearTermTest, FromVariableAndDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term(a, 3.0); + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 3.0); +} + +TEST(LinearTermTest, Negation) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = -LinearTerm(a, 3); + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, -3.0); +} + +TEST(LinearTermTest, DoubleTimesVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = 3 * a; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 3.0); +} + +TEST(LinearTermTest, VariableTimesDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = a * 3; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 3.0); +} + +TEST(LinearTermTest, DoubleTimesLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = 2 * LinearTerm(a, 3); + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 6.0); +} + +TEST(LinearTermTest, LinearTermTimesDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = LinearTerm(a, 3) * 2; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 6.0); +} + +TEST(LinearTermTest, LinearTermTimesAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LinearTerm term(a, 3); + term *= 2; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 6.0); +} + +TEST(LinearTermTest, VariableDividedByDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = a / 2; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 0.5); +} + +TEST(LinearTermTest, LinearTermDividedByDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearTerm term = LinearTerm(a, 4) / 2; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 2.0); +} + +TEST(LinearTermTest, LinearTermDividedByAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LinearTerm term(a, 4); + term /= 2; + EXPECT_EQ(term.variable, a); + EXPECT_EQ(term.coefficient, 2.0); +} + +// Define a reset function and a set of macros to test the constructor counters +// of LinearExpression. When the counters are disabled, define empty +// equivalents. +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS +void ResetExpressionCounters() { + LinearExpression::ResetCounters(); + QuadraticExpression::ResetCounters(); +} +#define EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(T, x) \ + EXPECT_EQ(T::num_calls_default_constructor_, x); +#define EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(T, x) \ + EXPECT_EQ(T::num_calls_copy_constructor_, x); +#define EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(T, x) \ + EXPECT_EQ(T::num_calls_move_constructor_, x); +#define EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(T, x) \ + EXPECT_EQ(T::num_calls_initializer_list_constructor_, x); +#define EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(x) \ + EXPECT_EQ(QuadraticExpression::num_calls_linear_expression_constructor_, x); +#define EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(x) \ + EXPECT_EQ(LinearExpression::num_calls_default_constructor_ + \ + LinearExpression::num_calls_copy_constructor_ + \ + LinearExpression::num_calls_move_constructor_ + \ + LinearExpression::num_calls_initializer_list_constructor_, \ + x) \ + << "num_calls_default_constructor: " \ + << LinearExpression::num_calls_default_constructor_ \ + << "\nnum_calls_copy_constructor: " \ + << LinearExpression::num_calls_copy_constructor_ \ + << "\nnum_calls_move_constructor: " \ + << LinearExpression::num_calls_move_constructor_ \ + << "\nnum_calls_initializer_list_constructor: " \ + << LinearExpression::num_calls_initializer_list_constructor_ +#define EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(x) \ + EXPECT_EQ(QuadraticExpression::num_calls_default_constructor_ + \ + QuadraticExpression::num_calls_copy_constructor_ + \ + QuadraticExpression::num_calls_move_constructor_ + \ + QuadraticExpression::num_calls_initializer_list_constructor_ + \ + QuadraticExpression::num_calls_linear_expression_constructor_, \ + x) \ + << "num_calls_default_constructor: " \ + << QuadraticExpression::num_calls_default_constructor_ \ + << "\nnum_calls_copy_constructor: " \ + << QuadraticExpression::num_calls_copy_constructor_ \ + << "\nnum_calls_move_constructor: " \ + << QuadraticExpression::num_calls_move_constructor_ \ + << "\nnum_calls_initializer_list_constructor: " \ + << QuadraticExpression::num_calls_initializer_list_constructor_ \ + << "\nnum_calls_linear_expression_constructor: " \ + << QuadraticExpression::num_calls_linear_expression_constructor_ +#else +void ResetExpressionCounters() {} +#define EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(T, x) +#define EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(T, x) +#define EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(T, x) +#define EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(T, x) +#define EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(x) +#define EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(x) +#define EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(x) +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS + +TEST(VariableTest, Negation) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + ResetExpressionCounters(); + { + const LinearExpression expr = -a; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}}, 0))); + } + + ResetExpressionCounters(); + { + const QuadraticExpression expr = -a; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -1}}, 0))); + } +} + +TEST(LinearExpressionTest, DefaultValue) { + const LinearExpression expr; + EXPECT_EQ(expr.offset(), 0.0); + EXPECT_THAT(expr.terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.terms(), IsEmpty()); +} + +TEST(LinearExpressionTest, EmptyInitializerList) { + const LinearExpression expr({}, 5); + EXPECT_EQ(expr.offset(), 5.0); + EXPECT_THAT(expr.terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.terms(), IsEmpty()); +} + +TEST(LinearExpressionTest, TermsFromSameModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearExpression expr({{a, 3}, {b, 5}, {a, -2}}, -1); + EXPECT_EQ(expr.offset(), -1.0); + EXPECT_THAT(expr.terms(), UnorderedElementsAre(Pair(a, 1), Pair(b, 5))); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.terms(), UnorderedElementsAre(Pair(a, 1), Pair(b, 5))); +} + +TEST(LinearExpressionDeathTest, TermsFromDifferentModels) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + EXPECT_DEATH(LinearExpression({{a, 3}, {b, 5}}, -1), + kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, ReassignDifferentModels) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + const LinearExpression expr_a = a + 2.0; + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + LinearExpression expr_b_to_overwrite = 3.0 * b + 1.0; + + expr_b_to_overwrite = expr_a; + EXPECT_THAT(expr_b_to_overwrite, IsIdentical(LinearExpression({{a, 1}}, 2))); + EXPECT_EQ(expr_b_to_overwrite.storage(), &model_a); +} + +TEST(LinearExpressionTest, MoveConstruction) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + LinearExpression expr_a = a + 2.0; + const LinearExpression expr_b = std::move(expr_a); + + EXPECT_THAT(expr_b, IsIdentical(LinearExpression({{a, 1}}, 2))); + EXPECT_EQ(expr_b.storage(), &model_a); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(expr_a.terms(), IsEmpty()); + EXPECT_EQ(expr_a.storage(), nullptr); +} + +TEST(LinearExpressionTest, MoveAssignment) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + LinearExpression expr_a = a + 2.0; + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + LinearExpression expr_b_to_overwrite = 3.0 * b + 1.0; + + expr_b_to_overwrite = std::move(expr_a); + + EXPECT_THAT(expr_b_to_overwrite, IsIdentical(LinearExpression({{a, 1}}, 2))); + EXPECT_EQ(expr_b_to_overwrite.storage(), &model_a); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(expr_a.terms(), IsEmpty()); + EXPECT_EQ(expr_a.storage(), nullptr); +} + +TEST(LinearExpressionTest, EvaluateEmpty) { + const LinearExpression empty_expr; + { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + VariableMap variable_values; + variable_values[a] = 10.0; + EXPECT_EQ(empty_expr.Evaluate(variable_values), 0.0); + EXPECT_EQ(empty_expr.EvaluateWithDefaultZero(variable_values), 0.0); + } + { + const VariableMap empty_values; + EXPECT_EQ(empty_expr.Evaluate(empty_values), 0.0); + EXPECT_EQ(empty_expr.EvaluateWithDefaultZero(empty_values), 0.0); + } +} + +TEST(LinearExpressionTest, EvaluateOnlyOffset) { + const LinearExpression constant_expr(8.0); + { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + VariableMap variable_values; + variable_values[a] = 10.0; + EXPECT_EQ(constant_expr.Evaluate(variable_values), 8.0); + EXPECT_EQ(constant_expr.EvaluateWithDefaultZero(variable_values), 8.0); + } + { + const VariableMap empty_values; + EXPECT_EQ(constant_expr.Evaluate(empty_values), 8.0); + EXPECT_EQ(constant_expr.EvaluateWithDefaultZero(empty_values), 8.0); + } +} + +TEST(LinearExpressionTest, SimpleEvaluate) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearExpression expr = 3.0 * a + 5.0 * b - 2.0; + VariableMap variable_values; + variable_values[a] = 10.0; + variable_values[b] = 100.0; + EXPECT_EQ(expr.Evaluate(variable_values), 528.0); +} + +TEST(LinearExpressionTest, SimpleEvaluateWithDefault) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearExpression expr = 3.0 * a + 5.0 * b - 2.0; + VariableMap variable_values; + variable_values[b] = 100.0; + EXPECT_EQ(expr.EvaluateWithDefaultZero(variable_values), 498.0); +} + +TEST(LinearExpressionTest, StableEvaluateAndEvaluateWithDefault) { + // Here we test that the floating point sum of numbers is done in the sorted + // order of the variables ids. To do so we rely on a specific floating point + // number sequence (obtained with a Python script doing random tries) which + // floating point sum changes depending on the order of operations: + // + // 56.66114901664141 + 76.288516611269 + 73.11902164661139 + + // 0.677336454040622 + 43.75820160525244 = 250.50422533381482 + // 56.66114901664141 + 76.288516611269 + 73.11902164661139 + + // 43.75820160525244 + 0.677336454040622 = 250.50422533381484 + // 56.66114901664141 + 76.288516611269 + 0.677336454040622 + + // 73.11902164661139 + 43.75820160525244 = 250.50422533381487 + // 76.288516611269 + 0.677336454040622 + 73.11902164661139 + + // 43.75820160525244 + 56.66114901664141 = 250.5042253338149 + // + // Here we will use the first value as the offset of the linear expression (to + // test that it always taken into account in the same order). + constexpr double kOffset = 56.66114901664141; + const std::vector coeffs = {76.288516611269, 73.11902164661139, + 0.677336454040622, 43.75820160525244}; + + ModelStorage storage; + std::vector vars; + VariableMap variable_values; + for (int i = 0; i < coeffs.size(); ++i) { + vars.emplace_back(&storage, storage.AddVariable(absl::StrCat("v_", i))); + variable_values.try_emplace(vars.back(), 1.0); + } + + LinearExpression expr = kOffset; + for (const int i : {3, 2, 0, 1}) { + expr += coeffs[i] * vars[i]; + } + + // Expected value for the sum which is: + // - offset first + // - then all terms sums in the order of variables' indices + // See the table in the comment above. + constexpr double kExpected = 250.50422533381482; + + // Test Evaluate(); + { + const double got = expr.Evaluate(variable_values); + EXPECT_EQ(got, kExpected) + << "got: " << RoundTripDoubleFormat(got) + << " expected: " << RoundTripDoubleFormat(kExpected); + } + + // Test EvaluateWithDefaultZero(); + { + const double got = expr.EvaluateWithDefaultZero(variable_values); + EXPECT_EQ(got, kExpected) + << "got: " << RoundTripDoubleFormat(got) + << " expected: " << RoundTripDoubleFormat(kExpected); + } +} + +TEST(LinearExpressionDeathTest, EvaluateMisingVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearExpression expr = 3.0 * a + 5.0 * b - 2.0; + VariableMap variable_values; + variable_values[b] = 100.0; + EXPECT_DEATH(expr.Evaluate(variable_values), ""); +} + +TEST(LinearExpressionDeathTest, EvaluateDifferentModels) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + const LinearExpression expr = 3.0 * a - 2.0; + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + VariableMap variable_values; + variable_values[b] = 100.0; + + EXPECT_DEATH(expr.Evaluate(variable_values), kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionDeathTest, EvaluateWithDefaultZeroDifferentModels) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + const LinearExpression expr = 3.0 * a - 2.0; + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + VariableMap variable_values; + variable_values[b] = 100.0; + + EXPECT_EQ(expr.EvaluateWithDefaultZero(variable_values), -2.0); +} + +TEST(LinearExpressionTest, FromDouble) { + const LinearExpression expr = 4.0; + + EXPECT_THAT(expr, IsIdentical(LinearExpression({}, 4))); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.terms(), IsEmpty()); +} + +TEST(LinearExpressionTest, FromVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearExpression expr = a; + + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 0))); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.terms(), UnorderedElementsAre(Pair(a, 1.0))); +} + +TEST(LinearExpressionTest, FromLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const LinearExpression expr = LinearTerm(a, 3.0); + + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 0))); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.terms(), UnorderedElementsAre(Pair(a, 3.0))); +} + +TEST(LinearExpressionTest, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + auto to_string = [](LinearExpression expression) { + std::ostringstream oss; + oss << expression; + return oss.str(); + }; + + EXPECT_EQ(to_string(LinearExpression()), "0"); + EXPECT_EQ(to_string(LinearExpression({}, -1)), "-1"); + EXPECT_EQ(to_string(LinearExpression({}, -1)), "-1"); + EXPECT_EQ(to_string(LinearExpression({{a, 0}}, -1)), "-1"); + EXPECT_EQ(to_string(LinearExpression({{a, 3}, {b, 5}, {a, -2}}, -1)), + "a + 5*b - 1"); + EXPECT_EQ(to_string(LinearExpression({{a, -1.0}, {b, -1.0}}, -2.0)), + "-a - b - 2"); + EXPECT_EQ(to_string(LinearExpression({{a, kNaN}, {b, -kNaN}}, -kNaN)), + "nan*a + nan*b + nan"); + EXPECT_EQ(to_string(LinearExpression({{a, kRoundTripTestNumber}})), + absl::StrCat(kRoundTripTestNumberStr, "*a")); + EXPECT_EQ(to_string(LinearExpression({}, kRoundTripTestNumber)), + kRoundTripTestNumberStr); +} + +TEST(LinearExpressionTest, Negation) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = -LinearExpression({{a, 3}, {b, -2}}, 5); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -3}, {b, 2}}, -5))); +} + +TEST(LinearExpressionTest, AdditionAssignmentDouble) { + LinearExpression expr; + + ResetExpressionCounters(); + expr += 3; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({}, 3))); + + ResetExpressionCounters(); + expr += -2; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({}, 1))); +} + +TEST(LinearExpressionTest, AdditionAssignmentVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + LinearExpression expr; + ResetExpressionCounters(); + expr += a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr += a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr += b; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 1}}, 0))); +} + +TEST(LinearExpressionDeathTest, AdditionAssignmentVariableOtherModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr; + expr += a; + EXPECT_DEATH(expr += b, kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, AdditionAssignmentLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + LinearExpression expr; + ResetExpressionCounters(); + expr += LinearTerm(a, 3); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr += LinearTerm(a, -2); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr += LinearTerm(b, -5); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}, {b, -5}}, 0))); +} + +TEST(LinearExpressionDeathTest, AdditionAssignmentLinearTermOtherModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr; + expr += LinearTerm(a, 3); + EXPECT_DEATH(expr += LinearTerm(b, 2), kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, AdditionAssignmentSelf) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression expr({{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + expr += expr; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 4}, {b, 8}}, 4))); +} + +TEST(LinearExpressionTest, AdditionAssignmentOtherExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + // First test with a default expression, not associated with any ModelStorage. + LinearExpression expr; + expr += LinearExpression({{a, 2}, {b, 4}}, 2); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 4}}, 2))); + + // Then add another expression with variables from the same ModelStorage. + expr += LinearExpression({{a, -3}, {c, 6}}, -4); + EXPECT_THAT(expr, + IsIdentical(LinearExpression({{a, -1}, {b, 4}, {c, 6}}, -2))); + + // Then add another expression without variables (i.e. having null + // ModelStorage). + expr += LinearExpression({}, 3); + EXPECT_THAT(expr, + IsIdentical(LinearExpression({{a, -1}, {b, 4}, {c, 6}}, 1))); +} + +TEST(LinearExpressionDeathTest, AdditionAssignmentOtherExpressionAndModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr({{a, 1}}, 0); + const LinearExpression other({{b, 1}}, 0); + EXPECT_DEATH(expr += other, kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, SubtractionAssignmentDouble) { + LinearExpression expr; + + ResetExpressionCounters(); + expr -= 3; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({}, -3))); + + ResetExpressionCounters(); + expr -= -2; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({}, -1))); +} + +TEST(LinearExpressionTest, SubtractionAssignmentVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + LinearExpression expr; + ResetExpressionCounters(); + expr -= a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr -= a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr -= b; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}, {b, -1}}, 0))); +} + +TEST(LinearExpressionDeathTest, SubtractionAssignmentVariableOtherModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr; + expr -= a; + EXPECT_DEATH(expr -= b, kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, SubtractionAssignmentLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + LinearExpression expr; + ResetExpressionCounters(); + expr -= LinearTerm(a, 3); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -3}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr -= LinearTerm(a, -2); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr -= LinearTerm(b, 5); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}, {b, -5}}, 0))); +} + +TEST(LinearExpressionDeathTest, SubtractionAssignmentLinearTermOtherModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr; + expr -= LinearTerm(a, 3); + EXPECT_DEATH(expr -= LinearTerm(b, 2), kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, SubtractionAssignmentOtherExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + // First test with a default expression, not associated with any Model. + LinearExpression expr; + expr -= LinearExpression({{a, 2}, {b, 4}}, 2); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}, {b, -4}}, -2))); + + // Then subtract another expression with variables from the same Model. + expr -= LinearExpression({{a, -3}, {c, 6}}, -4); + EXPECT_THAT(expr, + IsIdentical(LinearExpression({{a, 1}, {b, -4}, {c, -6}}, 2))); + + // Then subtract another expression without variables (i.e. having null + // ModelStorage). + expr -= LinearExpression({}, 3); + EXPECT_THAT(expr, + IsIdentical(LinearExpression({{a, 1}, {b, -4}, {c, -6}}, -1))); +} + +TEST(LinearExpressionTest, SubtractionAssignmentSelf) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression expr({{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + expr -= expr; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 0}, {b, 0}}, 0))); +} + +TEST(LinearExpressionDeathTest, SubtractionAssignmentOtherExpressionAndModel) { + ModelStorage model_a; + const Variable a(&model_a, model_a.AddVariable("a")); + + ModelStorage model_b; + const Variable b(&model_b, model_b.AddVariable("b")); + + LinearExpression expr({{a, 1}}, 0); + const LinearExpression other({{b, 1}}, 0); + EXPECT_DEATH(expr -= other, kObjectsFromOtherModelStorage); +} + +TEST(LinearExpressionTest, VariablePlusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = a + 3; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 3))); +} + +TEST(LinearExpressionTest, DoublePlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 + a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 3))); +} + +TEST(LinearExpressionTest, LinearTermPlusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 2 * a + 3; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}}, 3))); +} + +TEST(LinearExpressionTest, DoublePlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 + 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}}, 3))); +} + +TEST(LinearExpressionTest, LinearTermPlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * a + 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 5}}, 0))); +} + +TEST(LinearExpressionTest, LinearTermPlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * a + b; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}, {b, 1}}, 0))); +} + +TEST(LinearExpressionTest, VariablePlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = a + 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 0))); +} + +TEST(LinearExpressionTest, VariablePlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = a + b; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}, {b, 1}}, 0))); +} + +TEST(LinearExpressionTest, ExpressionPlusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + b + 1) + 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 1}}, 6))); +} + +TEST(LinearExpressionTest, DoublePlusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 5 + (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 1}}, 6))); +} + +TEST(LinearExpressionTest, ExpressionPlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + b + 1) + b; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 2}}, 1))); +} + +TEST(LinearExpressionTest, VariablePlusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = b + (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 2}}, 1))); +} + +TEST(LinearExpressionTest, ExpressionPlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + b + 1) + 3 * b; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 4}}, 1))); +} + +TEST(LinearExpressionTest, LinearTermPlusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * b + (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 4}}, 1))); +} + +TEST(LinearExpressionTest, ExpressionPlusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (3 * b + a + 2) + (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 2); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}, {b, 4}}, 3))); +} + +TEST(LinearExpressionTest, VariableMinusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = a - 3; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, -3))); +} + +TEST(LinearExpressionTest, DoubleMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 - a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}}, 3))); +} + +TEST(LinearExpressionTest, LinearTermMinusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 2 * a - 3; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}}, -3))); +} + +TEST(LinearExpressionTest, DoubleMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 - 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}}, 3))); +} + +TEST(LinearExpressionTest, LinearTermMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * a - 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}}, 0))); +} + +TEST(LinearExpressionTest, LinearTermMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * a - b; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}, {b, -1}}, 0))); +} + +TEST(LinearExpressionTest, VariableMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LinearExpression expr = a - 2 * a; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}}, 0))); +} + +TEST(LinearExpressionTest, VariableMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = a - b; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}, {b, -1}}, 0))); +} + +TEST(LinearExpressionTest, ExpressionMinusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + b + 1) - 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 1}}, -4))); +} + +TEST(LinearExpressionTest, DoubleMinusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 5 - (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}, {b, -1}}, 4))); +} + +TEST(LinearExpressionTest, ExpressionMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + 2 * b + 1) - b; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, 1}}, 1))); +} + +TEST(LinearExpressionTest, VariableMinusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = b - (2 * a + 2 * b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}, {b, -1}}, -1))); +} + +TEST(LinearExpressionTest, ExpressionMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (2 * a + b + 1) - 3 * b; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 2}, {b, -2}}, 1))); +} + +TEST(LinearExpressionTest, LinearTermMinusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 3 * b - (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -2}, {b, 2}}, -1))); +} + +TEST(LinearExpressionTest, ExpressionMinusExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = (3 * b + a + 2) - (2 * a + b + 1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 2); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, -1}, {b, 2}}, 1))); +} + +TEST(LinearExpressionTest, ExpressionTimesAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression expr({{a, 3}, {b, 2}}, -2); + ResetExpressionCounters(); + expr *= 2; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 6}, {b, 4}}, -4))); +} + +TEST(LinearExpressionTest, DoubleTimesExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = 2 * LinearExpression({{a, 3}, {b, 2}}, -2); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 6}, {b, 4}}, -4))); +} + +TEST(LinearExpressionTest, ExpressionTimesDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = LinearExpression({{a, 3}, {b, 2}}, -2) * 2; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 6}, {b, 4}}, -4))); +} + +TEST(LinearExpressionTest, ExpressionDividedByAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression expr({{a, 3}, {b, 2}}, -2); + ResetExpressionCounters(); + expr /= 2; + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1.5}, {b, 1}}, -1))); +} + +TEST(LinearExpressionTest, ExpressionDividedByDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LinearExpression expr = LinearExpression({{a, 3}, {b, 2}}, -2) / 2; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1.5}, {b, 1}}, -1))); +} + +TEST(LinearExpressionTest, AddSumInts) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LinearExpression expr = 3.0 * a + 5.0; + const std::vector to_add = {2, 7}; + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 14))); +} + +TEST(LinearExpressionTest, AddSumDoubles) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LinearExpression expr = 3.0 * a + 5.0; + const std::vector to_add({2.0, 7.0}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 14))); +} + +TEST(LinearExpressionTest, AddSumVariables) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + LinearExpression expr = 3.0 * a + 5.0; + const std::vector to_add({b, c, b}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}, {b, 2}, {c, 1}}, 5))); +} + +TEST(LinearExpressionTest, AddSumLinearTerms) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + LinearExpression expr = 3.0 * a + 5.0; + const std::vector to_add({2.0 * b, 1.0 * c, 4.0 * b}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}, {b, 6}, {c, 1}}, 5))); +} + +TEST(LinearExpressionTest, AddSumLinearExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression expr = 3.0 * a + 5.0; + const std::vector to_add({a + b, 4.0 * b - 1.0}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 4}, {b, 5}}, 4))); +} + +TEST(LinearExpressionTest, Sum) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + const std::vector summand({a, b, c, b}); + const auto expr = Sum(summand); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 1}, {b, 2}, {c, 1}}, 0))); +} + +TEST(LinearExpressionTest, AddInnerProductIntInt) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {2, 3, 4}; + const std::vector second = {1, -1, 10}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 44))); +} + +TEST(LinearExpressionTest, AddInnerProductDoubleDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {2.0, 3.0, 4.0}; + const std::vector second = {1.0, -1.0, 10.0}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 3}}, 44))); +} + +TEST(LinearExpressionTest, AddInnerProductDoubleVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {2.0, 3.0, 4.0}; + const std::vector second = {a, b, a}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 9}, {b, 3}}, 5))); +} + +TEST(LinearExpressionTest, AddInnerProductVariableInt) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {a, b, a}; + const std::vector second = {2, 3, 4}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 9}, {b, 3}}, 5))); +} + +TEST(LinearExpressionTest, AddInnerProductIntLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {2, 3, 4}; + const std::vector second = {2.0 * a, 4.0 * b, 1.0 * a}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 11}, {b, 12}}, 5))); +} + +TEST(LinearExpressionTest, AddInnerProductDoubleLinearExpr) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr = 3.0 * a + 5.0; + const std::vector first = {3.0 * b + 1, a + b}; + const std::vector second = {-1.0, 2.0}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 5}, {b, -1}}, 4))); +} + +TEST(LinearExpressionTest, InnerProduct) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const std::vector first = {a, b, a}; + const std::vector second = {2.0, 3.0, 4.0}; + const auto expr = InnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(LinearExpression({{a, 6}, {b, 3}}, 0))); +} + +TEST(LinearExpressionDeathTest, AddInnerProductSizeMismatchLeftMore) { + const std::vector left = {2.0, 3.0, 4.0}; + const std::vector right = {1.0, -1.0}; + LinearExpression expr; + EXPECT_DEATH(expr.AddInnerProduct(left, right), "left had more"); +} + +TEST(LinearExpressionDeathTest, AddInnerProductSizeMismatchRightMore) { + const std::vector left = {2.0, 3.0}; + const std::vector right = {1.0, -1.0, 10.0}; + LinearExpression expr; + EXPECT_DEATH(expr.AddInnerProduct(left, right), "right had more"); +} + +TEST(LinearExpressionTest, ExpressionGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = 3 * a + b + 2 >= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}, {b, 1}}, 2))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleLesserEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = 5 <= 3 * a + b + 2; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}, {b, 1}}, 2))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, LinearTermGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = 3 * a >= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}}, 0))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = 5 <= 3 * a; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}}, 0))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, VariableGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = a >= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 1}}, 0))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const LowerBoundedLinearExpression comparison = 5 <= a; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 1}}, 0))); + EXPECT_EQ(comparison.lower_bound, 5.0); +} + +TEST(LinearExpressionTest, ExpressionLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = 3 * a + b + 2 <= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}, {b, 1}}, 2))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleGreaterEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = 5 >= 3 * a + b + 2; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}, {b, 1}}, 2))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, LinearTermLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = 3 * a <= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}}, 0))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = 5 >= 3 * a; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 3}}, 0))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, VariableLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = a <= 5; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 1}}, 0))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, DoubleGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const UpperBoundedLinearExpression comparison = 5 >= a; + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(LinearExpression({{a, 1}}, 0))); + EXPECT_EQ(comparison.upper_bound, 5.0); +} + +TEST(LinearExpressionTest, LowerBoundedExpressionLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = (2 <= 2 * a + 3 * b + 5) <= 4; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleLesserEqualExpressionLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 <= 2 * a + 3 * b + 5 <= 4; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleGreaterEqualLowerBoundedExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 4 >= (2 * a + 3 * b + 5 >= 2); + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleGreaterEqualExpressionGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 4 >= 2 * a + 3 * b + 5 >= 2; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleLesserEqualUpperBoundedExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 <= (2 * a + 3 * b + 5 <= 4); + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, UpperBoundedExpressionGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = (4 >= 2 * a + 3 * b + 5) >= 2; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, 2}, {b, 3}}), -1.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 5); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionLesserEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 <= a + 3; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 1}, {b, 3}}), -2.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 2); +} + +TEST(LinearExpressionTest, ExpressionLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 <= 2 * b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, 1}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermLesserEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * b <= 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 2}, {b, 1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableLesserEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b <= 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 2}, {b, 2}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 <= b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, 2}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionGreaterEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 >= a + 3; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -2.0, LinearTerms({{a, 1}, {b, 3}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 2); +} + +TEST(LinearExpressionTest, ExpressionGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 >= 2 * b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 2}, {b, 1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermGreaterEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * b >= 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, 1}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableGreaterEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b >= 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, 2}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 >= b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 2}, {b, 2}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a <= 2 * b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, -2}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b <= 2 * a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, -2}, {b, 1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b <= a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, -1}, {b, 1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a <= b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -kInf, LinearTerms({{a, 2}, {b, -1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a >= 2 * b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + 0.0, LinearTerms({{a, 2}, {b, -2}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b >= 2 * a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + 0.0, LinearTerms({{a, -2}, {b, 1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = b >= a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + 0.0, LinearTerms({{a, -1}, {b, 1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a >= b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + 0.0, LinearTerms({{a, 2}, {b, -1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 == 3 * a + b + 2; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -3.0, LinearTerms({{a, -1}, {b, 2}}), -3.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 2); +} + +TEST(LinearExpressionTest, ExpressionEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 == 3 * a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, -1}, {b, 3}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 3 * a == 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, -1}, {b, 3}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 == a; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 1}, {b, 3}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = a == 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -5.0, LinearTerms({{a, 1}, {b, 3}}), -5.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, ExpressionEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a + 3 * b + 5 == 3; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -2.0, LinearTerms({{a, 2}, {b, 3}}), -2.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleEqualExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 3 == 2 * a + 3 * b + 5; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + -2.0, LinearTerms({{a, 2}, {b, 3}}), -2.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a == 3 * a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(0.0, LinearTerms({{a, -1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a == a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(0.0, LinearTerms({{a, 1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = a == 2 * a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(0.0, LinearTerms({{a, -1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, LinearTermEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 2 * a == 3; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(3.0, LinearTerms({{a, 2}}), 3.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 3 == 2 * a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(3.0, LinearTerms({{a, 2}}), 3.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = a == b; + EXPECT_THAT(comparison, BoundedLinearExpressionEquiv( + 0.0, LinearTerms({{a, 1}, {b, -1}}), 0.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, VariableEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = a == 3; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(3.0, LinearTerms({{a, 1}}), 3.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(LinearExpressionTest, DoubleEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 3 == a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(3.0, LinearTerms({{a, 1}}), 3.0)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(BoundedLinearExpressionTest, FromLowerBoundedExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = 3 <= a; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(3.0, LinearTerms({{a, 1}}), kInf)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(BoundedLinearExpressionTest, FromUpperBoundedExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedLinearExpression comparison = a <= 5; + EXPECT_THAT(comparison, + BoundedLinearExpressionEquiv(-kInf, LinearTerms({{a, 1}}), 5)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 1); +} + +TEST(BoundedLinearExpressionTest, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + auto to_string = [](BoundedLinearExpression bounded_expression) { + std::ostringstream oss; + oss << bounded_expression; + return oss.str(); + }; + + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression(), -1, 2)), + "-1 ≤ 0 ≤ 2"); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({}, -1), -1, 2)), + "-1 ≤ -1 ≤ 2"); + EXPECT_EQ(to_string(BoundedLinearExpression( + LinearExpression({{a, 1}, {b, 5}}, -1), -1, 2)), + "-1 ≤ a + 5*b - 1 ≤ 2"); + EXPECT_EQ(to_string(BoundedLinearExpression( + LinearExpression({{a, 1}, {b, 5}}, 0), -1, 2)), + "-1 ≤ a + 5*b ≤ 2"); + EXPECT_EQ( + to_string(BoundedLinearExpression(LinearExpression({a, 2}), -kInf, 2)), + "2*a ≤ 2"); + EXPECT_EQ( + to_string(BoundedLinearExpression(LinearExpression({a, 2}), -1, kInf)), + "2*a ≥ -1"); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({a, 2}), 3, 3)), + "2*a = 3"); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({a, 2}), + kRoundTripTestNumber, kInf)), + absl::StrCat("2*a ≥ ", kRoundTripTestNumberStr)); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({a, 2}), -kInf, + kRoundTripTestNumber)), + absl::StrCat("2*a ≤ ", kRoundTripTestNumberStr)); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({a, 2}), 0.0, + kRoundTripTestNumber)), + absl::StrCat("0 ≤ 2*a ≤ ", kRoundTripTestNumberStr)); + EXPECT_EQ(to_string(BoundedLinearExpression(LinearExpression({a, 2}), + kRoundTripTestNumber, 3000.0)), + absl::StrCat(kRoundTripTestNumberStr, " ≤ 2*a ≤ 3000")); +} + +//////////////////////////////////////////////////////////////////////////////// +// Quadratic tests +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// QuadraticTermKey +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticTermKeyTest, Constructors) { + ModelStorage storage; + const VariableId u_id = storage.AddVariable(); + const VariableId v_id = storage.AddVariable(); + + { + const QuadraticTermKey in_order_key(&storage, {u_id, v_id}); + EXPECT_EQ(in_order_key.storage(), &storage); + EXPECT_EQ(in_order_key.typed_id(), QuadraticProductId(u_id, v_id)); + + const QuadraticTermKey out_of_order_key(&storage, {v_id, u_id}); + EXPECT_EQ(in_order_key, in_order_key); + } + + const Variable u(&storage, u_id); + const Variable v(&storage, v_id); + { + const QuadraticTermKey in_order_key(u, v); + EXPECT_EQ(in_order_key.storage(), &storage); + EXPECT_EQ(in_order_key.typed_id(), QuadraticProductId(u_id, v_id)); + + const QuadraticTermKey out_of_order_key(v, u); + EXPECT_EQ(in_order_key, in_order_key); + } +} + +TEST(QuadraticTermKeyDeathTest, ConstructorChecksOnDifferentModels) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, storage.AddVariable("b")); + + EXPECT_DEATH(QuadraticTermKey(a, b), internal::kObjectsFromOtherModelStorage); +} + +TEST(QuadraticTermKeyTest, Accessors) { + ModelStorage storage; + const VariableId u_id = storage.AddVariable(); + const VariableId v_id = storage.AddVariable(); + + const QuadraticProductId id(u_id, v_id); + const QuadraticTermKey key(&storage, id); + EXPECT_EQ(key.storage(), &storage); + const ModelStorage& const_model = storage; + EXPECT_EQ(key.storage(), &const_model); + EXPECT_EQ(key.typed_id(), id); + EXPECT_EQ(key.first().typed_id(), u_id); + EXPECT_EQ(key.second().typed_id(), v_id); + EXPECT_EQ(key.first().storage(), &const_model); + EXPECT_EQ(key.second().storage(), &const_model); +} + +TEST(QuadraticTermKeyTest, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable()); + + auto to_string = [](QuadraticTermKey k) { + std::ostringstream oss; + oss << k; + return oss.str(); + }; + + EXPECT_EQ(to_string(QuadraticTermKey(a, a)), "(a, a)"); + EXPECT_EQ(to_string(QuadraticTermKey(a, b)), + absl::StrCat("(a, __var#", b.id(), "__)")); +} + +TEST(QuadraticTermKeyTest, EqualityComparison) { + ModelStorage storage; + const VariableId u_id = storage.AddVariable(); + const VariableId v_id = storage.AddVariable(); + const QuadraticProductId qp_id(u_id, v_id); + const QuadraticTermKey key(&storage, qp_id); + EXPECT_TRUE(key == QuadraticTermKey(&storage, qp_id)); + EXPECT_FALSE(key != QuadraticTermKey(&storage, qp_id)); + EXPECT_FALSE(key == + QuadraticTermKey(&storage, QuadraticProductId(u_id, u_id))); + EXPECT_TRUE(key != + QuadraticTermKey(&storage, QuadraticProductId(u_id, u_id))); + + const ModelStorage other_storage; + EXPECT_FALSE(key == QuadraticTermKey(&other_storage, qp_id)); + EXPECT_TRUE(key != QuadraticTermKey(&other_storage, qp_id)); +} + +//////////////////////////////////////////////////////////////////////////////// +// QuadraticTerm (no arithmetic) +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticTermTest, FromVariablesAndDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + { + // Verify that the same variable can appear in both slots + const QuadraticTerm term(a, a, 1.2); + EXPECT_EQ(term.first_variable(), a); + EXPECT_EQ(term.second_variable(), a); + EXPECT_EQ(term.coefficient(), 1.2); + } + { + const QuadraticTerm term(b, a, 1.2); + EXPECT_EQ(term.first_variable(), b); + EXPECT_EQ(term.second_variable(), a); + EXPECT_EQ(term.coefficient(), 1.2); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// QuadraticExpression (no arithmetic) +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticExpressionTest, DefaultValue) { + const QuadraticExpression expr; + EXPECT_EQ(expr.offset(), 0.0); + EXPECT_THAT(expr.linear_terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, EmptyInitializerList) { + const QuadraticExpression expr({}, {}, 0.0); + EXPECT_EQ(expr.offset(), 0.0); + EXPECT_THAT(expr.linear_terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, FromDouble) { + const QuadraticExpression expr = 4.0; + + EXPECT_EQ(expr.offset(), 4.0); + EXPECT_THAT(expr.linear_terms(), IsEmpty()); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), nullptr); + EXPECT_THAT(expr.linear_terms(), IsEmpty()); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, FromVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const QuadraticExpression expr = a; + + EXPECT_EQ(expr.offset(), 0.0); + EXPECT_THAT(expr.linear_terms(), UnorderedElementsAre(Pair(a, 1.0))); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.linear_terms(), UnorderedElementsAre(Pair(a, 1.0))); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, FromLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const QuadraticExpression expr = LinearTerm(a, 3.0); + + EXPECT_EQ(expr.offset(), 0.0); + EXPECT_THAT(expr.linear_terms(), UnorderedElementsAre(Pair(a, 3.0))); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.linear_terms(), UnorderedElementsAre(Pair(a, 3.0))); + EXPECT_THAT(expr.quadratic_terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, FromLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression linear_expr({{a, 1.2}, {b, 1.3}}, 1.4); + ResetExpressionCounters(); + + const QuadraticExpression quadratic_expr = std::move(linear_expr); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(quadratic_expr, + IsIdentical(QuadraticExpression({}, {{a, 1.2}, {b, 1.3}}, 1.4))); + // We attempt to test that the we successfully moved out of linear_expr. Since + // there is an implicit move constructor call, we can only test that the + // terms_ field is empty, but not that offset_ is set to zero as well. + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(linear_expr.terms(), IsEmpty()); +} + +TEST(QuadraticExpressionTest, FromQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + const QuadraticExpression expr = QuadraticTerm(a, a, 3.0); + + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, a, 3}}, {}, 0))); + EXPECT_EQ(expr.storage(), &storage); + EXPECT_THAT(expr.linear_terms(), IsEmpty()); + EXPECT_THAT(expr.quadratic_terms(), + UnorderedElementsAre(Pair(QuadraticTermKey(a, a), 3.0))); +} + +TEST(QuadraticExpressionTest, TermsFromSameModelOk) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + const QuadraticExpression expr({{a, b, 1.2}, {c, a, 2.5}, {b, a, -1.1}}, + {{a, 1.3}}, 1.2); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression( + {{a, b, 1.2 - 1.1}, {a, c, 2.5}}, {{a, 1.3}}, 1.2))); + EXPECT_EQ(expr.storage(), &storage); +} + +TEST(QuadraticExpressionDeathTest, TermsFromDifferentModelsFail) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + + ModelStorage second_model; + const Variable b(&second_model, second_model.AddVariable("b")); + const Variable c(&second_model, second_model.AddVariable("c")); + + EXPECT_DEATH(QuadraticExpression({}, {{a, 3.0}, {b, 5.0}}, 0.0), + kObjectsFromOtherModelStorage); + EXPECT_DEATH(QuadraticExpression({{a, b, 1.2}}, {}, 0.0), + kObjectsFromOtherModelStorage); + EXPECT_DEATH(QuadraticExpression({{a, a, 1.4}, {b, c, 1.3}}, {}, 0.0), + kObjectsFromOtherModelStorage); + EXPECT_DEATH(QuadraticExpression({{b, c, 1.3}}, {{a, 1.4}}, 0.0), + kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, ReassignDifferentModels) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + const Variable b(&first_model, first_model.AddVariable("b")); + const QuadraticExpression first_expr({{a, b, 1.0}}, {}, 5.7); + + ModelStorage second_model; + const Variable c(&second_model, second_model.AddVariable("c")); + QuadraticExpression second_expr_to_overwrite({}, {{c, 1.2}}, 3.4); + + second_expr_to_overwrite = first_expr; + EXPECT_THAT(second_expr_to_overwrite, + IsIdentical(QuadraticExpression({{a, b, 1.0}}, {}, 5.7))); + EXPECT_EQ(second_expr_to_overwrite.storage(), &first_model); +} + +TEST(QuadraticExpressionTest, MoveConstruction) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + const Variable b(&first_model, first_model.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.0}}, {{a, 3.0}}, 5.7); + + const QuadraticExpression second_expr = std::move(first_expr); + + EXPECT_THAT(second_expr, + IsIdentical(QuadraticExpression({{a, b, 1.0}}, {{a, 3.0}}, 5.7))); + EXPECT_EQ(second_expr.storage(), &first_model); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(first_expr, IsIdentical(QuadraticExpression())); + EXPECT_EQ(first_expr.storage(), nullptr); +} + +TEST(QuadraticExpressionTest, MoveAssignment) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + const Variable b(&first_model, first_model.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.0}}, {{a, 3.0}}, 5.7); + + ModelStorage second_model; + const Variable c(&second_model, second_model.AddVariable("c")); + QuadraticExpression second_expr_to_overwrite({}, {{c, 1.2}}, 3.4); + + second_expr_to_overwrite = std::move(first_expr); + EXPECT_THAT(second_expr_to_overwrite, + IsIdentical(QuadraticExpression({{a, b, 1.0}}, {{a, 3.0}}, 5.7))); + EXPECT_EQ(second_expr_to_overwrite.storage(), &first_model); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(first_expr, IsIdentical(QuadraticExpression())); + EXPECT_EQ(first_expr.storage(), nullptr); +} + +TEST(QuadraticExpressionTest, EvaluateEmpty) { + const QuadraticExpression expr; + { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + VariableMap variable_values; + variable_values[a] = 10.0; + variable_values[b] = 11.0; + EXPECT_EQ(expr.Evaluate(variable_values), 0.0); + EXPECT_EQ(expr.EvaluateWithDefaultZero(variable_values), 0.0); + } + { + VariableMap variable_values; + EXPECT_EQ(expr.Evaluate(variable_values), 0.0); + EXPECT_EQ(expr.EvaluateWithDefaultZero(variable_values), 0.0); + } +} + +TEST(QuadraticExpressionTest, EvaluateOnlyLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticExpression expr({}, {{a, 1.2}}, 3.4); + { + VariableMap variable_values; + variable_values[a] = 10.0; + variable_values[b] = 11.0; + EXPECT_THAT(expr.Evaluate(variable_values), 10 * 1.2 + 3.4); + EXPECT_THAT(expr.EvaluateWithDefaultZero(variable_values), 10 * 1.2 + 3.4); + } + { + VariableMap variable_values; + EXPECT_THAT(expr.EvaluateWithDefaultZero(variable_values), 3.4); + } +} + +TEST(QuadraticExpressionTest, SimpleEvaluate) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticExpression expr({{a, b, 1.0}}, {{a, 3.0}, {b, 5.0}}, 2.0); + VariableMap variable_values; + variable_values[a] = 10.0; + variable_values[b] = 100.0; + EXPECT_THAT(expr.Evaluate(variable_values), + 1.0 * 10.0 * 100.0 + 3.0 * 10.0 + 5.0 * 100.0 + 2.0); +} + +TEST(QuadraticExpressionTest, SimpleEvaluateWithDefault) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticExpression expr({{a, a, 4.0}, {b, a, 1.0}}, + {{b, 5.0}, {a, 3.0}}, 2.0); + VariableMap variable_values; + variable_values[a] = 10.0; + EXPECT_THAT(expr.EvaluateWithDefaultZero(variable_values), + 4.0 * 10.0 * 10.0 + 3.0 * 10.0 + 2.0); +} + +TEST(QuadraticExpressionTest, StableEvaluateAndEvaluateWithDefault) { + // Here we test that the floating point sum of numbers is done in the sorted + // order of the variables ids and variables id pairs. To do so we rely on a + // specific floating point number sequence (obtained with a Python script + // doing random tries) which floating point sum changes depending on the order + // of operations: + // + // 56.66114901664141 + 76.288516611269 + 73.11902164661139 + + // 0.677336454040622 + 43.75820160525244 = 250.50422533381482 + // 56.66114901664141 + 76.288516611269 + 73.11902164661139 + + // 43.75820160525244 + 0.677336454040622 = 250.50422533381484 + // 56.66114901664141 + 76.288516611269 + 0.677336454040622 + + // 73.11902164661139 + 43.75820160525244 = 250.50422533381487 + // 76.288516611269 + 0.677336454040622 + 73.11902164661139 + + // 43.75820160525244 + 56.66114901664141 = 250.5042253338149 + // + // Here we will use the first value as the offset of the linear expression (to + // test that it always taken into account in the same order). + constexpr double kOffset = 56.66114901664141; + const std::vector linear_coeffs = { + 76.288516611269, 73.11902164661139, 0.677336454040622, 43.75820160525244}; + const std::vector quadratic_coeffs = { + 76.288516611269, 0.677336454040622, 73.11902164661139, 43.75820160525244, + 56.66114901664141}; + + ModelStorage storage; + std::vector vars; + VariableMap variable_values; + for (int i = 0; i < linear_coeffs.size(); ++i) { + vars.emplace_back(&storage, storage.AddVariable(absl::StrCat("v_", i))); + variable_values.try_emplace(vars.back(), 1.0); + } + + QuadraticExpression expr = kOffset; + for (const int i : {3, 2, 0, 1}) { + expr += linear_coeffs[i] * vars[i]; + } + const std::vector> quad_term_keys = { + {0, 0}, {0, 1}, {0, 2}, {1, 1}, {1, 2}}; + for (const int i : {4, 0, 3, 1, 2}) { + const auto [v1, v2] = quad_term_keys[i]; + expr += quadratic_coeffs[i] * vars[v1] * vars[v2]; + } + + // Expected value for the sum which is: + // - offset first + // - then all linear terms sums in the order of variables' indices + // - then all quadratic terms sums in the order of variables' indices' pairs + double expected = kOffset; + for (const double v : linear_coeffs) { + expected += v; + } + for (const double v : quadratic_coeffs) { + expected += v; + } + + // Test Evaluate(); + { + const double got = expr.Evaluate(variable_values); + EXPECT_EQ(got, expected) + << "got: " << RoundTripDoubleFormat(got) + << " expected: " << RoundTripDoubleFormat(expected); + } + + // Test EvaluateWithDefaultZero(); + { + const double got = expr.EvaluateWithDefaultZero(variable_values); + EXPECT_EQ(got, expected) + << "got: " << RoundTripDoubleFormat(got) + << " expected: " << RoundTripDoubleFormat(expected); + } +} + +TEST(QuadraticExpressionDeathTest, EvaluateMissingVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticExpression expr({{b, a, 1.0}}, {{b, 5.0}, {a, 3.0}}, 2.0); + VariableMap variable_values; + variable_values[a] = 10.0; + EXPECT_DEATH(expr.Evaluate(variable_values), ""); +} + +TEST(QuadraticExpressionDeathTest, EvaluateDifferentModels) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + const Variable b(&first_model, first_model.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.0}}, {}, 2.0); + + ModelStorage second_model; + const Variable c(&second_model, second_model.AddVariable("c")); + VariableMap variable_values; + variable_values[c] = 100.0; + + EXPECT_DEATH(expr.Evaluate(variable_values), kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, EvaluateWithDefaultZeroDifferentModels) { + ModelStorage first_model; + const Variable a(&first_model, first_model.AddVariable("a")); + const Variable b(&first_model, first_model.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.0}}, {}, 2.0); + + ModelStorage second_model; + const Variable c(&second_model, second_model.AddVariable("c")); + VariableMap variable_values; + variable_values[c] = 100.0; + + EXPECT_EQ(expr.EvaluateWithDefaultZero(variable_values), 2.0); +} + +TEST(QuadraticExpressionTest, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + auto to_string = [](QuadraticExpression expr) { + std::ostringstream oss; + oss << expr; + return oss.str(); + }; + + EXPECT_EQ(to_string(QuadraticExpression()), "0"); + EXPECT_EQ(to_string(QuadraticExpression({}, {}, -1)), "-1"); + EXPECT_EQ(to_string(QuadraticExpression( + {}, {{a, 3}, {b, -5}, {a, -2}, {b, 0}}, -1)), + "a - 5*b - 1"); + EXPECT_EQ(to_string(QuadraticExpression( + {{a, b, -1.2}, {a, a, -1.3}, {b, b, 1.0}}, {{a, 1.4}}, 1.5)), + "-1.3*a² - 1.2*a*b + b² + 1.4*a + 1.5"); + EXPECT_EQ( + to_string(QuadraticExpression({{a, b, kRoundTripTestNumber}}, {}, 0.0)), + absl::StrCat(kRoundTripTestNumberStr, "*a*b")); + EXPECT_EQ( + to_string(QuadraticExpression({}, {{a, kRoundTripTestNumber}}, 0.0)), + absl::StrCat(kRoundTripTestNumberStr, "*a")); + EXPECT_EQ(to_string(QuadraticExpression({}, {}, kRoundTripTestNumber)), + kRoundTripTestNumberStr); +} + +//////////////////////////////////////////////////////////////////////////////// +// Arithmetic (non-member) +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------- Addition (+) ---------------------------------- + +TEST(QuadraticExpressionTest, DoublePlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = 3.4 + term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {}, 3.4))); +} + +TEST(QuadraticExpressionTest, DoublePlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = 7.8 + std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 3.4}}, + 7.8 + 5.6))); +} + +TEST(QuadraticExpressionTest, VariablePlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = b + term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, VariablePlusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(b, c, 1.2); + + EXPECT_DEATH(a + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, VariablePlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = b + std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{b, 1 * 3.4 + 1}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, VariablePlusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 1.2}}, {{b, 3.4}}, 5.6); + + EXPECT_DEATH(a + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearTermPlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm first_term(a, 1.2); + const QuadraticTerm second_term(b, a, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term + second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 3.4}}, {{a, 1.2}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, LinearTermPlusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term(c, 1.2); + + EXPECT_DEATH(term + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearTermPlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 1.2); + QuadraticExpression expr({{a, b, 3.4}}, {{b, 5.6}}, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = term + std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 3.4}}, {{a, 1.2}, {b, 5.6}}, 7.8))); +} + +TEST(QuadraticExpressionDeathTest, + LinearTermPlusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 3.4}}, {{b, 5.6}}, 7.8); + + EXPECT_DEATH(term + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionPlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr({{a, 1.2}}, 3.4); + const QuadraticTerm term(b, a, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) + term; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 5.6}}, {{a, 1.2}}, 3.4))); +} + +TEST(QuadraticExpressionDeathTest, + LinearExpressionPlusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 5.6); + + EXPECT_DEATH(expr + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionPlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearExpression first_expr({{a, 1.2}}, 3.4); + QuadraticExpression second_expr({{a, b, 5.6}}, {{b, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result = first_expr + std::move(second_expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 5.6}}, {{a, 1.2}, {b, 7.8}}, 3.4 + 9.0))); +} + +TEST(QuadraticExpressionDeathTest, + LinearExpressionPlusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 5.6}}, {{b, 7.8}}, 9.0); + + EXPECT_DEATH(expr + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = term + 3.4; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {}, 3.4))); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = term + a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{a, 1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermPlusVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable other_var(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(term + other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm first_term(a, b, 1.2); + const LinearTerm second_term(a, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term + second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{a, 3.4}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermPlusLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term({c, 3.4}); + + EXPECT_DEATH(term + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + LinearExpression expr({{a, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = term + std::move(expr); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{a, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticTermPlusLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 1.2}}, 1.3); + + EXPECT_DEATH(term + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm first_term(a, b, 1.2); + const QuadraticTerm second_term(b, b, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term + second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}, {b, b, 3.4}}, {}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermPlusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 1.2); + + EXPECT_DEATH(term + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermPlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + QuadraticExpression expr({{a, b, 3.4}}, {{b, 5.6}}, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = term + std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2 + 3.4}}, + {{b, 5.6}}, 7.8))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticTermPlusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const QuadraticTerm term(a, a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 1.2}}, {{b, 1.3}}, 1.4); + + EXPECT_DEATH(term + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) + 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 3.4}}, + 5.6 + 7.8))); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) + a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, 1}, {b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticExpressionPlusVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable other_var(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(expr + other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const LinearTerm term(a, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) + term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, 7.8}, {b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionPlusLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term(c, 7.8); + + EXPECT_DEATH(expr + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const LinearExpression second_expr({{a, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(first_expr) + second_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, 7.8}, {b, 3.4}}, 5.6 + 9))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionPlusLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 7.8}}, 9.0); + + EXPECT_DEATH(expr + other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const QuadraticTerm term(a, a, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) + term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, 7.8}, {a, b, 1.2}}, {{b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionPlusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 7.8); + + EXPECT_DEATH(expr + other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionPlusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + QuadraticExpression second_expr({{b, b, 7.8}}, {{a, 9.0}}, 1.3); + ResetExpressionCounters(); + + const QuadraticExpression result = + std::move(first_expr) + std::move(second_expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}, {b, b, 7.8}}, + {{a, 9}, {b, 3.4}}, 5.6 + 1.3))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionPlusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{c, c, 1.2}}, {{c, 3.4}}, 5.6); + + EXPECT_DEATH(expr + other_expr, kObjectsFromOtherModelStorage); +} + +// --------------------------- Subtraction (-) --------------------------------- + +TEST(QuadraticTermTest, QuadraticTermNegation) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term = QuadraticTerm(a, b, 1.2); + + const QuadraticTerm result = -term; + + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); + EXPECT_EQ(result.coefficient(), -1.2); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionNegation) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = -std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, -1.2}}, + {{b, -3.4}}, -5.6))); +} + +TEST(QuadraticExpressionTest, DoubleMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = 3.4 - term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, -1.2}}, {}, 3.4))); +} + +TEST(QuadraticExpressionTest, DoubleMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = 7.8 - std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, -1.2}}, + {{b, -3.4}}, 7.8 - 5.6))); +} + +TEST(QuadraticExpressionTest, VariableMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = b - term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, -1.2}}, {{b, 1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, VariableMinusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(b, c, 1.2); + + EXPECT_DEATH(a - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, VariableMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = b - std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, -1.2}}, + {{b, 1 - 3.4}}, -5.6))); +} + +TEST(QuadraticExpressionDeathTest, VariableMinusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 1.2}}, {{b, 1.3}}, 1.4); + + EXPECT_DEATH(a - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearTermMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm first_term(a, 1.2); + const QuadraticTerm second_term(b, a, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term - second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, -3.4}}, {{a, 1.2}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, LinearTermMinusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(b, c, 1.2); + + EXPECT_DEATH(term - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearTermMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 1.2); + QuadraticExpression expr({{a, b, 3.4}}, {{b, 5.6}}, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = term - std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, -3.4}}, {{a, 1.2}, {b, -5.6}}, -7.8))); +} + +TEST(QuadraticExpressionDeathTest, + LinearTermMinusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 1.2}}, {{b, 1.3}}, 1.4); + + EXPECT_DEATH(term - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + LinearExpression expr({{a, 1.2}}, 3.4); + const QuadraticTerm term(b, a, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) - term; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, -5.6}}, + {{a, 1.2}}, 3.4))); +} + +TEST(QuadraticExpressionDeathTest, + LinearExpressionMinusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(b, c, 5.6); + + EXPECT_DEATH(expr - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearExpression first_expr({{a, 1.2}}, 3.4); + QuadraticExpression second_expr({{a, b, 5.6}}, {{b, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result = first_expr - std::move(second_expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, -5.6}}, {{a, 1.2}, {b, -7.8}}, 3.4 - 9.0))); +} + +TEST(QuadraticExpressionDeathTest, + LinearExpressionMinusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{b, c, 1.2}}, {{b, 1.3}}, 1.4); + + EXPECT_DEATH(expr - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = term - 3.4; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {}, -3.4))); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + ResetExpressionCounters(); + + const QuadraticExpression result = term - a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{a, -1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermMinusVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable other_var(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(term - other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm first_term(a, b, 1.2); + const LinearTerm second_term(a, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term - second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{a, -3.4}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermMinusLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term({c, 3.4}); + + EXPECT_DEATH(term - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + LinearExpression expr({{a, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = term - std::move(expr); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, + {{a, -3.4}}, -5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticTermMinusLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 1.2}}, 1.3); + + EXPECT_DEATH(term - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm first_term(a, b, 1.2); + const QuadraticTerm second_term(b, b, 3.4); + ResetExpressionCounters(); + + const QuadraticExpression result = first_term - second_term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}, {b, b, -3.4}}, {}, 0))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticTermMinusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 1.2); + + EXPECT_DEATH(term - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticTermMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + QuadraticExpression expr({{a, b, 3.4}}, {{b, 5.6}}, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = term - std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2 - 3.4}}, + {{b, -5.6}}, -7.8))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticTermMinusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{c, c, 1.2}}, {{c, 1.3}}, 1.4); + + EXPECT_DEATH(term - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) - 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 3.4}}, + 5.6 - 7.8))); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) - a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, -1}, {b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, QuadraticExpressionMinusVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable other_var(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(expr - other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const LinearTerm term(a, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) - term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, -7.8}, {b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionMinusLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term(c, 7.8); + + EXPECT_DEATH(expr - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const LinearExpression second_expr({{a, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(first_expr) - second_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2}}, {{a, -7.8}, {b, 3.4}}, 5.6 - 9))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionMinusLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 7.8}}, 9.0); + + EXPECT_DEATH(expr - other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + const QuadraticTerm term(a, a, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) - term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, -7.8}, {a, b, 1.2}}, {{b, 3.4}}, 5.6))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionMinusQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 7.8); + + EXPECT_DEATH(expr - other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionMinusQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression first_expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + QuadraticExpression second_expr({{b, b, 7.8}}, {{a, 9.0}}, 1.3); + ResetExpressionCounters(); + + const QuadraticExpression result = + std::move(first_expr) - std::move(second_expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression({{a, b, 1.2}, {b, b, -7.8}}, + {{a, -9}, {b, 3.4}}, 5.6 - 1.3))); +} + +TEST(QuadraticExpressionDeathTest, + QuadraticExpressionMinusQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_expr({{c, c, 1.2}}, {{c, 3.4}}, 5.6); + + EXPECT_DEATH(expr - other_expr, kObjectsFromOtherModelStorage); +} + +// ---------------------------- Multiplication (*) ----------------------------- + +TEST(QuadraticTermTest, DoubleTimesQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + const QuadraticTerm result = 3.4 * term; + + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); + EXPECT_THAT(result.coefficient(), 3.4 * 1.2); +} + +TEST(QuadraticExpressionTest, DoubleTimesQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = 7.8 * std::move(expr); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 7.8 * 1.2}}, {{b, 7.8 * 3.4}}, 7.8 * 5.6))); +} + +TEST(QuadraticTermTest, VariableTimesVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm result = a * b; + + EXPECT_EQ(result.coefficient(), 1.0); + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); +} + +TEST(QuadraticTermDeathTest, VariableTimesVariableOtherModel) { + ModelStorage storage; + const Variable var(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable other_var(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(var * other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticTermTest, VariableTimesLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 1.2); + + const QuadraticTerm result = b * term; + + EXPECT_EQ(result.coefficient(), 1.2); + EXPECT_EQ(result.first_variable(), b); + EXPECT_EQ(result.second_variable(), a); +} + +TEST(QuadraticTermDeathTest, VariableTimesLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearTerm other_term(b, 1.2); + + EXPECT_DEATH(a * other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, VariableTimesLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + { + const LinearExpression expr({{a, 1.2}, {b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = a * expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, 1.2}, {a, b, 3.4}}, {{a, 5.6}}, 0))); + } + + // Now we test that we do not introduce extra terms if there is a zero offset. + { + const LinearExpression expr_no_offset({{a, 1.2}, {b, 3.4}}, 0.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_offset = a * expr_no_offset; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result_no_offset, IsIdentical(QuadraticExpression( + {{a, a, 1.2}, {a, b, 3.4}}, {}, 0))); + } +} + +TEST(QuadraticExpressionDeathTest, VariableTimesLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearExpression other_expr({{b, 1.2}}, 3.4); + + EXPECT_DEATH(a * other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticTermTest, LinearTermTimesVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term = LinearTerm(a, 1.2); + + const QuadraticTerm result = term * b; + + EXPECT_EQ(result.coefficient(), 1.2); + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); +} + +TEST(QuadraticTermDeathTest, LinearTermTimesVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term = LinearTerm(a, 1.2); + + ModelStorage other_storage; + const Variable other_var(&other_storage, + other_storage.AddVariable("other_var")); + + EXPECT_DEATH(term * other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticTermTest, LinearTermTimesLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm first_term(a, 1.2); + const LinearTerm second_term(b, 3.4); + + const QuadraticTerm result = first_term * second_term; + + EXPECT_THAT(result.coefficient(), 1.2 * 3.4); + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); +} + +TEST(QuadraticTermDeathTest, LinearTermTimesLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearTerm other_term(b, 1.2); + + EXPECT_DEATH(term * other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearTermTimesLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 1.2); + { + const LinearExpression expr({{a, 3.4}, {b, 5.6}}, 7.8); + ResetExpressionCounters(); + + const QuadraticExpression result = term * expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, 1.2 * 3.4}, {a, b, 1.2 * 5.6}}, + {{a, 1.2 * 7.8}}, 0))); + } + + // Now we test that we do not introduce extra terms if there is a zero offset. + { + const LinearExpression expr_no_offset({{a, 3.4}, {b, 5.6}}, 0.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_offset = term * expr_no_offset; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result_no_offset, + IsIdentical(QuadraticExpression( + {{a, a, 1.2 * 3.4}, {a, b, 1.2 * 5.6}}, {}, 0))); + } +} + +TEST(QuadraticExpressionDeathTest, LinearTermTimesLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearTerm term(a, 1.2); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearExpression other_expr({{b, 3.4}}, 5.6); + + EXPECT_DEATH(term * other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionTimesVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + { + const LinearExpression expr({{a, 1.2}, {b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = expr * a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, 1.2}, {a, b, 3.4}}, {{a, 5.6}}, 0))); + } + + // Now we test that we do not introduce extra terms if there is a zero offset. + { + const LinearExpression expr_no_offset({{a, 1.2}, {b, 3.4}}, 0.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_offset = expr_no_offset * a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result_no_offset, IsIdentical(QuadraticExpression( + {{a, a, 1.2}, {a, b, 3.4}}, {}, 0))); + } +} + +TEST(QuadraticExpressionDeathTest, LinearExpressionTimesVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + + EXPECT_DEATH(expr * b, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionTimesLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const LinearTerm term(a, 7.8); + { + const LinearExpression expr({{a, 1.2}, {b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = expr * term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, a, 1.2 * 7.8}, {a, b, 3.4 * 7.8}}, + {{a, 5.6 * 7.8}}, 0))); + } + + // Now we test that we do not introduce extra terms if there is a zero offset. + { + const LinearExpression expr_no_offset({{a, 1.2}, {b, 3.4}}, 0.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_offset = expr_no_offset * term; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result_no_offset, + IsIdentical(QuadraticExpression( + {{a, a, 1.2 * 7.8}, {a, b, 3.4 * 7.8}}, {}, 0))); + } +} + +TEST(QuadraticExpressionDeathTest, LinearExpressionTimesLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearTerm other_term(b, 5.6); + + EXPECT_DEATH(expr * other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, LinearExpressionTimesLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + { + const LinearExpression expr({{a, 1.2}, {b, 3.4}}, 5.6); + const LinearExpression other_expr({{a, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result = expr * other_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, + IsIdentical(QuadraticExpression( + {{a, a, 1.2 * 7.8}, {a, b, 3.4 * 7.8}}, + {{a, 1.2 * 9 + 5.6 * 7.8}, {b, 3.4 * 9}}, 5.6 * 9))); + } + + // Now we test that we do not introduce extra terms if there is a zero offset + // from the left-hand-side expression. + { + const LinearExpression expr_no_offset({{a, 1.2}, {b, 3.4}}, 0.0); + const LinearExpression other_expr({{a, 7.8}}, 9.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_lhs_offset = + expr_no_offset * other_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT( + result_no_lhs_offset, + IsIdentical(QuadraticExpression({{a, a, 1.2 * 7.8}, {a, b, 3.4 * 7.8}}, + {{a, 1.2 * 9}, {b, 3.4 * 9}}, 0))); + } + // Now we test that we do not introduce extra terms if there is a zero offset + // from the right-hand-side expression. + { + const LinearExpression expr({{a, 1.2}, {b, 3.4}}, 5.6); + const LinearExpression other_expr_no_offset({{a, 7.8}}, 0.0); + ResetExpressionCounters(); + + const QuadraticExpression result_no_rhs_offset = + expr * other_expr_no_offset; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT( + result_no_rhs_offset, + IsIdentical(QuadraticExpression({{a, a, 1.2 * 7.8}, {a, b, 3.4 * 7.8}}, + {{a, 5.6 * 7.8}}, 0))); + } +} + +TEST(QuadraticExpressionDeathTest, + LinearExpressionTimesLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const LinearExpression expr({{a, 1.2}}, 3.4); + + ModelStorage other_storage; + const Variable b(&other_storage, other_storage.AddVariable("b")); + const LinearExpression other_expr({{b, 5.6}}, 7.8); + + EXPECT_DEATH(expr * other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticTermTest, QuadraticTermTimesDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + const QuadraticTerm result = term * 3.4; + + EXPECT_THAT(result.coefficient(), 1.2 * 3.4); + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionTimesDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) * 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2 * 7.8}}, {{b, 3.4 * 7.8}}, 5.6 * 7.8))); +} + +// ------------------------------- Division (/) -------------------------------- +// 1 QuadraticTerm, 1 QuadraticExpression + +TEST(QuadraticTermTest, QuadraticTermDividedByDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const QuadraticTerm term(a, b, 1.2); + + const QuadraticTerm result = term / 3.4; + + EXPECT_THAT(result.coefficient(), 1.2 / 3.4); + EXPECT_EQ(result.first_variable(), a); + EXPECT_EQ(result.second_variable(), b); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionDividedByDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + const QuadraticExpression result = std::move(expr) / 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_THAT(result, IsIdentical(QuadraticExpression( + {{a, b, 1.2 / 7.8}}, {{b, 3.4 / 7.8}}, 5.6 / 7.8))); +} + +//////////////////////////////////////////////////////////////////////////////// +// Arithmetic (assignment operators) +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------- Addition (+) ---------------------------------- + +TEST(QuadraticExpressionTest, AdditionAssignmentDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + expr += 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 3.4}}, + 5.6 + 7.8))); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr += a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 1}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr += a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 2}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr += b; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 2}, {b, 1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, AdditionAssignmentVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable other_var(&other_storage, + other_storage.AddVariable("other_var")); + + EXPECT_DEATH(expr += other_var, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr += LinearTerm(a, 3); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 3}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr += LinearTerm(a, -2); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 1}}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr += LinearTerm(b, -5); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 1}, {b, -5}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, AdditionAssignmentLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term(c, 1.0); + + EXPECT_DEATH(expr += other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + const LinearExpression another_expr({{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + + expr += another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 2}, {b, 4}}, 2))); + + // Then add another expression with variables from the same ModelStorage. + const LinearExpression yet_another_expr({{a, -3}, {b, 6}}, -4); + ResetExpressionCounters(); + + expr += yet_another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT( + expr, IsIdentical(QuadraticExpression({}, {{a, 2 - 3}, {b, 4 + 6}}, -2))); + + // Then add another expression without variables (i.e. having null + // ModelStorage). + const LinearExpression no_vars_expr({}, 3); + ResetExpressionCounters(); + + expr += no_vars_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT( + expr, IsIdentical(QuadraticExpression({}, {{a, 2 - 3}, {b, 4 + 6}}, 1))); +} + +TEST(QuadraticExpressionDeathTest, + AdditionAssignmentLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 1.0}}, 2.0); + + EXPECT_DEATH(expr += other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr += QuadraticTerm(a, b, 3); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, b, 3}}, {}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr += QuadraticTerm(a, a, -2); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, a, -2}, {a, b, 3}}, {}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr += QuadraticTerm(a, b, -4); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT( + expr, IsIdentical(QuadraticExpression({{a, a, -2}, {a, b, -1}}, {}, 0))); +} + +TEST(QuadraticExpressionDeathTest, AdditionAssignmentQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 1.2); + + EXPECT_DEATH(expr += other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + const QuadraticExpression another_expr({{a, c, 2.4}}, {{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + + expr += another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, c, 2.4}}, + {{a, 2}, {b, 4}}, 2))); + + // Then add another expression with variables from the same ModelStorage. + const QuadraticExpression yet_another_expr({{c, b, 1.1}}, {{a, -3}, {c, 6}}, + -4); + ResetExpressionCounters(); + + expr += yet_another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, c, 2.4}, {b, c, 1.1}}, + {{a, -1}, {b, 4}, {c, 6}}, -2))); + + // Then add another expression without variables (i.e. having null + // ModelStorage). + const QuadraticExpression no_vars_expr({}, {}, 3); + ResetExpressionCounters(); + + expr += no_vars_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, c, 2.4}, {b, c, 1.1}}, + {{a, -1}, {b, 4}, {c, 6}}, 1))); +} + +TEST(QuadraticExpressionDeathTest, + AdditionAssignmentQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_term({{c, c, 1.2}}, {{c, 3.4}}, 5.6); + + EXPECT_DEATH(expr += other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, AdditionAssignmentSelf) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({}, {{a, 2}, {b, 4}}, 2.0); + ResetExpressionCounters(); + + expr += expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, 4}, {b, 8}}, 4))); +} + +// --------------------------- Subtraction (-) --------------------------------- + +TEST(QuadraticExpressionTest, SubtractionAssignmentDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + expr -= 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, b, 1.2}}, {{b, 3.4}}, + 5.6 - 7.8))); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr -= a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -1}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr -= a; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -2}}, 0))); + + // Subtract another variable from the same ModelStorage. + ResetExpressionCounters(); + expr -= b; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({}, {{a, -2}, {b, -1}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, SubtractionAssignmentVariableOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + + EXPECT_DEATH(expr -= c, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr -= LinearTerm(a, 3); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -3}}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // subtracting the same variable. + ResetExpressionCounters(); + expr -= LinearTerm(a, -2); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -1}}, 0))); + + // Subtract another variable from the same ModelStorage. + ResetExpressionCounters(); + expr -= LinearTerm(b, -5); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({}, {{a, -1}, {b, 5}}, 0))); +} + +TEST(QuadraticExpressionDeathTest, SubtractionAssignmentLinearTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearTerm other_term(c, 1.0); + + EXPECT_DEATH(expr -= other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + const LinearExpression another_expr({{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + + expr -= another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({}, {{a, -2}, {b, -4}}, -2))); + + // Then add another expression with variables from the same ModelStorage. + const LinearExpression yet_another_expr({{a, -3}, {b, 6}}, -4); + ResetExpressionCounters(); + + expr -= yet_another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({}, {{a, 1}, {b, -10}}, 2))); + + // Then subtract another expression without variables (i.e. having null + // ModelStorage). + const LinearExpression no_vars_expr({}, 3); + ResetExpressionCounters(); + + expr -= no_vars_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({}, {{a, 1}, {b, -10}}, -1))); +} + +TEST(QuadraticExpressionDeathTest, + SubtractionAssignmentLinearExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const LinearExpression other_expr({{c, 1.0}}, 2.0); + + EXPECT_DEATH(expr -= other_expr, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + ResetExpressionCounters(); + + expr -= QuadraticTerm(a, b, 3); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, b, -3}}, {}, 0))); + + // Reuse the previous expression now connected to a ModelStorage to test + // adding the same variable. + ResetExpressionCounters(); + expr -= QuadraticTerm(a, a, -2); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, a, 2}, {a, b, -3}}, {}, 0))); + + // Add another variable from the same ModelStorage. + ResetExpressionCounters(); + expr -= QuadraticTerm(a, b, -4); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, a, 2}, {a, b, 1}}, {}, 0))); +} + +TEST(QuadraticExpressionDeathTest, + SubtractionAssignmentQuadraticTermOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticTerm other_term(c, c, 1.2); + + EXPECT_DEATH(expr -= other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + // First test with a default expression, not associated with any ModelStorage. + QuadraticExpression expr; + const QuadraticExpression another_expr({{a, c, 2.4}}, {{a, 2}, {b, 4}}, 2); + ResetExpressionCounters(); + + expr -= another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, c, -2.4}}, + {{a, -2}, {b, -4}}, -2))); + + // Then add another expression with variables from the same ModelStorage. + QuadraticExpression yet_another_expr({{c, b, 1.1}}, {{a, -3}, {c, 6}}, -4); + ResetExpressionCounters(); + + expr -= yet_another_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, c, -2.4}, {b, c, -1.1}}, + {{a, 1}, {b, -4}, {c, -6}}, 2))); + + // Then add another expression without variables (i.e. having null + // ModelStorage). + const QuadraticExpression no_vars_expr({}, {}, 3); + ResetExpressionCounters(); + + expr -= no_vars_expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, + IsIdentical(QuadraticExpression({{a, c, -2.4}, {b, c, -1.1}}, + {{a, 1}, {b, -4}, {c, -6}}, -1))); +} + +TEST(QuadraticExpressionDeathTest, + SubtractionAssignmentQuadraticExpressionOtherModel) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 1.3}}, 1.4); + + ModelStorage other_storage; + const Variable c(&other_storage, other_storage.AddVariable("c")); + const QuadraticExpression other_term({{c, c, 1.2}}, {{c, 3.4}}, 5.6); + + EXPECT_DEATH(expr -= other_term, kObjectsFromOtherModelStorage); +} + +TEST(QuadraticExpressionTest, SubtractionAssignmentSelf) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + expr -= expr; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression({{a, b, 1.2 - 1.2}}, + {{b, 3.4 - 3.4}}, 0))); +} + +// ---------------------------- Multiplication (*) ----------------------------- + +TEST(QuadraticTermTest, QuadraticTermTimesDoubleAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticTerm term = QuadraticTerm(a, b, 1.2); + + term *= 2.0; + + EXPECT_EQ(term.first_variable(), a); + EXPECT_EQ(term.second_variable(), b); + EXPECT_EQ(term.coefficient(), 2.4); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionTimesDoubleAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + expr *= 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression( + {{a, b, 1.2 * 7.8}}, {{b, 3.4 * 7.8}}, 5.6 * 7.8))); +} + +// ------------------------------- Division (/) -------------------------------- + +TEST(QuadraticTermTest, QuadraticTermDividedByDoubleAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticTerm term = QuadraticTerm(a, b, 1.2); + + term /= 2.0; + + EXPECT_EQ(term.first_variable(), a); + EXPECT_EQ(term.second_variable(), b); + EXPECT_EQ(term.coefficient(), 0.6); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionDividedByDoubleAssignment) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr({{a, b, 1.2}}, {{b, 3.4}}, 5.6); + ResetExpressionCounters(); + + expr /= 7.8; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS(0); + EXPECT_THAT(expr, IsIdentical(QuadraticExpression( + {{a, b, 1.2 / 7.8}}, {{b, 3.4 / 7.8}}, 5.6 / 7.8))); +} + +TEST(QuadraticExpressionTest, AddSumInts) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add = {2, 7}; + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 14)); +} + +TEST(QuadraticExpressionTest, AddSumDoubles) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add({2.0, 7.0}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 14)); +} + +TEST(QuadraticExpressionTest, AddSumVariables) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add({b, c, b}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 2 * b + c + 5)); +} + +TEST(QuadraticExpressionTest, AddSumLinearTerms) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const Variable c(&storage, storage.AddVariable("c")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add({2.0 * b, 1.0 * c, 4.0 * b}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 6 * b + c + 5)); +} + +TEST(QuadraticExpressionTest, AddSumLinearExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add({a + b, 4.0 * b - 1.0}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 4 * a + 5 * b + 4)); +} + +TEST(QuadraticExpressionTest, AddSumQuadraticTerms) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add({a * a, 2 * a * b}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(3 * a * a + 2 * a * b + 3 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddSumQuadraticExpressions) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector to_add( + {a * a - 1, 2 * a * b + 3 * b * b + 2}); + expr.AddSum(to_add); + EXPECT_THAT(expr, IsIdentical(3 * a * a + 2 * a * b + 3 * b * b + 3 * a + 6)); +} + +TEST(QuadraticExpressionTest, Sum) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const std::vector summand( + {a * a, 2 * a * b + 3 * b + 4, 5 * b * a + 6 * b + 7}); + EXPECT_THAT(QuadraticExpression::Sum(summand), + IsIdentical(a * a + 7 * a * b + 9 * b + 11)); +} + +TEST(QuadraticExpressionTest, AddInnerProductIntInt) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {2, 3, 4}; + const std::vector second = {1, -1, 10}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 44)); +} + +TEST(QuadraticExpressionTest, AddInnerProductDoubleDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {2.0, 3.0, 4.0}; + const std::vector second = {1.0, -1.0, 10.0}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 3 * a + 44)); +} + +TEST(QuadraticExpressionTest, AddInnerProductDoubleVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {2.0, 3.0, 4.0}; + const std::vector second = {a, b, a}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 9 * a + 3 * b + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductIntLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {2, 3, 4}; + const std::vector second = {2.0 * a, 4.0 * b, 1.0 * a}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 11 * a + 12 * b + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductDoubleLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {-1.0, 2.0}; + const std::vector second = {3.0 * b + 1, a + b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 5 * a - b + 4)); +} + +TEST(QuadraticExpressionTest, AddInnerProductDoubleQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {-1.0, 2.0}; + const std::vector second = {3.0 * a * a, 4 * a * b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(-a * a + 8 * a * b + 3 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductDoubleQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {-1.0, 2.0}; + const std::vector second = {3.0 * a * b + 1, + 4 * a * a + b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(10 * a * a - 3 * a * b + 3 * a + 2 * b + 4)); +} + +TEST(QuadraticExpressionTest, AddInnerProductVariableVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {a, b, a}; + const std::vector second = {a, a, b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(3 * a * a + 2 * a * b + 3 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductVariableLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {a, b, a}; + const std::vector second = {2 * a, 3 * a, 4 * b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(4 * a * a + 7 * a * b + 3 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductVariableLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {a, b, a}; + const std::vector second = {2 * a + 3, 4 * a + 5 * b, 6}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, + IsIdentical(4 * a * a + 4 * a * b + 5 * b * b + 12 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductLinearTermLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {1 * a, 2 * a, 3 * b}; + const std::vector second = {1 * a, 2 * b, 3 * b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(3 * a * a + 4 * a * b + 9 * b * b + 3 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductLinearTermLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {1 * a, 2 * b, 3 * a}; + const std::vector second = {2 * a + 3, 4 * a + 5 * b, 6}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, + IsIdentical(4 * a * a + 8 * a * b + 10 * b * b + 24 * a + 5)); +} + +TEST(QuadraticExpressionTest, AddInnerProductLinearExpressionLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + QuadraticExpression expr = 2.0 * a * a + 3.0 * a + 5.0; + const std::vector first = {3 * b + a + 1, 2 * a - 2}; + const std::vector second = {2 * a + 3, 3 * a + 5 * b}; + expr.AddInnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(10 * a * a + 16 * a * b + 2 * a - b + 8)); +} + +TEST(QuadraticExpressionTest, InnerProduct) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + const std::vector first = {a, b, a}; + const std::vector second = {2.0 * a, 3.0 * a, 4.0 * b}; + const auto expr = QuadraticExpression::InnerProduct(first, second); + EXPECT_THAT(expr, IsIdentical(2 * a * a + 7 * a * b)); +} + +TEST(QuadraticExpressionDeathTest, AddInnerProductSizeMismatchLeftMore) { + const std::vector left = {2.0, 3.0, 4.0}; + const std::vector right = {1.0, -1.0}; + QuadraticExpression expr; + EXPECT_DEATH(expr.AddInnerProduct(left, right), "left had more"); +} + +TEST(QuadraticExpressionDeathTest, AddInnerProductSizeMismatchRightMore) { + const std::vector left = {2.0, 3.0}; + const std::vector right = {1.0, -1.0, 10.0}; + QuadraticExpression expr; + EXPECT_DEATH(expr.AddInnerProduct(left, right), "right had more"); +} + +//////////////////////////////////////////////////////////////////////////////// +// Quadratic greater than (>=) operators +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticExpressionTest, DoubleGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const UpperBoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 2}}, {}, 0))); + EXPECT_EQ(comparison.upper_bound, 3); +} + +TEST(QuadraticExpressionTest, DoubleGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const UpperBoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 2}}, {{b, 3}}, 4))); + EXPECT_EQ(comparison.upper_bound, 3); +} + +TEST(QuadraticExpressionTest, + DoubleGreaterEqualLowerBoundedQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 6; + LowerBoundedQuadraticExpression rhs = (2 * a * b + 3 * b + 4 >= 5); + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(1, QuadraticTerms({{a, b, 2}}), + LinearTerms({{b, 3}}), 2)); +} + +TEST(QuadraticExpressionTest, VariableGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 1}}), kInf)); +} + +TEST(QuadraticExpressionTest, VariableGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 1}, {b, -3}}), kInf)); +} + +TEST(QuadraticExpressionTest, LinearTermGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}}), kInf)); +} + +TEST(QuadraticExpressionTest, LinearTermGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -3}}), kInf)); +} + +TEST(QuadraticExpressionTest, LinearExpressionGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}}), kInf)); +} + +TEST(QuadraticExpressionTest, LinearExpressionGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + QuadraticExpression rhs = 2 * a * b + 4 * b + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 1, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const double rhs = 4; + + ResetExpressionCounters(); + const LowerBoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 3}}, {}, 0))); + EXPECT_EQ(comparison.lower_bound, 4); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -1}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const LinearTerm rhs = 4 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + LinearExpression rhs = 4 * a + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const QuadraticTerm rhs = 4 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 0, QuadraticTerms({{a, a, -4}, {a, b, 3}}), + LinearTerms(), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticTermGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + QuadraticExpression rhs = 4 * a * a + 5 * b + 6; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 6, QuadraticTerms({{a, a, -4}, {a, b, 3}}), + LinearTerms({{b, -5}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const double rhs = 6; + + ResetExpressionCounters(); + const LowerBoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 3}}, {{b, 4}}, 5))); + EXPECT_EQ(comparison.lower_bound, 6); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionGreaterEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -1}, {b, 4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionGreaterEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const LinearTerm rhs = 6 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionGreaterEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + LinearExpression rhs = 6 * a + 7; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 2, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), kInf)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionGreaterEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const QuadraticTerm rhs = 6 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{b, 4}}), kInf)); +} + +TEST(QuadraticExpressionTest, + QuadraticExpressionGreaterEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + QuadraticExpression rhs = 6 * a * a + 7 * a + 8; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) >= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 3, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{a, -7}, {b, 4}}), kInf)); +} + +TEST(QuadraticExpressionTest, + UpperBoundedQuadraticExpressionGreaterEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + UpperBoundedQuadraticExpression lhs = (2 * a * b + 3 * b + 4 <= 5); + const double rhs = 1; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) >= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-3, QuadraticTerms({{a, b, 2}}), + LinearTerms({{b, 3}}), 1)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Quadratic less than (<=) operators +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticExpressionTest, DoubleLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const LowerBoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 2}}, {}, 0))); + EXPECT_EQ(comparison.lower_bound, 3); +} + +TEST(QuadraticExpressionTest, DoubleLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const LowerBoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 2}}, {{b, 3}}, 4))); + EXPECT_EQ(comparison.lower_bound, 3); +} + +TEST(QuadraticExpressionTest, + DoubleLesserEqualUpperBoundedQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 1; + UpperBoundedQuadraticExpression rhs = (2 * a * b + 3 * b + 4 <= 5); + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-3, QuadraticTerms({{a, b, 2}}), + LinearTerms({{b, 3}}), 1)); +} + +TEST(QuadraticExpressionTest, VariableLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 1}}), 0)); +} + +TEST(QuadraticExpressionTest, VariableLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 1}, {b, -3}}), 4)); +} + +TEST(QuadraticExpressionTest, LinearTermLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}}), 0)); +} + +TEST(QuadraticExpressionTest, LinearTermLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -3}}), 4)); +} + +TEST(QuadraticExpressionTest, LinearExpressionLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}}), -4)); +} + +TEST(QuadraticExpressionTest, LinearExpressionLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + QuadraticExpression rhs = 2 * a * b + 4 * b + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -4}}), 1)); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const double rhs = 4; + + ResetExpressionCounters(); + const UpperBoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 3}}, {}, 0))); + EXPECT_EQ(comparison.upper_bound, 4); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -1}}), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const LinearTerm rhs = 4 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -4}}), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + LinearExpression rhs = 4 * a + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -4}}), 5)); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const QuadraticTerm rhs = 4 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, -4}, {a, b, 3}}), + LinearTerms(), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + QuadraticExpression rhs = 4 * a * a + 5 * b + 6; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, -4}, {a, b, 3}}), + LinearTerms({{b, -5}}), 6)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const double rhs = 6; + + ResetExpressionCounters(); + const UpperBoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison.expression, + IsIdentical(QuadraticExpression({{a, b, 3}}, {{b, 4}}, 5))); + EXPECT_EQ(comparison.upper_bound, 6); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionLesserEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -1}, {b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionLesserEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const LinearTerm rhs = 6 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionLesserEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + LinearExpression rhs = 6 * a + 7; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), 2)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionLesserEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const QuadraticTerm rhs = 6 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, + QuadraticExpressionLesserEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + QuadraticExpression rhs = 6 * a * a + 7 * a + 8; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) <= std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{a, -7}, {b, 4}}), 3)); +} + +TEST(QuadraticExpressionTest, + LowerBoundedQuadraticExpressionLesserEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LowerBoundedQuadraticExpression lhs = (2 * a * b + 3 * b + 4 >= 5); + const double rhs = 6; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) <= rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(1, QuadraticTerms({{a, b, 2}}), + LinearTerms({{b, 3}}), 2)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Quadratic equals (==) operators +//////////////////////////////////////////////////////////////////////////////// + +TEST(QuadraticExpressionTest, DoubleEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(3, QuadraticTerms({{a, b, 2}}), + LinearTerms(), 3)); +} + +TEST(QuadraticExpressionTest, DoubleEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const double lhs = 3; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-1, QuadraticTerms({{a, b, 2}}), + LinearTerms({{b, 3}}), -1)); +} + +TEST(QuadraticExpressionTest, VariableEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, 2}}), + LinearTerms({{a, -1}}), 0)); +} + +TEST(QuadraticExpressionTest, VariableEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const Variable lhs = a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 1}, {b, -3}}), 4)); +} + +TEST(QuadraticExpressionTest, LinearTermEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, 2}}), + LinearTerms({{a, -3}}), 0)); +} + +TEST(QuadraticExpressionTest, LinearTermEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const LinearTerm lhs = 3 * a; + QuadraticExpression rhs = 2 * a * b + 3 * b + 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -3}}), 4)); +} + +TEST(QuadraticExpressionTest, LinearExpressionEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + const QuadraticTerm rhs = 2 * a * b; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) == rhs; + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 4); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-4, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}}), -4)); +} + +TEST(QuadraticExpressionTest, LinearExpressionEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + LinearExpression lhs = 3 * a + 4; + QuadraticExpression rhs = 2 * a * b + 4 * b + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 1, QuadraticTerms({{a, b, -2}}), + LinearTerms({{a, 3}, {b, -4}}), 1)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const double rhs = 4; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(4, QuadraticTerms({{a, b, 3}}), + LinearTerms(), 4)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, -3}}), + LinearTerms({{a, 1}}), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const LinearTerm rhs = 4 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(0, QuadraticTerms({{a, b, -3}}), + LinearTerms({{a, 4}}), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + LinearExpression rhs = 4 * a + 5; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == std::move(rhs); + + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -4}}), 5)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + const QuadraticTerm rhs = 4 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 0, QuadraticTerms({{a, a, 4}, {a, b, -3}}), + LinearTerms(), 0)); +} + +TEST(QuadraticExpressionTest, QuadraticTermEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const QuadraticTerm lhs = 3 * a * b; + QuadraticExpression rhs = 4 * a * a + 5 * b + 6; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = lhs == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -6, QuadraticTerms({{a, a, 4}, {a, b, -3}}), + LinearTerms({{b, 5}}), -6)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualDouble) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const double rhs = 6; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(-1, QuadraticTerms({{a, b, -3}}), + LinearTerms({{b, -4}}), -1)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualVariable) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const Variable rhs = a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -1}, {b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualLinearTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const LinearTerm rhs = 6 * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + LinearExpression rhs = 6 * a + 7; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 2, QuadraticTerms({{a, b, 3}}), + LinearTerms({{a, -6}, {b, 4}}), 2)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualQuadraticTerm) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + const QuadraticTerm rhs = 6 * a * a; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = std::move(lhs) == rhs; + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + -5, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{b, 4}}), -5)); +} + +TEST(QuadraticExpressionTest, QuadraticExpressionEqualQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + QuadraticExpression lhs = 3 * a * b + 4 * b + 5; + QuadraticExpression rhs = 6 * a * a + 7 * a + 8; + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = + std::move(lhs) == std::move(rhs); + + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 3); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(comparison, BoundedQuadraticExpressionEquiv( + 3, QuadraticTerms({{a, a, -6}, {a, b, 3}}), + LinearTerms({{a, -7}, {b, 4}}), 3)); +} + +TEST(BoundedQuadraticExpressionTest, FromVariablesEquality) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + const internal::VariablesEquality linear_comparison = a == b; + ResetExpressionCounters(); + + const BoundedQuadraticExpression quadratic_comparison(linear_comparison); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison, + BoundedQuadraticExpressionEquiv( + 0, QuadraticTerms(), LinearTerms({{a, 1}, {b, -1}}), 0)); +} + +TEST(LowerBoundedQuadraticExpressionTest, FromLowerBoundedLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LowerBoundedLinearExpression linear_comparison = 2 * a >= 3; + ResetExpressionCounters(); + + const LowerBoundedQuadraticExpression quadratic_comparison( + std::move(linear_comparison)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison.expression, + IsIdentical(QuadraticExpression({}, {{a, 2}}, 0))); + EXPECT_EQ(quadratic_comparison.lower_bound, 3); +} + +TEST(BoundedQuadraticExpressionTest, FromLowerBoundedLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + LowerBoundedLinearExpression linear_comparison = 2 * a >= 3; + ResetExpressionCounters(); + + const BoundedQuadraticExpression quadratic_comparison( + std::move(linear_comparison)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison, + BoundedQuadraticExpressionEquiv(3, QuadraticTerms(), + LinearTerms({{a, 2}}), kInf)); +} + +TEST(UpperBoundedQuadraticExpressionTest, FromUpperBoundedLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + UpperBoundedLinearExpression linear_comparison = 2 * a <= 3; + ResetExpressionCounters(); + + const UpperBoundedQuadraticExpression quadratic_comparison( + std::move(linear_comparison)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison.expression, + IsIdentical(QuadraticExpression({}, {{a, 2}}, 0))); + EXPECT_EQ(quadratic_comparison.upper_bound, 3); +} + +TEST(BoundedQuadraticExpressionTest, FromUpperBoundedLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + UpperBoundedLinearExpression linear_comparison = 2 * a <= 3; + ResetExpressionCounters(); + + const BoundedQuadraticExpression quadratic_comparison( + std::move(linear_comparison)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison, + BoundedQuadraticExpressionEquiv(-kInf, QuadraticTerms(), + LinearTerms({{a, 2}}), 3)); +} + +TEST(BoundedQuadraticExpressionTest, FromBoundedLinearExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + BoundedLinearExpression linear_comparison = (2 <= 3 * a <= 4); + ResetExpressionCounters(); + + const BoundedQuadraticExpression quadratic_comparison( + std::move(linear_comparison)); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(LinearExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(LinearExpression, 0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(1); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_THAT(quadratic_comparison, + BoundedQuadraticExpressionEquiv(2, QuadraticTerms(), + LinearTerms({{a, 3}}), 4)); +} + +TEST(BoundedQuadraticExpressionTest, FromLowerBoundedQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = 3 <= a * a; + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv(3.0, QuadraticTerms({{a, a, 1}}), + LinearTerms(), kInf)); +} + +TEST(BoundedQuadraticExpressionTest, FromUpperBoundedQuadraticExpression) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + + ResetExpressionCounters(); + const BoundedQuadraticExpression comparison = a * a <= 5; + EXPECT_THAT(comparison, + BoundedQuadraticExpressionEquiv( + -kInf, QuadraticTerms({{a, a, 1}}), LinearTerms(), 5)); + EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS(0); + EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR(0); + EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_COPY_CONSTRUCTOR(QuadraticExpression, 0); + EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR(QuadraticExpression, 2); + EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR(QuadraticExpression, 1); +} + +TEST(BoundedQuadraticExpressionTest, OutputStreaming) { + ModelStorage storage; + const Variable a(&storage, storage.AddVariable("a")); + const Variable b(&storage, storage.AddVariable("b")); + + auto to_string = [](BoundedQuadraticExpression bounded_expression) { + std::ostringstream oss; + oss << bounded_expression; + return oss.str(); + }; + + EXPECT_EQ(to_string(BoundedQuadraticExpression(QuadraticExpression(), -1, 2)), + "-1 ≤ 0 ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {}, -1), -1, 2)), + "-1 ≤ -1 ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1}, {b, 5}}, -1), -1, 2)), + "-1 ≤ a + 5*b - 1 ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 1}, {b, 5}}, 0), -1, 2)), + "-1 ≤ a + 5*b ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 2}}, 0), -kInf, 2)), + "2*a ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 2}}, 0), -1, kInf)), + "2*a ≥ -1"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({}, {{a, 2}}, 0), 3, 3)), + "2*a = 3"); + + EXPECT_EQ( + to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 3}, {a, b, 4}}, {{a, 1}, {b, 5}}, -1), -1, + 2)), + "-1 ≤ 3*a² + 4*a*b + a + 5*b - 1 ≤ 2"); + EXPECT_EQ( + to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 3}, {a, b, 4}}, {{a, 1}, {b, 5}}, 0), -1, + 2)), + "-1 ≤ 3*a² + 4*a*b + a + 5*b ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), -kInf, 2)), + "2*a² ≤ 2"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), -1, kInf)), + "2*a² ≥ -1"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), 3, 3)), + "2*a² = 3"); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), -kInf, + kRoundTripTestNumber)), + absl::StrCat("2*a² ≤ ", kRoundTripTestNumberStr)); + EXPECT_EQ( + to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), kRoundTripTestNumber, kInf)), + absl::StrCat("2*a² ≥ ", kRoundTripTestNumberStr)); + EXPECT_EQ(to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), kRoundTripTestNumber, + kRoundTripTestNumber)), + absl::StrCat("2*a² = ", kRoundTripTestNumberStr)); + EXPECT_EQ( + to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), kRoundTripTestNumber, 3000)), + absl::StrCat(kRoundTripTestNumberStr, " ≤ 2*a² ≤ 3000")); + EXPECT_EQ( + to_string(BoundedQuadraticExpression( + QuadraticExpression({{a, a, 2}}, {}, 0), 0.0, kRoundTripTestNumber)), + absl::StrCat("0 ≤ 2*a² ≤ ", kRoundTripTestNumberStr)); +} + +#undef EXPECT_NUM_CALLS_DEFAULT_CONSTRUCTOR +#undef EXPECT_NUM_CALLS_COPY_CONSTRUCTOR +#undef EXPECT_NUM_CALLS_MOVE_CONSTRUCTOR +#undef EXPECT_NUM_CALLS_INITIALIZER_LIST_CONSTRUCTOR +#undef EXPECT_NUM_CALLS_LINEAR_TO_QUADRATIC_CONSTRUCTOR +#undef EXPECT_NUM_CALLS_TO_LINEAR_EXPRESSION_CONSTRUCTORS +#undef EXPECT_NUM_CALLS_TO_QUADRATIC_EXPRESSION_CONSTRUCTORS + +} // namespace +} // namespace math_opt +} // namespace operations_research From 39ed3a95c535ef94b85b0ada3e371d4d509b3de6 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Tue, 2 Sep 2025 17:57:50 +0200 Subject: [PATCH 024/491] base: cleanup --- ortools/base/BUILD.bazel | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 86720ddd936..029fe704168 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -305,15 +305,9 @@ cc_library( cc_library( name = "hash", - srcs = [ - "hash.cc", - ], - hdrs = [ - "hash.h", - ], - deps = [ - "@abseil-cpp//absl/strings", - ], + srcs = ["hash.cc"], + hdrs = ["hash.h"], + deps = ["@abseil-cpp//absl/strings"], ) cc_library( From 26ec0b6457379dd7380c94ae0f727a32f9389c4d Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Tue, 2 Sep 2025 18:13:27 +0200 Subject: [PATCH 025/491] bazel: remove deprecated python_version --- ortools/algorithms/python/BUILD.bazel | 1 - ortools/init/python/BUILD.bazel | 1 - ortools/scheduling/python/BUILD.bazel | 1 - ortools/set_cover/python/BUILD.bazel | 1 - ortools/util/python/BUILD.bazel | 1 - 5 files changed, 5 deletions(-) diff --git a/ortools/algorithms/python/BUILD.bazel b/ortools/algorithms/python/BUILD.bazel index 1d3ff6c81ff..de7c3e4f728 100644 --- a/ortools/algorithms/python/BUILD.bazel +++ b/ortools/algorithms/python/BUILD.bazel @@ -72,7 +72,6 @@ pybind_extension( py_test( name = "knapsack_solver_test", srcs = ["knapsack_solver_test.py"], - python_version = "PY3", deps = [ ":knapsack_solver", requirement("absl-py"), diff --git a/ortools/init/python/BUILD.bazel b/ortools/init/python/BUILD.bazel index b541eeb843f..9e1a2dc4e5b 100644 --- a/ortools/init/python/BUILD.bazel +++ b/ortools/init/python/BUILD.bazel @@ -37,7 +37,6 @@ pybind_extension( py_test( name = "init_test", srcs = ["init_test.py"], - python_version = "PY3", deps = [ ":init", requirement("absl-py"), diff --git a/ortools/scheduling/python/BUILD.bazel b/ortools/scheduling/python/BUILD.bazel index 2eff545f5a2..bf588fe58e4 100644 --- a/ortools/scheduling/python/BUILD.bazel +++ b/ortools/scheduling/python/BUILD.bazel @@ -35,7 +35,6 @@ py_test( data = [ "//ortools/scheduling/testdata:j301_1.sm", ], - python_version = "PY3", deps = [ ":rcpsp", "//ortools/scheduling:rcpsp_py_proto", diff --git a/ortools/set_cover/python/BUILD.bazel b/ortools/set_cover/python/BUILD.bazel index 68b5c6ed663..777e8fab30b 100644 --- a/ortools/set_cover/python/BUILD.bazel +++ b/ortools/set_cover/python/BUILD.bazel @@ -35,7 +35,6 @@ pybind_extension( py_test( name = "set_cover_test", srcs = ["set_cover_test.py"], - python_version = "PY3", deps = [ ":set_cover", "//ortools/set_cover:set_cover_py_pb2", diff --git a/ortools/util/python/BUILD.bazel b/ortools/util/python/BUILD.bazel index 26fba1de178..bc2d226875e 100644 --- a/ortools/util/python/BUILD.bazel +++ b/ortools/util/python/BUILD.bazel @@ -39,7 +39,6 @@ pybind_extension( py_test( name = "sorted_interval_list_test", srcs = ["sorted_interval_list_test.py"], - python_version = "PY3", deps = [ ":sorted_interval_list", requirement("absl-py"), From aa0d201d98b63bd473e986722a56aeb90ac9884f Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Tue, 2 Sep 2025 18:11:30 +0200 Subject: [PATCH 026/491] deps: update abseil-cpp to 20250814.0 --- MODULE.bazel | 4 ++-- ortools/math_opt/solvers/message_callback_data.cc | 4 ++-- ortools/math_opt/storage/update_trackers.h | 10 +++++----- ortools/third_party_solvers/xpress_environment.cc | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5766db1dd1b..2f2d0bb627b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -41,10 +41,10 @@ bazel_dep(name = "rules_python", version = "1.5.1") # OR-Tools C++ dependencies # keep sorted go/buildifier#keep-sorted -bazel_dep(name = "abseil-cpp", version = "20250512.1") +bazel_dep(name = "abseil-cpp", version = "20250814.0") bazel_dep(name = "bzip2", version = "1.0.8.bcr.2") bazel_dep(name = "eigen", version = "3.4.0.bcr.3") -bazel_dep(name = "fuzztest", version = "20250214.0") +bazel_dep(name = "fuzztest", version = "20250805.0") bazel_dep(name = "riegeli", version = "0.0.0-20241218-3385e3c") # otherwise fuzztest use a borken version bazel_dep(name = "glpk", version = "5.0.bcr.4") bazel_dep(name = "google_benchmark", version = "1.9.2") diff --git a/ortools/math_opt/solvers/message_callback_data.cc b/ortools/math_opt/solvers/message_callback_data.cc index 78789636041..d37f5da7ae0 100644 --- a/ortools/math_opt/solvers/message_callback_data.cc +++ b/ortools/math_opt/solvers/message_callback_data.cc @@ -72,7 +72,7 @@ void BufferedMessageCallback::OnMessage(absl::string_view message) { } std::vector messages; { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); messages = message_callback_data_.Parse(message); } // Do not hold lock during callback to user code. @@ -87,7 +87,7 @@ void BufferedMessageCallback::Flush() { } std::vector messages; { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); messages = message_callback_data_.Flush(); } // Do not hold lock during callback to user code. diff --git a/ortools/math_opt/storage/update_trackers.h b/ortools/math_opt/storage/update_trackers.h index 7e67c7e1c71..c2f0511e996 100644 --- a/ortools/math_opt/storage/update_trackers.h +++ b/ortools/math_opt/storage/update_trackers.h @@ -159,7 +159,7 @@ void UpdateTrackers::UpdateHasPendingActions() { template template UpdateTrackerId UpdateTrackers::NewUpdateTracker(Args&&... args) { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); const UpdateTrackerId update_tracker = next_update_tracker_; ++next_update_tracker_; @@ -192,7 +192,7 @@ UpdateTrackers::FindTracker(const std::vector& v, template void UpdateTrackers::DeleteUpdateTracker( const UpdateTrackerId update_tracker) { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // The delete trackers may still be in pending_new_trackers_. We have to // remove it from there. @@ -233,7 +233,7 @@ UpdateTrackers::GetUpdatedTrackers() { // Using a non-relaxed memory order has significant impact on performances // here. if (has_pending_actions_.load(std::memory_order_relaxed)) { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // Flush removed trackers. gtl::STLEraseAllFromSequenceIf( @@ -262,7 +262,7 @@ UpdateTrackers::GetUpdatedTrackers() { template Data& UpdateTrackers::GetData(UpdateTrackerId update_tracker) { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // Note that here we could apply the pending actions. We don't do so to keep // the symmetry with the const overload below. @@ -290,7 +290,7 @@ Data& UpdateTrackers::GetData(UpdateTrackerId update_tracker) { template const Data& UpdateTrackers::GetData( UpdateTrackerId update_tracker) const { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // The tracker may still be in pending_new_trackers_. { const auto found_new = FindTracker(pending_new_trackers_, update_tracker); diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 04d872d7a6b..b41ca59b08a 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -247,7 +247,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath) { static DynamicLibrary* xpress_library = new DynamicLibrary; static absl::Mutex mutex(absl::kConstInit); - absl::MutexLock lock(&mutex); + absl::MutexLock lock(mutex); absl::call_once(xpress_loading_done, []() { const std::vector canonical_paths = From d0cac550d8a3b9cc1748adb3023472d48afad610 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 3 Sep 2025 10:42:18 +0200 Subject: [PATCH 027/491] cmake: bump abseil-cpp to 20250814.0 note: also need to bump fuzztest to 2025-08-05 --- Dependencies.txt | 2 +- cmake/dependencies/CMakeLists.txt | 8 ++++---- cmake/host.CMakeLists.txt | 4 ++-- ...l-cpp-20250512.0.patch => abseil-cpp-20250814.0.patch} | 0 ...uzztest-2025-02-14.patch => fuzztest-2025-08-05.patch} | 0 5 files changed, 7 insertions(+), 7 deletions(-) rename patches/{abseil-cpp-20250512.0.patch => abseil-cpp-20250814.0.patch} (100%) rename patches/{fuzztest-2025-02-14.patch => fuzztest-2025-08-05.patch} (100%) diff --git a/Dependencies.txt b/Dependencies.txt index 5d072a8acb3..bb85df63cf1 100644 --- a/Dependencies.txt +++ b/Dependencies.txt @@ -1,5 +1,5 @@ ZLIB=1.3.1 -abseil-cpp=20250512.0 +abseil-cpp=20250814.0 Protobuf=v31.1 Eigen=3.4.0 Re2=2024-07-02 diff --git a/cmake/dependencies/CMakeLists.txt b/cmake/dependencies/CMakeLists.txt index 2efbe88c33f..1db79c53c10 100644 --- a/cmake/dependencies/CMakeLists.txt +++ b/cmake/dependencies/CMakeLists.txt @@ -114,10 +114,10 @@ if(BUILD_absl) FetchContent_Declare( absl GIT_REPOSITORY "https://github.com/abseil/abseil-cpp.git" - GIT_TAG "20250512.0" + GIT_TAG "20250814.0" GIT_SHALLOW TRUE PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/../../patches/abseil-cpp-20250512.0.patch" + "${CMAKE_CURRENT_LIST_DIR}/../../patches/abseil-cpp-20250814.0.patch" OVERRIDE_FIND_PACKAGE ) FetchContent_MakeAvailable(absl) @@ -533,10 +533,10 @@ if(BUILD_fuzztest) FetchContent_Declare( fuzztest GIT_REPOSITORY https://github.com/google/fuzztest.git - GIT_TAG 2025-02-14 + GIT_TAG 2025-08-05 GIT_SHALLOW TRUE PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/../../patches/fuzztest-2025-02-14.patch" + "${CMAKE_CURRENT_LIST_DIR}/../../patches/fuzztest-2025-08-05.patch" ) FetchContent_MakeAvailable(fuzztest) list(POP_BACK CMAKE_MESSAGE_INDENT) diff --git a/cmake/host.CMakeLists.txt b/cmake/host.CMakeLists.txt index 6b63f172578..45be19e63e9 100644 --- a/cmake/host.CMakeLists.txt +++ b/cmake/host.CMakeLists.txt @@ -106,10 +106,10 @@ set(ABSL_BUILD_TESTING OFF) FetchContent_Declare( absl GIT_REPOSITORY "https://github.com/abseil/abseil-cpp.git" - GIT_TAG "20250512.0" + GIT_TAG "20250814.0" GIT_SHALLOW TRUE PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/@PATCHES_PATH@/abseil-cpp-20250512.0.patch" + "${CMAKE_CURRENT_LIST_DIR}/@PATCHES_PATH@/abseil-cpp-20250814.0.patch" ) FetchContent_MakeAvailable(absl) list(POP_BACK CMAKE_MESSAGE_INDENT) diff --git a/patches/abseil-cpp-20250512.0.patch b/patches/abseil-cpp-20250814.0.patch similarity index 100% rename from patches/abseil-cpp-20250512.0.patch rename to patches/abseil-cpp-20250814.0.patch diff --git a/patches/fuzztest-2025-02-14.patch b/patches/fuzztest-2025-08-05.patch similarity index 100% rename from patches/fuzztest-2025-02-14.patch rename to patches/fuzztest-2025-08-05.patch From e2930d40c1dcab9454b79f38b9aa0919ec73f812 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Wed, 3 Sep 2025 09:40:22 +0000 Subject: [PATCH 028/491] Remove `python_version` and `srcs_version` from bazel `py_binary` --- examples/python/code_samples.bzl | 6 ------ ortools/algorithms/samples/code_samples.bzl | 4 ---- ortools/graph/samples/code_samples.bzl | 4 ---- ortools/linear_solver/samples/code_samples.bzl | 4 ---- ortools/pdlp/python/BUILD.bazel | 2 -- ortools/sat/samples/code_samples.bzl | 6 +----- 6 files changed, 1 insertion(+), 25 deletions(-) diff --git a/examples/python/code_samples.bzl b/examples/python/code_samples.bzl index 3ed33a7220f..bf95011b863 100644 --- a/examples/python/code_samples.bzl +++ b/examples/python/code_samples.bzl @@ -39,8 +39,6 @@ def code_sample_compile_py(name): srcs = [name + ".py"], main = name + ".py", deps = PYTHON_DEPS, - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_test_py(name): @@ -50,8 +48,6 @@ def code_sample_test_py(name): srcs = [name + ".py"], main = name + ".py", deps = PYTHON_DEPS, - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_test_arg_py(name, suffix, args, data): @@ -63,8 +59,6 @@ def code_sample_test_arg_py(name, suffix, args, data): data = data, args = args, deps = PYTHON_DEPS, - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_py(name): diff --git a/ortools/algorithms/samples/code_samples.bzl b/ortools/algorithms/samples/code_samples.bzl index 0fdece1acee..5d85abce64e 100644 --- a/ortools/algorithms/samples/code_samples.bzl +++ b/ortools/algorithms/samples/code_samples.bzl @@ -49,8 +49,6 @@ def code_sample_py(name): requirement("absl-py"), requirement("numpy"), ], - python_version = "PY3", - srcs_version = "PY3", ) py_test( @@ -63,8 +61,6 @@ def code_sample_py(name): requirement("absl-py"), requirement("numpy"), ], - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_cc_py(name): diff --git a/ortools/graph/samples/code_samples.bzl b/ortools/graph/samples/code_samples.bzl index 176a7da3cbc..c347f79218c 100644 --- a/ortools/graph/samples/code_samples.bzl +++ b/ortools/graph/samples/code_samples.bzl @@ -75,8 +75,6 @@ def code_sample_py(name): requirement("absl-py"), requirement("numpy"), ], - python_version = "PY3", - srcs_version = "PY3", ) py_test( @@ -91,8 +89,6 @@ def code_sample_py(name): requirement("absl-py"), requirement("numpy"), ], - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_cc_py(name): diff --git a/ortools/linear_solver/samples/code_samples.bzl b/ortools/linear_solver/samples/code_samples.bzl index 41be2f3cd02..5b387375d06 100644 --- a/ortools/linear_solver/samples/code_samples.bzl +++ b/ortools/linear_solver/samples/code_samples.bzl @@ -58,8 +58,6 @@ def code_sample_py(name): "//ortools/init/python:init", "//ortools/linear_solver/python:model_builder", ], - python_version = "PY3", - srcs_version = "PY3", ) py_test( @@ -77,8 +75,6 @@ def code_sample_py(name): requirement("numpy"), requirement("pandas"), ], - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_java(name): diff --git a/ortools/pdlp/python/BUILD.bazel b/ortools/pdlp/python/BUILD.bazel index 9b8f675819a..423313a3f1a 100644 --- a/ortools/pdlp/python/BUILD.bazel +++ b/ortools/pdlp/python/BUILD.bazel @@ -42,8 +42,6 @@ py_test( data = [ ":pdlp.so", ], - python_version = "PY3", - srcs_version = "PY3", deps = [ requirement("absl-py"), requirement("numpy"), diff --git a/ortools/sat/samples/code_samples.bzl b/ortools/sat/samples/code_samples.bzl index 1b5971cafd0..ebe67f14d34 100644 --- a/ortools/sat/samples/code_samples.bzl +++ b/ortools/sat/samples/code_samples.bzl @@ -13,10 +13,10 @@ """Helper macro to compile and test code samples.""" -load("@rules_go//go:def.bzl", "go_test") load("@pip_deps//:requirements.bzl", "requirement") load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_test.bzl", "cc_test") +load("@rules_go//go:def.bzl", "go_test") load("@rules_java//java:java_test.bzl", "java_test") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") @@ -71,8 +71,6 @@ def code_sample_py(name): requirement("pandas"), "//ortools/sat/python:cp_model", ], - python_version = "PY3", - srcs_version = "PY3", ) py_test( @@ -89,8 +87,6 @@ def code_sample_py(name): requirement("pandas"), requirement("protobuf"), ], - python_version = "PY3", - srcs_version = "PY3", ) def code_sample_cc_go_py(name): From 23f8f2acbe5f6d368eb48ea10df7a59dd0e46567 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 3 Sep 2025 13:15:24 +0200 Subject: [PATCH 029/491] cmake: fix fuzztest patch --- patches/fuzztest-2025-08-05.patch | 127 ++++-------------------------- 1 file changed, 14 insertions(+), 113 deletions(-) diff --git a/patches/fuzztest-2025-08-05.patch b/patches/fuzztest-2025-08-05.patch index d288eb54188..2d6f830bf43 100644 --- a/patches/fuzztest-2025-08-05.patch +++ b/patches/fuzztest-2025-08-05.patch @@ -1,8 +1,8 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 1e34de3..dbacb91 100644 +index f9f71df..321caf5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -13,7 +13,7 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") +@@ -28,7 +28,7 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") set (COMPILER_CLANG 1) else () @@ -12,22 +12,21 @@ index 1e34de3..dbacb91 100644 if (COMPILER_GCC AND (FUZZTEST_FUZZING_MODE OR (FUZZTEST_COMPATIBILITY_MODE STREQUAL "libfuzzer"))) diff --git a/cmake/BuildDependencies.cmake b/cmake/BuildDependencies.cmake -index 1f4f08d..cc4d0ba 100644 +index d8565c0..eb79f61 100644 --- a/cmake/BuildDependencies.cmake +++ b/cmake/BuildDependencies.cmake -@@ -3,20 +3,20 @@ cmake_minimum_required(VERSION 3.19) +@@ -17,20 +17,20 @@ cmake_minimum_required(VERSION 3.19) include(FetchContent) set(absl_URL https://github.com/abseil/abseil-cpp.git) --set(absl_TAG 20240116.0) -+set(absl_TAG 20250512.0) +-set(absl_TAG d04b964d82ed5146f7e5e34701a5ba69f9514c9a) ++set(absl_TAG 20250814.0) set(re2_URL https://github.com/google/re2.git) --set(re2_TAG 2024-02-01) -+set(re2_TAG 2024-07-02) + set(re2_TAG 2024-07-02) set(gtest_URL https://github.com/google/googletest.git) --set(gtest_TAG v1.14.0) +-set(gtest_TAG v1.16.0) +set(gtest_TAG v1.17.0) # From https://www.antlr.org/download.html @@ -35,12 +34,12 @@ index 1f4f08d..cc4d0ba 100644 set(antlr_cpp_MD5 acf7371bd7562188712751266d8a7b90) set(proto_URL https://github.com/protocolbuffers/protobuf.git) --set(proto_TAG v28.2) +-set(proto_TAG v30.2) +set(proto_TAG v31.1) set(nlohmann_json_URL https://github.com/nlohmann/json.git) - set(nlohmann_json_TAG v3.11.2) -@@ -27,7 +27,7 @@ if(POLICY CMP0135) + set(nlohmann_json_TAG v3.11.3) +@@ -44,7 +44,7 @@ if(POLICY CMP0135) endif() FetchContent_Declare( @@ -49,7 +48,7 @@ index 1f4f08d..cc4d0ba 100644 GIT_REPOSITORY ${absl_URL} GIT_TAG ${absl_TAG} ) -@@ -68,7 +68,7 @@ endif () +@@ -93,7 +93,7 @@ endif () set(ABSL_PROPAGATE_CXX_STD ON) set(ABSL_ENABLE_INSTALL ON) @@ -58,19 +57,11 @@ index 1f4f08d..cc4d0ba 100644 set(RE2_BUILD_TESTING OFF) FetchContent_MakeAvailable(re2) -@@ -76,6 +76,7 @@ FetchContent_MakeAvailable(re2) - set(GTEST_HAS_ABSL ON) - FetchContent_MakeAvailable(googletest) - -+set(ANTLR_BUILD_CPP_TESTS OFF) - FetchContent_MakeAvailable(antlr_cpp) - - if (FUZZTEST_BUILD_TESTING) diff --git a/cmake/FuzzTestHelpers.cmake b/cmake/FuzzTestHelpers.cmake -index b907ff8..bcd3328 100644 +index 2869a77..8462fdd 100644 --- a/cmake/FuzzTestHelpers.cmake +++ b/cmake/FuzzTestHelpers.cmake -@@ -100,6 +100,13 @@ function(fuzztest_cc_library) +@@ -114,6 +114,13 @@ function(fuzztest_cc_library) ${FUZZTEST_CC_LIB_LINKOPTS} ${FUZZTEST_DEFAULT_LINKOPTS} ) @@ -84,96 +75,6 @@ index b907ff8..bcd3328 100644 set_property( TARGET ${_NAME} -diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt -index 3d41c8b..7e4dc30 100644 ---- a/fuzztest/CMakeLists.txt -+++ b/fuzztest/CMakeLists.txt -@@ -122,41 +122,43 @@ fuzztest_cc_library( - GTest::gtest - ) - --fuzztest_cc_library( -- NAME -- llvm_fuzzer_main -- SRCS -- "llvm_fuzzer_main.cc" -- DEPS -- fuzztest::init_fuzztest -- absl::flags -- absl::flags_parse -- GTest::gtest --) -+if (FUZZTEST_BUILD_TESTING) -+ fuzztest_cc_library( -+ NAME -+ llvm_fuzzer_main -+ SRCS -+ "llvm_fuzzer_main.cc" -+ DEPS -+ fuzztest::init_fuzztest -+ absl::flags -+ absl::flags_parse -+ GTest::gtest -+ ) - --fuzztest_cc_library( -- NAME -- llvm_fuzzer_wrapper -- SRCS -- "llvm_fuzzer_wrapper.cc" -- DEPS -- fuzztest::coverage -- fuzztest::domain_core -- fuzztest::fuzztest -- fuzztest::io -- fuzztest::llvm_fuzzer_main -- fuzztest::logging -- absl::core_headers -- absl::flags -- absl::log -- absl::no_destructor -- absl::random_random -- absl::random_bit_gen_ref -- absl::strings -- absl::string_view -- absl::synchronization -- re2::re2 --) -+ fuzztest_cc_library( -+ NAME -+ llvm_fuzzer_wrapper -+ SRCS -+ "llvm_fuzzer_wrapper.cc" -+ DEPS -+ fuzztest::coverage -+ fuzztest::domain_core -+ fuzztest::fuzztest -+ fuzztest::io -+ fuzztest::llvm_fuzzer_main -+ fuzztest::logging -+ absl::core_headers -+ absl::flags -+ absl::log -+ absl::no_destructor -+ absl::random_random -+ absl::random_bit_gen_ref -+ absl::strings -+ absl::string_view -+ absl::synchronization -+ re2::re2 -+ ) -+endif() - - ################################################################################ - # Internal -@@ -484,6 +486,7 @@ fuzztest_cc_library( - "internal/logging.cc" - DEPS - absl::strings -+ absl::synchronization - ) - - fuzztest_cc_library( diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 79ada1a..124c76b 100644 --- a/tools/CMakeLists.txt From fea4db45024545a291fb7bd2cc2139c828f280e7 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 3 Sep 2025 14:34:51 +0200 Subject: [PATCH 030/491] util: fix down_cast usage --- ortools/util/proto_tools.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ortools/util/proto_tools.h b/ortools/util/proto_tools.h index 8d8eb3c15c6..0a7ea33d4c7 100644 --- a/ortools/util/proto_tools.h +++ b/ortools/util/proto_tools.h @@ -102,7 +102,7 @@ absl::StatusOr SafeProtoDownCast(google::protobuf::Message* proto) { Proto::default_instance().GetDescriptor(); const google::protobuf::Descriptor* actual_descriptor = proto->GetDescriptor(); - if (actual_descriptor == expected_descriptor) return down_cast(proto); + if (actual_descriptor == expected_descriptor) return reinterpret_cast(proto); return absl::InvalidArgumentError(absl::StrFormat( "Expected message type '%s', but got type '%s'", expected_descriptor->full_name(), actual_descriptor->full_name())); @@ -116,7 +116,7 @@ absl::StatusOr SafeProtoConstDownCast( const google::protobuf::Descriptor* actual_descriptor = proto->GetDescriptor(); if (actual_descriptor == expected_descriptor) { - return down_cast(proto); + return reinterpret_cast(proto); } return absl::InvalidArgumentError(absl::StrFormat( "Expected message type '%s', but got type '%s'", From 064b5d4fa4c01afa5af690f582c902b3de0d9e89 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 3 Sep 2025 14:35:00 +0200 Subject: [PATCH 031/491] bazelrc: cleanup --- .bazelrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelrc b/.bazelrc index c407d5cca1d..37af3f95a27 100644 --- a/.bazelrc +++ b/.bazelrc @@ -52,7 +52,7 @@ test --test_output=errors --test_timeout_filters=-eternal # Put user-specific options in .bazelrc.user try-import %workspace%/.bazelrc.user -# googletest +# Enable absl::string_view support in @googletest build --define absl=1 # asan From aac84a1645125b3acbecaaae5cf2b8a7ed2a219b Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 3 Sep 2025 14:38:56 +0200 Subject: [PATCH 032/491] cmake: fixup abseil-cpp bump --- cmake/java.cmake | 1 - cmake/python.cmake | 1 - ortools/dotnet/Google.OrTools.runtime.csproj.in | 1 - patches/fuzztest-2025-08-05.patch | 10 +++++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cmake/java.cmake b/cmake/java.cmake index 6d74bc848c9..f90f8d95126 100644 --- a/cmake/java.cmake +++ b/cmake/java.cmake @@ -380,7 +380,6 @@ add_custom_command( $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> - $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> diff --git a/cmake/python.cmake b/cmake/python.cmake index 366c746b3aa..4f5385b62b7 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -556,7 +556,6 @@ add_custom_command( $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> - $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> diff --git a/ortools/dotnet/Google.OrTools.runtime.csproj.in b/ortools/dotnet/Google.OrTools.runtime.csproj.in index cb57d49ab20..18c79c965e5 100644 --- a/ortools/dotnet/Google.OrTools.runtime.csproj.in +++ b/ortools/dotnet/Google.OrTools.runtime.csproj.in @@ -83,7 +83,6 @@ $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> - $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> diff --git a/patches/fuzztest-2025-08-05.patch b/patches/fuzztest-2025-08-05.patch index 2d6f830bf43..ef9f31fae3d 100644 --- a/patches/fuzztest-2025-08-05.patch +++ b/patches/fuzztest-2025-08-05.patch @@ -12,7 +12,7 @@ index f9f71df..321caf5 100644 if (COMPILER_GCC AND (FUZZTEST_FUZZING_MODE OR (FUZZTEST_COMPATIBILITY_MODE STREQUAL "libfuzzer"))) diff --git a/cmake/BuildDependencies.cmake b/cmake/BuildDependencies.cmake -index d8565c0..eb79f61 100644 +index d8565c0..24b7f52 100644 --- a/cmake/BuildDependencies.cmake +++ b/cmake/BuildDependencies.cmake @@ -17,20 +17,20 @@ cmake_minimum_required(VERSION 3.19) @@ -57,6 +57,14 @@ index d8565c0..eb79f61 100644 set(RE2_BUILD_TESTING OFF) FetchContent_MakeAvailable(re2) +@@ -101,6 +101,7 @@ FetchContent_MakeAvailable(re2) + set(GTEST_HAS_ABSL ON) + FetchContent_MakeAvailable(googletest) + ++set(ANTLR_BUILD_CPP_TESTS OFF) + FetchContent_MakeAvailable(antlr_cpp) + + if (FUZZTEST_BUILD_TESTING) diff --git a/cmake/FuzzTestHelpers.cmake b/cmake/FuzzTestHelpers.cmake index 2869a77..8462fdd 100644 --- a/cmake/FuzzTestHelpers.cmake From 54cb9b0446079d028fdecef1aa27449d4876c1f2 Mon Sep 17 00:00:00 2001 From: Nils K <24257556+septatrix@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:51:55 +0200 Subject: [PATCH 033/491] Add py.typed marker to ortools.math_opt.python package --- ortools/math_opt/python/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ortools/math_opt/python/py.typed diff --git a/ortools/math_opt/python/py.typed b/ortools/math_opt/python/py.typed new file mode 100644 index 00000000000..e69de29bb2d From df70a721bcd9513ef1bde7a7dc4730b4e732c008 Mon Sep 17 00:00:00 2001 From: Mizux Seiha Date: Thu, 4 Sep 2025 10:26:43 +0200 Subject: [PATCH 034/491] fuzztest: remove subprocess to remove once https://github.com/google/fuzztest/issues/1845 is fixed --- patches/fuzztest-2025-08-05.patch | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/patches/fuzztest-2025-08-05.patch b/patches/fuzztest-2025-08-05.patch index ef9f31fae3d..37dcc566a22 100644 --- a/patches/fuzztest-2025-08-05.patch +++ b/patches/fuzztest-2025-08-05.patch @@ -83,6 +83,46 @@ index 2869a77..8462fdd 100644 set_property( TARGET ${_NAME} +diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt +index 61f6da8..84fe3a7 100644 +--- a/fuzztest/internal/CMakeLists.txt ++++ b/fuzztest/internal/CMakeLists.txt +@@ -434,35 +434,6 @@ fuzztest_cc_library( + absl::cord + ) + +-fuzztest_cc_library( +- NAME +- subprocess +- HDRS +- "subprocess.h" +- SRCS +- "subprocess.cc" +- DEPS +- fuzztest::logging +- absl::flat_hash_map +- absl::function_ref +- absl::span +- absl::strings +- absl::string_view +- absl::time +-) +- +-fuzztest_cc_test( +- NAME +- subprocess_test +- SRCS +- "subprocess_test.cc" +- DEPS +- fuzztest::subprocess +- absl::strings +- absl::time +- GTest::gmock_main +-) +- + fuzztest_cc_library( + NAME + table_of_recent_compares diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 79ada1a..124c76b 100644 --- a/tools/CMakeLists.txt From 46d5998e6d2d6b541229901a32d7b73e5656dc4c Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Thu, 4 Sep 2025 13:57:58 +0000 Subject: [PATCH 035/491] [bazel][NFC] Inline and simplify platform selection --- ortools/algorithms/BUILD.bazel | 19 +-------------- ortools/base/BUILD.bazel | 27 +++------------------ ortools/constraint_solver/BUILD.bazel | 15 ------------ ortools/flatzinc/BUILD.bazel | 26 ++------------------- ortools/glop/BUILD.bazel | 27 +++------------------ ortools/graph/BUILD.bazel | 19 +-------------- ortools/lp_data/BUILD.bazel | 27 +++------------------ ortools/routing/BUILD.bazel | 19 +-------------- ortools/third_party_solvers/BUILD.bazel | 19 +-------------- ortools/util/BUILD.bazel | 31 ++++--------------------- ortools/util/java/BUILD.bazel | 17 +------------- 11 files changed, 20 insertions(+), 226 deletions(-) diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 6c1affd1f3e..e5a3531c333 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -20,21 +20,6 @@ package(default_visibility = ["//visibility:public"]) # Description: # Home of algorithms used in OR solvers -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - # OSS solvers bool_flag( name = "with_cbc", @@ -110,9 +95,7 @@ cc_test( name = "radix_sort_test", srcs = ["radix_sort_test.cc"], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), deps = [ diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 029fe704168..99b673ec257 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -16,21 +16,6 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - filegroup( name = "base_swig", srcs = [ @@ -82,9 +67,7 @@ cc_library( "-DOR_TOOLS_PATCH=9999", ], linkopts = select({ - "on_linux": [], - "on_macos": ["-framework CoreFoundation"], - "on_windows": [], + "@platforms//os:macos": ["-framework CoreFoundation"], "//conditions:default": [], }), deps = [ @@ -180,9 +163,7 @@ cc_library( name = "dump_vars", hdrs = ["dump_vars.h"], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), deps = [ @@ -197,9 +178,7 @@ cc_test( size = "small", srcs = ["dump_vars_test.cc"], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), deps = [ diff --git a/ortools/constraint_solver/BUILD.bazel b/ortools/constraint_solver/BUILD.bazel index 30f26b67840..aadc4870dcc 100644 --- a/ortools/constraint_solver/BUILD.bazel +++ b/ortools/constraint_solver/BUILD.bazel @@ -20,21 +20,6 @@ load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_python//python:py_library.bzl", "py_library") -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - proto_library( name = "assignment_proto", srcs = ["assignment.proto"], diff --git a/ortools/flatzinc/BUILD.bazel b/ortools/flatzinc/BUILD.bazel index ec57d91cccd..8d84487fbd9 100644 --- a/ortools/flatzinc/BUILD.bazel +++ b/ortools/flatzinc/BUILD.bazel @@ -20,27 +20,6 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = [ - "@platforms//os:linux", - ], -) - -config_setting( - name = "on_macos", - constraint_values = [ - "@platforms//os:macos", - ], -) - -config_setting( - name = "on_windows", - constraint_values = [ - "@platforms//os:windows", - ], -) - # --------------------------------------------------------------------------- # Baseline flatzinc model. # --------------------------------------------------------------------------- @@ -88,9 +67,8 @@ cc_library( name = "parser_lex_lib", srcs = ["parser.yy.cc"], copts = select({ - "on_linux": ["-Wno-unused-function"], # parser.yy.cc - "on_macos": ["-Wno-unused-function"], # parser.yy.cc - "on_windows": [], + "@platforms//os:linux": ["-Wno-unused-function"], # parser.yy.cc + "@platforms//os:macos": ["-Wno-unused-function"], # parser.yy.cc "//conditions:default": [], }), deps = [ diff --git a/ortools/glop/BUILD.bazel b/ortools/glop/BUILD.bazel index 56a7c2862bb..6ad186182d9 100644 --- a/ortools/glop/BUILD.bazel +++ b/ortools/glop/BUILD.bazel @@ -20,34 +20,13 @@ package( default_visibility = ["//visibility:public"], ) -config_setting( - name = "on_linux", - constraint_values = [ - "@platforms//os:linux", - ], -) - -config_setting( - name = "on_macos", - constraint_values = [ - "@platforms//os:macos", - ], -) - -config_setting( - name = "on_windows", - constraint_values = [ - "@platforms//os:windows", - ], -) - # Floating-point code in this directory must not be compiled with # dangerous optimizations. For example do not assume that FP expressions # are associative. This is what -fno-fast-math is for. SAFE_FP_CODE = select({ - "on_linux": ["-fno-fast-math"], - "on_macos": [], # no_fast_math is the default. - "on_windows": [], # /fp:precise is the default. + "@platforms//os:linux": ["-fno-fast-math"], + "@platforms//os:macos": [], # no_fast_math is the default. + "@platforms//os:windows": [], # /fp:precise is the default. "//conditions:default": [], }) diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index b5602ce58d6..808840b9066 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -17,21 +17,6 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - cc_library( name = "graph", hdrs = ["graph.h"], @@ -533,9 +518,7 @@ cc_library( srcs = ["min_cost_flow.cc"], hdrs = ["min_cost_flow.h"], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), visibility = ["//visibility:public"], diff --git a/ortools/lp_data/BUILD.bazel b/ortools/lp_data/BUILD.bazel index 15cd8880c95..d6c9b489c49 100644 --- a/ortools/lp_data/BUILD.bazel +++ b/ortools/lp_data/BUILD.bazel @@ -15,34 +15,13 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = [ - "@platforms//os:linux", - ], -) - -config_setting( - name = "on_macos", - constraint_values = [ - "@platforms//os:macos", - ], -) - -config_setting( - name = "on_windows", - constraint_values = [ - "@platforms//os:windows", - ], -) - # Floating-point code in this directory must not be compiled with # dangerous optimizations. For example do not assume that FP expressions # are associative. This is what -fno-fast-math is for. SAFE_FP_CODE = select({ - "on_linux": ["-fno-fast-math"], - "on_macos": [], # no_fast_math is the default. - "on_windows": [], # /fp:precise is the default. + "@platforms//os:linux": ["-fno-fast-math"], + "@platforms//os:macos": [], # no_fast_math is the default. + "@platforms//os:windows": [], # /fp:precise is the default. "//conditions:default": [], }) diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel index 2a89e691daf..760ae652f13 100644 --- a/ortools/routing/BUILD.bazel +++ b/ortools/routing/BUILD.bazel @@ -19,21 +19,6 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - proto_library( name = "enums_proto", srcs = ["enums.proto"], @@ -236,9 +221,7 @@ cc_library( "search.h", ], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), deps = [ diff --git a/ortools/third_party_solvers/BUILD.bazel b/ortools/third_party_solvers/BUILD.bazel index a7eb2776a65..7c9c6289225 100644 --- a/ortools/third_party_solvers/BUILD.bazel +++ b/ortools/third_party_solvers/BUILD.bazel @@ -15,28 +15,11 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - cc_library( name = "dynamic_library", hdrs = ["dynamic_library.h"], linkopts = select({ - "on_linux": ["-Wl,--no-as-needed -ldl"], - "on_macos": [], - "on_windows": [], + "@platforms//os:linux": ["-Wl,--no-as-needed -ldl"], "//conditions:default": [], }), deps = [ diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index 13479741eca..744a1940c7e 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -18,27 +18,6 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) -config_setting( - name = "on_linux", - constraint_values = [ - "@platforms//os:linux", - ], -) - -config_setting( - name = "on_macos", - constraint_values = [ - "@platforms//os:macos", - ], -) - -config_setting( - name = "on_windows", - constraint_values = [ - "@platforms//os:windows", - ], -) - # OptionalBoolean proto_library( name = "optional_boolean_proto", @@ -132,9 +111,7 @@ cc_library( srcs = ["piecewise_linear_function.cc"], hdrs = ["piecewise_linear_function.h"], copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], + "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), deps = [ @@ -244,9 +221,9 @@ cc_library( # You must also set this flag if you depend on this target and use # its methods related to IEEE-754 rounding modes. copts = select({ - "on_linux": ["-frounding-math"], - "on_macos": ["-frounding-math"], - "on_windows": [], + "@platforms//os:linux": ["-frounding-math"], + "@platforms//os:macos": ["-frounding-math"], + "@platforms//os:windows": [], "//conditions:default": ["-frounding-math"], }), deps = [ diff --git a/ortools/util/java/BUILD.bazel b/ortools/util/java/BUILD.bazel index 1496f519235..799eaffd898 100644 --- a/ortools/util/java/BUILD.bazel +++ b/ortools/util/java/BUILD.bazel @@ -15,21 +15,6 @@ load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") -config_setting( - name = "on_linux", - constraint_values = ["@platforms//os:linux"], -) - -config_setting( - name = "on_macos", - constraint_values = ["@platforms//os:macos"], -) - -config_setting( - name = "on_windows", - constraint_values = ["@platforms//os:windows"], -) - filegroup( name = "vector_swig", srcs = [ @@ -71,7 +56,7 @@ ortools_java_wrap_cc( "//ortools/base:base_swig", ], swig_opt = select({ - "on_linux": "-DSWIGWORDSIZE64", + "@platforms//os:linux": "-DSWIGWORDSIZE64", "//conditions:default": "", }), visibility = ["//visibility:public"], From 21b94fb65159396d53f60c5ca32559badc768154 Mon Sep 17 00:00:00 2001 From: Mizux Seiha Date: Thu, 4 Sep 2025 17:22:32 +0200 Subject: [PATCH 036/491] cmake: fix protocol-buffer-matchers build --- cmake/cpp.cmake | 1 + ortools/base/BUILD.bazel | 1 + ortools/base/CMakeLists.txt | 24 +- ortools/base/protocol-buffer-matchers.cc | 385 +++++++++++++++++++ ortools/base/protocol-buffer-matchers.h | 352 +---------------- ortools/math_opt/cpp/CMakeLists.txt | 2 +- ortools/math_opt/elemental/CMakeLists.txt | 2 +- ortools/math_opt/solver_tests/CMakeLists.txt | 44 +-- ortools/math_opt/solvers/CMakeLists.txt | 63 +-- 9 files changed, 474 insertions(+), 400 deletions(-) create mode 100644 ortools/base/protocol-buffer-matchers.cc diff --git a/cmake/cpp.cmake b/cmake/cpp.cmake index 7581aeb93c3..e2f28057200 100644 --- a/cmake/cpp.cmake +++ b/cmake/cpp.cmake @@ -196,6 +196,7 @@ function(ortools_cxx_test) target_compile_options(${TEST_NAME} PRIVATE ${TEST_COMPILE_OPTIONS}) target_link_libraries(${TEST_NAME} PRIVATE ${PROJECT_NAMESPACE}::ortools + ${PROJECT_NAMESPACE}::base_gmock ${TEST_LINK_LIBRARIES} ) target_link_options(${TEST_NAME} PRIVATE ${TEST_LINK_OPTIONS}) diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 99b673ec257..227a612d488 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -443,6 +443,7 @@ cc_library( cc_library( name = "protocol-buffer-matchers", + srcs = ["protocol-buffer-matchers.cc"], hdrs = ["protocol-buffer-matchers.h"], deps = [ "@abseil-cpp//absl/log:check", diff --git a/ortools/base/CMakeLists.txt b/ortools/base/CMakeLists.txt index 61c3524d4fb..3fb969f8b32 100644 --- a/ortools/base/CMakeLists.txt +++ b/ortools/base/CMakeLists.txt @@ -13,8 +13,9 @@ file(GLOB _SRCS "*.h" "*.cc") list(FILTER _SRCS EXCLUDE REGEX "/.*_test.cc") +list(FILTER _SRCS EXCLUDE REGEX "/gmock\.h") list(FILTER _SRCS EXCLUDE REGEX "/status-matchers\.h") -list(FILTER _SRCS EXCLUDE REGEX "/protocol-buffer-matchers\.h") +list(FILTER _SRCS EXCLUDE REGEX "/protocol-buffer-matchers\..*") set(NAME ${PROJECT_NAME}_base) @@ -46,6 +47,25 @@ target_link_libraries(${NAME} PRIVATE #add_library(${PROJECT_NAMESPACE}::base ALIAS ${NAME}) if(BUILD_TESTING) +ortools_cxx_library( + NAME + base_gmock + SOURCES + "gmock.h" + "protocol-buffer-matchers.cc" + "protocol-buffer-matchers.h" + "status-matchers.h" + TYPE + STATIC + LINK_LIBRARIES + absl::log + absl::strings + GTest::gtest + GTest::gmock + protobuf::libprotobuf + TESTING +) + file(GLOB _TEST_SRCS "*_test.cc") foreach(_FULL_FILE_NAME IN LISTS _TEST_SRCS) get_filename_component(_NAME ${_FULL_FILE_NAME} NAME_WE) @@ -57,9 +77,9 @@ if(BUILD_TESTING) ${_FILE_NAME} LINK_LIBRARIES benchmark::benchmark + base_gmock GTest::gtest GTest::gtest_main - GTest::gmock ) endforeach() endif() diff --git a/ortools/base/protocol-buffer-matchers.cc b/ortools/base/protocol-buffer-matchers.cc new file mode 100644 index 00000000000..fa25757b478 --- /dev/null +++ b/ortools/base/protocol-buffer-matchers.cc @@ -0,0 +1,385 @@ +// Copyright 2010-2025 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. + +// emulates g3/testing/base/public/gmock_utils/protocol-buffer-matchers.cc +#include "ortools/base/protocol-buffer-matchers.h" + +namespace testing { +namespace internal { +// Utilities. +class StringErrorCollector : public ::google::protobuf::io::ErrorCollector { + public: + explicit StringErrorCollector(std::string* error_text) + : error_text_(error_text) {} + + void RecordError(int line, int column, absl::string_view message) override { + std::ostringstream stream; + stream << line << '(' << column << "): " << message << std::endl; + *error_text_ += stream.str(); + } + + void RecordWarning(int line, int column, absl::string_view message) override { + std::ostringstream stream; + stream << line << '(' << column << "): " << message << std::endl; + *error_text_ += stream.str(); + } + + private: + std::string* error_text_; + StringErrorCollector(const StringErrorCollector&) = delete; + StringErrorCollector& operator=(const StringErrorCollector&) = delete; +}; + +bool ParsePartialFromAscii(const std::string& pb_ascii, + ::google::protobuf::Message* proto, + std::string* error_text) { + ::google::protobuf::TextFormat::Parser parser; + StringErrorCollector collector(error_text); + parser.RecordErrorsTo(&collector); + parser.AllowPartialMessage(true); + return parser.ParseFromString(pb_ascii, proto); +} + +// Returns true iff p and q can be compared (i.e. have the same descriptor). +bool ProtoComparable(const ::google::protobuf::Message& p, + const ::google::protobuf::Message& q) { + return p.GetDescriptor() == q.GetDescriptor(); +} + +template +std::string JoinStringPieces(const Container& strings, + absl::string_view separator) { + std::stringstream stream; + absl::string_view sep = ""; + for (const absl::string_view str : strings) { + stream << sep << str; + sep = separator; + } + return stream.str(); +} + +// Find all the descriptors for the ingore_fields. +std::vector GetFieldDescriptors( + const ::google::protobuf::Descriptor* proto_descriptor, + const std::vector& ignore_fields) { + std::vector ignore_descriptors; + std::vector remaining_descriptors; + + const ::google::protobuf::DescriptorPool* pool = + proto_descriptor->file()->pool(); + for (const std::string& name : ignore_fields) { + if (const ::google::protobuf::FieldDescriptor* field = + pool->FindFieldByName(name)) { + ignore_descriptors.push_back(field); + } else { + remaining_descriptors.push_back(name); + } + } + + CHECK(remaining_descriptors.empty()) + << "Could not find fields for proto " << proto_descriptor->full_name() + << " with fully qualified names: " + << JoinStringPieces(remaining_descriptors, ","); + return ignore_descriptors; +} + +// Sets the ignored fields corresponding to ignore_fields in differencer. Dies +// if any is invalid. +void SetIgnoredFieldsOrDie( + const ::google::protobuf::Descriptor& root_descriptor, + const std::vector& ignore_fields, + ::google::protobuf::util::MessageDifferencer* differencer) { + if (!ignore_fields.empty()) { + std::vector ignore_descriptors = + GetFieldDescriptors(&root_descriptor, ignore_fields); + for (std::vector::iterator it = + ignore_descriptors.begin(); + it != ignore_descriptors.end(); ++it) { + differencer->IgnoreField(*it); + } + } +} + +// A criterion that ignores a field path. +class IgnoreFieldPathCriteria + : public ::google::protobuf::util::MessageDifferencer::IgnoreCriteria { + public: + explicit IgnoreFieldPathCriteria( + const std::vector< + ::google::protobuf::util::MessageDifferencer::SpecificField>& + field_path) + : ignored_field_path_(field_path) {} + + bool IsIgnored( + const ::google::protobuf::Message& message1, + const ::google::protobuf::Message& message2, + const ::google::protobuf::FieldDescriptor* field, + const std::vector< + ::google::protobuf::util::MessageDifferencer::SpecificField>& + parent_fields) override { + // The off by one is for the current field. + if (parent_fields.size() + 1 != ignored_field_path_.size()) { + return false; + } + for (size_t i = 0; i < parent_fields.size(); ++i) { + const auto& cur_field = parent_fields[i]; + const auto& ignored_field = ignored_field_path_[i]; + // We could compare pointers but it's not guaranteed that descriptors come + // from the same pool. + if (cur_field.field->full_name() != ignored_field.field->full_name()) { + return false; + } + + // repeated_field[i] is ignored if repeated_field is ignored. To put it + // differently: if ignored_field specifies an index, we ignore only a + // field with the same index. + if (ignored_field.index != -1 && ignored_field.index != cur_field.index) { + return false; + } + } + return field->full_name() == ignored_field_path_.back().field->full_name(); + } + + private: + const std::vector + ignored_field_path_; +}; + +// Parses a field path and returns individual components. +std::vector +ParseFieldPathOrDie(const std::string& relative_field_path, + const ::google::protobuf::Descriptor& root_descriptor) { + std::vector + field_path; + + // We're parsing a dot-separated list of elements that can be either: + // - field names + // - extension names + // - indexed field names + // The parser is very permissive as to what is a field name, then we check + // the field name against the descriptor. + + // Regular parsers. Consume() does not handle optional captures so we split it + // in two regexps. + const std::regex field_regex(R"(([^.()[\]]+))"); + const std::regex field_subscript_regex(R"(([^.()[\]]+)\[(\d+)\])"); + const std::regex extension_regex(R"(\(([^)]+)\))"); + + const auto begin = std::begin(relative_field_path); + auto it = begin; + const auto end = std::end(relative_field_path); + while (it != end) { + // Consume a dot, except on the first iteration. + if (it != std::begin(relative_field_path) && *(it++) != '.') { + LOG(FATAL) << "Cannot parse field path '" << relative_field_path + << "' at offset " << std::distance(begin, it) + << ": expected '.'"; + } + // Try to consume a field name. If that fails, consume an extension name. + ::google::protobuf::util::MessageDifferencer::SpecificField field; + std::smatch match_results; + if (std::regex_search(it, end, match_results, field_subscript_regex) || + std::regex_search(it, end, match_results, field_regex)) { + std::string name = match_results[1].str(); + if (field_path.empty()) { + field.field = root_descriptor.FindFieldByName(name); + CHECK(field.field) << "No such field '" << name << "' in message '" + << root_descriptor.full_name() << "'"; + } else { + const ::google::protobuf::util::MessageDifferencer::SpecificField& + parent = field_path.back(); + field.field = parent.field->message_type()->FindFieldByName(name); + CHECK(field.field) << "No such field '" << name << "' in '" + << parent.field->full_name() << "'"; + } + if (match_results.size() > 2 && match_results[2].matched) { + std::string number = match_results[2].str(); + field.index = std::stoi(number); + } + + } else if (std::regex_search(it, end, match_results, extension_regex)) { + std::string name = match_results[1].str(); + field.field = ::google::protobuf::DescriptorPool::generated_pool() + ->FindExtensionByName(name); + CHECK(field.field) << "No such extension '" << name << "'"; + if (field_path.empty()) { + CHECK(root_descriptor.IsExtensionNumber(field.field->number())) + << "Extension '" << name << "' does not extend message '" + << root_descriptor.full_name() << "'"; + } else { + const ::google::protobuf::util::MessageDifferencer::SpecificField& + parent = field_path.back(); + CHECK(parent.field->message_type()->IsExtensionNumber( + field.field->number())) + << "Extension '" << name << "' does not extend '" + << parent.field->full_name() << "'"; + } + } else { + LOG(FATAL) << "Cannot parse field path '" << relative_field_path + << "' at offset " << std::distance(begin, it) + << ": expected field or extension"; + } + auto consume = match_results[0].length(); + it += consume; + field_path.push_back(field); + } + + CHECK(!field_path.empty()); + CHECK(field_path.back().index == -1) + << "Terminally ignoring fields by index is currently not supported ('" + << relative_field_path << "')"; + + return field_path; +} + +// Sets the ignored field paths corresponding to field_paths in differencer. +// Dies if any path is invalid. +void SetIgnoredFieldPathsOrDie( + const ::google::protobuf::Descriptor& root_descriptor, + const std::vector& field_paths, + ::google::protobuf::util::MessageDifferencer* differencer) { + for (const std::string& field_path : field_paths) { + differencer->AddIgnoreCriteria(new IgnoreFieldPathCriteria( + ParseFieldPathOrDie(field_path, root_descriptor))); + } +} + +// Configures a MessageDifferencer and DefaultFieldComparator to use the logic +// described in comp. The configured differencer is the output of this function, +// but a FieldComparator must be provided to keep ownership clear. +void ConfigureDifferencer( + const ProtoComparison& comp, + ::google::protobuf::util::DefaultFieldComparator* comparator, + ::google::protobuf::util::MessageDifferencer* differencer, + const ::google::protobuf::Descriptor* descriptor) { + differencer->set_message_field_comparison(comp.field_comp); + differencer->set_scope(comp.scope); + comparator->set_float_comparison(comp.float_comp); + comparator->set_treat_nan_as_equal(comp.treating_nan_as_equal); + differencer->set_repeated_field_comparison(comp.repeated_field_comp); + SetIgnoredFieldsOrDie(*descriptor, comp.ignore_fields, differencer); + SetIgnoredFieldPathsOrDie(*descriptor, comp.ignore_field_paths, differencer); + if (comp.float_comp == kProtoApproximate && + (comp.has_custom_margin || comp.has_custom_fraction)) { + // Two fields will be considered equal if they're within the fraction _or_ + // within the margin. So setting the fraction to 0.0 makes this effectively + // a "SetMargin". Similarly, setting the margin to 0.0 makes this + // effectively a "SetFraction". + comparator->SetDefaultFractionAndMargin(comp.float_fraction, + comp.float_margin); + } + differencer->set_field_comparator(comparator); + if (comp.differencer_config_function) { + comp.differencer_config_function(comparator, differencer); + } +} + +// Returns true iff actual and expected are comparable and match. The +// comp argument specifies how the two are compared. +bool ProtoCompare(const ProtoComparison& comp, + const ::google::protobuf::Message& actual, + const ::google::protobuf::Message& expected) { + if (!ProtoComparable(actual, expected)) return false; + + ::google::protobuf::util::MessageDifferencer differencer; + ::google::protobuf::util::DefaultFieldComparator field_comparator; + ConfigureDifferencer(comp, &field_comparator, &differencer, + actual.GetDescriptor()); + + // It's important for 'expected' to be the first argument here, as + // Compare() is not symmetric. When we do a partial comparison, + // only fields present in the first argument of Compare() are + // considered. + return differencer.Compare(expected, actual); +} + +// Describes the types of the expected and the actual protocol buffer. +std::string DescribeTypes(const ::google::protobuf::Message& expected, + const ::google::protobuf::Message& actual) { + std::ostringstream s; + s << "whose type should be " << expected.GetDescriptor()->full_name() + << " but actually is " << actual.GetDescriptor()->full_name(); + return s.str(); +} + +// Prints the protocol buffer pointed to by proto. +std::string PrintProtoPointee(const ::google::protobuf::Message* proto) { + if (proto == NULL) return ""; + return "which points to " + ::testing::PrintToString(*proto); +} + +// Describes the differences between the two protocol buffers. +std::string DescribeDiff(const ProtoComparison& comp, + const ::google::protobuf::Message& actual, + const ::google::protobuf::Message& expected) { + ::google::protobuf::util::MessageDifferencer differencer; + ::google::protobuf::util::DefaultFieldComparator field_comparator; + ConfigureDifferencer(comp, &field_comparator, &differencer, + actual.GetDescriptor()); + + std::string diff; + differencer.ReportDifferencesToString(&diff); + + // We must put 'expected' as the first argument here, as Compare() + // reports the diff in terms of how the protobuf changes from the + // first argument to the second argument. + differencer.Compare(expected, actual); + + // Removes the trailing '\n' in the diff to make the output look nicer. + if (diff.length() > 0 && *(diff.end() - 1) == '\n') { + diff.erase(diff.end() - 1); + } + + return "with the difference:\n" + diff; +} + +bool ProtoMatcherBase::MatchAndExplain( + const ::google::protobuf::Message& arg, + bool is_matcher_for_pointer, // true iff this matcher is used to match + // a protobuf pointer. + ::testing::MatchResultListener* listener) const { + if (must_be_initialized_ && !arg.IsInitialized()) { + *listener << "which isn't fully initialized"; + return false; + } + + const google::protobuf::Message* const expected = + CreateExpectedProto(arg, listener); + if (expected == NULL) return false; + + // Protobufs of different types cannot be compared. + const bool comparable = ProtoComparable(arg, *expected); + const bool match = comparable && ProtoCompare(comp(), arg, *expected); + + // Explaining the match result is expensive. We don't want to waste + // time calculating an explanation if the listener isn't interested. + if (listener->IsInterested()) { + const char* sep = ""; + if (is_matcher_for_pointer) { + *listener << PrintProtoPointee(&arg); + sep = ",\n"; + } + + if (!comparable) { + *listener << sep << DescribeTypes(*expected, arg); + } else if (!match) { + *listener << sep << DescribeDiff(comp(), arg, *expected); + } + } + + DeleteExpectedProto(expected); + return match; +} + +} // namespace internal +} // namespace testing diff --git a/ortools/base/protocol-buffer-matchers.h b/ortools/base/protocol-buffer-matchers.h index 943c9f5c3c7..7caff338adb 100644 --- a/ortools/base/protocol-buffer-matchers.h +++ b/ortools/base/protocol-buffer-matchers.h @@ -146,333 +146,32 @@ struct ProtoComparison { const bool kMustBeInitialized = true; const bool kMayBeUninitialized = false; -class StringErrorCollector : public ::google::protobuf::io::ErrorCollector { - public: - explicit StringErrorCollector(std::string* error_text) - : error_text_(error_text) {} - - void RecordError(int line, int column, absl::string_view message) override { - std::ostringstream stream; - stream << line << '(' << column << "): " << message << std::endl; - *error_text_ += stream.str(); - } - - void RecordWarning(int line, int column, absl::string_view message) override { - std::ostringstream stream; - stream << line << '(' << column << "): " << message << std::endl; - *error_text_ += stream.str(); - } - - private: - std::string* error_text_; - StringErrorCollector(const StringErrorCollector&) = delete; - StringErrorCollector& operator=(const StringErrorCollector&) = delete; -}; - // Parses the TextFormat representation of a protobuf, allowing required fields // to be missing. Returns true if successful. bool ParsePartialFromAscii(const std::string& pb_ascii, ::google::protobuf::Message* proto, - std::string* error_text) { - ::google::protobuf::TextFormat::Parser parser; - StringErrorCollector collector(error_text); - parser.RecordErrorsTo(&collector); - parser.AllowPartialMessage(true); - return parser.ParseFromString(pb_ascii, proto); -} + std::string* error_text); // Returns true iff p and q can be compared (i.e. have the same descriptor). bool ProtoComparable(const ::google::protobuf::Message& p, - const ::google::protobuf::Message& q) { - return p.GetDescriptor() == q.GetDescriptor(); -} - -template -std::string JoinStringPieces(const Container& strings, - absl::string_view separator) { - std::stringstream stream; - absl::string_view sep = ""; - for (const absl::string_view str : strings) { - stream << sep << str; - sep = separator; - } - return stream.str(); -} - -// Find all the descriptors for the ingore_fields. -std::vector GetFieldDescriptors( - const ::google::protobuf::Descriptor* proto_descriptor, - const std::vector& ignore_fields) { - std::vector ignore_descriptors; - std::vector remaining_descriptors; - - const ::google::protobuf::DescriptorPool* pool = - proto_descriptor->file()->pool(); - for (const std::string& name : ignore_fields) { - if (const ::google::protobuf::FieldDescriptor* field = - pool->FindFieldByName(name)) { - ignore_descriptors.push_back(field); - } else { - remaining_descriptors.push_back(name); - } - } - - CHECK(remaining_descriptors.empty()) - << "Could not find fields for proto " << proto_descriptor->full_name() - << " with fully qualified names: " - << JoinStringPieces(remaining_descriptors, ","); - return ignore_descriptors; -} - -// Sets the ignored fields corresponding to ignore_fields in differencer. Dies -// if any is invalid. -void SetIgnoredFieldsOrDie( - const ::google::protobuf::Descriptor& root_descriptor, - const std::vector& ignore_fields, - ::google::protobuf::util::MessageDifferencer* differencer) { - if (!ignore_fields.empty()) { - std::vector ignore_descriptors = - GetFieldDescriptors(&root_descriptor, ignore_fields); - for (std::vector::iterator it = - ignore_descriptors.begin(); - it != ignore_descriptors.end(); ++it) { - differencer->IgnoreField(*it); - } - } -} - -// A criterion that ignores a field path. -class IgnoreFieldPathCriteria - : public ::google::protobuf::util::MessageDifferencer::IgnoreCriteria { - public: - explicit IgnoreFieldPathCriteria( - const std::vector< - ::google::protobuf::util::MessageDifferencer::SpecificField>& - field_path) - : ignored_field_path_(field_path) {} - - bool IsIgnored( - const ::google::protobuf::Message& message1, - const ::google::protobuf::Message& message2, - const ::google::protobuf::FieldDescriptor* field, - const std::vector< - ::google::protobuf::util::MessageDifferencer::SpecificField>& - parent_fields) override { - // The off by one is for the current field. - if (parent_fields.size() + 1 != ignored_field_path_.size()) { - return false; - } - for (size_t i = 0; i < parent_fields.size(); ++i) { - const auto& cur_field = parent_fields[i]; - const auto& ignored_field = ignored_field_path_[i]; - // We could compare pointers but it's not guaranteed that descriptors come - // from the same pool. - if (cur_field.field->full_name() != ignored_field.field->full_name()) { - return false; - } - - // repeated_field[i] is ignored if repeated_field is ignored. To put it - // differently: if ignored_field specifies an index, we ignore only a - // field with the same index. - if (ignored_field.index != -1 && ignored_field.index != cur_field.index) { - return false; - } - } - return field->full_name() == ignored_field_path_.back().field->full_name(); - } - - private: - const std::vector - ignored_field_path_; -}; - -// Parses a field path and returns individual components. -std::vector -ParseFieldPathOrDie(const std::string& relative_field_path, - const ::google::protobuf::Descriptor& root_descriptor) { - std::vector - field_path; - - // We're parsing a dot-separated list of elements that can be either: - // - field names - // - extension names - // - indexed field names - // The parser is very permissive as to what is a field name, then we check - // the field name against the descriptor. - - // Regular parsers. Consume() does not handle optional captures so we split it - // in two regexps. - const std::regex field_regex(R"(([^.()[\]]+))"); - const std::regex field_subscript_regex(R"(([^.()[\]]+)\[(\d+)\])"); - const std::regex extension_regex(R"(\(([^)]+)\))"); - - const auto begin = std::begin(relative_field_path); - auto it = begin; - const auto end = std::end(relative_field_path); - while (it != end) { - // Consume a dot, except on the first iteration. - if (it != std::begin(relative_field_path) && *(it++) != '.') { - LOG(FATAL) << "Cannot parse field path '" << relative_field_path - << "' at offset " << std::distance(begin, it) - << ": expected '.'"; - } - // Try to consume a field name. If that fails, consume an extension name. - ::google::protobuf::util::MessageDifferencer::SpecificField field; - std::smatch match_results; - if (std::regex_search(it, end, match_results, field_subscript_regex) || - std::regex_search(it, end, match_results, field_regex)) { - std::string name = match_results[1].str(); - if (field_path.empty()) { - field.field = root_descriptor.FindFieldByName(name); - CHECK(field.field) << "No such field '" << name << "' in message '" - << root_descriptor.full_name() << "'"; - } else { - const ::google::protobuf::util::MessageDifferencer::SpecificField& - parent = field_path.back(); - field.field = parent.field->message_type()->FindFieldByName(name); - CHECK(field.field) << "No such field '" << name << "' in '" - << parent.field->full_name() << "'"; - } - if (match_results.size() > 2 && match_results[2].matched) { - std::string number = match_results[2].str(); - field.index = std::stoi(number); - } - - } else if (std::regex_search(it, end, match_results, extension_regex)) { - std::string name = match_results[1].str(); - field.field = ::google::protobuf::DescriptorPool::generated_pool() - ->FindExtensionByName(name); - CHECK(field.field) << "No such extension '" << name << "'"; - if (field_path.empty()) { - CHECK(root_descriptor.IsExtensionNumber(field.field->number())) - << "Extension '" << name << "' does not extend message '" - << root_descriptor.full_name() << "'"; - } else { - const ::google::protobuf::util::MessageDifferencer::SpecificField& - parent = field_path.back(); - CHECK(parent.field->message_type()->IsExtensionNumber( - field.field->number())) - << "Extension '" << name << "' does not extend '" - << parent.field->full_name() << "'"; - } - } else { - LOG(FATAL) << "Cannot parse field path '" << relative_field_path - << "' at offset " << std::distance(begin, it) - << ": expected field or extension"; - } - auto consume = match_results[0].length(); - it += consume; - field_path.push_back(field); - } - - CHECK(!field_path.empty()); - CHECK(field_path.back().index == -1) - << "Terminally ignoring fields by index is currently not supported ('" - << relative_field_path << "')"; - - return field_path; -} - -// Sets the ignored field paths corresponding to field_paths in differencer. -// Dies if any path is invalid. -void SetIgnoredFieldPathsOrDie( - const ::google::protobuf::Descriptor& root_descriptor, - const std::vector& field_paths, - ::google::protobuf::util::MessageDifferencer* differencer) { - for (const std::string& field_path : field_paths) { - differencer->AddIgnoreCriteria(new IgnoreFieldPathCriteria( - ParseFieldPathOrDie(field_path, root_descriptor))); - } -} - -// Configures a MessageDifferencer and DefaultFieldComparator to use the logic -// described in comp. The configured differencer is the output of this function, -// but a FieldComparator must be provided to keep ownership clear. -void ConfigureDifferencer( - const ProtoComparison& comp, - ::google::protobuf::util::DefaultFieldComparator* comparator, - ::google::protobuf::util::MessageDifferencer* differencer, - const ::google::protobuf::Descriptor* descriptor) { - differencer->set_message_field_comparison(comp.field_comp); - differencer->set_scope(comp.scope); - comparator->set_float_comparison(comp.float_comp); - comparator->set_treat_nan_as_equal(comp.treating_nan_as_equal); - differencer->set_repeated_field_comparison(comp.repeated_field_comp); - SetIgnoredFieldsOrDie(*descriptor, comp.ignore_fields, differencer); - SetIgnoredFieldPathsOrDie(*descriptor, comp.ignore_field_paths, differencer); - if (comp.float_comp == kProtoApproximate && - (comp.has_custom_margin || comp.has_custom_fraction)) { - // Two fields will be considered equal if they're within the fraction _or_ - // within the margin. So setting the fraction to 0.0 makes this effectively - // a "SetMargin". Similarly, setting the margin to 0.0 makes this - // effectively a "SetFraction". - comparator->SetDefaultFractionAndMargin(comp.float_fraction, - comp.float_margin); - } - differencer->set_field_comparator(comparator); - if (comp.differencer_config_function) { - comp.differencer_config_function(comparator, differencer); - } -} + const ::google::protobuf::Message& q); // Returns true iff actual and expected are comparable and match. The // comp argument specifies how the two are compared. bool ProtoCompare(const ProtoComparison& comp, const ::google::protobuf::Message& actual, - const ::google::protobuf::Message& expected) { - if (!ProtoComparable(actual, expected)) return false; - - ::google::protobuf::util::MessageDifferencer differencer; - ::google::protobuf::util::DefaultFieldComparator field_comparator; - ConfigureDifferencer(comp, &field_comparator, &differencer, - actual.GetDescriptor()); - - // It's important for 'expected' to be the first argument here, as - // Compare() is not symmetric. When we do a partial comparison, - // only fields present in the first argument of Compare() are - // considered. - return differencer.Compare(expected, actual); -} + const ::google::protobuf::Message& expected); // Describes the types of the expected and the actual protocol buffer. std::string DescribeTypes(const ::google::protobuf::Message& expected, - const ::google::protobuf::Message& actual) { - std::ostringstream s; - s << "whose type should be " << expected.GetDescriptor()->full_name() - << " but actually is " << actual.GetDescriptor()->full_name(); - return s.str(); -} - + const ::google::protobuf::Message& actual); // Prints the protocol buffer pointed to by proto. -std::string PrintProtoPointee(const ::google::protobuf::Message* proto) { - if (proto == NULL) return ""; - return "which points to " + ::testing::PrintToString(*proto); -} +std::string PrintProtoPointee(const ::google::protobuf::Message* proto); // Describes the differences between the two protocol buffers. std::string DescribeDiff(const ProtoComparison& comp, const ::google::protobuf::Message& actual, - const ::google::protobuf::Message& expected) { - ::google::protobuf::util::MessageDifferencer differencer; - ::google::protobuf::util::DefaultFieldComparator field_comparator; - ConfigureDifferencer(comp, &field_comparator, &differencer, - actual.GetDescriptor()); - - std::string diff; - differencer.ReportDifferencesToString(&diff); - - // We must put 'expected' as the first argument here, as Compare() - // reports the diff in terms of how the protobuf changes from the - // first argument to the second argument. - differencer.Compare(expected, actual); - - // Removes the trailing '\n' in the diff to make the output look nicer. - if (diff.length() > 0 && *(diff.end() - 1) == '\n') { - diff.erase(diff.end() - 1); - } - - return "with the difference:\n" + diff; -} + const ::google::protobuf::Message& expected); // Common code for implementing EqualsProto and EquivToProto. class ProtoMatcherBase { @@ -581,42 +280,9 @@ class ProtoMatcherBase { const ProtoComparison& comp() const { return *comp_; } private: - bool MatchAndExplain( - const ::google::protobuf::Message& arg, - bool is_matcher_for_pointer, // true iff this matcher is used to match a protobuf pointer. - ::testing::MatchResultListener* listener) const { - if (must_be_initialized_ && !arg.IsInitialized()) { - *listener << "which isn't fully initialized"; - return false; - } - - const ::google::protobuf::Message* const expected = - CreateExpectedProto(arg, listener); - if (expected == NULL) return false; - - // Protobufs of different types cannot be compared. - const bool comparable = ProtoComparable(arg, *expected); - const bool match = comparable && ProtoCompare(comp(), arg, *expected); - - // Explaining the match result is expensive. We don't want to waste - // time calculating an explanation if the listener isn't interested. - if (listener->IsInterested()) { - const char* sep = ""; - if (is_matcher_for_pointer) { - *listener << PrintProtoPointee(&arg); - sep = ",\n"; - } - - if (!comparable) { - *listener << sep << DescribeTypes(*expected, arg); - } else if (!match) { - *listener << sep << DescribeDiff(comp(), arg, *expected); - } - } - - DeleteExpectedProto(expected); - return match; - } + bool MatchAndExplain(const ::google::protobuf::Message& arg, + bool is_matcher_for_pointer, + ::testing::MatchResultListener* listener) const; const bool must_be_initialized_; std::unique_ptr comp_; diff --git a/ortools/math_opt/cpp/CMakeLists.txt b/ortools/math_opt/cpp/CMakeLists.txt index ba86195715f..8dbf3aa9184 100644 --- a/ortools/math_opt/cpp/CMakeLists.txt +++ b/ortools/math_opt/cpp/CMakeLists.txt @@ -36,9 +36,9 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES + ortools::base_gmock absl::log absl::status absl::strings - GTest::gmock TESTING ) diff --git a/ortools/math_opt/elemental/CMakeLists.txt b/ortools/math_opt/elemental/CMakeLists.txt index 8656b602548..c1fe79df97f 100644 --- a/ortools/math_opt/elemental/CMakeLists.txt +++ b/ortools/math_opt/elemental/CMakeLists.txt @@ -61,7 +61,7 @@ if(BUILD_TESTING) ${_FILE_NAME} LINK_LIBRARIES benchmark::benchmark - GTest::gmock + ortools::base_gmock GTest::gtest_main ortools::math_opt_matchers ortools::math_opt_elemental_matcher diff --git a/ortools/math_opt/solver_tests/CMakeLists.txt b/ortools/math_opt/solver_tests/CMakeLists.txt index 5bc081ca769..7e0fdfdb00d 100644 --- a/ortools/math_opt/solver_tests/CMakeLists.txt +++ b/ortools/math_opt/solver_tests/CMakeLists.txt @@ -22,7 +22,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log TESTING ) @@ -36,7 +36,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log absl::status absl::strings @@ -56,7 +56,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_test_models @@ -72,7 +72,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_base_solver_test @@ -88,7 +88,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_test_models @@ -104,7 +104,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_base_solver_test TESTING @@ -118,7 +118,7 @@ ortools_cxx_library( # SOURCES # "unregistered_solver_test.cc" # LINK_LIBRARIES -# GTest::gmock +# ortools::base_gmock # GTest::gmock_main #) @@ -131,7 +131,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log absl::status ortools::math_opt_matchers @@ -148,7 +148,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_base_solver_test @@ -164,7 +164,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::strings ortools::math_opt_matchers TESTING @@ -179,7 +179,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_base_solver_test @@ -195,7 +195,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log absl::status absl::strings @@ -212,7 +212,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log absl::status ortools::math_opt_base_solver_test @@ -228,7 +228,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_test_models @@ -244,7 +244,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers TESTING @@ -259,7 +259,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers TESTING @@ -274,7 +274,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers TESTING @@ -289,7 +289,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers TESTING @@ -304,7 +304,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers TESTING @@ -331,7 +331,7 @@ ortools_cxx_test( SOURCES "test_models_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main ortools::math_opt_test_models ortools::math_opt_matchers @@ -347,7 +347,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log ortools::math_opt_matchers ortools::math_opt_test_models @@ -363,7 +363,7 @@ ortools_cxx_library( TYPE STATIC LINK_LIBRARIES - GTest::gmock + ortools::base_gmock absl::log absl::status absl::strings diff --git a/ortools/math_opt/solvers/CMakeLists.txt b/ortools/math_opt/solvers/CMakeLists.txt index be2c08b6fd1..f945b9a6e73 100644 --- a/ortools/math_opt/solvers/CMakeLists.txt +++ b/ortools/math_opt/solvers/CMakeLists.txt @@ -69,7 +69,7 @@ if(USE_SCIP) SOURCES "gscip_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status ortools::math_opt_matchers @@ -97,7 +97,7 @@ if(USE_GLOP) SOURCES "glop_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status ortools::math_opt_matchers @@ -125,7 +125,7 @@ ortools_cxx_test( SOURCES "cp_sat_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status ortools::math_opt_matchers @@ -163,7 +163,7 @@ if(USE_PDLP) SOURCES "pdlp_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status "$" @@ -191,7 +191,7 @@ if(USE_GLPK) SOURCES "glpk_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status absl::time @@ -223,7 +223,7 @@ if(USE_HIGHS) SOURCES "highs_solver_test.cc" LINK_LIBRARIES - GTest::gmock + ortools::base_gmock GTest::gmock_main absl::status ortools::math_opt_matchers @@ -245,30 +245,31 @@ endif() if(USE_XPRESS) ortools_cxx_test( - NAME - math_opt_solvers_xpress_solver_test - SOURCES - "xpress_solver_test.cc" - LINK_LIBRARIES - GTest::gtest - absl::status - ortools::math_opt_matchers - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" - "$" + NAME + math_opt_solvers_xpress_solver_test + SOURCES + "xpress_solver_test.cc" + LINK_LIBRARIES + ortools::base_gmock + GTest::gmock_main + absl::status + ortools::math_opt_matchers + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" ) endif() From a0a16117a3c85066ab2c950671ce726748707b13 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Thu, 4 Sep 2025 15:31:00 +0000 Subject: [PATCH 037/491] rework java_wrap_cc --- bazel/swig_java.bzl | 28 ++++++----- ortools/algorithms/java/BUILD.bazel | 8 ++-- ortools/graph/java/BUILD.bazel | 6 +-- ortools/init/java/BUILD.bazel | 8 ++-- ortools/java/com/google/ortools/BUILD.bazel | 1 + .../google/ortools/modelbuilder/BUILD.bazel | 2 + .../java/com/google/ortools/sat/BUILD.bazel | 2 + ortools/linear_solver/java/BUILD.bazel | 8 ++-- ortools/sat/java/BUILD.bazel | 14 +++--- ortools/util/java/BUILD.bazel | 48 ++++++------------- 10 files changed, 59 insertions(+), 66 deletions(-) diff --git a/bazel/swig_java.bzl b/bazel/swig_java.bzl index 17439585e37..e320982c81c 100644 --- a/bazel/swig_java.bzl +++ b/bazel/swig_java.bzl @@ -15,6 +15,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") load("@rules_java//java:java_library.bzl", "java_library") load("@rules_java//java/common:java_common.bzl", "java_common") @@ -33,7 +34,9 @@ def _create_src_jar(ctx, java_runtime_info, input_dir, output_jar): ) def _java_wrap_cc_impl(ctx): - src = ctx.file.src + if len(ctx.files.srcs) != 1: + fail("There must be exactly one *.swig file", attr = "srcs") + swig_src = ctx.files.srcs[0] outfile = ctx.outputs.outfile outhdr = ctx.outputs.outhdr @@ -65,7 +68,7 @@ def _java_wrap_cc_impl(ctx): swig_args.add("-module", ctx.attr.module) for include_path in depset(transitive = include_path_sets).to_list(): swig_args.add("-I" + include_path) - swig_args.add(src.path) + swig_args.add(swig_src.path) generated_c_files = [outfile] if ctx.attr.use_directors: generated_c_files.append(outhdr) @@ -74,7 +77,7 @@ def _java_wrap_cc_impl(ctx): swig_lib = {"SWIG_LIB": paths.dirname(ctx.files._swig_lib[0].path)} ctx.actions.run( outputs = generated_c_files + [java_files_dir], - inputs = depset([src] + ctx.files.swig_includes + ctx.files._swig_lib, transitive = header_sets), + inputs = depset([swig_src] + ctx.files.swig_includes + ctx.files._swig_lib, transitive = header_sets), env = swig_lib, executable = ctx.executable._swig, arguments = [swig_args], @@ -92,10 +95,13 @@ It's expected that the `swig` binary exists in the host's path. """, implementation = _java_wrap_cc_impl, attrs = { - "src": attr.label( - doc = "Single swig source file.", - allow_single_file = True, - mandatory = True, + "srcs": attr.label_list( + allow_empty = False, + allow_files = [".swig", ".i"], + flags = ["DIRECT_COMPILE_TIME_INPUT", "ORDER_INDEPENDENT"], + doc = """ +A list of one swig source. + """, ), "deps": attr.label_list( doc = "C++ dependencies.", @@ -141,9 +147,9 @@ It's expected that the `swig` binary exists in the host's path. }, ) -def ortools_java_wrap_cc( +def java_wrap_cc( name, - src, + srcs, package, deps = [], java_deps = [], @@ -159,7 +165,7 @@ def ortools_java_wrap_cc( Args: name: target name. - src: single .i source file. + srcs: A list of one swig source. package: package of generated Java files. deps: C++ deps. java_deps: Java deps. @@ -183,7 +189,7 @@ def ortools_java_wrap_cc( _java_wrap_cc( name = wrapper_name, - src = src, + srcs = srcs, package = package, outfile = outfile, outhdr = outhdr if use_directors else None, diff --git a/ortools/algorithms/java/BUILD.bazel b/ortools/algorithms/java/BUILD.bazel index 5b84c09b2f7..273c2e6302f 100644 --- a/ortools/algorithms/java/BUILD.bazel +++ b/ortools/algorithms/java/BUILD.bazel @@ -13,16 +13,16 @@ # Description: java wrapping of the C++ code at ../ -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -ortools_java_wrap_cc( +java_wrap_cc( name = "knapsacksolver", - src = "knapsack_solver.swig", + srcs = ["knapsack_solver.swig"], module = "operations_research_algorithms", package = "com.google.ortools.algorithms", swig_includes = [ "//ortools/base:base_swig", - "//ortools/util/java:vector_swig", + "//ortools/util/java:vector.swig", ], visibility = ["//visibility:public"], deps = [ diff --git a/ortools/graph/java/BUILD.bazel b/ortools/graph/java/BUILD.bazel index 4818a860925..cf26da752ce 100644 --- a/ortools/graph/java/BUILD.bazel +++ b/ortools/graph/java/BUILD.bazel @@ -15,11 +15,11 @@ load("@contrib_rules_jvm//java:defs.bzl", "java_junit5_test") load("@rules_jvm_external//:defs.bzl", "artifact") -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -ortools_java_wrap_cc( +java_wrap_cc( name = "graph", - src = "graph.swig", + srcs = ["graph.swig"], module = "graph", package = "com.google.ortools.graph", swig_includes = [ diff --git a/ortools/init/java/BUILD.bazel b/ortools/init/java/BUILD.bazel index fad473f8acd..b1194417a9b 100644 --- a/ortools/init/java/BUILD.bazel +++ b/ortools/init/java/BUILD.bazel @@ -13,16 +13,16 @@ # Description: java wrapping of the C++ code at ../ -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -ortools_java_wrap_cc( +java_wrap_cc( name = "init", - src = "init.i", + srcs = ["init.i"], module = "operations_research_init", package = "com.google.ortools.init", swig_includes = [ "//ortools/base:base_swig", - "//ortools/util/java:absl_string_view_swig", + "//ortools/util/java:absl_string_view.swig", ], visibility = ["//visibility:public"], deps = [ diff --git a/ortools/java/com/google/ortools/BUILD.bazel b/ortools/java/com/google/ortools/BUILD.bazel index 188af3bce3f..4153265e462 100644 --- a/ortools/java/com/google/ortools/BUILD.bazel +++ b/ortools/java/com/google/ortools/BUILD.bazel @@ -12,6 +12,7 @@ # limitations under the License. load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_java//java:java_library.bzl", "java_library") # Utilities to load native libraries in java or-tools. cc_binary( diff --git a/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel b/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel index 27065e7aa03..a3bf9a04b77 100644 --- a/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel +++ b/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@rules_java//java:java_library.bzl", "java_library") + package(default_visibility = ["//visibility:public"]) # Utilities to load native libraries in java or-tools. diff --git a/ortools/java/com/google/ortools/sat/BUILD.bazel b/ortools/java/com/google/ortools/sat/BUILD.bazel index 9f3f6f13bcc..e147f70d4af 100644 --- a/ortools/java/com/google/ortools/sat/BUILD.bazel +++ b/ortools/java/com/google/ortools/sat/BUILD.bazel @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@rules_java//java:java_library.bzl", "java_library") + package(default_visibility = ["//visibility:public"]) # Utilities to load native libraries in java or-tools. diff --git a/ortools/linear_solver/java/BUILD.bazel b/ortools/linear_solver/java/BUILD.bazel index 0cf3c002268..ff9a9a8247e 100644 --- a/ortools/linear_solver/java/BUILD.bazel +++ b/ortools/linear_solver/java/BUILD.bazel @@ -15,16 +15,16 @@ load("@contrib_rules_jvm//java:defs.bzl", "java_junit5_test") load("@rules_jvm_external//:defs.bzl", "artifact") -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -ortools_java_wrap_cc( +java_wrap_cc( name = "modelbuilder", - src = "modelbuilder.swig", + srcs = ["modelbuilder.swig"], module = "modelbuilder", package = "com.google.ortools.modelbuilder", swig_includes = [ "//ortools/base:base_swig", - "//ortools/util/java:vector_swig", + "//ortools/util/java:vector.swig", ], visibility = ["//visibility:public"], deps = [ diff --git a/ortools/sat/java/BUILD.bazel b/ortools/sat/java/BUILD.bazel index 76bc1a8521b..6641a8befb2 100644 --- a/ortools/sat/java/BUILD.bazel +++ b/ortools/sat/java/BUILD.bazel @@ -15,23 +15,23 @@ load("@contrib_rules_jvm//java:defs.bzl", "java_junit5_test") load("@rules_jvm_external//:defs.bzl", "artifact") -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -ortools_java_wrap_cc( +java_wrap_cc( name = "sat", - src = "sat.swig", + srcs = ["sat.swig"], java_deps = [ "//ortools/sat:cp_model_java_proto", "//ortools/sat:sat_parameters_java_proto", - "//ortools/util/java:sorted_interval_list", + "//ortools/util/java:sorted_interval_list_swig", "@protobuf//java/core", ], package = "com.google.ortools.sat", swig_includes = [ "//ortools/base:base_swig", - "//ortools/util/java:proto_swig", - "//ortools/util/java:sorted_interval_list_swig", - "//ortools/util/java:vector_swig", + "//ortools/util/java:proto.i", + "//ortools/util/java:sorted_interval_list.swig", + "//ortools/util/java:vector.swig", ], use_directors = True, visibility = ["//visibility:public"], diff --git a/ortools/util/java/BUILD.bazel b/ortools/util/java/BUILD.bazel index 799eaffd898..0735f25937e 100644 --- a/ortools/util/java/BUILD.bazel +++ b/ortools/util/java/BUILD.bazel @@ -11,48 +11,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Description: java wrapping of the code in ../ +# Description: java wrapping of (some of) the code in ../, plus +# some generic utilities for SWIG java. -load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("//bazel:swig_java.bzl", "java_wrap_cc") -filegroup( - name = "vector_swig", - srcs = [ - "vector.swig", - ], - visibility = ["//visibility:public"], -) - -filegroup( - name = "proto_swig", - srcs = [ - "proto.i", - ], - visibility = ["//visibility:public"], -) - -filegroup( - name = "absl_string_view_swig", - srcs = [ +# These .swig files are currently just included in other .swig files. +# TODO(user): Create dedicated java_wrap_cc rules and unit tests. +exports_files( + [ "absl_string_view.swig", - ], - visibility = ["//visibility:public"], -) - -filegroup( - name = "sorted_interval_list_swig", - srcs = [ + "proto.i", "sorted_interval_list.swig", + "tuple_set.swig", + "vector.swig", ], - visibility = ["//visibility:public"], ) -ortools_java_wrap_cc( - name = "sorted_interval_list", - src = "sorted_interval_list.swig", +java_wrap_cc( + name = "sorted_interval_list_swig", + srcs = ["sorted_interval_list.swig"], package = "com.google.ortools.util", swig_includes = [ - ":vector_swig", + ":vector.swig", "//ortools/base:base_swig", ], swig_opt = select({ @@ -61,6 +42,7 @@ ortools_java_wrap_cc( }), visibility = ["//visibility:public"], deps = [ + "//ortools/base", "//ortools/util:sorted_interval_list", ], ) From 95b4b062ed7ce08ff70f75126a3d08d14099b58f Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Thu, 4 Sep 2025 21:28:43 +0000 Subject: [PATCH 038/491] [bazel] Fix visibility and target name mismatch --- .../google/ortools/modelbuilder/BUILD.bazel | 3 +- .../java/com/google/ortools/sat/BUILD.bazel | 3 +- ortools/math_opt/solvers/BUILD.bazel | 1 - ortools/math_opt/testing/BUILD.bazel | 2 +- ortools/math_opt/validators/BUILD.bazel | 2 +- ortools/routing/BUILD.bazel | 20 +++++++++-- ortools/sat/BUILD.bazel | 33 ++++--------------- ortools/sat/java/BUILD.bazel | 2 +- ortools/set_cover/BUILD.bazel | 2 ++ ortools/util/java/BUILD.bazel | 2 +- ortools/util/python/BUILD.bazel | 3 +- 11 files changed, 35 insertions(+), 38 deletions(-) diff --git a/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel b/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel index a3bf9a04b77..d256b1e8a42 100644 --- a/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel +++ b/ortools/java/com/google/ortools/modelbuilder/BUILD.bazel @@ -15,7 +15,7 @@ load("@rules_java//java:java_library.bzl", "java_library") package(default_visibility = ["//visibility:public"]) -# Utilities to load native libraries in java or-tools. +# Additional dependency to use when using the wrapped model_builder library. java_library( name = "modelbuilder", srcs = [ @@ -31,7 +31,6 @@ java_library( "Variable.java", "WeightedSumExpression.java", ], - visibility = ["//visibility:public"], deps = [ "//ortools/java/com/google/ortools:Loader", "//ortools/linear_solver/java:modelbuilder", diff --git a/ortools/java/com/google/ortools/sat/BUILD.bazel b/ortools/java/com/google/ortools/sat/BUILD.bazel index e147f70d4af..ebfb8fc2b86 100644 --- a/ortools/java/com/google/ortools/sat/BUILD.bazel +++ b/ortools/java/com/google/ortools/sat/BUILD.bazel @@ -15,7 +15,7 @@ load("@rules_java//java:java_library.bzl", "java_library") package(default_visibility = ["//visibility:public"]) -# Utilities to load native libraries in java or-tools. +# Additional dependency to use when using the wrapped sat solver library. java_library( name = "sat", srcs = [ @@ -43,7 +43,6 @@ java_library( "TableConstraint.java", "WeightedSumExpression.java", ], - visibility = ["//visibility:public"], deps = [ "//ortools/java/com/google/ortools:Loader", "//ortools/sat:cp_model_java_proto", diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index dd709bb88b9..28a3065cd46 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -394,7 +394,6 @@ cc_library( "pdlp_solver.cc", "pdlp_solver.h", ], - visibility = ["//visibility:public"], deps = [ ":pdlp_bridge", "//ortools/base:protoutil", diff --git a/ortools/math_opt/testing/BUILD.bazel b/ortools/math_opt/testing/BUILD.bazel index dc2bfcb7bbc..ec32eaf17b0 100644 --- a/ortools/math_opt/testing/BUILD.bazel +++ b/ortools/math_opt/testing/BUILD.bazel @@ -13,7 +13,7 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") -package(default_visibility = ["//ortools:__subpackages__"]) +package(default_visibility = ["//visibility:public"]) cc_library( name = "param_name", diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index f9b4cb4ac4e..0e6e5f40008 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -13,7 +13,7 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") -package(default_visibility = ["//ortools:__subpackages__"]) +package(default_visibility = ["//visibility:public"]) cc_library( name = "ids_validator", diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel index 760ae652f13..2db48f86de8 100644 --- a/ortools/routing/BUILD.bazel +++ b/ortools/routing/BUILD.bazel @@ -17,26 +17,28 @@ load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") -package(default_visibility = ["//visibility:public"]) - proto_library( name = "enums_proto", srcs = ["enums.proto"], + visibility = ["//visibility:public"], ) cc_proto_library( name = "enums_cc_proto", + visibility = ["//visibility:public"], deps = [":enums_proto"], ) java_proto_library( name = "enums_java_proto", + visibility = ["//visibility:public"], deps = [":enums_proto"], ) proto_library( name = "ils_proto", srcs = ["ils.proto"], + visibility = ["//visibility:public"], deps = [ ":enums_proto", ":heuristic_parameters_proto", @@ -45,6 +47,7 @@ proto_library( cc_proto_library( name = "ils_cc_proto", + visibility = ["//visibility:public"], deps = [":ils_proto"], ) @@ -55,12 +58,14 @@ py_proto_library( java_proto_library( name = "ils_java_proto", + visibility = ["//visibility:public"], deps = [":ils_proto"], ) proto_library( name = "parameters_proto", srcs = ["parameters.proto"], + visibility = ["//visibility:public"], deps = [ ":enums_proto", ":heuristic_parameters_proto", @@ -74,21 +79,25 @@ proto_library( cc_proto_library( name = "parameters_cc_proto", + visibility = ["//visibility:public"], deps = [":parameters_proto"], ) java_proto_library( name = "parameters_java_proto", + visibility = ["//visibility:public"], deps = [":parameters_proto"], ) py_proto_library( name = "parameters_py_pb2", + visibility = ["//visibility:public"], deps = [":parameters_proto"], ) py_proto_library( name = "enums_py_pb2", + visibility = ["//visibility:public"], deps = [":enums_proto"], ) @@ -96,6 +105,7 @@ cc_library( name = "parameters", srcs = ["parameters.cc"], hdrs = ["parameters.h"], + visibility = ["//visibility:public"], deps = [ ":enums_cc_proto", ":heuristic_parameters_cc_proto", @@ -124,6 +134,7 @@ cc_library( name = "parameters_utils", srcs = ["parameters_utils.cc"], hdrs = ["parameters_utils.h"], + visibility = ["//visibility:public"], deps = [ ":heuristic_parameters_cc_proto", ":parameters_cc_proto", @@ -135,6 +146,7 @@ cc_library( cc_library( name = "types", hdrs = ["types.h"], + visibility = ["//visibility:public"], deps = [ "//ortools/util:piecewise_linear_function", "//ortools/util:strong_integers", @@ -145,6 +157,7 @@ cc_library( name = "utils", srcs = ["utils.cc"], hdrs = ["utils.h"], + visibility = ["//visibility:public"], deps = [ "//ortools/util:saturated_arithmetic", "@abseil-cpp//absl/log:check", @@ -156,6 +169,7 @@ cc_library( name = "neighborhoods", srcs = ["neighborhoods.cc"], hdrs = ["neighborhoods.h"], + visibility = ["//visibility:public"], deps = [ ":types", ":utils", @@ -172,6 +186,7 @@ cc_library( name = "index_manager", srcs = ["index_manager.cc"], hdrs = ["index_manager.h"], + visibility = ["//visibility:public"], deps = [ ":types", "//ortools/base", @@ -224,6 +239,7 @@ cc_library( "@platforms//os:windows": ["/Zc:preprocessor"], "//conditions:default": [], }), + visibility = ["//visibility:public"], deps = [ ":breaks", ":enums_cc_proto", diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index 1c4c778779f..dc2decdce2a 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -965,9 +965,8 @@ proto_library( cc_proto_library( name = "boolean_problem_cc_proto", visibility = [ - "//ops/netarch/wand/transport:__subpackages__", - "//ortools:__subpackages__", "//ortools/bop:__pkg__", + "//visibility:public", ], deps = [":boolean_problem_proto"], ) @@ -1816,10 +1815,7 @@ cc_library( name = "integer_base", srcs = ["integer_base.cc"], hdrs = ["integer_base.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":sat_base", "//ortools/base", @@ -1850,10 +1846,7 @@ cc_library( name = "integer", srcs = ["integer.cc"], hdrs = ["integer.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":integer_base", ":model", @@ -2131,10 +2124,7 @@ cc_library( name = "precedences", srcs = ["precedences.cc"], hdrs = ["precedences.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":clause", ":cp_constraints", @@ -2214,10 +2204,7 @@ cc_library( name = "integer_expr", srcs = ["integer_expr.cc"], hdrs = ["integer_expr.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":cp_constraints", ":integer", @@ -2701,10 +2688,7 @@ cc_library( name = "linear_constraint", srcs = ["linear_constraint.cc"], hdrs = ["linear_constraint.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":integer", ":integer_base", @@ -3375,10 +3359,7 @@ cc_library( name = "cp_constraints", srcs = ["cp_constraints.cc"], hdrs = ["cp_constraints.h"], - visibility = [ - "//learning/brain/experimental/telamon:__pkg__", - "//ortools:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [ ":integer", ":integer_base", diff --git a/ortools/sat/java/BUILD.bazel b/ortools/sat/java/BUILD.bazel index 6641a8befb2..3df97a7c364 100644 --- a/ortools/sat/java/BUILD.bazel +++ b/ortools/sat/java/BUILD.bazel @@ -23,7 +23,7 @@ java_wrap_cc( java_deps = [ "//ortools/sat:cp_model_java_proto", "//ortools/sat:sat_parameters_java_proto", - "//ortools/util/java:sorted_interval_list_swig", + "//ortools/util/java:sorted_interval_list", "@protobuf//java/core", ], package = "com.google.ortools.sat", diff --git a/ortools/set_cover/BUILD.bazel b/ortools/set_cover/BUILD.bazel index 3e936cfd331..8951f0fb786 100644 --- a/ortools/set_cover/BUILD.bazel +++ b/ortools/set_cover/BUILD.bazel @@ -38,6 +38,7 @@ cc_proto_library( py_proto_library( name = "set_cover_py_pb2", + visibility = ["//visibility:public"], deps = [":set_cover_proto"], ) @@ -261,6 +262,7 @@ cc_proto_library( py_proto_library( name = "capacity_py_pb2", + visibility = ["//visibility:public"], deps = [":capacity_proto"], ) diff --git a/ortools/util/java/BUILD.bazel b/ortools/util/java/BUILD.bazel index 0735f25937e..b0c2818d0cd 100644 --- a/ortools/util/java/BUILD.bazel +++ b/ortools/util/java/BUILD.bazel @@ -29,7 +29,7 @@ exports_files( ) java_wrap_cc( - name = "sorted_interval_list_swig", + name = "sorted_interval_list", srcs = ["sorted_interval_list.swig"], package = "com.google.ortools.util", swig_includes = [ diff --git a/ortools/util/python/BUILD.bazel b/ortools/util/python/BUILD.bazel index bc2d226875e..1f78f70ae7f 100644 --- a/ortools/util/python/BUILD.bazel +++ b/ortools/util/python/BUILD.bazel @@ -11,7 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Python wrapper for sorted_interval_list. +# Description: python wrapping of (some of) the code in ../, plus +# some generic utilities for SWIG python. load("@pip_deps//:requirements.bzl", "requirement") load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") From 2531d23ed4c6ad7f853d809b265638cff9bf39be Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Thu, 4 Sep 2025 22:02:09 +0000 Subject: [PATCH 039/491] [bazel] configure CI options in .bazelrc instead of Dockerfile --- .bazelrc | 37 +++++++++++++++++++++++++----- bazel/docker/almalinux/Dockerfile | 10 ++------ bazel/docker/alpine/Dockerfile | 10 ++------ bazel/docker/archlinux/Dockerfile | 10 ++------ bazel/docker/debian/Dockerfile | 10 ++------ bazel/docker/fedora/Dockerfile | 10 ++------ bazel/docker/opensuse/Dockerfile | 10 ++------ bazel/docker/rockylinux/Dockerfile | 10 ++------ bazel/docker/ubuntu/Dockerfile | 10 ++------ 9 files changed, 47 insertions(+), 70 deletions(-) diff --git a/.bazelrc b/.bazelrc index 37af3f95a27..f5b47ccc066 100644 --- a/.bazelrc +++ b/.bazelrc @@ -6,13 +6,12 @@ build --flag_alias=with_bop=//ortools/linear_solver:with_bop build --flag_alias=with_cbc=//ortools/linear_solver:with_cbc build --flag_alias=with_clp=//ortools/linear_solver:with_clp build --flag_alias=with_cp_sat=//ortools/linear_solver:with_cp_sat +build --flag_alias=with_cplex=//ortools/linear_solver:with_cplex build --flag_alias=with_glop=//ortools/linear_solver:with_glop build --flag_alias=with_glpk=//ortools/linear_solver:with_glpk build --flag_alias=with_highs=//ortools/linear_solver:with_highs build --flag_alias=with_pdlp=//ortools/linear_solver:with_pdlp build --flag_alias=with_scip=//ortools/linear_solver:with_scip - -build --flag_alias=with_cplex=//ortools/linear_solver:with_cplex build --flag_alias=with_xpress=//ortools/linear_solver:with_xpress # Sets the default Apple platform to macOS. @@ -43,11 +42,37 @@ build:windows --host_cxxopt="/std:c++20" startup --windows_enable_symlinks build:windows --enable_runfiles -# Print command lines for build commands. -# build --subcommands=pretty_print -# Print test logs for failed tests. -test --test_output=errors --test_timeout_filters=-eternal +############################################################################### +# Options for continuous integration. +############################################################################### + +# All build options also apply to test as described by the "Option precedence" +# section in https://bazel.build/run/bazelrc#bazelrc-syntax-semantics. + +# Note for anybody considering using --compilation_mode=opt in CI, it builds +# most files twice, one PIC version for shared libraries in tests, and one +# non-PIC version for binaries. +build:ci --copt=-O1 + +# Speedup bazel using a ramdisk. +build:ci --sandbox_base=/dev/shm + +# Show as many errors as possible. +build:ci --keep_going + +# Show test errors. +build:ci --test_output=errors + +# Override timeout for tests +build:ci --test_timeout_filters=-eternal + +# Only show failing tests to reduce output +build:ci --test_summary=terse + +# Attempt to work around intermittent issue while trying to fetch remote blob. +# See e.g. https://github.com/bazelbuild/bazel/issues/18694. +build:ci --remote_default_exec_properties=cache-silo-key=CleverPeafowl # Put user-specific options in .bazelrc.user try-import %workspace%/.bazelrc.user diff --git a/bazel/docker/almalinux/Dockerfile b/bazel/docker/almalinux/Dockerfile index 221755fc26e..f37d7fca863 100644 --- a/bazel/docker/almalinux/Dockerfile +++ b/bazel/docker/almalinux/Dockerfile @@ -38,13 +38,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/alpine/Dockerfile b/bazel/docker/alpine/Dockerfile index 5908e0fb036..1427921dfc6 100644 --- a/bazel/docker/alpine/Dockerfile +++ b/bazel/docker/alpine/Dockerfile @@ -20,13 +20,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/archlinux/Dockerfile b/bazel/docker/archlinux/Dockerfile index 727cc90a779..a72afc6b467 100644 --- a/bazel/docker/archlinux/Dockerfile +++ b/bazel/docker/archlinux/Dockerfile @@ -16,13 +16,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/debian/Dockerfile b/bazel/docker/debian/Dockerfile index 798da5d08e3..1957bfd2680 100644 --- a/bazel/docker/debian/Dockerfile +++ b/bazel/docker/debian/Dockerfile @@ -26,13 +26,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/fedora/Dockerfile b/bazel/docker/fedora/Dockerfile index f65861728fd..6fa485e6af3 100644 --- a/bazel/docker/fedora/Dockerfile +++ b/bazel/docker/fedora/Dockerfile @@ -35,13 +35,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/opensuse/Dockerfile b/bazel/docker/opensuse/Dockerfile index 0e45533185c..feb630ee74d 100644 --- a/bazel/docker/opensuse/Dockerfile +++ b/bazel/docker/opensuse/Dockerfile @@ -32,13 +32,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/rockylinux/Dockerfile b/bazel/docker/rockylinux/Dockerfile index 7e9e76e680e..d9dfe0a5091 100644 --- a/bazel/docker/rockylinux/Dockerfile +++ b/bazel/docker/rockylinux/Dockerfile @@ -38,13 +38,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/ubuntu/Dockerfile b/bazel/docker/ubuntu/Dockerfile index 83fbe80f51e..88c3010b1c4 100644 --- a/bazel/docker/ubuntu/Dockerfile +++ b/bazel/docker/ubuntu/Dockerfile @@ -32,13 +32,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... From 6ef48f87aa3d1836597f82cd2fb63791e7c82603 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Thu, 4 Sep 2025 22:15:02 +0000 Subject: [PATCH 040/491] Remove RAM disk as workers are failing with "no space left on device" --- .bazelrc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.bazelrc b/.bazelrc index f5b47ccc066..6df7d28fa01 100644 --- a/.bazelrc +++ b/.bazelrc @@ -55,9 +55,6 @@ build:windows --enable_runfiles # non-PIC version for binaries. build:ci --copt=-O1 -# Speedup bazel using a ramdisk. -build:ci --sandbox_base=/dev/shm - # Show as many errors as possible. build:ci --keep_going From 20032eecd6ee0cee369ce7024c7f03727f2a4d67 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 08:29:57 +0200 Subject: [PATCH 041/491] ci: introduce ci config in bazel --- .bazelrc | 34 ++++++++++++++++++++++++------ bazel/docker/almalinux/Dockerfile | 10 ++------- bazel/docker/alpine/Dockerfile | 10 ++------- bazel/docker/archlinux/Dockerfile | 10 ++------- bazel/docker/debian/Dockerfile | 10 ++------- bazel/docker/fedora/Dockerfile | 10 ++------- bazel/docker/opensuse/Dockerfile | 10 ++------- bazel/docker/rockylinux/Dockerfile | 10 ++------- bazel/docker/ubuntu/Dockerfile | 10 ++------- 9 files changed, 44 insertions(+), 70 deletions(-) diff --git a/.bazelrc b/.bazelrc index 37af3f95a27..6df7d28fa01 100644 --- a/.bazelrc +++ b/.bazelrc @@ -6,13 +6,12 @@ build --flag_alias=with_bop=//ortools/linear_solver:with_bop build --flag_alias=with_cbc=//ortools/linear_solver:with_cbc build --flag_alias=with_clp=//ortools/linear_solver:with_clp build --flag_alias=with_cp_sat=//ortools/linear_solver:with_cp_sat +build --flag_alias=with_cplex=//ortools/linear_solver:with_cplex build --flag_alias=with_glop=//ortools/linear_solver:with_glop build --flag_alias=with_glpk=//ortools/linear_solver:with_glpk build --flag_alias=with_highs=//ortools/linear_solver:with_highs build --flag_alias=with_pdlp=//ortools/linear_solver:with_pdlp build --flag_alias=with_scip=//ortools/linear_solver:with_scip - -build --flag_alias=with_cplex=//ortools/linear_solver:with_cplex build --flag_alias=with_xpress=//ortools/linear_solver:with_xpress # Sets the default Apple platform to macOS. @@ -43,11 +42,34 @@ build:windows --host_cxxopt="/std:c++20" startup --windows_enable_symlinks build:windows --enable_runfiles -# Print command lines for build commands. -# build --subcommands=pretty_print -# Print test logs for failed tests. -test --test_output=errors --test_timeout_filters=-eternal +############################################################################### +# Options for continuous integration. +############################################################################### + +# All build options also apply to test as described by the "Option precedence" +# section in https://bazel.build/run/bazelrc#bazelrc-syntax-semantics. + +# Note for anybody considering using --compilation_mode=opt in CI, it builds +# most files twice, one PIC version for shared libraries in tests, and one +# non-PIC version for binaries. +build:ci --copt=-O1 + +# Show as many errors as possible. +build:ci --keep_going + +# Show test errors. +build:ci --test_output=errors + +# Override timeout for tests +build:ci --test_timeout_filters=-eternal + +# Only show failing tests to reduce output +build:ci --test_summary=terse + +# Attempt to work around intermittent issue while trying to fetch remote blob. +# See e.g. https://github.com/bazelbuild/bazel/issues/18694. +build:ci --remote_default_exec_properties=cache-silo-key=CleverPeafowl # Put user-specific options in .bazelrc.user try-import %workspace%/.bazelrc.user diff --git a/bazel/docker/almalinux/Dockerfile b/bazel/docker/almalinux/Dockerfile index 221755fc26e..f37d7fca863 100644 --- a/bazel/docker/almalinux/Dockerfile +++ b/bazel/docker/almalinux/Dockerfile @@ -38,13 +38,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/alpine/Dockerfile b/bazel/docker/alpine/Dockerfile index 5908e0fb036..1427921dfc6 100644 --- a/bazel/docker/alpine/Dockerfile +++ b/bazel/docker/alpine/Dockerfile @@ -20,13 +20,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/archlinux/Dockerfile b/bazel/docker/archlinux/Dockerfile index 727cc90a779..a72afc6b467 100644 --- a/bazel/docker/archlinux/Dockerfile +++ b/bazel/docker/archlinux/Dockerfile @@ -16,13 +16,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/debian/Dockerfile b/bazel/docker/debian/Dockerfile index 798da5d08e3..1957bfd2680 100644 --- a/bazel/docker/debian/Dockerfile +++ b/bazel/docker/debian/Dockerfile @@ -26,13 +26,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/fedora/Dockerfile b/bazel/docker/fedora/Dockerfile index f65861728fd..6fa485e6af3 100644 --- a/bazel/docker/fedora/Dockerfile +++ b/bazel/docker/fedora/Dockerfile @@ -35,13 +35,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/opensuse/Dockerfile b/bazel/docker/opensuse/Dockerfile index 0e45533185c..feb630ee74d 100644 --- a/bazel/docker/opensuse/Dockerfile +++ b/bazel/docker/opensuse/Dockerfile @@ -32,13 +32,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/rockylinux/Dockerfile b/bazel/docker/rockylinux/Dockerfile index 7e9e76e680e..d9dfe0a5091 100644 --- a/bazel/docker/rockylinux/Dockerfile +++ b/bazel/docker/rockylinux/Dockerfile @@ -38,13 +38,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... diff --git a/bazel/docker/ubuntu/Dockerfile b/bazel/docker/ubuntu/Dockerfile index 83fbe80f51e..88c3010b1c4 100644 --- a/bazel/docker/ubuntu/Dockerfile +++ b/bazel/docker/ubuntu/Dockerfile @@ -32,13 +32,7 @@ COPY . . FROM devel AS build RUN bazel version -RUN bazel build \ - -c opt \ - --subcommands=true \ - //ortools/... //examples/... +RUN bazel build --config=ci //ortools/... //examples/... FROM build AS test -RUN bazel test \ - -c opt \ - --test_output=errors \ - //ortools/... //examples/... +RUN bazel test --config=ci //ortools/... //examples/... From 88b857dadd06a80e3d17588a946d36f95dafa997 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 08:52:46 +0200 Subject: [PATCH 042/491] ci: rework bazel install --- bazel/docker/almalinux/Dockerfile | 7 ++++--- bazel/docker/debian/Dockerfile | 13 +++++-------- bazel/docker/fedora/Dockerfile | 7 ++++--- bazel/docker/opensuse/Dockerfile | 7 ++++--- bazel/docker/rockylinux/Dockerfile | 7 ++++--- bazel/docker/ubuntu/Dockerfile | 14 +++++--------- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/bazel/docker/almalinux/Dockerfile b/bazel/docker/almalinux/Dockerfile index f37d7fca863..527fef22a51 100644 --- a/bazel/docker/almalinux/Dockerfile +++ b/bazel/docker/almalinux/Dockerfile @@ -14,10 +14,11 @@ RUN echo "source /opt/rh/gcc-toolset-13/enable" >> /etc/bashrc SHELL ["/bin/bash", "--login", "-c"] # Install Bazelisk +ARG TARGETARCH=amd64 RUN wget \ -https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-amd64 \ -&& chmod +x bazelisk-linux-amd64 \ -&& mv bazelisk-linux-amd64 /usr/local/bin/bazel +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel # Install Java RUN dnf -y update \ diff --git a/bazel/docker/debian/Dockerfile b/bazel/docker/debian/Dockerfile index 1957bfd2680..cf2a6643829 100644 --- a/bazel/docker/debian/Dockerfile +++ b/bazel/docker/debian/Dockerfile @@ -10,15 +10,12 @@ RUN apt-get update -qq \ && apt-get install -yq default-jdk \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Install Bazel -RUN curl https://bazel.build/bazel-release.pub.gpg | apt-key add - +# Install Bazelisk ARG TARGETARCH=amd64 -RUN echo "deb [arch=$TARGETARCH] https://storage.googleapis.com/bazel-apt stable jdk1.8" | tee /etc/apt/sources.list.d/bazel.list -RUN apt-get update -qq \ -&& apt-get install -yq bazel \ -&& apt-get clean \ -&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN wget \ +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel FROM env AS devel WORKDIR /home/project diff --git a/bazel/docker/fedora/Dockerfile b/bazel/docker/fedora/Dockerfile index 6fa485e6af3..fc3cb001aa5 100644 --- a/bazel/docker/fedora/Dockerfile +++ b/bazel/docker/fedora/Dockerfile @@ -13,10 +13,11 @@ RUN dnf -y update \ && dnf clean all # Install Bazelisk +ARG TARGETARCH=amd64 RUN wget \ -https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-amd64 \ -&& chmod +x bazelisk-linux-amd64 \ -&& mv bazelisk-linux-amd64 /usr/local/bin/bazel +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel # Install Java RUN dnf -y update \ diff --git a/bazel/docker/opensuse/Dockerfile b/bazel/docker/opensuse/Dockerfile index feb630ee74d..0b66dae1af7 100644 --- a/bazel/docker/opensuse/Dockerfile +++ b/bazel/docker/opensuse/Dockerfile @@ -10,10 +10,11 @@ RUN zypper update -y \ ENV CC=gcc CXX=g++ # Install Bazelisk +ARG TARGETARCH=amd64 RUN wget \ -https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-amd64 \ -&& chmod +x bazelisk-linux-amd64 \ -&& mv bazelisk-linux-amd64 /usr/local/bin/bazel +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel # Install Java JDK and Maven RUN zypper refresh \ diff --git a/bazel/docker/rockylinux/Dockerfile b/bazel/docker/rockylinux/Dockerfile index d9dfe0a5091..94c0901f8c5 100644 --- a/bazel/docker/rockylinux/Dockerfile +++ b/bazel/docker/rockylinux/Dockerfile @@ -14,10 +14,11 @@ RUN echo "source /opt/rh/gcc-toolset-13/enable" >> /etc/bashrc SHELL ["/bin/bash", "--login", "-c"] # Install Bazelisk +ARG TARGETARCH=amd64 RUN wget \ -https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-amd64 \ -&& chmod +x bazelisk-linux-amd64 \ -&& mv bazelisk-linux-amd64 /usr/local/bin/bazel +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel # Install Java RUN dnf -y update \ diff --git a/bazel/docker/ubuntu/Dockerfile b/bazel/docker/ubuntu/Dockerfile index 88c3010b1c4..0328ded5243 100644 --- a/bazel/docker/ubuntu/Dockerfile +++ b/bazel/docker/ubuntu/Dockerfile @@ -15,16 +15,12 @@ RUN apt update -qq \ && apt clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Install Bazel -RUN apt install -y apt-transport-https curl gnupg \ -&& curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg \ -&& mv bazel-archive-keyring.gpg /usr/share/keyrings +# Install Bazelisk ARG TARGETARCH=amd64 -RUN echo "deb [arch=$TARGETARCH signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | tee /etc/apt/sources.list.d/bazel.list -RUN apt update -qq \ -&& apt install -yq bazel \ -&& apt clean \ -&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN wget \ +https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-${TARGETARCH} \ +&& chmod +x bazelisk-linux-${TARGETARCH} \ +&& mv bazelisk-linux-${TARGETARCH} /usr/local/bin/bazel FROM env AS devel WORKDIR /home/project From 9802f1ee7b1c77cf7ef58f232a62d20755140bc3 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 10:59:20 +0200 Subject: [PATCH 043/491] cmake: patch abseil-cpp to work with older GCC note: cherry pick https://github.com/abseil/abseil-cpp/commit/ba9a180d22e62edcd5f6c56b50287b286f96fc33 --- patches/abseil-cpp-20250814.0.patch | 107 +++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/patches/abseil-cpp-20250814.0.patch b/patches/abseil-cpp-20250814.0.patch index 78af605623f..ad52cb5629f 100644 --- a/patches/abseil-cpp-20250814.0.patch +++ b/patches/abseil-cpp-20250814.0.patch @@ -1,5 +1,5 @@ diff --git a/absl/flags/declare.h b/absl/flags/declare.h -index 8d2a856..a154046 100644 +index 8d2a856e..a1540467 100644 --- a/absl/flags/declare.h +++ b/absl/flags/declare.h @@ -59,10 +59,15 @@ ABSL_NAMESPACE_END @@ -18,3 +18,108 @@ index 8d2a856..a154046 100644 +#endif // _MSC_VER #endif // ABSL_FLAGS_DECLARE_H_ +diff --git a/absl/log/CMakeLists.txt b/absl/log/CMakeLists.txt +index eb19bec0..13b2d240 100644 +--- a/absl/log/CMakeLists.txt ++++ b/absl/log/CMakeLists.txt +@@ -47,6 +47,7 @@ absl_cc_library( + absl::base + absl::config + absl::core_headers ++ absl::has_ostream_operator + absl::leak_check + absl::log_internal_nullguard + absl::log_internal_nullstream +diff --git a/absl/log/check_test_impl.inc b/absl/log/check_test_impl.inc +index 5a7caf47..7bcedd40 100644 +--- a/absl/log/check_test_impl.inc ++++ b/absl/log/check_test_impl.inc +@@ -13,6 +13,8 @@ + // See the License for the specific language governing permissions and + // limitations under the License. + ++// SKIP_ABSL_INLINE_NAMESPACE_CHECK ++ + #ifndef ABSL_LOG_CHECK_TEST_IMPL_H_ + #define ABSL_LOG_CHECK_TEST_IMPL_H_ + +@@ -241,6 +243,18 @@ TEST(CHECKTest, TestBinaryChecksWithPrimitives) { + ABSL_TEST_CHECK_LT(1, 2); + } + ++TEST(CHECKTest, TestBinaryChecksWithStringComparison) { ++ const std::string a = "a"; ++ ABSL_TEST_CHECK_EQ(a, "a"); ++ ABSL_TEST_CHECK_NE(a, "b"); ++ ABSL_TEST_CHECK_GE(a, a); ++ ABSL_TEST_CHECK_GE("b", a); ++ ABSL_TEST_CHECK_LE(a, "a"); ++ ABSL_TEST_CHECK_LE(a, "b"); ++ ABSL_TEST_CHECK_GT("b", a); ++ ABSL_TEST_CHECK_LT(a, "b"); ++} ++ + // For testing using CHECK*() on anonymous enums. + enum { CASE_A, CASE_B }; + +diff --git a/absl/log/internal/BUILD.bazel b/absl/log/internal/BUILD.bazel +index 1ba9d766..005861f9 100644 +--- a/absl/log/internal/BUILD.bazel ++++ b/absl/log/internal/BUILD.bazel +@@ -82,6 +82,7 @@ cc_library( + "//absl/base:nullability", + "//absl/debugging:leak_check", + "//absl/strings", ++ "//absl/strings:has_ostream_operator", + ], + ) + +diff --git a/absl/log/internal/check_op.h b/absl/log/internal/check_op.h +index 4554475d..c6078640 100644 +--- a/absl/log/internal/check_op.h ++++ b/absl/log/internal/check_op.h +@@ -40,6 +40,7 @@ + #include "absl/log/internal/nullstream.h" + #include "absl/log/internal/strip.h" + #include "absl/strings/has_absl_stringify.h" ++#include "absl/strings/has_ostream_operator.h" + #include "absl/strings/string_view.h" + + // `ABSL_LOG_INTERNAL_STRIP_STRING_LITERAL` wraps string literals that +@@ -357,21 +358,12 @@ std::enable_if_t::value, + StringifyToStreamWrapper> + Detect(...); // Ellipsis has lowest preference when int passed. + +-// is_streamable is true for types that have an output stream operator<<. +-template +-struct is_streamable : std::false_type {}; +- +-template +-struct is_streamable() +- << std::declval())>> +- : std::true_type {}; +- + // This overload triggers when T is neither possible to print nor an enum. + template + std::enable_if_t, std::is_enum, + std::is_pointer, std::is_same, +- is_streamable, HasAbslStringify>>, ++ HasOstreamOperator, HasAbslStringify>>, + UnprintableWrapper> + Detect(...); + +@@ -382,9 +374,10 @@ Detect(...); + // one backed by another integer is converted to (u)int64_t. + template + std::enable_if_t< +- std::conjunction_v< +- std::is_enum, std::negation>, +- std::negation>, std::negation>>, ++ std::conjunction_v, ++ std::negation>, ++ std::negation>, ++ std::negation>>, + std::conditional_t< + std::is_same_v, bool> || + std::is_same_v, char> || From c42acae3475878ffac496bfed03e5f9cf5942b77 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 09:20:19 +0000 Subject: [PATCH 044/491] [bazel] fix pdlp tests --- ortools/pdlp/BUILD.bazel | 31 +++++++++++-------------------- ortools/pdlp/gtest_main.cc | 24 ------------------------ 2 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 ortools/pdlp/gtest_main.cc diff --git a/ortools/pdlp/BUILD.bazel b/ortools/pdlp/BUILD.bazel index dcef03144ec..91a0e872f42 100644 --- a/ortools/pdlp/BUILD.bazel +++ b/ortools/pdlp/BUILD.bazel @@ -36,9 +36,9 @@ cc_test( name = "scheduler_test", srcs = ["scheduler_test.cc"], deps = [ - ":gtest_main", ":scheduler", ":solvers_cc_proto", + "//ortools/base:gmock_main", "@abseil-cpp//absl/functional:any_invocable", ], ) @@ -77,15 +77,6 @@ py_proto_library( ], ) -cc_library( - name = "gtest_main", - srcs = ["gtest_main.cc"], - deps = [ - "//ortools/base", - "//ortools/base:gmock", - ], -) - cc_library( name = "iteration_stats", srcs = ["iteration_stats.cc"], @@ -107,13 +98,13 @@ cc_test( name = "iteration_stats_test", srcs = ["iteration_stats_test.cc"], deps = [ - ":gtest_main", ":iteration_stats", ":quadratic_program", ":sharded_quadratic_program", ":solve_log_cc_proto", ":solvers_cc_proto", ":test_util", + "//ortools/base:gmock_main", "//ortools/base:protobuf_util", "@eigen", ], @@ -162,7 +153,6 @@ cc_test( srcs = ["primal_dual_hybrid_gradient_test.cc"], shard_count = 3, deps = [ - ":gtest_main", ":iteration_stats", ":primal_dual_hybrid_gradient", ":quadratic_program", @@ -173,6 +163,7 @@ cc_test( ":termination", ":test_util", "//ortools/base", + "//ortools/base:gmock_main", "//ortools/glop:parameters_cc_proto", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/lp_data", @@ -204,9 +195,9 @@ cc_test( size = "small", srcs = ["quadratic_program_test.cc"], deps = [ - ":gtest_main", ":quadratic_program", ":test_util", + "//ortools/base:gmock_main", "//ortools/base:protobuf_util", "//ortools/base:status_macros", "//ortools/linear_solver:linear_solver_cc_proto", @@ -260,13 +251,13 @@ cc_test( size = "small", srcs = ["sharded_optimization_utils_test.cc"], deps = [ - ":gtest_main", ":quadratic_program", ":sharded_optimization_utils", ":sharded_quadratic_program", ":sharder", ":solve_log_cc_proto", ":test_util", + "//ortools/base:gmock_main", "@eigen", ], ) @@ -292,11 +283,11 @@ cc_test( name = "sharded_quadratic_program_test", srcs = ["sharded_quadratic_program_test.cc"], deps = [ - ":gtest_main", ":quadratic_program", ":sharded_quadratic_program", ":sharder", ":test_util", + "//ortools/base:gmock_main", "@eigen", ], ) @@ -324,11 +315,11 @@ cc_test( size = "small", srcs = ["sharder_test.cc"], deps = [ - ":gtest_main", ":scheduler", ":sharder", ":solvers_cc_proto", "//ortools/base", + "//ortools/base:gmock_main", "//ortools/base:mathutil", "@abseil-cpp//absl/random:distributions", "@eigen", @@ -351,9 +342,9 @@ cc_test( name = "solvers_proto_validation_test", srcs = ["solvers_proto_validation_test.cc"], deps = [ - ":gtest_main", ":solvers_cc_proto", ":solvers_proto_validation", + "//ortools/base:gmock_main", "//ortools/base:protobuf_util", "@abseil-cpp//absl/status", "@abseil-cpp//absl/strings", @@ -376,10 +367,10 @@ cc_test( size = "small", srcs = ["termination_test.cc"], deps = [ - ":gtest_main", ":solve_log_cc_proto", ":solvers_cc_proto", ":termination", + "//ortools/base:gmock_main", "//ortools/base:protobuf_util", ], ) @@ -402,9 +393,9 @@ cc_test( name = "test_util_test", srcs = ["test_util_test.cc"], deps = [ - ":gtest_main", ":test_util", "//ortools/base", + "//ortools/base:gmock_main", "@abseil-cpp//absl/types:span", "@eigen", ], @@ -430,13 +421,13 @@ cc_test( name = "trust_region_test", srcs = ["trust_region_test.cc"], deps = [ - ":gtest_main", ":quadratic_program", ":sharded_optimization_utils", ":sharded_quadratic_program", ":sharder", ":test_util", ":trust_region", + "//ortools/base:gmock_main", "@abseil-cpp//absl/strings", "@eigen", ], diff --git a/ortools/pdlp/gtest_main.cc b/ortools/pdlp/gtest_main.cc deleted file mode 100644 index 428d419f2ff..00000000000 --- a/ortools/pdlp/gtest_main.cc +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2010-2025 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 "gtest/gtest.h" -#include "ortools/base/commandlineflags.h" -#include "ortools/base/init_google.h" -#include "ortools/base/logging.h" - -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - google::InitGoogleLogging(argv[0]); - absl::ParseCommandLine(argc, argv); - return RUN_ALL_TESTS(); -} From f0d89b381d985cd18614b0ca466d515375f22c8f Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 11:35:42 +0200 Subject: [PATCH 045/491] [NFC] Organize .bazelrc into logical sections. --- .bazelrc | 64 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/.bazelrc b/.bazelrc index 6df7d28fa01..76932a33d6c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,7 +1,10 @@ # Enable logging rc options. common --announce_rc -# ----CONFIG OPTIONS---- +############################################################################### +# Global options for building OR-Tools. +############################################################################### + build --flag_alias=with_bop=//ortools/linear_solver:with_bop build --flag_alias=with_cbc=//ortools/linear_solver:with_cbc build --flag_alias=with_clp=//ortools/linear_solver:with_clp @@ -14,26 +17,44 @@ build --flag_alias=with_pdlp=//ortools/linear_solver:with_pdlp build --flag_alias=with_scip=//ortools/linear_solver:with_scip build --flag_alias=with_xpress=//ortools/linear_solver:with_xpress -# Sets the default Apple platform to macOS. -build --apple_platform_type=macos - -# By default, build OR-Tools in C++ 17 mode, with various extra flags per -# platform. -build --enable_platform_specific_config +# Enable absl::string_view support in @googletest +build --define absl=1 # Fix the python version build --@rules_python//python/config_settings:python_version=3.12 -# Per platform parameters. -build:linux --cxxopt="-std=c++17" --cxxopt=-Wno-sign-compare -build:linux --host_cxxopt="-std=c++17" --host_cxxopt=-Wno-sign-compare +############################################################################### +# Per plaform options +############################################################################### + +# By default, build with various extra flags per platform. +build --enable_platform_specific_config +############################################################################### +# Options for Linux. +############################################################################### + +build:linux --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 +build:linux --cxxopt=-Wno-sign-compare --host_cxxopt=-Wno-sign-compare + + +############################################################################### +# Options for macOS. +############################################################################### + +# Sets the default Apple platform to macOS. +build --apple_platform_type=macos build:macos --features=-supports_dynamic_linker -build:macos --cxxopt="-std=c++17" --cxxopt=-Wno-sign-compare --cxxopt=-mmacos-version-min=10.15 --cxxopt=-Wno-dangling-field -build:macos --host_cxxopt="-std=c++17" --host_cxxopt=-Wno-sign-compare --host_cxxopt=-mmacos-version-min=10.15 --host_cxxopt=-Wno-dangling-field +build:macos --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 +build:macos --cxxopt=-Wno-sign-compare --host_cxxopt=-Wno-sign-compare +build:macos --cxxopt=-Wno-dangling-field --host_cxxopt=-Wno-dangling-field +build:macos --cxxopt=-mmacos-version-min=10.15 --host_cxxopt=-mmacos-version-min=10.15 -build:windows --cxxopt="/std:c++20" -build:windows --host_cxxopt="/std:c++20" +############################################################################### +# Options for Windows. +############################################################################### + +build:windows --cxxopt=/std:c++20 --host_cxxopt=/std:c++20 # Enable the runfiles symlink tree on Windows. This makes it possible to build # the pip package on Windows without an intermediate data-file archive, as the @@ -42,7 +63,6 @@ build:windows --host_cxxopt="/std:c++20" startup --windows_enable_symlinks build:windows --enable_runfiles - ############################################################################### # Options for continuous integration. ############################################################################### @@ -71,13 +91,10 @@ build:ci --test_summary=terse # See e.g. https://github.com/bazelbuild/bazel/issues/18694. build:ci --remote_default_exec_properties=cache-silo-key=CleverPeafowl -# Put user-specific options in .bazelrc.user -try-import %workspace%/.bazelrc.user - -# Enable absl::string_view support in @googletest -build --define absl=1 +############################################################################### +# Options for asan. +################################################################################ -# asan build:asan --strip=never build:asan --copt -fsanitize=address build:asan --copt -DADDRESS_SANITIZER @@ -85,3 +102,8 @@ build:asan --copt -O1 build:asan --copt -g build:asan --copt -fno-omit-frame-pointer build:asan --linkopt -fsanitize=address + +############################################################################### +# Put user-specific options in .bazelrc.user +################################################################################ +try-import %workspace%/.bazelrc.user From cd56b71d2c23855a6394828ec5d7dc3e384fa96c Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 12:03:16 +0000 Subject: [PATCH 046/491] Fix //examples/cpp:integer_programming_test by implementing linear_solver::nodes() for HiGHS. --- ortools/linear_solver/highs_interface.cc | 15 +++++++++++---- .../proto_solver/highs_proto_solver.cc | 6 +++++- .../proto_solver/highs_proto_solver.h | 14 ++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/ortools/linear_solver/highs_interface.cc b/ortools/linear_solver/highs_interface.cc index ec4c3727676..d780825e5b5 100644 --- a/ortools/linear_solver/highs_interface.cc +++ b/ortools/linear_solver/highs_interface.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -102,6 +103,7 @@ class HighsInterface : public MPSolverInterface { void NonIncrementalChange(); const bool solve_as_a_mip_; + std::optional solve_info_; }; HighsInterface::HighsInterface(MPSolver* const solver, bool solve_as_a_mip) @@ -140,8 +142,9 @@ MPSolver::ResultStatus HighsInterface::Solve(const MPSolverParameters& param) { } // Set parameters. + solve_info_ = HighsSolveInfo(); absl::StatusOr response = - HighsSolveProto(std::move(request)); + HighsSolveProto(std::move(request), &*solve_info_); if (!response.ok()) { LOG(ERROR) << "Unexpected error solving with Highs: " << response.status(); @@ -163,7 +166,10 @@ MPSolver::ResultStatus HighsInterface::Solve(const MPSolverParameters& param) { return result_status_; } -void HighsInterface::Reset() { ResetExtractionInformation(); } +void HighsInterface::Reset() { + ResetExtractionInformation(); + solve_info_.reset(); +} void HighsInterface::SetOptimizationDirection(bool maximize) { NonIncrementalChange(); @@ -215,8 +221,9 @@ int64_t HighsInterface::iterations() const { } int64_t HighsInterface::nodes() const { - LOG(DFATAL) << "Number of nodes only available for discrete problems"; - return MPSolverInterface::kUnknownNumberOfNodes; + QCHECK(solve_info_.has_value()) + << "Number of nodes only available after solve"; + return solve_info_->mip_node_count; } MPSolver::BasisStatus HighsInterface::row_status(int constraint_index) const { diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.cc b/ortools/linear_solver/proto_solver/highs_proto_solver.cc index 5a0c564e9eb..41622b11f0f 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.cc @@ -46,7 +46,7 @@ absl::Status SetSolverSpecificParameters(const std::string& parameters, Highs& highs); absl::StatusOr HighsSolveProto( - LazyMutableCopy request) { + LazyMutableCopy request, HighsSolveInfo* solve_info) { MPSolutionResponse response; const std::optional> optional_model = GetMPModelOrPopulateResponse(request, &response); @@ -262,6 +262,10 @@ absl::StatusOr HighsSolveProto( } } + if (solve_info != nullptr) { + solve_info->mip_node_count = highs.getInfo().mip_node_count; + } + const absl::Duration solving_duration = absl::Now() - time_before; user_timer.Stop(); response.mutable_solve_info()->set_solve_wall_time_seconds( diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.h b/ortools/linear_solver/proto_solver/highs_proto_solver.h index 633373b4710..200f2093819 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.h +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.h @@ -14,8 +14,7 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_HIGHS_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_HIGHS_PROTO_SOLVER_H_ -#include -#include +#include #include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" @@ -23,9 +22,16 @@ namespace operations_research { -// Solve the input MIP model with the HIGHS solver. +// Information about the Highs solve. +struct HighsSolveInfo { + int64_t mip_node_count; // The number of nodes generated by the MIP solver. +}; + +// Solve the input MIP model with the HIGHS solver and fills `solve_info` is +// provided. absl::StatusOr HighsSolveProto( - LazyMutableCopy request); + LazyMutableCopy request, + HighsSolveInfo* solve_info = nullptr); } // namespace operations_research From 0972170c9272519f00bc504687380b424449dd4c Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 12:05:37 +0000 Subject: [PATCH 047/491] Fix typo --- ortools/linear_solver/proto_solver/highs_proto_solver.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.h b/ortools/linear_solver/proto_solver/highs_proto_solver.h index 200f2093819..152c380eac2 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.h +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.h @@ -27,7 +27,7 @@ struct HighsSolveInfo { int64_t mip_node_count; // The number of nodes generated by the MIP solver. }; -// Solve the input MIP model with the HIGHS solver and fills `solve_info` is +// Solve the input MIP model with the HIGHS solver and fills `solve_info` if // provided. absl::StatusOr HighsSolveProto( LazyMutableCopy request, From f574a2bda07a2e7550fc786204342f48c7932aa5 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 14:07:52 +0000 Subject: [PATCH 048/491] Fix `generic_max_flow_test.cc` - Add missing `operator!=` for `StrongUint16` - Fix test logic for Solve in `SmallFlowTypes` --- ortools/graph/generic_max_flow_test.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ortools/graph/generic_max_flow_test.cc b/ortools/graph/generic_max_flow_test.cc index 5fa975ac786..141dbce0160 100644 --- a/ortools/graph/generic_max_flow_test.cc +++ b/ortools/graph/generic_max_flow_test.cc @@ -294,6 +294,7 @@ constexpr bool operator>=(StrongUint16 a, StrongUint16 b) { return a.v >= b.v; } constexpr bool operator>(StrongUint16 a, StrongUint16 b) { return a.v > b.v; } constexpr bool operator<(StrongUint16 a, StrongUint16 b) { return a.v < b.v; } constexpr bool operator==(StrongUint16 a, StrongUint16 b) { return a.v == b.v; } +constexpr bool operator!=(StrongUint16 a, StrongUint16 b) { return a.v != b.v; } // This is a bit hacky, but we need to define the overload in the std namespace. } // namespace @@ -333,8 +334,10 @@ TYPED_TEST(GenericMaxFlowTest, SmallFlowTypes) { max_flow_A.SetArcCapacity(arc, capa); max_flow_B.SetArcCapacity(arc, capa); } - EXPECT_EQ(max_flow_A.Solve(), MaxFlowA::OPTIMAL); - EXPECT_EQ(max_flow_B.Solve(), MaxFlowB::OPTIMAL); + EXPECT_TRUE(max_flow_A.Solve()); + EXPECT_EQ(max_flow_A.status(), MaxFlowA::OPTIMAL); + EXPECT_TRUE(max_flow_B.Solve()); + EXPECT_EQ(max_flow_B.status(), MaxFlowB::OPTIMAL); EXPECT_EQ(max_flow_A.GetOptimalFlow(), max_flow_B.GetOptimalFlow()); } From 812b63a7e428ad2b57c0d506ebf939130be77d78 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 14:35:45 +0200 Subject: [PATCH 049/491] base: export protocol-buffer-matchers from google3 --- ortools/base/protocol-buffer-matchers.cc | 15 +++++++++++++-- ortools/base/protocol-buffer-matchers.h | 24 +++++++++++++----------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/ortools/base/protocol-buffer-matchers.cc b/ortools/base/protocol-buffer-matchers.cc index fa25757b478..bba51ab653b 100644 --- a/ortools/base/protocol-buffer-matchers.cc +++ b/ortools/base/protocol-buffer-matchers.cc @@ -14,6 +14,17 @@ // emulates g3/testing/base/public/gmock_utils/protocol-buffer-matchers.cc #include "ortools/base/protocol-buffer-matchers.h" +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/strings/string_view.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/io/tokenizer.h" +#include "google/protobuf/message.h" +#include "google/protobuf/text_format.h" +#include "google/protobuf/util/message_differencer.h" + namespace testing { namespace internal { // Utilities. @@ -314,7 +325,7 @@ std::string DescribeTypes(const ::google::protobuf::Message& expected, // Prints the protocol buffer pointed to by proto. std::string PrintProtoPointee(const ::google::protobuf::Message* proto) { - if (proto == NULL) return ""; + if (proto == nullptr) return ""; return "which points to " + ::testing::PrintToString(*proto); } @@ -355,7 +366,7 @@ bool ProtoMatcherBase::MatchAndExplain( const google::protobuf::Message* const expected = CreateExpectedProto(arg, listener); - if (expected == NULL) return false; + if (expected == nullptr) return false; // Protobufs of different types cannot be compared. const bool comparable = ProtoComparable(arg, *expected); diff --git a/ortools/base/protocol-buffer-matchers.h b/ortools/base/protocol-buffer-matchers.h index 7caff338adb..535027ab6cc 100644 --- a/ortools/base/protocol-buffer-matchers.h +++ b/ortools/base/protocol-buffer-matchers.h @@ -58,9 +58,11 @@ // "bar: 'x'")); #include -#include +#include +#include +#include +#include #include -#include #include #include #include @@ -69,11 +71,10 @@ #include "absl/log/log.h" #include "absl/strings/string_view.h" #include "gmock/gmock-matchers.h" -#include "gmock/gmock.h" #include "google/protobuf/descriptor.h" -#include "google/protobuf/io/tokenizer.h" #include "google/protobuf/message.h" #include "google/protobuf/util/message_differencer.h" +#include "gtest/gtest.h" namespace testing { using DifferencerConfigFunction = @@ -172,7 +173,6 @@ std::string PrintProtoPointee(const ::google::protobuf::Message* proto); std::string DescribeDiff(const ProtoComparison& comp, const ::google::protobuf::Message& actual, const ::google::protobuf::Message& expected); - // Common code for implementing EqualsProto and EquivToProto. class ProtoMatcherBase { public: @@ -280,9 +280,11 @@ class ProtoMatcherBase { const ProtoComparison& comp() const { return *comp_; } private: - bool MatchAndExplain(const ::google::protobuf::Message& arg, - bool is_matcher_for_pointer, - ::testing::MatchResultListener* listener) const; + bool MatchAndExplain( + const ::google::protobuf::Message& arg, + bool is_matcher_for_pointer, // true iff this matcher is used to match a + // protobuf pointer. + ::testing::MatchResultListener* listener) const; const bool must_be_initialized_; std::unique_ptr comp_; @@ -347,15 +349,15 @@ class ProtoStringMatcher : public ProtoMatcherBase { expected, // The text representing the expected protobuf. bool must_be_initialized, // Must the argument be fully initialized? const ProtoComparison comp) // How to compare the two protobufs. - : ProtoMatcherBase(must_be_initialized, comp), expected_(std::string(expected)) {} + : ProtoMatcherBase(must_be_initialized, comp), + expected_(std::string(expected)) {} // Parses the expected string as a protobuf of the same type as arg, // and returns the parsed protobuf (or NULL when the parse fails). // The caller must call DeleteExpectedProto() on the return value // later. virtual const MessageType* CreateExpectedProto( - const MessageType& arg, - ::testing::MatchResultListener* listener) const { + const MessageType& arg, ::testing::MatchResultListener* listener) const { ::google::protobuf::Message* expected_proto = arg.New(); // We don't insist that the expected string parses as an // *initialized* protobuf. Otherwise EqualsProto("...") may From 3c45aca3537a935e1c668311fa33ed5a972a28ca Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 14:36:23 +0200 Subject: [PATCH 050/491] cmake: fix missing absl::log_entry in packages * fix python test run on unix * fix dotnet package * fix java package --- cmake/java.cmake | 1 + cmake/python.cmake | 1 + ortools/dotnet/Google.OrTools.runtime.csproj.in | 1 + 3 files changed, 3 insertions(+) diff --git a/cmake/java.cmake b/cmake/java.cmake index f90f8d95126..1231165f5ff 100644 --- a/cmake/java.cmake +++ b/cmake/java.cmake @@ -365,6 +365,7 @@ add_custom_command( $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> + $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> diff --git a/cmake/python.cmake b/cmake/python.cmake index 4f5385b62b7..76add7fd6c4 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -541,6 +541,7 @@ add_custom_command( $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> + $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> $<${need_unix_absl_lib}:$> diff --git a/ortools/dotnet/Google.OrTools.runtime.csproj.in b/ortools/dotnet/Google.OrTools.runtime.csproj.in index 18c79c965e5..c565b1b1bb0 100644 --- a/ortools/dotnet/Google.OrTools.runtime.csproj.in +++ b/ortools/dotnet/Google.OrTools.runtime.csproj.in @@ -68,6 +68,7 @@ $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> + $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> $<@need_unix_absl_lib@:;$> From a7c53e8d5e4eb00e864ccd05acd7421ab0e04614 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 16:37:48 +0200 Subject: [PATCH 051/491] algorithms: make eternal test manual --- ortools/algorithms/BUILD.bazel | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index e5a3531c333..7b3d06da91c 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -180,6 +180,9 @@ cc_test( size = "large", timeout = "eternal", srcs = ["adjustable_k_ary_heap_stress_test.cc"], + tags = [ + "manual", + ], deps = [ ":adjustable_k_ary_heap", "//ortools/base:gmock_main", From 4b8919aa6774aa4841aa8d8b8c32a583ccc6768b Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Fri, 5 Sep 2025 16:39:56 +0200 Subject: [PATCH 052/491] Format BUILD.bazel --- ortools/algorithms/BUILD.bazel | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 7b3d06da91c..9f88e3a4c6e 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -180,9 +180,7 @@ cc_test( size = "large", timeout = "eternal", srcs = ["adjustable_k_ary_heap_stress_test.cc"], - tags = [ - "manual", - ], + tags = ["manual"], deps = [ ":adjustable_k_ary_heap", "//ortools/base:gmock_main", From 683f34b8c77fb5deba1db734680aa38836f2400f Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 18:12:48 +0200 Subject: [PATCH 053/491] bazel: add manual tag to eternal targets --- ortools/math_opt/solvers/BUILD.bazel | 1 + ortools/set_cover/BUILD.bazel | 1 + 2 files changed, 2 insertions(+) diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 28a3065cd46..0f1d3d43c0f 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -317,6 +317,7 @@ cc_test( timeout = "eternal", srcs = ["cp_sat_solver_test.cc"], shard_count = 10, + tags = ["manual"], deps = [ ":cp_sat_solver", "//ortools/base:gmock_main", diff --git a/ortools/set_cover/BUILD.bazel b/ortools/set_cover/BUILD.bazel index 8951f0fb786..d1bd94d6785 100644 --- a/ortools/set_cover/BUILD.bazel +++ b/ortools/set_cover/BUILD.bazel @@ -232,6 +232,7 @@ cc_test( size = "medium", timeout = "eternal", srcs = ["set_cover_test.cc"], + tags = ["manual"], deps = [ ":base_types", ":set_cover_cc_proto", From 0487ac1a404c133fc92178c16879a9c71d4d94e4 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Fri, 5 Sep 2025 18:13:32 +0200 Subject: [PATCH 054/491] ortools: use new MutexLock ctor --- ortools/lp_data/lp_decomposer.cc | 12 ++++++------ ortools/util/solve_interrupter.cc | 6 +++--- ortools/util/time_limit.h | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ortools/lp_data/lp_decomposer.cc b/ortools/lp_data/lp_decomposer.cc index a5f244a650d..154c12a87c3 100644 --- a/ortools/lp_data/lp_decomposer.cc +++ b/ortools/lp_data/lp_decomposer.cc @@ -36,7 +36,7 @@ LPDecomposer::LPDecomposer() : original_problem_(nullptr), clusters_(), mutex_() {} void LPDecomposer::Decompose(const LinearProgram* linear_problem) { - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); original_problem_ = linear_problem; clusters_.clear(); @@ -69,12 +69,12 @@ void LPDecomposer::Decompose(const LinearProgram* linear_problem) { } int LPDecomposer::GetNumberOfProblems() const { - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); return clusters_.size(); } const LinearProgram& LPDecomposer::original_problem() const { - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); return *original_problem_; } @@ -85,7 +85,7 @@ void LPDecomposer::ExtractLocalProblem(int problem_index, LinearProgram* lp) { lp->Clear(); - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); const std::vector& cluster = clusters_[problem_index]; StrictITIVector global_to_local( original_problem_->num_variables(), kInvalidCol); @@ -143,7 +143,7 @@ DenseRow LPDecomposer::AggregateAssignments( absl::Span assignments) const { CHECK_EQ(assignments.size(), clusters_.size()); - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); DenseRow global_assignment(original_problem_->num_variables(), Fractional(0.0)); for (int problem = 0; problem < assignments.size(); ++problem) { @@ -163,7 +163,7 @@ DenseRow LPDecomposer::ExtractLocalAssignment(int problem_index, CHECK_LT(problem_index, clusters_.size()); CHECK_EQ(assignment.size(), original_problem_->num_variables()); - absl::MutexLock mutex_lock(&mutex_); + absl::MutexLock mutex_lock(mutex_); const std::vector& cluster = clusters_[problem_index]; DenseRow local_assignment(ColIndex(cluster.size()), Fractional(0.0)); for (int i = 0; i < cluster.size(); ++i) { diff --git a/ortools/util/solve_interrupter.cc b/ortools/util/solve_interrupter.cc index d8c44c0ac90..2a0285c6973 100644 --- a/ortools/util/solve_interrupter.cc +++ b/ortools/util/solve_interrupter.cc @@ -25,7 +25,7 @@ namespace operations_research { void SolveInterrupter::Interrupt() { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // Here we don't use compare_exchange_strong since we need to hold the lock // before changing the value of interrupted_ anyway. So there is no need to @@ -51,7 +51,7 @@ void SolveInterrupter::Interrupt() { SolveInterrupter::CallbackId SolveInterrupter::AddInterruptionCallback( Callback callback) const { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); // We must make this call while holding the lock since we want to be sure that // the calls to the callbacks_ won't occur before we registered the new @@ -72,7 +72,7 @@ SolveInterrupter::CallbackId SolveInterrupter::AddInterruptionCallback( } void SolveInterrupter::RemoveInterruptionCallback(CallbackId id) const { - const absl::MutexLock lock(&mutex_); + const absl::MutexLock lock(mutex_); CHECK_EQ(callbacks_.erase(id), 1) << "unregistered callback id: " << id; } diff --git a/ortools/util/time_limit.h b/ortools/util/time_limit.h index 2fd664b1827..23e0642d622 100644 --- a/ortools/util/time_limit.h +++ b/ortools/util/time_limit.h @@ -343,37 +343,37 @@ class SharedTimeLimit { bool LimitReached() const { // Note, time_limit_->LimitReached() is not const, and changes internal // state of time_limit_, hence we need a writer's lock. - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return time_limit_->LimitReached(); } void Stop() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); *stopped_ = true; } void UpdateLocalLimit(TimeLimit* local_limit) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); local_limit->MergeWithGlobalTimeLimit(time_limit_); } void AdvanceDeterministicTime(double deterministic_duration) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); time_limit_->AdvanceDeterministicTime(deterministic_duration); } double GetTimeLeft() const { - absl::ReaderMutexLock lock(&mutex_); + absl::ReaderMutexLock lock(mutex_); return time_limit_->GetTimeLeft(); } double GetElapsedDeterministicTime() const { - absl::ReaderMutexLock lock(&mutex_); + absl::ReaderMutexLock lock(mutex_); return time_limit_->GetElapsedDeterministicTime(); } std::atomic* ExternalBooleanAsLimit() const { - absl::ReaderMutexLock lock(&mutex_); + absl::ReaderMutexLock lock(mutex_); // We can simply return the "external bool" and remain thread-safe because // it's wrapped in std::atomic. return time_limit_->ExternalBooleanAsLimit(); From 04845773200131c6501115440d465300bd2940af Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Mon, 8 Sep 2025 09:46:02 +0000 Subject: [PATCH 055/491] [bazel] remove broken tests for now so CI is more reliable We will re-enable them slowly. --- ortools/algorithms/BUILD.bazel | 19 ---------- ortools/base/BUILD.bazel | 13 ------- ortools/math_opt/core/python/BUILD.bazel | 23 ------------ ortools/math_opt/elemental/BUILD.bazel | 14 -------- ortools/math_opt/solvers/BUILD.bazel | 33 ----------------- ortools/math_opt/solvers/gscip/BUILD.bazel | 42 ---------------------- 6 files changed, 144 deletions(-) diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 9f88e3a4c6e..89721444fac 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -411,22 +411,3 @@ cc_library( "@abseil-cpp//absl/time", ], ) - -cc_test( - name = "n_choose_k_test", - srcs = ["n_choose_k_test.cc"], - deps = [ - ":n_choose_k", - "//ortools/base:dump_vars", - "//ortools/base:fuzztest", - "//ortools/base:gmock_main", - "//ortools/base:mathutil", - "//ortools/util:flat_matrix", - "@abseil-cpp//absl/numeric:int128", - "@abseil-cpp//absl/random", - "@abseil-cpp//absl/random:distributions", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@google_benchmark//:benchmark", - ], -) diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 227a612d488..cd5f8a4f97a 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -228,19 +228,6 @@ cc_library( ], ) -cc_library( - name = "fuzztest", - testonly = 1, - hdrs = ["fuzztest.h"], - deps = [ - "@abseil-cpp//absl/log:check", - "@fuzztest//fuzztest", - "@fuzztest//fuzztest:googletest_fixture_adapter", - "@fuzztest//fuzztest:init_fuzztest", - "@protobuf", - ], -) - cc_library( name = "gmock", hdrs = ["gmock.h"], diff --git a/ortools/math_opt/core/python/BUILD.bazel b/ortools/math_opt/core/python/BUILD.bazel index 4aec371d30a..c372b6d0fd3 100644 --- a/ortools/math_opt/core/python/BUILD.bazel +++ b/ortools/math_opt/core/python/BUILD.bazel @@ -11,9 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@pip_deps//:requirements.bzl", "requirement") load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") -load("@rules_python//python:py_test.bzl", "py_test") package(default_visibility = ["//ortools/math_opt:__subpackages__"]) @@ -60,24 +58,3 @@ pybind_extension( "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", ], ) - -py_test( - name = "solver_test", - size = "small", - srcs = ["solver_test.py"], - deps = [ - ":solver", - requirement("absl-py"), - "//ortools/math_opt:callback_py_pb2", - "//ortools/math_opt:model_parameters_py_pb2", - "//ortools/math_opt:model_py_pb2", - "//ortools/math_opt:model_update_py_pb2", - "//ortools/math_opt:parameters_py_pb2", - "//ortools/math_opt:result_py_pb2", - "//ortools/math_opt/solvers:cp_sat_solver", - "//ortools/math_opt/solvers:glop_solver", - "//ortools/math_opt/solvers:gscip_solver", - "//ortools/util/python:pybind_solve_interrupter", - "@pybind11_abseil//pybind11_abseil:status", - ], -) diff --git a/ortools/math_opt/elemental/BUILD.bazel b/ortools/math_opt/elemental/BUILD.bazel index 2b03c5d97e7..8b1c2de9591 100644 --- a/ortools/math_opt/elemental/BUILD.bazel +++ b/ortools/math_opt/elemental/BUILD.bazel @@ -540,20 +540,6 @@ cc_test( ], ) -cc_test( - name = "elemental_from_proto_fuzz_test", - srcs = ["elemental_from_proto_fuzz_test.cc"], - tags = ["componentid:1147829"], - deps = [ - ":elemental", - ":elemental_matcher", - "//ortools/base:fuzztest", - "//ortools/base:gmock_main", - "//ortools/math_opt:model_update_cc_proto", - "@abseil-cpp//absl/status:statusor", - ], -) - cc_test( name = "elemental_update_from_proto_test", srcs = ["elemental_update_from_proto_test.cc"], diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 0f1d3d43c0f..e56c1e304a4 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -72,39 +72,6 @@ cc_library( alwayslink = 1, ) -cc_test( - name = "gscip_solver_test", - size = "medium", - srcs = ["gscip_solver_test.cc"], - shard_count = 10, - deps = [ - ":gscip_solver", - "//ortools/base:gmock", - "//ortools/base:gmock_main", - "//ortools/math_opt/cpp:matchers", - "//ortools/math_opt/cpp:math_opt", - "//ortools/math_opt/solver_tests:callback_tests", - "//ortools/math_opt/solver_tests:generic_tests", - "//ortools/math_opt/solver_tests:infeasible_subsystem_tests", - "//ortools/math_opt/solver_tests:invalid_input_tests", - "//ortools/math_opt/solver_tests:ip_model_solve_parameters_tests", - "//ortools/math_opt/solver_tests:ip_multiple_solutions_tests", - "//ortools/math_opt/solver_tests:ip_parameter_tests", - "//ortools/math_opt/solver_tests:logical_constraint_tests", - "//ortools/math_opt/solver_tests:mip_tests", - "//ortools/math_opt/solver_tests:multi_objective_tests", - "//ortools/math_opt/solver_tests:qc_tests", - "//ortools/math_opt/solver_tests:qp_tests", - "//ortools/math_opt/solver_tests:second_order_cone_tests", - "//ortools/math_opt/solver_tests:status_tests", - "//ortools/math_opt/solvers/gscip:gscip_parameters", - "//ortools/math_opt/testing:param_name", - "//ortools/port:scoped_std_stream_capture", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - ], -) - cc_library( name = "gurobi_callback", srcs = ["gurobi_callback.cc"], diff --git a/ortools/math_opt/solvers/gscip/BUILD.bazel b/ortools/math_opt/solvers/gscip/BUILD.bazel index 8b3c6689e66..01474cd3895 100644 --- a/ortools/math_opt/solvers/gscip/BUILD.bazel +++ b/ortools/math_opt/solvers/gscip/BUILD.bazel @@ -123,33 +123,6 @@ cc_library( ], ) -cc_test( - name = "gscip_test", - size = "small", - srcs = ["gscip_test.cc"], - deps = [ - ":gscip", - ":gscip_cc_proto", - ":gscip_parameters", - ":gscip_testing", - "//ortools/base:file", - "//ortools/base:gmock", - "//ortools/base:gmock_main", - "//ortools/base:logging", - "//ortools/base:temp_file", - "@abseil-cpp//absl/cleanup", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/container:flat_hash_set", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/synchronization", - "@abseil-cpp//absl/time", - "@scip", - ], -) - cc_test( name = "gscip_io_test", size = "medium", @@ -235,21 +208,6 @@ cc_library( ], ) -cc_test( - name = "gscip_from_mp_model_proto_test", - srcs = ["gscip_from_mp_model_proto_test.cc"], - deps = [ - ":gscip", - ":gscip_cc_proto", - ":gscip_from_mp_model_proto", - ":gscip_testing", - "//ortools/base:gmock", - "//ortools/base:gmock_main", - "//ortools/base:parse_test_proto", - "//ortools/linear_solver:linear_solver_cc_proto", - ], -) - cc_library( name = "gscip_message_handler", srcs = ["gscip_message_handler.cc"], From b24f4bacf4b95d174017723bbf6a12621a5d2123 Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Mon, 8 Sep 2025 12:36:26 +0000 Subject: [PATCH 056/491] [bazel] Make sure @glpk dependency is not pulled when --with_glpk=False --- ortools/glpk/BUILD.bazel | 15 ++++++++++++++ ortools/math_opt/solvers/BUILD.bazel | 10 +++++++++ ortools/math_opt/solvers/glpk/BUILD.bazel | 25 +++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/ortools/glpk/BUILD.bazel b/ortools/glpk/BUILD.bazel index e23aef60c5b..16dfa0574b6 100644 --- a/ortools/glpk/BUILD.bazel +++ b/ortools/glpk/BUILD.bazel @@ -20,6 +20,11 @@ cc_library( name = "glpk_env_deleter", srcs = ["glpk_env_deleter.cc"], hdrs = ["glpk_env_deleter.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ "//ortools/base", "@glpk", @@ -30,6 +35,11 @@ cc_library( name = "glpk_formatters", srcs = ["glpk_formatters.cc"], hdrs = ["glpk_formatters.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ "//ortools/base", "@abseil-cpp//absl/strings", @@ -40,6 +50,11 @@ cc_library( cc_library( name = "glpk_computational_form", hdrs = ["glpk_computational_form.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ "@glpk", ], diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index e56c1e304a4..c84cbde2c17 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -434,6 +434,11 @@ cc_library( "glpk_solver.cc", "glpk_solver.h", ], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), visibility = ["//visibility:public"], deps = [ ":glpk_cc_proto", @@ -482,6 +487,11 @@ cc_library( cc_test( name = "glpk_solver_test", srcs = ["glpk_solver_test.cc"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":glpk_solver", "//ortools/base:gmock_main", diff --git a/ortools/math_opt/solvers/glpk/BUILD.bazel b/ortools/math_opt/solvers/glpk/BUILD.bazel index b96e5107a82..e11a2751a0d 100644 --- a/ortools/math_opt/solvers/glpk/BUILD.bazel +++ b/ortools/math_opt/solvers/glpk/BUILD.bazel @@ -21,6 +21,11 @@ cc_library( name = "rays", srcs = ["rays.cc"], hdrs = ["rays.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ "//ortools/base:logging", "//ortools/base:status_macros", @@ -38,6 +43,11 @@ cc_library( name = "glpk_sparse_vector", srcs = ["glpk_sparse_vector.cc"], hdrs = ["glpk_sparse_vector.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ "//ortools/base:logging", "@abseil-cpp//absl/log:check", @@ -47,6 +57,11 @@ cc_library( cc_test( name = "glpk_sparse_vector_test", srcs = ["glpk_sparse_vector_test.cc"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":glpk_sparse_vector", "//ortools/base:gmock_main", @@ -57,11 +72,21 @@ cc_library( name = "gap", srcs = ["gap.cc"], hdrs = ["gap.h"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), ) cc_test( name = "gap_test", srcs = ["gap_test.cc"], + target_compatible_with = + select({ + "//ortools/linear_solver:use_glpk": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":gap", "//ortools/base:gmock_main", From 24197c83ca96b87f0a21ebbdf2ff8f260d18e6c6 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 8 Sep 2025 16:08:17 +0200 Subject: [PATCH 057/491] math_opt: export fixup from google3 --- ortools/math_opt/cpp/message_callback.cc | 5 +- .../math_opt/python/ipc/proto_converter.py | 2 +- ortools/math_opt/python/py.typed | 0 .../math_opt/samples/cpp/coil_in_the_box.cc | 383 ++++++++++++++++++ ortools/math_opt/samples/python/BUILD.bazel | 1 - .../samples/python/linear_regression.py | 3 - ortools/math_opt/solvers/BUILD.bazel | 5 - .../solvers/gscip/gscip_event_handler_test.cc | 1 - ortools/math_opt/solvers/highs_solver.cc | 2 +- ortools/math_opt/solvers/osqp.proto | 4 +- 10 files changed, 389 insertions(+), 17 deletions(-) delete mode 100644 ortools/math_opt/python/py.typed create mode 100644 ortools/math_opt/samples/cpp/coil_in_the_box.cc diff --git a/ortools/math_opt/cpp/message_callback.cc b/ortools/math_opt/cpp/message_callback.cc index 7c33a6aa62a..472e6ed1357 100644 --- a/ortools/math_opt/cpp/message_callback.cc +++ b/ortools/math_opt/cpp/message_callback.cc @@ -92,7 +92,7 @@ MessageCallback PrinterMessageCallback(std::ostream& output_stream, MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix, const absl::SourceLocation loc) { - return [=](const std::vector& messages) { + return [=](absl::Span messages) { for (const std::string& message : messages) { LOG(INFO).AtLocation(loc.file_name(), loc.line()) << prefix << message; } @@ -127,8 +127,7 @@ MessageCallback RepeatedPtrFieldMessageCallback( // it uses an absl::Mutex that is not. const auto impl = std::make_shared>>(sink); - return - [=](const std::vector& messages) { impl->Call(messages); }; + return [=](absl::Span messages) { impl->Call(messages); }; } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/python/ipc/proto_converter.py b/ortools/math_opt/python/ipc/proto_converter.py index 80653525503..c5aaf382511 100644 --- a/ortools/math_opt/python/ipc/proto_converter.py +++ b/ortools/math_opt/python/ipc/proto_converter.py @@ -16,7 +16,7 @@ Provides several conversion functions to transform from/to protos exposed in the Operations Research API to the internal protos in -/ortools/math_opt/.*.proto. +ortools/math_opt/.*.proto. """ from google.protobuf import message diff --git a/ortools/math_opt/python/py.typed b/ortools/math_opt/python/py.typed deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/ortools/math_opt/samples/cpp/coil_in_the_box.cc b/ortools/math_opt/samples/cpp/coil_in_the_box.cc new file mode 100644 index 00000000000..130ad14523f --- /dev/null +++ b/ortools/math_opt/samples/cpp/coil_in_the_box.cc @@ -0,0 +1,383 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Solves the coil in the box problem, a variant of the snake in the box +// problem, see https://en.wikipedia.org/wiki/Snake-in-the-box. +// +// The problem is to find the longest cycle traversing a subset of the corners +// of the n-dimensional hypercube, such that for each corner you visit, you +// visit at most two adjacent corners. The cube has 2^n corners, giving an +// upper bound on the longest cycle length. We use a prize collecting TSP +// like MIP model to solve the problem below. We introduce the "Medusa cuts" +// for this problem, linear constraints that improve the LP relaxation without +// cutting off any integer points, that can be optionally included in the model. +// +// The best known solutions for coil in a box as a function of n are: +// +// n | best solution | best bound +// ---|---------------|------------ +// 2 | 4 | 4 +// 3 | 6 | 6 +// 4 | 8 | 8 +// 5 | 14 | 14 +// 6 | 26 | 26 +// 7 | 48 | 48 +// 8 | 96 | 96 +// 9 | 188 | ? +// 10 | 366 | ? +// 11 | 692 | ? +// 12 | 1344 | ? +// +// Below, we show results with the Medusa cuts on (and --fix). We are optimal +// up to n=6 (45s). All solves were run locally to avoid callback RPC overhead. +// +// clang-format off +// n | best solution | lp bound | 20m bound | logs +// ---|---------------|-------------|--------------|------- +// 6 | 26 | 30.8 | 26 (6.5s) | https://paste.googleplex.com/5930768672751616 +// 7 | 46 | 59.6 | 57.1 (5m) | https://paste.googleplex.com/6517880031805440 +// 8 | 78 | 116.2 (21s) | 114.9 | https://paste.googleplex.com/4818696077574144 +// 9 | 86 | 228.1 (6m) | 227.7 | https://paste.googleplex.com/4856789015330816 +// clang-format on +// +// For n=9, the problem (post presolve) has ~3k columns, ~12k rows, and ~150k +// nonzeros. +// +// clang-format off +// Without the Medusa cuts, we better primal solutions but worse bounds. +// n | best solution | lp bound | 20m bound | logs +// ---|---------------|----------------------------- +// 6 | 26 (<1s) | 35.2 | 26 (17s) | https://paste.googleplex.com/5421390584610816 +// 7 | 48 (1m) | 70 | 63.7 (5m) | https://paste.googleplex.com/4812751909945344 +// 8 | 88 | 140.8 | 134.5 | https://paste.googleplex.com/6498606298955776 +// 9 | 158 | 281 | 277 | https://paste.googleplex.com/5250122858102784 +// clang-format on +// +// Notes for someone trying to do better: +// * Using only 2-Medusa cuts (edit the code) has almost no effect (tiny +// improvement on the root LP relaxation). +// * The gurobi symmetry breaking does not seem to work, even when --fix=false +// and the "Symmetry" parameter is set to 2. The results are much worse +// whenever --fix=false. +// * For smaller instances, the remote callbacks are painful (182/200s in +// callback for n=6, medusa=false), but for large instances the RPC overhead +// does not matter. +// * With Medusa cuts, we have many more constraints than variables, and the LP +// becomes slow for large instances. +// * No attempt was made to optimize the parameters of QuickSeparate() below. +// +// Our MIP model is as follows. +// +// Data: +// * n: the dimension of the hypercube +// * G = (V, E): the hypercube as a graph, with vertices at the 2^n corners and +// edges between the corners differing in only one coordinate. +// * E(v) subset E: the edges where v is an endpoint. +// * N(v) subset V: the nodes neighboring v. +// * Cut(S) subset E: edges with exactly one endpoint in S. +// +// Variables: +// * y_e: do we use edge e in E +// * x_v: do we visit vertex v +// +// Model: +// max sum_{e in E} y_e +// s.t. sum_{e in E(v)} y_e = 2 x_v for all v in V +// x_v + x_w <= 1 + y_{v,w} for all (v, w) in E +// sum_{e in Cut(S)} y_e >= 2 (x_k + x_l - 1) for all S subset N +// 3 < |S| < |N| +// k in S, +// l not in S +// +// The first constraint (the degree constraint) says to use exactly two edges if +// we visit the node, and none otherwise. The second constraint enforces the +// invalidation of adjacent corners that are not visited directly from a node, +// it requires that we only visit two adjacent nodes if we include the edge +// between them. +// +// The final constraint is the "cutset" constraint from the PC-TSP, which +// ensures that all arcs selected form a single single, rather than multiple +// cycles (if you select a node inside S and outside S, there must be 2 units of +// flow over the cut). For more details on the implementation, see: +// * cs/ortools/math_opt/models/tsp/math_opt_circuit.h +// * cs/ortools/math_opt/models/tsp/circuit_constraint.h +// In particular, when --fix is set (so we know the first node is visited), +// there is a slightly simpler formulation of the constraint (see +// math_opt_circuit.h). +// +// The cutset constraints are separated exactly on integer solutions by +// traversing the selected arcs are looking for cycles. They are separated +// approximately for fractional solutions by rounding the solution and then +// checking each connected component. +// +// We can strengthen this constraint with the "Medusa cuts" as follows. Let +// d_v = 2*x_v (don't create the variable, just use a linear expression) give +// the degree of node v (we need this to make the constraints sparse). The cuts +// are parameterized by a dimension d < n. Take all n choose d hypercubes of +// size d that are a subset the n dimension hypercube. For each cube, take the +// internal edges (Medusa's head) and the edges on the cut with one exactly +// endpoint in the cube (Medusa's snakes). For d=2, we can use at most 4 of +// these edges, and for d=3, we can use at most 6 of these edges (this was shown +// by enumeration/cases, a clean proof would be an improvement). We can write +// these constraints as: +// sum_{v in Head} d_v - sum_{e in Head} y_e <= UB(d) +// Using O(2^d) nonzeros per cut. +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "base/context.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/base/status_macros.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/cpp/remote_solve.h" +#include "ortools/math_opt/cpp/stubby_remote_streaming_solve.h" +#include "ortools/math_opt/models/tsp/circuit_constraint.h" +#include "ortools/math_opt/models/tsp/math_opt_circuit.h" +#include "ortools/math_opt/rpc.stubby.h" + +ABSL_FLAG(int, dim, 4, "Dimension of the hyper cube to solve in."); +ABSL_FLAG(bool, gurobi, false, "Use gurobi instead of SCIP."); +ABSL_FLAG(std::optional, threads, std::nullopt, + "How many threads, the solver default if unset."); +ABSL_FLAG(bool, medusa, false, "Add medusa cuts to the formulation"); +ABSL_FLAG(bool, fix, false, "break symmetry by fixing variables"); +ABSL_FLAG(bool, remote, false, "Solve remotely with stubby"); +ABSL_FLAG(absl::Duration, time_limit, absl::Minutes(5), + "A limit on how long to run"); + +namespace { + +namespace math_opt = ::operations_research::math_opt; +namespace models = ::operations_research::models; +using Node = models::CircuitConstraint::Node; +using Edge = models::CircuitConstraint::Edge; +using ::util_intops::MakeStrongIntRange; + +// The cube has 2**n nodes, represented by the integers [0..2**n). The binary +// encoding of each node says which coordinates are zero and which are one. This +// function flips the dth bit, getting a neighboring node. +Node Neighbor(const Node v, int d) { return Node(v.value() ^ (1 << d)); } + +absl::StatusOr RunSolve(const math_opt::Model& model, + math_opt::SolverType solver_type, + math_opt::SolveArguments args) { + if (absl::GetFlag(FLAGS_remote)) { + args.message_callback = math_opt::PrinterMessageCallback(); + const base::WithDeadline deadline(absl::Now() + + 3 * absl::GetFlag(FLAGS_time_limit)); + ASSIGN_OR_RETURN(const std::unique_ptr stub, + math_opt::SolveServerStub()); + return math_opt::StubbyRemoteStreamingSolve(stub.get(), model, solver_type, + args); + } + return math_opt::Solve(model, solver_type, std::move(args)); +} + +absl::StatusOr OnMipSolution( + const models::MathOptCircuit& circuit, const math_opt::CallbackData& data) { + CHECK(data.solution.has_value()); + math_opt::CallbackResult result; + + ASSIGN_OR_RETURN(const std::vector cuts, + circuit.ExactSeparateIntegerSolution(*data.solution)); + for (const models::MathOptCircuit::Cutset& cutset : cuts) { + result.AddLazyConstraint(circuit.CreateCutsetConstraint(cutset)); + } + return result; +} + +absl::StatusOr OnMipNode( + const models::MathOptCircuit& circuit, const math_opt::CallbackData& data) { + math_opt::CallbackResult result; + if (!data.solution.has_value()) { + return result; + } + // The values of edge_threshold and min_violation should be in (0, 1). They + // were not tuned for this problem, values that worked well on other problems + // were reused. + ASSIGN_OR_RETURN(const std::vector cuts, + circuit.QuickSeparate(*data.solution, + /*edge_threshold=*/0.5, + /*min_violation*/ 0.05)); + + for (const models::MathOptCircuit::Cutset& cutset : cuts) { + result.AddLazyConstraint(circuit.CreateCutsetConstraint(cutset)); + } + return result; +} + +absl::Status Main() { + int dim = absl::GetFlag(FLAGS_dim); + Node num_nodes{std::pow(2, dim)}; + std::vector edges; + for (Node v : MakeStrongIntRange(num_nodes)) { + for (int d = 0; d < dim; ++d) { + const Node w = Neighbor(v, d); + if (v < w) { + edges.push_back({v, w}); + } + } + } + // All nodes are optional. + util_intops::StrongVector must_be_visited(num_nodes); + // To break symmetry, we can fix a few variables. + if (absl::GetFlag(FLAGS_fix)) { + Node start{0}; + Node one = Neighbor(start, 0); + Node two = Neighbor(one, 1); + must_be_visited[start] = true; + must_be_visited[one] = true; + must_be_visited[two] = true; + } + const models::CircuitConstraint circuit(must_be_visited, /*directed=*/false, + edges); + + math_opt::Model model; + const models::MathOptCircuit math_circuit(circuit, &model); + // We can also fix some edges. + if (absl::GetFlag(FLAGS_fix)) { + Node start{0}; + Node one = Neighbor(start, 0); + Node two = Neighbor(one, 1); + model.set_lower_bound(math_circuit.edge_var_or_die({start, one}), 1.0); + model.set_lower_bound(math_circuit.edge_var_or_die({one, two}), 1.0); + } + math_opt::LinearExpression nodes_hit; + for (Node v : MakeStrongIntRange(num_nodes)) { + nodes_hit += math_circuit.node(v); + for (int d = 0; d < dim; ++d) { + const Node w = Neighbor(v, d); + if (v < w) { + model.AddLinearConstraint(math_circuit.node(v) + math_circuit.node(w) <= + 1 + math_circuit.edge_var_or_die({v, w})); + } + } + } + if (absl::GetFlag(FLAGS_medusa)) { + util_intops::StrongVector num_adj; + for (const Node n : MakeStrongIntRange(num_nodes)) { + num_adj.push_back(2 * math_circuit.node(n)); + } + // 2-Medusa cuts + for (Node v1 : MakeStrongIntRange(num_nodes)) { + for (int i = 0; i < dim; ++i) { + for (int j = i + 1; j < dim; ++j) { + Node v2 = Neighbor(v1, i); + Node v3 = Neighbor(v1, j); + Node v4 = Neighbor(v2, j); + if (v1 > v2 && v1 > v3 && v1 > v4) { + math_opt::LinearExpression arcs = + num_adj[v1] + num_adj[v2] + num_adj[v3] + num_adj[v4]; + for (const auto edge : + std::vector({{v1, v2}, {v1, v3}, {v2, v4}, {v3, v4}})) { + arcs -= math_circuit.edge_var_or_die(edge); + } + model.AddLinearConstraint(arcs <= 4.0); + } + } + } + } + // 3-Medusa cuts + for (Node flip_none : MakeStrongIntRange(num_nodes)) { + for (int i = 0; i < dim; ++i) { + for (int j = i + 1; j < dim; ++j) { + for (int j = i + 1; j < dim; ++j) { + for (int k = j + 1; k < dim; ++k) { + Node flip_i = Neighbor(flip_none, i); + Node flip_j = Neighbor(flip_none, j); + Node flip_k = Neighbor(flip_none, k); + Node flip_ij = Neighbor(flip_i, j); + Node flip_ik = Neighbor(flip_i, k); + Node flip_jk = Neighbor(flip_j, k); + Node flip_ijk = Neighbor(flip_ij, k); + const std::vector nodes = {flip_none, flip_i, flip_j, + flip_k, flip_ij, flip_ik, + flip_jk, flip_ijk}; + if (flip_none != *absl::c_max_element(nodes)) { + continue; + } + math_opt::LinearExpression arcs; + for (const Node n : nodes) { + arcs += num_adj[n]; + } + std::vector internal_edges = { + {flip_none, flip_i}, {flip_none, flip_j}, + {flip_none, flip_k}, {flip_i, flip_ij}, + {flip_i, flip_ik}, {flip_j, flip_ij}, + {flip_j, flip_jk}, {flip_k, flip_ik}, + {flip_k, flip_jk}, {flip_ij, flip_ijk}, + {flip_jk, flip_ijk}, {flip_ik, flip_ijk}}; + + for (const auto edge : internal_edges) { + arcs -= math_circuit.edge_var_or_die(edge); + } + model.AddLinearConstraint(arcs <= 6.0); + } + } + } + } + } + } + model.Maximize(nodes_hit); + const math_opt::SolverType solver = absl::GetFlag(FLAGS_gurobi) + ? math_opt::SolverType::kGurobi + : math_opt::SolverType::kGscip; + math_opt::SolveArguments args; + args.callback_registration = { + .events = {math_opt::CallbackEvent::kMipNode, + math_opt::CallbackEvent::kMipSolution}, + .add_lazy_constraints = true}; + args.parameters = {.enable_output = true, + .time_limit = absl::GetFlag(FLAGS_time_limit), + .threads = absl::GetFlag(FLAGS_threads)}; + args.callback = + [&math_circuit]( + const math_opt::CallbackData& data) -> math_opt::CallbackResult { + switch (data.event) { + case math_opt::CallbackEvent::kMipNode: + return OnMipNode(math_circuit, data).value(); + case math_opt::CallbackEvent::kMipSolution: + return OnMipSolution(math_circuit, data).value(); + default: + LOG(FATAL) << "unexpected event: " << data.event; + } + }; + ASSIGN_OR_RETURN(const math_opt::SolveResult result, + RunSolve(model, solver, std::move(args))); + RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); + std::cout << "Objective value: " << result.objective_value() << std::endl; + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/python/BUILD.bazel b/ortools/math_opt/samples/python/BUILD.bazel index a5ae3c71197..b85d3cfcc1d 100644 --- a/ortools/math_opt/samples/python/BUILD.bazel +++ b/ortools/math_opt/samples/python/BUILD.bazel @@ -114,7 +114,6 @@ py_binary( deps = [ requirement("absl-py"), "//ortools/math_opt/python:mathopt", - "//ortools/math_opt/solvers:highs_solver", ], ) diff --git a/ortools/math_opt/samples/python/linear_regression.py b/ortools/math_opt/samples/python/linear_regression.py index b48367421d8..b44f86c9ac5 100644 --- a/ortools/math_opt/samples/python/linear_regression.py +++ b/ortools/math_opt/samples/python/linear_regression.py @@ -39,9 +39,6 @@ After solving the optimization problem above to recover values for beta, the in sample and out of sample loss (average squared prediction error) for the learned model are printed. - -For an advanced version, see: - ortools/math_opt/codelabs/regression/ """ from collections.abc import Sequence import dataclasses diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index e56c1e304a4..7bda1d707da 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -89,7 +89,6 @@ cc_library( "//ortools/math_opt/core:solver_interface", "//ortools/math_opt/core:sparse_vector_view", "//ortools/math_opt/solvers/gurobi:g_gurobi", - "//ortools/third_party_solvers:gurobi_environment", "//ortools/util:solve_interrupter", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/status", @@ -139,7 +138,6 @@ cc_library( "//ortools/math_opt/solvers/gurobi:g_gurobi", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", - "//ortools/third_party_solvers:gurobi_environment", "//ortools/util:solve_interrupter", "//ortools/util:testing_utils", "@abseil-cpp//absl/algorithm:container", @@ -283,7 +281,6 @@ cc_test( name = "cp_sat_solver_test", timeout = "eternal", srcs = ["cp_sat_solver_test.cc"], - shard_count = 10, tags = ["manual"], deps = [ ":cp_sat_solver", @@ -400,7 +397,6 @@ cc_test( name = "pdlp_solver_test", size = "small", srcs = ["pdlp_solver_test.cc"], - shard_count = 10, deps = [ ":pdlp_solver", "//ortools/base:gmock", @@ -575,7 +571,6 @@ cc_test( name = "highs_solver_test", srcs = ["highs_solver_test.cc"], flaky = 1, - shard_count = 10, # See note above ":highs_solver" rule. tags = ["nosan"], deps = [ diff --git a/ortools/math_opt/solvers/gscip/gscip_event_handler_test.cc b/ortools/math_opt/solvers/gscip/gscip_event_handler_test.cc index 963428c0e99..f3d485d8748 100644 --- a/ortools/math_opt/solvers/gscip/gscip_event_handler_test.cc +++ b/ortools/math_opt/solvers/gscip/gscip_event_handler_test.cc @@ -219,7 +219,6 @@ TEST(GScipEventHandlerDeathTest, ErrorReturnedByInit) { ASSERT_OK(handler.Register(gscip.get())); const absl::Status status = gscip->Solve().status(); - LOG(ERROR) << "status: " << status; if (!status.ok() && absl::StrContains(status.message(), "SCIP error code -8")) { // Write the expected marker only if we see the expected error. diff --git a/ortools/math_opt/solvers/highs_solver.cc b/ortools/math_opt/solvers/highs_solver.cc index ac730f51157..0bf7506b274 100644 --- a/ortools/math_opt/solvers/highs_solver.cc +++ b/ortools/math_opt/solvers/highs_solver.cc @@ -219,7 +219,7 @@ absl::StatusOr> MakeOptions( if (parameters.has_threads()) { // Do not assign result.threads = parameters.threads() here, this is // requires global synchronization. See - // cs/highs/src/lp_data/Highs.cpp:607 + // highs/src/lp_data/Highs.cpp:607 return util::InvalidArgumentErrorBuilder() << "threads not supported for HiGHS solver, this must be set using " "globals, see HiGHS documentation"; diff --git a/ortools/math_opt/solvers/osqp.proto b/ortools/math_opt/solvers/osqp.proto index 51031377465..f064d977c39 100644 --- a/ortools/math_opt/solvers/osqp.proto +++ b/ortools/math_opt/solvers/osqp.proto @@ -92,8 +92,8 @@ message OsqpSettingsProto { // LINT.ThenChange(osqp_solver.cc:merge_from_proto, // osqp_solver.cc:must_initialize, osqp_solver.cc:solve, // osqp_solver.cc:try_overwrite_with_current_values, -// //depot/osqp_cpp/include/osqp++.h:osqp_settings, -// //depot/osqp_cpp/src/osqp++.cc:settings_translation) +// osqp_cpp/include/osqp++.h:osqp_settings, +// osqp_cpp/src/osqp++.cc:settings_translation) // Solver-specific output for OSQP. message OsqpOutput { From 6986bf0eeb4fc6ff8e9c96e8df31ea6f9c75435b Mon Sep 17 00:00:00 2001 From: Guillaume Chatelet Date: Mon, 8 Sep 2025 14:21:18 +0000 Subject: [PATCH 058/491] Remove dead code --- ortools/graph/BUILD.bazel | 17 ----- ortools/graph/line_graph.h | 62 ------------------ ortools/graph/line_graph_test.cc | 104 ------------------------------- 3 files changed, 183 deletions(-) delete mode 100644 ortools/graph/line_graph.h delete mode 100644 ortools/graph/line_graph_test.cc diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index 808840b9066..2fdf41e7b44 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -612,17 +612,6 @@ cc_test( ], ) -cc_test( - name = "line_graph_test", - srcs = ["line_graph_test.cc"], - deps = [ - ":line_graph", - "//ortools/base:gmock_main", - "//ortools/graph", - "@abseil-cpp//absl/base:core_headers", - ], -) - # Linear Assignment with full-featured interface and efficient # implementation. cc_library( @@ -753,12 +742,6 @@ cc_library( ], ) -cc_library( - name = "line_graph", - hdrs = ["line_graph.h"], - deps = ["@abseil-cpp//absl/log"], -) - cc_test( name = "rooted_tree_test", srcs = ["rooted_tree_test.cc"], diff --git a/ortools/graph/line_graph.h b/ortools/graph/line_graph.h deleted file mode 100644 index ee849cfdbf2..00000000000 --- a/ortools/graph/line_graph.h +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2010-2025 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_GRAPH_LINE_GRAPH_H_ -#define OR_TOOLS_GRAPH_LINE_GRAPH_H_ - -#include "absl/log/log.h" - -namespace operations_research { - -// Builds a directed line graph for `graph` (see "directed line graph" in -// http://en.wikipedia.org/wiki/Line_graph). Arcs of the original graph -// become nodes and the new graph contains only nodes created from arcs in the -// original graph (we use the notation (a->b) for these new nodes); the index -// of the node (a->b) in the new graph is exactly the same as the index of the -// arc a->b in the original graph. -// An arc from node (a->b) to node (c->d) in the new graph is added if and only -// if b == c in the original graph. -// This method expects that `line_graph` is an empty graph (it has no nodes -// and no arcs). -// Returns false on an error. -template -bool BuildLineGraph(const GraphType& graph, GraphType* const line_graph) { - if (line_graph == nullptr) { - LOG(DFATAL) << "line_graph must not be NULL"; - return false; - } - if (line_graph->num_nodes() != 0) { - LOG(DFATAL) << "line_graph must be empty"; - return false; - } - // Sizing then filling. - using NodeIndex = typename GraphType::NodeIndex; - using ArcIndex = typename GraphType::ArcIndex; - ArcIndex num_arcs = 0; - for (const ArcIndex arc : graph.AllForwardArcs()) { - const NodeIndex head = graph.Head(arc); - num_arcs += graph.OutDegree(head); - } - line_graph->Reserve(graph.num_arcs(), num_arcs); - for (const ArcIndex arc : graph.AllForwardArcs()) { - const NodeIndex head = graph.Head(arc); - for (const ArcIndex outgoing_arc : graph.OutgoingArcs(head)) { - line_graph->AddArc(arc, outgoing_arc); - } - } - return true; -} - -} // namespace operations_research - -#endif // OR_TOOLS_GRAPH_LINE_GRAPH_H_ diff --git a/ortools/graph/line_graph_test.cc b/ortools/graph/line_graph_test.cc deleted file mode 100644 index 5ffeb913ef3..00000000000 --- a/ortools/graph/line_graph_test.cc +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2010-2025 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/graph/line_graph.h" - -#include "absl/base/macros.h" -#include "gtest/gtest.h" -#include "ortools/graph/graph.h" - -namespace operations_research { -namespace { - -// Empty fixture templates to collect the types of graphs on which we want to -// base the shortest paths template instances that we test. -template -class LineGraphDeathTest : public testing::Test {}; -template -class LineGraphTest : public testing::Test {}; - -typedef testing::Types<::util::ListGraph<>, ::util::ReverseArcListGraph<>> - GraphTypesForLineGraphTesting; - -TYPED_TEST_SUITE(LineGraphDeathTest, GraphTypesForLineGraphTesting); -TYPED_TEST_SUITE(LineGraphTest, GraphTypesForLineGraphTesting); - -TYPED_TEST(LineGraphDeathTest, NullLineGraph) { - TypeParam graph; -#ifndef NDEBUG - EXPECT_DEATH(BuildLineGraph(graph, nullptr), - "line_graph must not be NULL"); -#else - EXPECT_FALSE(BuildLineGraph(graph, nullptr)); -#endif -} - -TYPED_TEST(LineGraphDeathTest, NonEmptyLineGraph) { - TypeParam graph; - TypeParam line_graph(1, 1); - line_graph.AddArc(0, 0); -#ifndef NDEBUG - EXPECT_DEATH(BuildLineGraph(graph, &line_graph), - "line_graph must be empty"); -#else - EXPECT_FALSE(BuildLineGraph(graph, &line_graph)); -#endif -} - -TYPED_TEST(LineGraphDeathTest, LineGraphOfEmptyGraph) { - TypeParam graph; - TypeParam line_graph; - EXPECT_TRUE(BuildLineGraph(graph, &line_graph)); - EXPECT_EQ(0, line_graph.num_nodes()); - EXPECT_EQ(0, line_graph.num_arcs()); -} - -TYPED_TEST(LineGraphTest, LineGraphOfSingleton) { - TypeParam graph(1, 1); - graph.AddArc(0, 0); - TypeParam line_graph; - EXPECT_TRUE(BuildLineGraph(graph, &line_graph)); - EXPECT_EQ(1, line_graph.num_nodes()); - EXPECT_EQ(1, line_graph.num_arcs()); -} - -TYPED_TEST(LineGraphTest, LineGraph) { - const typename TypeParam::NodeIndex kNodes = 4; - const typename TypeParam::ArcIndex kArcs[][2] = { - {0, 1}, {0, 2}, {1, 2}, {2, 0}, {2, 3}}; - const typename TypeParam::ArcIndex kExpectedLineArcs[][2] = { - {0, 2}, {2, 3}, {3, 0}, {3, 1}, {2, 4}, {1, 4}, {1, 3}}; - TypeParam graph(kNodes, ABSL_ARRAYSIZE(kArcs)); - for (int i = 0; i < ABSL_ARRAYSIZE(kArcs); ++i) { - graph.AddArc(kArcs[i][0], kArcs[i][1]); - } - TypeParam line_graph; - EXPECT_TRUE(BuildLineGraph(graph, &line_graph)); - EXPECT_EQ(ABSL_ARRAYSIZE(kArcs), line_graph.num_nodes()); - EXPECT_EQ(ABSL_ARRAYSIZE(kExpectedLineArcs), line_graph.num_arcs()); - for (int i = 0; i < ABSL_ARRAYSIZE(kExpectedLineArcs); ++i) { - const typename TypeParam::NodeIndex expected_tail = kExpectedLineArcs[i][0]; - const typename TypeParam::NodeIndex expected_head = kExpectedLineArcs[i][1]; - bool found = false; - for (const auto arc : line_graph.OutgoingArcs(expected_tail)) { - if (line_graph.Head(arc) == expected_head) { - found = true; - break; - } - } - EXPECT_TRUE(found) << expected_tail << " " << expected_head; - } -} - -} // namespace -} // namespace operations_research From 5368e10078e726228999fc2f25367624f9fcebc2 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 8 Sep 2025 17:21:41 +0200 Subject: [PATCH 059/491] math_opt: disable broken sample --- ortools/math_opt/samples/cpp/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/ortools/math_opt/samples/cpp/CMakeLists.txt b/ortools/math_opt/samples/cpp/CMakeLists.txt index d4137bf4047..c116852e440 100644 --- a/ortools/math_opt/samples/cpp/CMakeLists.txt +++ b/ortools/math_opt/samples/cpp/CMakeLists.txt @@ -18,6 +18,7 @@ endif() if(BUILD_CXX_SAMPLES) file(GLOB CXX_SRCS "*.cc") list(FILTER CXX_SRCS EXCLUDE REGEX "area_socp.cc$") + list(FILTER CXX_SRCS EXCLUDE REGEX "coil_in_the_box.cc$") # missing base/context.h if(NOT USE_SCIP) list(FILTER CXX_SRCS EXCLUDE REGEX "cocktail_hour.cc$") From b0f57c69b7d3171c4a1e28cdbca243420dced7d2 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 8 Sep 2025 17:24:46 +0200 Subject: [PATCH 060/491] ci: disable cmake toolchain jobs flacky, too long without cache --- .github/workflows/aarch64_toolchain.yml | 2 +- .github/workflows/mips_toolchain.yml | 2 +- .github/workflows/powerpc_toolchain.yml | 2 +- cmake/README.md | 11 ----------- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/aarch64_toolchain.yml b/.github/workflows/aarch64_toolchain.yml index ba441f646c2..2dcd2f70780 100644 --- a/.github/workflows/aarch64_toolchain.yml +++ b/.github/workflows/aarch64_toolchain.yml @@ -1,7 +1,7 @@ # ref: https://toolchains.bootlin.com/ name: aarch64 Toolchain -on: [push, pull_request, workflow_dispatch] +on: [workflow_dispatch] concurrency: group: ${{github.workflow}}-${{github.ref}} diff --git a/.github/workflows/mips_toolchain.yml b/.github/workflows/mips_toolchain.yml index 1becd15d762..d75e27ce6b8 100644 --- a/.github/workflows/mips_toolchain.yml +++ b/.github/workflows/mips_toolchain.yml @@ -1,7 +1,7 @@ # ref: https://codescape.mips.com/components/toolchain/2021.09-01/downloads.html name: mips Toolchain -on: [push, pull_request, workflow_dispatch] +on: [workflow_dispatch] concurrency: group: ${{github.workflow}}-${{github.ref}} diff --git a/.github/workflows/powerpc_toolchain.yml b/.github/workflows/powerpc_toolchain.yml index d23e220bc72..76710e5ab07 100644 --- a/.github/workflows/powerpc_toolchain.yml +++ b/.github/workflows/powerpc_toolchain.yml @@ -1,7 +1,7 @@ # ref: https://toolchains.bootlin.com/ name: powerpc Toolchain -on: [push, pull_request, workflow_dispatch] +on: [workflow_dispatch] concurrency: group: ${{github.workflow}}-${{github.ref}} diff --git a/cmake/README.md b/cmake/README.md index ff84170a29b..ea958089667 100644 --- a/cmake/README.md +++ b/cmake/README.md @@ -51,17 +51,6 @@ Dockers: [docker_svg]: ./../../../actions/workflows/amd64_docker_cmake.yml/badge.svg?branch=main [docker_link]: ./../../../actions/workflows/amd64_docker_cmake.yml -[![Build Status][aarch64_toolchain_status]][aarch64_toolchain_link] -[![Build Status][mips_toolchain_status]][mips_toolchain_link] -[![Build Status][powerpc_toolchain_status]][powerpc_toolchain_link] - -[aarch64_toolchain_status]: ./../../../actions/workflows/aarch64_toolchain.yml/badge.svg?branch=main -[aarch64_toolchain_link]: ./../../../actions/workflows/aarch64_toolchain.yml -[mips_toolchain_status]: ./../../../actions/workflows/mips_toolchain.yml/badge.svg?branch=main -[mips_toolchain_link]: ./../../../actions/workflows/mips_toolchain.yml -[powerpc_toolchain_status]: ./../../../actions/workflows/powerpc_toolchain.yml/badge.svg?branch=main -[powerpc_toolchain_link]: ./../../../actions/workflows/powerpc_toolchain.yml - ## Introduction