// Copyright 2010-2021 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Matchers for MathOpt types, specifically SolveResult and nested fields. // // The matchers defined here are useful for writing unit tests checking that the // result of Solve(), absl::StatusOr, meets expectations. We give // some examples below. All code is assumed with the following setup: // // namespace operations_research::math_opt { // using ::testing::status::IsOkAndHolds; // // Model model; // const Variable x = model.AddContinuousVariable(0.0, 1.0); // const Variable y = model.AddContinuousVariable(0.0, 1.0); // const LinearConstraint c = model.AddLinearConstraint(x + y <= 1); // model.Maximize(2*x + y); // // Example 1.a: result is OK, optimal, and objective value approximately 42. // EXPECT_THAT(Solve(model, SOLVER_TYPE_GLOP), IsOkAndHolds(IsOptimal(42))); // // Example 1.b: equivalent to 1.a. // ASSERT_OK_AND_ASSIGN(const SolveResult result, // Solve(model, SOLVER_TYPE_GLOP)); // EXPECT_THAT(result, IsOptimal(42)); // // Example 2: result is OK, optimal, and best solution is x=1, y=0. // ASSERT_OK_AND_ASSIGN(const SolveResult result, // Solve(model, SOLVER_TYPE_GLOP)); // ASSERT_THAT(result, IsOptimal()); // EXPECT_THAT(result.variable_value(), IsNear({{x, 1}, {y, 0}}); // Note: the second ASSERT ensures that if the solution is not optimal, then // result.variable_value() will not run (the function will crash if the solver // didn't find a solution). Further, MathOpt guarantees there is a solution // when the termination reason is optimal. // // Example 3: result is OK, check the solution without specifying termination. // ASSERT_OK_AND_ASSIGN(const SolveResult result, // Solve(model, SOLVER_TYPE_GLOP)); // EXPECT_THAT(result, HasBestSolution({{x, 1}, {y, 0}})); // // Example 4: multiple possible termination reason, primal ray optional: // ASSERT_OK_AND_ASSIGN(const SolveResult result, // Solve(model, SOLVER_TYPE_GLOP)); // ASSERT_THAT(result, TerminatesWithOneOf( // TerminationReason::kUnbounded, // TerminationReason::kInfeasibleOrUnbounded)); // if(!result.primal_rays.empty()) { // EXPECT_THAT(result.primal_rays[0], PrimalRayIsNear({{x, 1,}, {y, 0}})); // } // // // Tips on writing good tests: // * Use ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(...)) to ensure // the test terminates immediately if Solve() does not return OK. // * If you ASSERT_THAT(result, IsOptimal()), you can assume that you have a // feasible primal solution afterwards. Otherwise, make no assumptions on // the contents of result (e.g. do not assume result contains a primal ray // just because the termination reason was UNBOUNDED). // * For problems that are infeasible, the termination reasons INFEASIBLE and // DUAL_INFEASIBLE are both possible. Likewise, for unbounded problems, you // can get both UNBOUNDED and DUAL_INFEASIBLE. See TerminatesWithOneOf() // below to make assertions in this case. Note also that some solvers have // solver specific parameters to ensure that DUAL_INFEASIBLE will not be // returned (e.g. for Gurobi, use DualReductions or InfUnbdInfo). // * The objective value and variable values should always be compared up to // a tolerance, even if your decision variables are integer. The matchers // defined have a configurable tolerance with default value 1e-5. // * Primal and dual rays are unique only up to a constant scaling. The // matchers provided rescale both expected and actual before comparing. // * Take care on problems with multiple optimal solutions. Do not rely on a // particular solution being returned in your test, as the test will break // when we upgrade the solver. // // This file also defines functions to let gunit print various MathOpt types. // // To see the error messages these matchers generate, run // blaze test experimental/users/rander/math_opt:matchers_error_messages // which is a fork of matchers_test.cc where the assertions are all negated // (note that every test should fail). #ifndef OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_ #define OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_ #include #include #include #include "gtest/gtest.h" #include "ortools/math_opt/cpp/linear_constraint.h" #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" namespace operations_research { namespace math_opt { constexpr double kMatcherDefaultTolerance = 1e-5; //////////////////////////////////////////////////////////////////////////////// // Matchers for IdMap //////////////////////////////////////////////////////////////////////////////// // Checks that the maps have identical keys and values within tolerance. This // factory will CHECK-fail if expected contains any NaN values. testing::Matcher> IsNear( VariableMap expected, double tolerance = kMatcherDefaultTolerance); // Checks that the keys of actual are a subset of the keys of expected, and that // for all shared keys, the values are within tolerance. This factory will // CHECK-fail if expected contains any NaN values, and any NaN values in the // expression compared against will result in the matcher failing. testing::Matcher> IsNearlySubsetOf( VariableMap expected, double tolerance = kMatcherDefaultTolerance); // Checks that the maps have identical keys and values within tolerance. This // factory will CHECK-fail if expected contains any NaN values, and any NaN // values in the expression compared against will result in the matcher failing. testing::Matcher> IsNear( LinearConstraintMap expected, double tolerance = kMatcherDefaultTolerance); // Checks that the keys of actual are a subset of the keys of expected, and that // for all shared keys, the values are within tolerance. This factory will // CHECK-fail if expected contains any NaN values, and any NaN values in the // expression compared against will result in the matcher failing. testing::Matcher> IsNearlySubsetOf( LinearConstraintMap expected, double tolerance = kMatcherDefaultTolerance); // Checks that the maps have identical keys and values within tolerance. Works // for VariableMap, LinearConstraintMap, among other realizations of IdMap. This // factory will CHECK-fail if expected contains any NaN values, and any NaN // values in the expression compared against will result in the matcher failing. template testing::Matcher> IsNear( IdMap expected, const double tolerance = kMatcherDefaultTolerance); // Checks that the keys of actual are a subset of the keys of expected, and that // for all shared keys, the values are within tolerance. Works for VariableMap, // LinearConstraintMap, among other realizations of IdMap. This factory will // CHECK-fail if expected contains any NaN values, and any NaN values in the // expression compared against will result in the matcher failing. template testing::Matcher> IsNearlySubsetOf( IdMap expected, const double tolerance = kMatcherDefaultTolerance); //////////////////////////////////////////////////////////////////////////////// // Matchers for LinearExpression and QuadraticExpression //////////////////////////////////////////////////////////////////////////////// // Checks that the expressions are structurally identical (i.e., internal maps // have the same keys and storage, coefficients are exactly equal). This factory // will CHECK-fail if expected contains any NaN values, and any NaN values in // the expression compared against will result in the matcher failing. testing::Matcher IsIdentical(LinearExpression expected); // Checks that the expressions are structurally identical (i.e., internal maps // have the same keys and storage, coefficients are exactly equal). This factory // will CHECK-fail if expected contains any NaN values, and any NaN values in // the expression compared against will result in the matcher failing. testing::Matcher IsIdentical(QuadraticExpression expected); //////////////////////////////////////////////////////////////////////////////// // Matchers for solutions //////////////////////////////////////////////////////////////////////////////// // Options for IsNear(Solution). struct SolutionMatcherOptions { double tolerance = kMatcherDefaultTolerance; bool check_primal = true; bool check_dual = true; bool check_basis = true; }; testing::Matcher IsNear(Solution expected, SolutionMatcherOptions options = {}); // Checks variables match and variable/objective values are within tolerance and // feasibility statuses are identical. testing::Matcher IsNear( PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); // Checks dual variables, reduced costs and objective are within tolerance and // feasibility statuses are identical. testing::Matcher IsNear( DualSolution expected, double tolerance = kMatcherDefaultTolerance); testing::Matcher BasisIs(const Basis& expected); //////////////////////////////////////////////////////////////////////////////// // Matchers for a Rays //////////////////////////////////////////////////////////////////////////////// // Checks variables match and that after rescaling, variable values are within // tolerance. testing::Matcher IsNear(PrimalRay expected, double tolerance = kMatcherDefaultTolerance); // Checks variables match and that after rescaling, variable values are within // tolerance. testing::Matcher PrimalRayIsNear( VariableMap expected_var_values, double tolerance = kMatcherDefaultTolerance); // Checks that dual variables and reduced costs are defined for the same // set of Variables/LinearConstraints, and that their rescaled values are within // tolerance. testing::Matcher IsNear(DualRay expected, double tolerance = kMatcherDefaultTolerance); //////////////////////////////////////////////////////////////////////////////// // Matchers for a SolveResult //////////////////////////////////////////////////////////////////////////////// // Checks the following: // * The termination reason is optimal. // * If expected_objective contains a value, there is at least one feasible // solution and that solution has an objective value within tolerance of // expected_objective. testing::Matcher IsOptimal( std::optional expected_objective = std::nullopt, double tolerance = kMatcherDefaultTolerance); testing::Matcher IsOptimalWithSolution( double expected_objective, VariableMap expected_variable_values, double tolerance = kMatcherDefaultTolerance); testing::Matcher IsOptimalWithDualSolution( double expected_objective, LinearConstraintMap expected_dual_values, VariableMap expected_reduced_costs, double tolerance = kMatcherDefaultTolerance); // Checks the following: // * The result has the expected termination reason. testing::Matcher TerminatesWith(TerminationReason expected); // Checks that the result has one of the allowed termination reasons. testing::Matcher TerminatesWithOneOf( const std::vector& allowed); // Checks the following: // * The result has termination reason kFeasible or kNoSolutionFound. // * The limit is expected, or is kUndetermined if allow_limit_undetermined. testing::Matcher TerminatesWithLimit( Limit expected, bool allow_limit_undetermined = false); // Checks the following: // * The result has termination reason kFeasible. // * The limit is expected, or is kUndetermined if allow_limit_undetermined. testing::Matcher TerminatesWithReasonFeasible( Limit expected, bool allow_limit_undetermined = false); // Checks the following: // * The result has termination reason kNoSolutionFound. // * The limit is expected, or is kUndetermined if allow_limit_undetermined. testing::Matcher TerminatesWithReasonNoSolutionFound( Limit expected, bool allow_limit_undetermined = false); // SolveResult has a primal solution matching expected within tolerance. testing::Matcher HasSolution( PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); // SolveResult has a dual solution matching expected within // tolerance. testing::Matcher HasDualSolution( DualSolution expected, double tolerance = kMatcherDefaultTolerance); // Actual SolveResult contains a primal ray that matches expected within // tolerance. testing::Matcher HasPrimalRay( PrimalRay expected, double tolerance = kMatcherDefaultTolerance); // Actual SolveResult contains a primal ray with variable values equivalent to // (under L_inf scaling) expected_vars up to tolerance. testing::Matcher HasPrimalRay( VariableMap expected_vars, double tolerance = kMatcherDefaultTolerance); // Actual SolveResult contains a dual ray that matches expected within // tolerance. testing::Matcher HasDualRay( DualRay expected, double tolerance = kMatcherDefaultTolerance); // Configures SolveResult matcher IsConsistentWith() below. struct SolveResultMatcherOptions { double tolerance = 1e-5; bool first_solution_only = true; bool check_dual = true; bool check_rays = true; // If the expected result has termination reason kInfeasible, kUnbounded, or // kDualInfeasasible, the primal solution, dual solution, and basis are // ignored unless check_solutions_if_inf_or_unbounded is true. // // TODO(b/201099290): this is perhaps not a good default. Gurobi as // implemented is returning primal solutions for both unbounded and // infeasible problems. We need to add unit tests that inspect this value // and turn them on one solver at a time with a new parameter on // SimpleLpTestParameters. bool check_solutions_if_inf_or_unbounded = false; bool check_basis = false; // In linear programming, the following outcomes are all possible // // Primal LP | Dual LP | Possible MathOpt Termination Reasons // ----------------------------------------------------------------- // 1. Infeasible | Unbounded | kInfeasible // 2. Optimal | Optimal | kOptimal // 3. Unbounded | Infeasible | kUnbounded, kInfeasibleOrUnbounded // 4. Infeasible | Infeasible | kInfeasible, kInfeasibleOrUnbounded // // (Above "Optimal" means that an optimal solution exists. This is a statement // about the existence of optimal solutions and certificates of // infeasibility/unboundedness, not about the outcome of applying any // particular algorithm.) // // When writing your unit test, you can typically tell which case of 1-4 you // are in, but in cases 3-4 you do not know which termination reason will be // returned. In some situations, it may not be clear if you are in case 1 or // case 4 as well. // // When inf_or_unb_soft_match=false, the matcher must exactly specify the // status returned by the solver. For cases 3-4, this is implementation // dependent and we do not recommend this. When // inf_or_unb_soft_match=true: // * kInfeasible can also match kInfeasibleOrUnbounded // * kUnbounded can also match kInfeasibleOrUnbounded // * kInfeasibleOrUnbounded can also match kInfeasible and kUnbounded. // For case 2, inf_or_unb_soft_match has no effect. // // To build the strongest possible matcher (accepting the minimal set of // termination reasons): // * If you know you are in case 1, se inf_or_unb_soft_match=false // (soft_match=true over-matches) // * For case 3, use inf_or_unb_soft_match=false and // termination_reason=kUnbounded (kInfeasibleOrUnbounded over-matches). // * For case 4 (or if you are unsure of case 1 vs case 4), use // inf_or_unb_soft_match=true and // termination_reason=kInfeasible (kInfeasibleOrUnbounded over-matches). // * If you cannot tell if you are in case 3 or case 4, use // inf_or_unb_soft_match=true and termination reason // kInfeasibleOrUnbounded. // // If the above is too complicated, always setting // inf_or_unb_soft_match=true and using any of the expected MathOpt // termination reasons from the above table will give a matcher that is // slightly too lenient. bool inf_or_unb_soft_match = true; }; // Tests that two SolveResults are equivalent. Basic use: // // SolveResult expected; // // Fill in expected... // ASSERT_OK_AND_ASSIGN(SolveResult actual, Solve(model, solver_type)); // EXPECT_THAT(actual, IsConsistentWith(expected)); // // Equivalence is defined as follows: // * The termination reasons are the same. // - For infeasible and unbounded problems, see // options.inf_or_unb_soft_match. // * The solve stats are ignored. // * For both primal and dual solutions, either expected and actual are // both empty, or their first entries satisfy IsNear() at options.tolerance. // - Not checked if options.check_solutions_if_inf_or_unbounded and the // problem is infeasible or unbounded (default). // - If options.first_solution_only is false, check the entire list of // solutions matches in the same order. // - Dual solution is not checked if options.check_dual=false // * For both the primal and dual rays, either expected and actual are both // empty, or any ray in expected IsNear() any ray in actual (which is up // to a rescaling) at options.tolerance. // - Not checked if options.check_rays=false // - If options.first_solution_only is false, check the entire list of // solutions matches in the same order. // * The basis is not checked by default. If enabled, checked with BasisIs(). // - Enable with options.check_basis // // This function is symmetric in that: // EXPECT_THAT(actual, IsConsistentWith(expected)); // EXPECT_THAT(expected, IsConsistentWith(actual)); // agree on matching, they only differ in strings produced. Per gmock // conventions, prefer the former. // // For problems with either primal or dual infeasibility, see // SolveResultMatcherOptions::inf_or_unb_soft_match for guidance on how to // best set the termination reason and inf_or_unb_soft_match. testing::Matcher IsConsistentWith( const SolveResult& expected, const SolveResultMatcherOptions& options = {}); //////////////////////////////////////////////////////////////////////////////// // Rarely used //////////////////////////////////////////////////////////////////////////////// // Actual UpdateResult.did_update is true. testing::Matcher DidUpdate(); //////////////////////////////////////////////////////////////////////////////// // Implementation details //////////////////////////////////////////////////////////////////////////////// // TODO(b/200835670): use the << operator on Termination instead once it // supports quoting/escaping on termination.detail. void PrintTo(const Termination& termination, std::ostream* os); void PrintTo(const PrimalSolution& primal_solution, std::ostream* os); void PrintTo(const DualSolution& dual_solution, std::ostream* os); void PrintTo(const PrimalRay& primal_ray, std::ostream* os); void PrintTo(const DualRay& dual_ray, std::ostream* os); void PrintTo(const Basis& basis, std::ostream* os); void PrintTo(const Solution& solution, std::ostream* os); void PrintTo(const SolveResult& result, std::ostream* os); // We do not want to rely on ::testing::internal::ContainerPrinter because we // want to sort the keys. template void PrintTo(const IdMap& id_map, std::ostream* const os) { constexpr int kMaxPrint = 10; int num_added = 0; *os << "{"; for (const K k : id_map.SortedKeys()) { if (num_added > 0) { *os << ", "; } if (num_added >= kMaxPrint) { *os << "...(size=" << id_map.size() << ")"; break; } *os << "{" << k << ", " << ::testing::PrintToString(id_map.at(k)) << "}"; ++num_added; } *os << "}"; } } // namespace math_opt } // namespace operations_research #endif // OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_