// 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/linear_programming_constraint.h" #include #include #include #include #include #include "absl/log/check.h" #include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/lp_data/lp_types.h" #include "ortools/sat/cp_model_solver.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/integer_search.h" #include "ortools/sat/linear_constraint.h" #include "ortools/sat/linear_constraint_manager.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/sat/sat_solver.h" #include "ortools/util/strong_integers.h" namespace operations_research { namespace sat { namespace { TEST(ScatteredIntegerVectorTest, BasicDenseBehavior) { ScatteredIntegerVector v; v.ClearAndResize(10); v.AddLinearExpressionMultiple(IntegerValue(4), {glop::ColIndex(2)}, {IntegerValue(3)}, IntegerValue(3)); v.AddLinearExpressionMultiple(IntegerValue(3), {glop::ColIndex(1)}, {IntegerValue(3)}, IntegerValue(3)); v.AddLinearExpressionMultiple(IntegerValue(5), {glop::ColIndex(2)}, {IntegerValue(3)}, IntegerValue(3)); const std::vector> expected{ {glop::ColIndex(1), IntegerValue(3 * 3)}, {glop::ColIndex(2), IntegerValue(3 * 4 + 5 * 3)}}; EXPECT_FALSE(v.IsSparse()); EXPECT_EQ(v.GetTerms(), expected); } TEST(ScatteredIntegerVectorTest, BasicSparseBehavior) { ScatteredIntegerVector v; v.ClearAndResize(100000); v.AddLinearExpressionMultiple(IntegerValue(4), {glop::ColIndex(2)}, {IntegerValue(3)}, IntegerValue(3)); v.AddLinearExpressionMultiple(IntegerValue(3), {glop::ColIndex(1)}, {IntegerValue(3)}, IntegerValue(3)); v.AddLinearExpressionMultiple(IntegerValue(5), {glop::ColIndex(2)}, {IntegerValue(3)}, IntegerValue(3)); const std::vector> expected{ {glop::ColIndex(1), IntegerValue(3 * 3)}, {glop::ColIndex(2), IntegerValue(3 * 4 + 5 * 3)}}; EXPECT_EQ(v.GetTerms(), expected); EXPECT_TRUE(v.IsSparse()); } // TODO(user): Check that SAT solutions respect linear equations. struct LPProblem { const std::vector integer_lb; const std::vector integer_ub; const std::vector constraint_lb; const std::vector constraint_ub; const std::vector> constraint_integer_indices; const std::vector> constraint_integer_coefs; const std::vector objective_indices; int num_integer_vars() const { return integer_lb.size(); } int num_constraints() const { return constraint_lb.size(); } int num_objectives() const { return objective_indices.size(); } }; // Generates a problem that encodes a permutation as a bipartite matching // from size 'left' nodes to size 'right' nodes. // Decision variables are edges linking left nodes to right nodes. // For a given node, only one adjacent edge can be present, // which is encoded by one constraint per node. LPProblem GeneratePermutationProblem(int size) { const std::vector edge_lb(size * size, IntegerValue(0)); const std::vector edge_ub(size * size, IntegerValue(1)); const std::vector node_lb(2 * size, IntegerValue(1)); const std::vector node_ub(2 * size, IntegerValue(1)); std::vector> node_constraint_indices; std::vector> node_constraint_coefs; // Left and right nodes are indexed by [0, size). // The edge (left, right) has number left * size + right. const std::vector ones(size, IntegerValue(1)); for (int left = 0; left < size; left++) { std::vector indices; for (int right = 0; right < size; right++) { indices.push_back(left * size + right); } node_constraint_indices.push_back(indices); node_constraint_coefs.push_back(ones); } for (int right = 0; right < size; right++) { std::vector indices; for (int left = 0; left < size; left++) { indices.push_back(left * size + right); } node_constraint_indices.push_back(indices); node_constraint_coefs.push_back(ones); } return LPProblem{edge_lb, edge_ub, node_lb, node_ub, node_constraint_indices, node_constraint_coefs, {}}; } int CountSolutionsOfLPProblemUsingSAT(const LPProblem& problem) { Model model; model.GetOrCreate()->set_add_lp_constraints_lazily(false); std::vector cp_variables; const int num_cp_vars = problem.num_integer_vars(); for (int i = 0; i < num_cp_vars; i++) { IntegerVariable var = model.Add(NewIntegerVariable( problem.integer_lb[i].value(), problem.integer_ub[i].value())); cp_variables.push_back(var); } LinearProgrammingConstraint* lp = new LinearProgrammingConstraint(&model, cp_variables); model.TakeOwnership(lp); const int num_constraints = problem.num_constraints(); for (int c = 0; c < num_constraints; c++) { LinearConstraint ct(problem.constraint_lb[c], problem.constraint_ub[c]); const int num_integer = problem.constraint_integer_indices[c].size(); ct.resize(num_integer); for (int j = 0; j < num_integer; j++) { ct.vars[j] = cp_variables[problem.constraint_integer_indices[c][j]]; ct.coeffs[j] = problem.constraint_integer_coefs[c][j]; } lp->AddLinearConstraint(std::move(ct)); } lp->RegisterWith(&model); int num_solutions = 0; while (SolveIntegerProblemWithLazyEncoding(&model) == SatSolver::Status::FEASIBLE) { model.Add(ExcludeCurrentSolutionAndBacktrack()); num_solutions++; } return num_solutions; } TEST(LinearProgrammingConstraintTest, CountPermutations) { int factorial_of_size = 1; for (int size = 2; size < 6; size++) { factorial_of_size *= size; LPProblem problem = GeneratePermutationProblem(size); ASSERT_EQ(CountSolutionsOfLPProblemUsingSAT(problem), factorial_of_size); } } TEST(LinearProgrammingConstraintTest, SimpleInfeasibility) { // The following flow is infeasible, LP should detect it. // // source // = 2/2 0/2 // ---------> ---------> A // | | // | | // 0/2 | | 0/2 // | | // V V // B -------> -------> // 0/2 [3,4] = sink Model model; // We need this parameter at false to detect it on the first propagation. model.Add(NewSatParameters("add_lp_constraints_lazily:false")); // Variables for the source and sink demands, and flow amounts. const IntegerVariable source = model.Add(NewIntegerVariable(2, 2)); const IntegerVariable source_a = model.Add(NewIntegerVariable(0, 2)); const IntegerVariable source_b = model.Add(NewIntegerVariable(0, 2)); const IntegerVariable a_sink = model.Add(NewIntegerVariable(0, 2)); const IntegerVariable b_sink = model.Add(NewIntegerVariable(0, 2)); const IntegerVariable sink = model.Add(NewIntegerVariable(3, 4)); // LP Constraint and flow conservation equalities. LinearProgrammingConstraint* lp = new LinearProgrammingConstraint( &model, {source, source_a, source_b, a_sink, b_sink, sink}); model.TakeOwnership(lp); LinearConstraintBuilder ct_source(IntegerValue(0), IntegerValue(0)); ct_source.AddTerm(source, IntegerValue(1)); ct_source.AddTerm(source_a, IntegerValue(-1)); ct_source.AddTerm(source_b, IntegerValue(-1)); lp->AddLinearConstraint(ct_source.Build()); LinearConstraintBuilder ct_a(IntegerValue(0), IntegerValue(0)); ct_a.AddTerm(source_a, IntegerValue(1)); ct_a.AddTerm(a_sink, IntegerValue(-1)); lp->AddLinearConstraint(ct_a.Build()); LinearConstraintBuilder ct_b(IntegerValue(0), IntegerValue(0)); ct_b.AddTerm(source_b, IntegerValue(1)); ct_b.AddTerm(b_sink, IntegerValue(-1)); lp->AddLinearConstraint(ct_b.Build()); LinearConstraintBuilder ct_sink(IntegerValue(0), IntegerValue(0)); ct_sink.AddTerm(a_sink, IntegerValue(1)); ct_sink.AddTerm(b_sink, IntegerValue(1)); ct_sink.AddTerm(sink, IntegerValue(-1)); lp->AddLinearConstraint(ct_sink.Build()); lp->RegisterWith(&model); ASSERT_FALSE(model.GetOrCreate()->Propagate()); } TEST(LinearProgrammingConstraintTest, EmptyLP) { Model model; model.Add(NewSatParameters("linearization_level:2")); LinearProgrammingConstraint* lp = new LinearProgrammingConstraint(&model, {}); model.TakeOwnership(lp); lp->RegisterWith(&model); ASSERT_TRUE(model.GetOrCreate()->Propagate()); } // This tests that the scaling of reduced costs is done correctly, // using the problem // min 64 x s.t. 0 <= 27x <= 81, x in [0, 50] // The reduced cost of x should be 64. // This will be wrong if the reduced cost ignores the objective scaling. TEST(LinearProgrammingConstraintTest, ReducedCostScalingObjective) { Model m; IntegerVariable x = m.Add(NewIntegerVariable(0, 50)); LinearProgrammingConstraint* lp = new LinearProgrammingConstraint(&m, {x}); m.TakeOwnership(lp); LinearConstraintBuilder ct(IntegerValue(0), IntegerValue(81)); ct.AddTerm(x, IntegerValue(27)); lp->AddLinearConstraint(ct.Build()); IntegerVariable obj = m.Add(NewIntegerVariable(0, 64 * 50)); lp->SetObjectiveCoefficient(x, IntegerValue(64)); lp->SetMainObjectiveVariable(obj); lp->RegisterWith(&m); lp->Propagate(); const auto& reduced_costs = *m.GetOrCreate(); CHECK_LE(std::abs(reduced_costs[x] - 64), 1e-6); } // This tests that the scaling of reduced costs is done correctly, // using the problem // min 64 x + 32 y s.t. 0 <= 27x + 9y <= 81, x in [0, 50], y in [0, 20] // The reduced cost of x should be 64, y should be 20. // This will be wrong if the reduced cost ignores the column scaling. TEST(LinearProgrammingConstraintTest, ReducedCostScalingColumns) { Model m; IntegerVariable x = m.Add(NewIntegerVariable(0, 50)); IntegerVariable y = m.Add(NewIntegerVariable(0, 20)); LinearProgrammingConstraint* lp = new LinearProgrammingConstraint(&m, {x, y}); m.TakeOwnership(lp); LinearConstraintBuilder ct(IntegerValue(0), IntegerValue(81)); ct.AddTerm(x, IntegerValue(27)); ct.AddTerm(y, IntegerValue(9)); lp->AddLinearConstraint(ct.Build()); IntegerVariable obj = m.Add(NewIntegerVariable(0, 64 * 50 + 32 * 20)); lp->SetObjectiveCoefficient(x, IntegerValue(64)); lp->SetObjectiveCoefficient(y, IntegerValue(32)); lp->SetMainObjectiveVariable(obj); lp->RegisterWith(&m); lp->Propagate(); const auto& reduced_costs = *m.GetOrCreate(); CHECK_LE(std::abs(reduced_costs[x] - 64), 1e-6); CHECK_LE(std::abs(reduced_costs[y] - 32), 1e-6); } } // namespace } // namespace sat } // namespace operations_research