// 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/sat/cp_model_postsolve.h" #include #include #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/parse_test_proto.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/util/logging.h" #include "ortools/util/sorted_interval_list.h" namespace operations_research { namespace sat { namespace { using ::google::protobuf::contrib::parse_proto::ParseTestProto; // Note that the postsolve is already tested in many of our solver tests or // random presolve tests. We just have a small unit test here. TEST(PostsolveResponseTest, BasicExample) { // Fixing z will allow the postsolve code to reconstruct all values. const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { name: 'x' domain: 0 domain: 10 } variables { name: 'y' domain: 0 domain: 10 } variables { name: 'z' domain: 0 domain: 10 } constraints { linear { vars: [ 0, 1, 2 ] coeffs: [ 1, 2, -3 ] domain: [ 5, 5 ] } } constraints { linear { vars: [ 1, 2 ] coeffs: [ 3, -1 ] domain: [ 5, 5 ] } } )pb"); std::vector solution = {1}; std::vector postsolve_mapping = {2}; // The solution fix z. PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, postsolve_mapping, &solution); // x + 2y - 3z = 5 // 3y - z = 5 // z = 1 EXPECT_THAT(solution, ::testing::ElementsAre(4, 2, 1)); } TEST(PostsolveResponseTest, ExactlyOneExample1) { const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { name: 'x' domain: 0 domain: 1 } variables { name: 'y' domain: 0 domain: 1 } variables { name: 'z' domain: 0 domain: 1 } constraints { exactly_one { literals: [ 0, 1, 2 ] } } )pb"); std::vector solution = {1}; std::vector postsolve_mapping = {2}; // The solution fix z. PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, postsolve_mapping, &solution); EXPECT_THAT(solution, ::testing::ElementsAre(0, 0, 1)); } TEST(PostsolveResponseTest, ExactlyOneExample2) { const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { name: 'x' domain: 0 domain: 1 } variables { name: 'y' domain: 0 domain: 1 } variables { name: 'z' domain: 0 domain: 1 } constraints { exactly_one { literals: [ 0, 1, 2 ] } } )pb"); std::vector solution = {0}; std::vector postsolve_mapping = {2}; // The solution fix z. PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, postsolve_mapping, &solution); // One variable is set to one. EXPECT_THAT(solution, ::testing::ElementsAre(0, 1, 0)); } TEST(PostsolveResponseTest, FixedTarget) { // Fixing z will allow the postsolve code to reconstruct all values. const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { name: 'index' domain: [ 0, 1 ] } variables { name: 'a' domain: [ 5, 5 ] } variables { name: 'b' domain: [ 7, 7 ] } variables { name: 'target' domain: [ 7, 7 ] } constraints { element { linear_index { vars: 0 coeffs: 1 } linear_target { vars: 3 coeffs: 1 } exprs { vars: 1, coeffs: 1 } exprs { vars: 2, coeffs: 1 } } } )pb"); std::vector solution; std::vector postsolve_mapping = {}; PostsolveResponse(/*num_variables_in_original_model=*/4, mapping_proto, postsolve_mapping, &solution); EXPECT_THAT(solution, ::testing::ElementsAre(1, 5, 7, 7)); } TEST(PostsolveResponseTest, FixedIndex) { const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { domain: [ 7, 7 ] } variables { domain: [ 3, 3 ] } variables { domain: [ 0, 129 ] } variables { domain: [ 2, 2 ] } constraints { element { linear_index { vars: 3 coeffs: 1 } linear_target { vars: 2 coeffs: 1 } exprs { vars: 0, coeffs: 1 } exprs { vars: 1, coeffs: 1 } exprs { vars: 0, coeffs: 1 } } } )pb"); std::vector solution; std::vector postsolve_mapping = {}; PostsolveResponse(/*num_variables_in_original_model=*/4, mapping_proto, postsolve_mapping, &solution); EXPECT_THAT(solution, ::testing::ElementsAre(7, 3, 7, 2)); } // Note that our postolve code is "limited" when it come to solving a single // linear equation since we should only encounter "simple" case. TEST(PostsolveResponseTest, TrickyLinearCase) { // The equation is 2x + y = z // // It mostly work all the time, except if we decide to make z - y not a // multiple of two. This is not necessarily detected by our presolve since // 2 * [0, 124] is too complex to represent. Yet for any value of x and y // there is a possible z, but the reverse is not true, since y = 1, z = 0 is // not feasible. // // The presolve should deal with that by putting z first so that the // postsolve code do not fail. const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { name: 'x' domain: [ 0, 124 ] } variables { name: 'y' domain: [ 0, 1 ] } variables { name: 'z' domain: [ 0, 255 ] } constraints { linear { vars: [ 2, 0, 1 ] coeffs: [ -1, 2, 1 ] domain: [ 0, 0 ] } } )pb"); // The likely response (there are many possible). std::vector solution; CpSolverResponse response; response.set_status(OPTIMAL); std::vector postsolve_mapping; PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, postsolve_mapping, &solution); EXPECT_THAT(solution, ::testing::ElementsAre(0, 0, 0)); } // This used to fail because we where computing the EXACT domain atteignable // by the sum of discrete domains, which have a lot of disjoint part. // // But our presolve was fine, because adding each of them to the loose rhs // domain just result in a domain with a small complexity. TEST(PostsolveResponseTest, ComplexityIssue) { CpModelProto mapping_proto; // N variables such that their sum can be and even number. If we try to // compute the exact domains of their sum, we are quadratic in compexity. const int num_variables = 30; for (int i = 0; i < num_variables; ++i) { IntegerVariableProto* var = mapping_proto.add_variables(); var->add_domain(0); var->add_domain(0); const int value = 1 << (1 + i); var->add_domain(value); var->add_domain(value); } // A linear constraint sum variable in [0, 1e9]. ConstraintProto* ct = mapping_proto.add_constraints(); ct->mutable_linear()->add_domain(0); ct->mutable_linear()->add_domain(1e9); for (int i = 0; i < num_variables; ++i) { ct->mutable_linear()->add_vars(i); ct->mutable_linear()->add_coeffs(1); } // The likely response (there are many possible). std::vector solution; std::vector postsolve_mapping; PostsolveResponse(num_variables, mapping_proto, postsolve_mapping, &solution); ASSERT_EQ(solution.size(), num_variables); } TEST(FillTightenedDomainInResponseTest, BasicBehavior) { // Original model. const CpModelProto original_model = ParseTestProto(R"pb( variables { name: 'x' domain: [ 0, 124 ] } variables { domain: [ 0, 1 ] } variables { domain: [ 0, 255 ] } )pb"); // We might have more variable there. // Also the domains might be tighter. const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { domain: [ 0, 100 ] } variables { domain: [ 0, 1 ] } variables { domain: [ 0, 255 ] } variables { domain: [ 0, 17 ] } variables { domain: [ 0, 18 ] } )pb"); // Lets assume the presolved mode contains 3 variables, 2 in common. std::vector postsolve_mapping{0, 2, 4}; std::vector search_bounds{Domain(0, 100), Domain(0, 0), Domain(3, 7)}; // Call the postsolving. SolverLogger logger; CpSolverResponse response; FillTightenedDomainInResponse(original_model, mapping_proto, postsolve_mapping, search_bounds, &response, &logger); // Lets test by constructing a model for easy comparison. CpModelProto returned_model; for (const IntegerVariableProto& var : response.tightened_variables()) { *returned_model.add_variables() = var; } const CpModelProto expected_model = ParseTestProto(R"pb( variables { name: 'x' domain: [ 0, 100 ] } # presolve reduced the domain. variables { domain: [ 0, 1 ] } # no info. variables { domain: [ 0, 0 ] } # was fixed by search. )pb"); EXPECT_THAT(returned_model, testing::EqualsProto(expected_model)); } TEST(FillTightenedDomainInResponseTest, WithAffine) { // Original model. const CpModelProto original_model = ParseTestProto(R"pb( variables { domain: [ 0, 124 ] } variables { domain: [ 0, 50 ] } variables { domain: [ 0, 255 ] } )pb"); // We might have more variable there. // Also the domains might be tighter. const CpModelProto mapping_proto = ParseTestProto(R"pb( variables { domain: [ 0, 100 ] } variables { domain: [ 0, 50 ] } variables { domain: [ 0, 100 ] } variables { domain: [ 0, 17 ] } variables { domain: [ 0, 18 ] } variables { domain: [ 0, 19 ] } constraints { linear { vars: [ 0, 3 ] coeffs: [ 2, 1 ] domain: [ 10, 10 ] } } constraints { linear { vars: [ 1, 4 ] coeffs: [ 1, 1 ] domain: [ 10, 10 ] } } constraints { linear { vars: [ 5, 2 ] coeffs: [ 2, 1 ] domain: [ 10, 10 ] } } )pb"); std::vector postsolve_mapping{3, 4, 5}; std::vector search_bounds{Domain(0, 20), Domain(0, 20), Domain(3, 5)}; // Call the postsolving. SolverLogger logger; logger.EnableLogging(true); CpSolverResponse response; FillTightenedDomainInResponse(original_model, mapping_proto, postsolve_mapping, search_bounds, &response, &logger); // Lets test by constructing a model for easy comparison. CpModelProto returned_model; for (const IntegerVariableProto& var : response.tightened_variables()) { *returned_model.add_variables() = var; } const CpModelProto expected_model = ParseTestProto(R"pb( variables { domain: [ 0, 5 ] } # 2 * v = 10 - [0, 17] variables { domain: [ 0, 10 ] } # v = 10 - [0, 18] variables { domain: [ 0, 4 ] } # v = 10 - 2 * [3, 5] )pb"); EXPECT_THAT(returned_model, testing::EqualsProto(expected_model)); } } // namespace } // namespace sat } // namespace operations_research