Files
ortools-clone/ortools/sat/cuts_test.cc
Laurent Perron dadaff8ca3 fix #4746
2025-11-05 15:17:34 +01:00

1176 lines
44 KiB
C++

// 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/cuts.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include "absl/numeric/int128.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_vector.h"
#include "ortools/sat/implied_bounds.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_base.h"
#include "ortools/sat/linear_constraint.h"
#include "ortools/sat/linear_constraint_manager.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_base.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/util/fp_utils.h"
#include "ortools/util/sorted_interval_list.h"
#include "ortools/util/strong_integers.h"
namespace operations_research {
namespace sat {
namespace {
using ::testing::DoubleNear;
using ::testing::EndsWith;
using ::testing::StartsWith;
std::vector<IntegerValue> IntegerValueVector(absl::Span<const int> values) {
std::vector<IntegerValue> result;
for (const int v : values) result.push_back(IntegerValue(v));
return result;
}
TEST(GetSuperAdditiveRoundingFunctionTest, AllSmallValues) {
const int max_divisor = 25;
for (IntegerValue max_t(1); max_t <= 9; ++max_t) {
for (IntegerValue max_scaling(1); max_scaling <= 9; max_scaling++) {
for (IntegerValue divisor(1); divisor <= max_divisor; ++divisor) {
for (IntegerValue rhs_remainder(1); rhs_remainder < divisor;
++rhs_remainder) {
const std::string info = absl::StrCat(
" rhs_remainder = ", rhs_remainder.value(),
" divisor = ", divisor.value(), " max_t = ", max_t.value(),
" max_scaling = ", max_scaling.value());
const auto f = GetSuperAdditiveRoundingFunction(
rhs_remainder, divisor,
std::min(max_t,
GetFactorT(rhs_remainder, divisor, IntegerValue(100))),
max_scaling);
ASSERT_EQ(f(IntegerValue(0)), 0) << info;
ASSERT_GE(f(divisor), 1) << info;
ASSERT_LE(f(divisor), max_scaling * max_t) << info;
for (IntegerValue a(0); a < divisor; ++a) {
IntegerValue min_diff = kMaxIntegerValue;
for (IntegerValue b(1); b < divisor; ++b) {
min_diff = std::min(min_diff, f(a + b) - f(a) - f(b));
ASSERT_GE(min_diff, 0)
<< info << ", f(" << a << ")=" << f(a) << " + f(" << b
<< ")=" << f(b) << " <= f(" << a + b << ")=" << f(a + b);
}
// TODO(user): Our discretized "mir" function is not always
// maximal. Try to fix it?
if (a <= rhs_remainder || max_scaling != 2) continue;
if (rhs_remainder * max_t < divisor / 2) continue;
// min_diff > 0 shows that our function is dominated (i.e. not
// maximal) since f(a) could be increased by 1/2.
ASSERT_EQ(min_diff, 0)
<< "Not maximal at " << info << " f(" << a << ") = " << f(a)
<< " min_diff:" << min_diff;
}
}
}
}
}
}
TEST(GetSuperAdditiveStrengtheningFunction, AllSmallValues) {
for (const int64_t rhs : {13, 14}) { // test odd/even
for (int64_t min_magnitude = 1; min_magnitude <= rhs; ++min_magnitude) {
const auto f = GetSuperAdditiveStrengtheningFunction(rhs, min_magnitude);
// Check super additivity in -[50, 50]
for (int a = -50; a <= 50; ++a) {
for (int b = -50; b <= 50; ++b) {
ASSERT_LE(f(a) + f(b), f(a + b))
<< " a=" << a << " b=" << b << " min=" << min_magnitude
<< " rhs=" << rhs;
}
}
}
}
}
TEST(GetSuperAdditiveStrengtheningMirFunction, AllSmallValues) {
for (const int64_t rhs : {13, 14}) { // test odd/even
for (int64_t scaling = 1; scaling <= rhs; ++scaling) {
const auto f = GetSuperAdditiveStrengtheningMirFunction(rhs, scaling);
// Check super additivity in -[50, 50]
for (int a = -50; a <= 50; ++a) {
for (int b = -50; b <= 50; ++b) {
ASSERT_LE(f(a) + f(b), f(a + b))
<< " a=" << a << " b=" << b << " scaling=" << scaling
<< " rhs=" << rhs;
}
}
}
}
}
TEST(CutDataTest, ComputeViolation) {
CutData cut;
cut.rhs = 2;
cut.terms.push_back({.lp_value = 1.2, .coeff = 1});
cut.terms.push_back({.lp_value = 0.5, .coeff = 2});
EXPECT_COMPARABLE(cut.ComputeViolation(), 0.2, 1e-10);
}
template <class Helper>
std::string GetCutString(const Helper& helper) {
LinearConstraint ct;
CutDataBuilder builder;
EXPECT_TRUE(builder.ConvertToLinearConstraint(helper.cut(), &ct));
return ct.DebugString();
}
TEST(CoverCutHelperTest, SimpleExample) {
// 6x0 + 4x1 + 10x2 <= 9.
std::vector<IntegerVariable> vars = {IntegerVariable(0), IntegerVariable(2),
IntegerVariable(4)};
std::vector<IntegerValue> coeffs = IntegerValueVector({6, 4, 10});
std::vector<IntegerValue> lbs = IntegerValueVector({0, 0, 0});
std::vector<double> lp_values{1.0, 0.5, 0.1}; // Tight.
// Note(user): the ub of the last variable is not used. But the first two
// are even though only the second one is required for the validity of the
// cut.
std::vector<IntegerValue> ubs = IntegerValueVector({1, 1, 10});
CutData data;
data.FillFromParallelVectors(IntegerValue(9), vars, coeffs, lp_values, lbs,
ubs);
data.ComplementForPositiveCoefficients();
Model model;
CoverCutHelper helper(&model);
EXPECT_TRUE(helper.TrySimpleKnapsack(data));
EXPECT_EQ(GetCutString(helper), "1*I0 1*I1 1*I2 <= 1");
EXPECT_EQ(helper.Info(), "lift=1");
}
// I tried to reproduce bug 169094958, but if the base constraint is tight,
// the bug was triggered only due to numerical imprecision. A simple way to
// trigger it is like with this test if the given LP value just violate the
// initial constraint.
TEST(CoverCutHelperTest, WeirdExampleWithViolatedConstraint) {
// x0 + x1 <= 9.
std::vector<IntegerVariable> vars = {IntegerVariable(0), IntegerVariable(2)};
std::vector<IntegerValue> coeffs = IntegerValueVector({1, 1});
std::vector<IntegerValue> lbs = IntegerValueVector({
0,
0,
});
std::vector<IntegerValue> ubs = IntegerValueVector({10, 13});
std::vector<double> lp_values{0.0, 12.6}; // violated.
CutData data;
data.FillFromParallelVectors(IntegerValue(9), vars, coeffs, lp_values, lbs,
ubs);
data.ComplementForPositiveCoefficients();
Model model;
CoverCutHelper helper(&model);
EXPECT_TRUE(helper.TrySimpleKnapsack(data));
EXPECT_EQ(GetCutString(helper), "1*I0 1*I1 <= 9");
EXPECT_EQ(helper.Info(), "lift=1");
}
TEST(CoverCutHelperTest, LetchfordSouliLifting) {
const int n = 10;
const IntegerValue rhs = IntegerValue(16);
std::vector<IntegerVariable> vars;
std::vector<IntegerValue> coeffs =
IntegerValueVector({5, 5, 5, 5, 15, 13, 9, 8, 8, 8});
for (int i = 0; i < n; ++i) {
vars.push_back(IntegerVariable(2 * i));
}
std::vector<IntegerValue> lbs(n, IntegerValue(0));
std::vector<IntegerValue> ubs(n, IntegerValue(1));
std::vector<double> lps(n, 0.0);
for (int i = 0; i < 4; ++i) {
lps[i] = 0.9;
}
CutData data;
data.FillFromParallelVectors(rhs, vars, coeffs, lps, lbs, ubs);
data.ComplementForPositiveCoefficients();
Model model;
CoverCutHelper helper(&model);
EXPECT_TRUE(helper.TryWithLetchfordSouliLifting(data));
EXPECT_EQ(GetCutString(helper),
"1*I0 1*I1 1*I2 1*I3 3*I4 3*I5 2*I6 1*I7 1*I8 1*I9 <= 3");
// For now, we only support Booleans in the cover.
// Note that we don't care for variable not in the cover though.
data.terms[3].bound_diff = IntegerValue(2);
EXPECT_FALSE(helper.TryWithLetchfordSouliLifting(data));
}
LinearConstraint IntegerRoundingCutWithBoundsFromTrail(
const RoundingOptions& options, IntegerValue rhs,
absl::Span<const IntegerVariable> vars,
absl::Span<const IntegerValue> coeffs, absl::Span<const double> lp_values,
Model* model) {
std::vector<IntegerValue> lbs;
std::vector<IntegerValue> ubs;
auto* integer_trail = model->Get<IntegerTrail>();
for (int i = 0; i < vars.size(); ++i) {
lbs.push_back(integer_trail->LowerBound(vars[i]));
ubs.push_back(integer_trail->UpperBound(vars[i]));
}
CutData data;
data.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs);
data.ComplementForSmallerLpValues();
IntegerRoundingCutHelper helper(model);
EXPECT_TRUE(helper.ComputeCut(options, data, nullptr));
CutDataBuilder builder;
LinearConstraint constraint;
EXPECT_TRUE(builder.ConvertToLinearConstraint(helper.cut(), &constraint));
return constraint;
}
TEST(IntegerRoundingCutTest, LetchfordLodiExample1) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10));
// 6x0 + 4x1 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(4)};
std::vector<double> lp_values{1.5, 0.0};
RoundingOptions options;
options.max_scaling = 2;
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
options, rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "2*I0 1*I1 <= 2");
}
TEST(IntegerRoundingCutTest, LetchfordLodiExample1Modified) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 1));
// 6x0 + 4x1 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(4)};
// x1 is at its upper bound here.
std::vector<double> lp_values{5.0 / 6.0, 1.0};
// Note that the cut is only valid because the bound of x1 is one here.
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "1*I0 1*I1 <= 1");
}
TEST(IntegerRoundingCutTest, LetchfordLodiExample2) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10));
// 6x0 + 4x1 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(4)};
std::vector<double> lp_values{0.0, 2.25};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "3*I0 2*I1 <= 4");
}
TEST(IntegerRoundingCutTest, LetchfordLodiExample2WithNegatedCoeff) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(-3, 0));
// 6x0 - 4x1 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(-4)};
std::vector<double> lp_values{0.0, -2.25};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
// We actually do not return like in the example "3*I0 -2*I1 <= 4"
// But the simpler I0 - I1 <= 2 which has the same violation (0.25) but a
// better norm.
EXPECT_EQ(constraint.DebugString(), "1*I0 -1*I1 <= 2");
}
// This used to trigger a failure with a wrong implied bound code path.
TEST(IntegerRoundingCutTest, TestCaseUsedForDebugging) {
Model model;
// Variable values are in comment.
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 3)); // 1
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 4)); // 0
const IntegerVariable x2 = model.Add(NewIntegerVariable(0, 2)); // 1
const IntegerVariable x3 = model.Add(NewIntegerVariable(0, 1)); // 0
const IntegerVariable x4 = model.Add(NewIntegerVariable(0, 3)); // 1
// The constraint is tight with value above (-5 - 4 + 7 == -2).
const IntegerValue rhs = IntegerValue(-2);
std::vector<IntegerVariable> vars = {x0, x1, x2, x3, x4};
std::vector<IntegerValue> coeffs = IntegerValueVector({-5, -1, -4, -7, 7});
// The constraint is tight under LP (-5 * 0.4 == -2).
std::vector<double> lp_values{0.4, 0.0, -1e-16, 0.0, 0.0};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "-2*I0 -1*I1 -2*I2 -2*I3 2*I4 <= -2");
}
// The algo should find a "divisor" 2 when it lead to a good cut.
//
// TODO(user): Double check that such divisor will always be found? Of course,
// if the initial constraint coefficient are too high, then it will not, but
// that is okay since such cut efficacity will be bad anyway.
TEST(IntegerRoundingCutTest, ZeroHalfCut) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x2 = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x3 = model.Add(NewIntegerVariable(0, 10));
// 6x0 + 4x1 + 8x2 + 7x3 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1, x2, x3};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(4),
IntegerValue(8), IntegerValue(7)};
std::vector<double> lp_values{0.25, 1.25, 0.3125, 0.0};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "3*I0 2*I1 4*I2 3*I3 <= 4");
}
TEST(IntegerRoundingCutTest, LargeCoeffWithSmallImprecision) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 5));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 5));
// 1e6 x0 - x1 <= 1.5e6.
const IntegerValue rhs = IntegerValue(1.5e6);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(1e6), IntegerValue(-1)};
// Note thate without adjustement, this returns 2 * I0 - I1 <= 2.
// TODO(user): expose parameters so this can be verified other than manually?
std::vector<double> lp_values{1.5, 0.1};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "1*I0 <= 1");
}
TEST(IntegerRoundingCutTest, LargeCoeffWithSmallImprecision2) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 5));
const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 5));
// 1e6 x0 + 999999 * x1 <= 1.5e6.
const IntegerValue rhs = IntegerValue(1.5e6);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(1e6), IntegerValue(999999)};
// Note thate without adjustement, this returns 2 * I0 + I1 <= 2.
// TODO(user): expose parameters so this can be verified other than manually?
std::vector<double> lp_values{1.49, 0.1};
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
RoundingOptions(), rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "1*I0 1*I1 <= 1");
}
TEST(IntegerRoundingCutTest, MirOnLargerConstraint) {
Model model;
std::vector<IntegerVariable> vars(10);
for (int i = 0; i < 10; ++i) {
vars[i] = model.Add(NewIntegerVariable(0, 5));
}
// sum (i + 1) x_i <= 16.
const IntegerValue rhs = IntegerValue(16);
std::vector<IntegerValue> coeffs;
for (int i = 0; i < vars.size(); ++i) {
coeffs.push_back(IntegerValue(i + 1));
}
std::vector<double> lp_values(vars.size(), 0.0);
lp_values[9] = 1.6; // 10 * 1.6 == 16
RoundingOptions options;
options.max_scaling = 4;
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
options, rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(), "1*I6 2*I7 3*I8 4*I9 <= 4");
}
TEST(IntegerRoundingCutTest, MirOnLargerConstraint2) {
Model model;
std::vector<IntegerVariable> vars(10);
for (int i = 0; i < 10; ++i) vars[i] = model.Add(NewIntegerVariable(0, 5));
// sum (i + 1) x_i <= 16.
const IntegerValue rhs = IntegerValue(16);
std::vector<IntegerValue> coeffs;
for (int i = 0; i < vars.size(); ++i) {
coeffs.push_back(IntegerValue(i + 1));
}
std::vector<double> lp_values(vars.size(), 0.0);
lp_values[4] = 5.5 / 5.0;
lp_values[9] = 1.05;
RoundingOptions options;
options.max_scaling = 4;
LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail(
options, rhs, vars, coeffs, lp_values, &model);
EXPECT_EQ(constraint.DebugString(),
"2*I1 3*I2 4*I3 6*I4 6*I5 8*I6 9*I7 10*I8 12*I9 <= 18");
}
std::vector<IntegerValue> ToIntegerValues(const std::vector<int64_t> input) {
std::vector<IntegerValue> output;
for (const int64_t v : input) output.push_back(IntegerValue(v));
return output;
}
std::vector<IntegerVariable> ToIntegerVariables(
const std::vector<int64_t> input) {
std::vector<IntegerVariable> output;
for (const int64_t v : input) output.push_back(IntegerVariable(v));
return output;
}
// This used to fail as I was coding the CL when I was trying to force t==1
// in the GetSuperAdditiveRoundingFunction() code.
TEST(IntegerRoundingCutTest, RegressionTest) {
RoundingOptions options;
options.max_scaling = 4;
const IntegerValue rhs = int64_t{7469520585651099083};
std::vector<IntegerVariable> vars = ToIntegerVariables(
{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26,
28, 30, 32, 34, 36, 38, 42, 44, 46, 48, 50, 52, 54, 56});
std::vector<IntegerValue> coeffs = ToIntegerValues(
{22242929208935956LL, 128795791007031270LL, 64522773588815932LL,
106805487542181976LL, 136903984044996548LL, 177476314670499137LL,
364043443034395LL, 28002509947960647LL, 310965596097558939LL,
103949088324014599LL, 41400520193055115LL, 50111468002532494LL,
53821870865384327LL, 68690238549704032LL, 75189534851923882LL,
136250652059774801LL, 169776580612315087LL, 172493907306536826LL,
13772608007357656LL, 74052819842959090LL, 134400722410234077LL,
5625133860678171LL, 299572729577293761LL, 81099235700461109LL,
178989907222373586LL, 16642124499479353LL, 110378717916671350LL,
41703587448036910LL});
std::vector<double> lp_values = {
0, 0, 2.51046, 0.0741114, 0.380072, 5.17238, 0,
0, 13.2214, 0, 0.635977, 0, 0, 3.39859,
1.15936, 0.165207, 2.29673, 2.19505, 0, 0, 2.31191,
0, 0.785149, 0.258119, 2.26978, 0, 0.970046, 0};
std::vector<IntegerValue> lbs(28, IntegerValue(0));
std::vector<IntegerValue> ubs(28, IntegerValue(99));
ubs[8] = 17;
std::vector<IntegerValue> solution =
ToIntegerValues({0, 3, 0, 2, 2, 2, 0, 1, 5, 1, 1, 1, 1, 2,
0, 2, 1, 3, 1, 1, 4, 1, 6, 2, 3, 0, 1, 1});
EXPECT_EQ(coeffs.size(), vars.size());
EXPECT_EQ(lp_values.size(), vars.size());
EXPECT_EQ(lbs.size(), vars.size());
EXPECT_EQ(ubs.size(), vars.size());
EXPECT_EQ(solution.size(), vars.size());
// The solution is a valid integer solution of the inequality.
{
IntegerValue activity(0);
for (int i = 0; i < vars.size(); ++i) {
activity += solution[i] * coeffs[i];
}
EXPECT_LE(activity, rhs);
}
CutData data;
data.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs);
Model model;
IntegerRoundingCutHelper helper(&model);
// TODO(user): Actually this fail, so we don't compute a cut here.
EXPECT_FALSE(helper.ComputeCut(options, data, nullptr));
}
void InitializeLpValues(absl::Span<const double> values, Model* model) {
auto* lp_values = model->GetOrCreate<ModelLpValues>();
lp_values->resize(2 * values.size());
for (int i = 0; i < values.size(); ++i) {
(*lp_values)[IntegerVariable(2 * i)] = values[i];
(*lp_values)[IntegerVariable(2 * i + 1)] = -values[i];
}
}
TEST(SquareCutGeneratorTest, TestBelowCut) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(0, 5));
IntegerVariable y = model.Add(NewIntegerVariable(0, 25));
InitializeLpValues({2.0, 12.0}, &model);
CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
square.generate_cuts(manager);
EXPECT_EQ(1, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
EndsWith("-5*I0 1*I1 <= 0"));
}
TEST(SquareCutGeneratorTest, TestBelowCutWithOffset) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(1, 25));
InitializeLpValues({2.0, 12.0}, &model);
CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
square.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
EndsWith("-6*I0 1*I1 <= -5"));
}
TEST(SquareCutGeneratorTest, TestNoBelowCut) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(1, 25));
InitializeLpValues({2.0, 6.0}, &model);
CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
square.generate_cuts(manager);
ASSERT_EQ(0, manager->num_cuts());
}
TEST(SquareCutGeneratorTest, TestAboveCut) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(1, 25));
InitializeLpValues({2.5, 6.25}, &model);
CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
square.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
StartsWith("-6 <= -5*I0 1*I1"));
}
TEST(SquareCutGeneratorTest, TestNearlyAboveCut) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(1, 25));
InitializeLpValues({2.4, 5.99999}, &model);
CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
square.generate_cuts(manager);
ASSERT_EQ(0, manager->num_cuts());
}
TEST(MultiplicationCutGeneratorTest, TestCut1) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(2, 3));
IntegerVariable z = model.Add(NewIntegerVariable(1, 15));
InitializeLpValues({1.2, 2.1, 2.1}, &model);
CutGenerator mult =
CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
mult.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
EndsWith("2*I0 1*I1 -1*I2 <= 2"));
}
TEST(MultiplicationCutGeneratorTest, TestCut2) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(2, 3));
IntegerVariable z = model.Add(NewIntegerVariable(1, 15));
InitializeLpValues({4.9, 2.8, 12.0}, &model);
CutGenerator mult =
CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
mult.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
EndsWith("3*I0 5*I1 -1*I2 <= 15"));
}
TEST(MultiplicationCutGeneratorTest, TestCut3) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 5));
IntegerVariable y = model.Add(NewIntegerVariable(2, 3));
IntegerVariable z = model.Add(NewIntegerVariable(1, 15));
InitializeLpValues({1.2, 2.1, 4.4}, &model);
CutGenerator mult =
CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
mult.generate_cuts(manager);
ASSERT_EQ(2, manager->num_cuts());
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
StartsWith("3 <= 3*I0 1*I1 -1*I2"));
EXPECT_THAT(manager->AllConstraints().back().constraint.DebugString(),
StartsWith("10 <= 2*I0 5*I1 -1*I2"));
}
TEST(MultiplicationCutGeneratorTest, TestNoCut1) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 50));
IntegerVariable y = model.Add(NewIntegerVariable(2, 30));
IntegerVariable z = model.Add(NewIntegerVariable(1, 1500));
InitializeLpValues({40.0, 20.0, 799.0}, &model);
CutGenerator mult =
CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
mult.generate_cuts(manager);
ASSERT_EQ(0, manager->num_cuts());
}
TEST(MultiplicationCutGeneratorTest, TestNoCut2) {
Model model;
IntegerVariable x = model.Add(NewIntegerVariable(1, 50));
IntegerVariable y = model.Add(NewIntegerVariable(2, 30));
IntegerVariable z = model.Add(NewIntegerVariable(1, 1500));
InitializeLpValues({40.0, 20.0, 801.0}, &model);
CutGenerator mult =
CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
mult.generate_cuts(manager);
ASSERT_EQ(0, manager->num_cuts());
}
TEST(AllDiffCutGeneratorTest, TestCut) {
Model model;
Domain domain(10);
domain = domain.UnionWith(Domain(15));
domain = domain.UnionWith(Domain(25));
IntegerVariable x = model.Add(NewIntegerVariable(domain));
IntegerVariable y = model.Add(NewIntegerVariable(domain));
IntegerVariable z = model.Add(NewIntegerVariable(domain));
InitializeLpValues({15.0, 15.0, 15.0}, &model);
CutGenerator all_diff = CreateAllDifferentCutGenerator({x, y, z}, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
all_diff.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
EXPECT_EQ(manager->AllConstraints().front().constraint.DebugString(),
"50 <= 1*I0 1*I1 1*I2 <= 50");
}
TEST(AllDiffCutGeneratorTest, TestCut2) {
Model model;
Domain domain(10);
domain = domain.UnionWith(Domain(15));
domain = domain.UnionWith(Domain(25));
IntegerVariable x = model.Add(NewIntegerVariable(domain));
IntegerVariable y = model.Add(NewIntegerVariable(domain));
IntegerVariable z = model.Add(NewIntegerVariable(domain));
InitializeLpValues({13.0, 10.0, 12.0}, &model);
CutGenerator all_diff = CreateAllDifferentCutGenerator({x, y, z}, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
all_diff.generate_cuts(manager);
ASSERT_EQ(2, manager->num_cuts());
EXPECT_EQ(manager->AllConstraints().front().constraint.DebugString(),
"25 <= 1*I1 1*I2 <= 40");
EXPECT_EQ(manager->AllConstraints().back().constraint.DebugString(),
"50 <= 1*I0 1*I1 1*I2 <= 50");
}
// We model the maximum of 3 affine functions:
// f0(x) = 1
// f1(x) = -x0 - 2x1
// f2(x) = -x0 + x1
// over the box domain -1 <= x0, x1 <= 1. For this data, there are 9 possible
// maximum corner cuts. I denote each by noting which function f^i each input
// variable x_j gets assigned:
// (1) x0 -> f0, x1 -> f0: y <= 0x0 + 0x1 + 1z_0 + 3z_1 + 2z_2
// (2) x0 -> f0, x1 -> f1: y <= 0x0 - 2x1 + 3z_0 + 1z_1 + 4z_2
// (3) x0 -> f0, x1 -> f2: y <= 0x0 + x1 + 2z_0 + 4z_1 + 1z_2
// (4) x0 -> f1, x1 -> f0: y <= -x0 + 0x1 + 2z_0 + 2z_1 + 1z_2
// (5) x0 -> f1, x1 -> f1: y <= -x0 - 2x1 + 4z_0 + 0z_1 + 3z_2
// (6) x0 -> f1, x1 -> f2: y <= -x0 + x1 + 3z_0 + 3z_1 + 0z_2
// (7) x0 -> f2, x1 -> f0: y <= -x0 + 0x1 + 2z_0 + 2z_1 + 1z_2
// (8) x0 -> f2, x1 -> f1: y <= -x0 - 2x1 + 4z_0 + 0z_1 + 3z_2
// (9) x0 -> f2, x1 -> f2: y <= -x0 + x1 + 3z_0 + 3z_1 + 0z_2
TEST(LinMaxCutsTest, BasicCuts1) {
Model model;
IntegerVariable x0 = model.Add(NewIntegerVariable(-1, 1));
IntegerVariable x1 = model.Add(NewIntegerVariable(-1, 1));
IntegerVariable target = model.Add(NewIntegerVariable(-100, 100));
LinearExpression f0;
f0.offset = IntegerValue(1);
LinearExpression f1;
f1.vars = {x0, x1};
f1.coeffs = {IntegerValue(-1), IntegerValue(-2)};
LinearExpression f2;
f2.vars = {x0, x1};
f2.coeffs = {IntegerValue(-1), IntegerValue(1)};
std::vector<LinearExpression> exprs = {f0, f1, f2};
std::vector<IntegerVariable> z_vars;
for (int i = 0; i < exprs.size(); ++i) {
IntegerVariable z = model.Add(NewIntegerVariable(0, 1));
z_vars.push_back(z);
}
CutGenerator max_cuts =
CreateLinMaxCutGenerator(target, exprs, z_vars, &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
InitializeLpValues({-1.0, 1.0, 2.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}, &model);
max_cuts.generate_cuts(manager);
ASSERT_EQ(1, manager->num_cuts());
// x vars are I0,I1 respectively, target is I2, z_vars are I3,I4,I5
// respectively.
// Most violated inequality is 2.
EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(),
StartsWith("0 <= -2*I1 -1*I2 3*I3 1*I4 4*I5"));
InitializeLpValues({-1.0, -1.0, 2.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0},
&model);
max_cuts.generate_cuts(manager);
ASSERT_EQ(2, manager->num_cuts());
// Most violated inequality is 3.
EXPECT_THAT(manager->AllConstraints().back().constraint.DebugString(),
StartsWith("0 <= 1*I1 -1*I2 2*I3 4*I4 1*I5"));
}
// We model the maximum of 3 affine functions:
// f0(x) = 1
// f1(x) = x
// f2(x) = -x
// target = max(f0, f1, f2)
// x in [-10, 10]
TEST(LinMaxCutsTest, AffineCuts1) {
Model model;
const IntegerValue zero(0);
const IntegerValue one(1);
IntegerVariable x = model.Add(NewIntegerVariable(-10, 10));
IntegerVariable target = model.Add(NewIntegerVariable(1, 100));
LinearExpression target_expr;
target_expr.vars.push_back(target);
target_expr.coeffs.push_back(one);
std::vector<std::pair<IntegerValue, IntegerValue>> affines = {
{zero, one}, {one, zero}, {-one, zero}};
LinearConstraintBuilder builder(&model);
ASSERT_TRUE(
BuildMaxAffineUpConstraint(target_expr, x, affines, &model, &builder));
// Note, the cut is not normalized.
EXPECT_EQ(builder.Build().DebugString(), "20*I1 <= 200");
}
// We model the maximum of 3 affine functions:
// f0(x) = 1
// f1(x) = x
// f2(x) = -x
// target = max(f0, f1, f2)
// x in [-1, 10]
TEST(LinMaxCutsTest, AffineCuts2) {
Model model;
const IntegerValue zero(0);
const IntegerValue one(1);
IntegerVariable x = model.Add(NewIntegerVariable(-1, 10));
IntegerVariable target = model.Add(NewIntegerVariable(1, 100));
LinearExpression target_expr;
target_expr.vars.push_back(target);
target_expr.coeffs.push_back(one);
std::vector<std::pair<IntegerValue, IntegerValue>> affines = {
{zero, one}, {one, zero}, {-one, zero}};
LinearConstraintBuilder builder(&model);
ASSERT_TRUE(
BuildMaxAffineUpConstraint(target_expr, x, affines, &model, &builder));
EXPECT_EQ(builder.Build().DebugString(), "-9*I0 11*I1 <= 20");
}
// We model the maximum of 3 affine functions:
// f0(x) = 1
// f1(x) = x
// f2(x) = -x
// target = max(f0, f1, f2)
// x fixed
TEST(LinMaxCutsTest, AffineCutsFixedVar) {
Model model;
const IntegerValue zero(0);
const IntegerValue one(1);
IntegerVariable x = model.Add(NewIntegerVariable(2, 2));
IntegerVariable target = model.Add(NewIntegerVariable(0, 100));
LinearExpression target_expr;
target_expr.vars.push_back(target);
target_expr.coeffs.push_back(one);
std::vector<std::pair<IntegerValue, IntegerValue>> affines = {
{zero, one}, {one, zero}, {-one, zero}};
CutGenerator max_cuts =
CreateMaxAffineCutGenerator(target_expr, x, affines, "test", &model);
auto* manager = model.GetOrCreate<LinearConstraintManager>();
InitializeLpValues({2.0, 8.0}, &model);
max_cuts.generate_cuts(manager);
EXPECT_EQ(0, manager->num_cuts());
}
MATCHER_P3(CutDataIs, coeff_matcher, lp_matcher, range_matcher, "") {
return ExplainMatchResult(coeff_matcher, arg.coeff.value(),
result_listener) &&
ExplainMatchResult(range_matcher, arg.bound_diff.value(),
result_listener) &&
ExplainMatchResult(lp_matcher, arg.lp_value, result_listener);
}
TEST(ImpliedBoundsProcessorTest, PositiveBasicTest) {
Model model;
model.GetOrCreate<SatParameters>()->set_use_implied_bounds(true);
const BooleanVariable b = model.Add(NewBooleanVariable());
const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1));
const IntegerVariable x = model.Add(NewIntegerVariable(2, 9));
auto* integer_encoder = model.GetOrCreate<IntegerEncoder>();
auto* integer_trail = model.GetOrCreate<IntegerTrail>();
auto* implied_bounds = model.GetOrCreate<ImpliedBounds>();
integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view,
IntegerValue(1));
implied_bounds->Add(Literal(b, true),
IntegerLiteral::GreaterOrEqual(x, IntegerValue(5)));
// Lp solution.
ImpliedBoundsProcessor processor({x, b_view}, integer_trail, implied_bounds);
util_intops::StrongVector<IntegerVariable, double> lp_values(1000);
lp_values[x] = 4.0;
lp_values[b_view] = 2.0 / 3.0; // 2.0 + b_view_value * (5-2) == 4.0
processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values);
// Lets look at the term X.
CutData data;
CutTerm X;
X.coeff = 1;
X.lp_value = 2.0;
X.bound_diff = 7;
X.expr_vars[0] = x;
X.expr_coeffs[0] = 1;
X.expr_coeffs[1] = 0;
X.expr_offset = -2;
data.terms.push_back(X);
processor.CacheDataForCut(IntegerVariable(100), &data);
const IntegerValue t(1);
std::vector<CutTerm> new_terms;
EXPECT_TRUE(processor.TryToExpandWithLowerImpliedbound(
t, /*complement=*/false, &data.terms[0], &data.rhs, &new_terms));
EXPECT_EQ(0, processor.MutableCutBuilder()->AddOrMergeBooleanTerms(
absl::MakeSpan(new_terms), t, &data));
EXPECT_EQ(data.terms.size(), 2);
EXPECT_THAT(data.terms[0], CutDataIs(1, DoubleNear(0.0, 1.0e-6), 7));
EXPECT_THAT(data.terms[1], CutDataIs(3, DoubleNear(0.666667, 1.0e-6), 1));
EXPECT_EQ(data.terms[1].expr_offset, 0);
}
// Same as above but with b.Negated()
TEST(ImpliedBoundsProcessorTest, NegativeBasicTest) {
Model model;
model.GetOrCreate<SatParameters>()->set_use_implied_bounds(true);
const BooleanVariable b = model.Add(NewBooleanVariable());
const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1));
const IntegerVariable x = model.Add(NewIntegerVariable(2, 9));
auto* integer_encoder = model.GetOrCreate<IntegerEncoder>();
auto* integer_trail = model.GetOrCreate<IntegerTrail>();
auto* implied_bounds = model.GetOrCreate<ImpliedBounds>();
integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view,
IntegerValue(1));
implied_bounds->Add(Literal(b, false), // False here.
IntegerLiteral::GreaterOrEqual(x, IntegerValue(5)));
// Lp solution.
ImpliedBoundsProcessor processor({x, b_view}, integer_trail, implied_bounds);
util_intops::StrongVector<IntegerVariable, double> lp_values(1000);
lp_values[x] = 4.0;
lp_values[b_view] = 1.0 - 2.0 / 3.0; // 1 - value above.
processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values);
// Lets look at the term X.
CutData data;
CutTerm X;
X.coeff = 1;
X.lp_value = 2.0;
X.bound_diff = 7;
X.expr_vars[0] = x;
X.expr_coeffs[0] = 1;
X.expr_coeffs[1] = 0;
X.expr_offset = -2;
data.terms.push_back(X);
processor.CacheDataForCut(IntegerVariable(100), &data);
const IntegerValue t(1);
std::vector<CutTerm> new_terms;
EXPECT_TRUE(processor.TryToExpandWithLowerImpliedbound(
t, /*complement=*/false, &data.terms[0], &data.rhs, &new_terms));
EXPECT_EQ(0, processor.MutableCutBuilder()->AddOrMergeBooleanTerms(
absl::MakeSpan(new_terms), t, &data));
EXPECT_EQ(data.terms.size(), 2);
EXPECT_THAT(data.terms[0], CutDataIs(1, DoubleNear(0.0, 1.0e-6), 7));
EXPECT_THAT(data.terms[1], CutDataIs(3, DoubleNear(0.666667, 1.0e-6), 1));
// This is the only change, we have 1 - bool there actually.
EXPECT_EQ(data.terms[1].expr_offset, 1);
EXPECT_EQ(data.terms[1].expr_coeffs[0], -1);
EXPECT_EQ(data.terms[1].expr_vars[0], b_view);
}
TEST(ImpliedBoundsProcessorTest, DecompositionTest) {
Model model;
model.GetOrCreate<SatParameters>()->set_use_implied_bounds(true);
const BooleanVariable b = model.Add(NewBooleanVariable());
const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1));
const BooleanVariable c = model.Add(NewBooleanVariable());
const IntegerVariable c_view = model.Add(NewIntegerVariable(0, 1));
const IntegerVariable x = model.Add(NewIntegerVariable(2, 9));
auto* integer_encoder = model.GetOrCreate<IntegerEncoder>();
auto* integer_trail = model.GetOrCreate<IntegerTrail>();
auto* implied_bounds = model.GetOrCreate<ImpliedBounds>();
integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view,
IntegerValue(1));
integer_encoder->AssociateToIntegerEqualValue(Literal(c, true), c_view,
IntegerValue(1));
implied_bounds->Add(Literal(b, true),
IntegerLiteral::GreaterOrEqual(x, IntegerValue(5)));
implied_bounds->Add(Literal(c, true),
IntegerLiteral::LowerOrEqual(x, IntegerValue(2)));
// Lp solution.
ImpliedBoundsProcessor processor({x, b_view, c_view}, integer_trail,
implied_bounds);
util_intops::StrongVector<IntegerVariable, double> lp_values(1000);
lp_values[x] = 4.0;
lp_values[NegationOf(x)] = -4.0;
lp_values[b_view] = 2.0 / 3.0; // 2.0 + b_view_value * (5-2) == 4.0
lp_values[c_view] = 0.5;
processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values);
// Lets look at the term X.
CutTerm X;
X.coeff = 1;
X.lp_value = 2.0;
X.bound_diff = 7;
X.expr_vars[0] = x;
X.expr_coeffs[0] = 1;
X.expr_coeffs[1] = 0;
X.expr_offset = -2;
CutData data;
data.terms.push_back(X);
processor.CacheDataForCut(IntegerVariable(100), &data);
X = data.terms[0];
// X - 2 = 3 * B + slack;
CutTerm bool_term;
CutTerm slack_term;
EXPECT_TRUE(processor.DecomposeWithImpliedLowerBound(X, IntegerValue(1),
bool_term, slack_term));
EXPECT_THAT(bool_term, CutDataIs(3, DoubleNear(0.666667, 1.0e-6), 1));
EXPECT_THAT(slack_term, CutDataIs(1, DoubleNear(0.0, 1.0e-6), 7));
// (9 - X) = 7 * C + slack;
CutTerm Y = X;
absl::int128 unused;
Y.Complement(&unused);
Y.coeff = -Y.coeff;
EXPECT_TRUE(processor.DecomposeWithImpliedLowerBound(Y, IntegerValue(1),
bool_term, slack_term));
EXPECT_THAT(bool_term, CutDataIs(7, DoubleNear(0.5, 1.0e-6), 1));
EXPECT_THAT(slack_term, CutDataIs(1, DoubleNear(1.5, 1.0e-6), 7));
// X - 2 = 7 * (1 - C) - slack;
EXPECT_TRUE(processor.DecomposeWithImpliedUpperBound(X, IntegerValue(1),
bool_term, slack_term));
EXPECT_THAT(bool_term, CutDataIs(7, DoubleNear(0.5, 1.0e-6), 1));
EXPECT_THAT(slack_term, CutDataIs(-1, DoubleNear(1.5, 1.0e-6), 7));
}
TEST(CutDataTest, SimpleExample) {
Model model;
const IntegerVariable x0 = model.Add(NewIntegerVariable(7, 10));
const IntegerVariable x1 = model.Add(NewIntegerVariable(-3, 20));
// 6x0 - 4x1 <= 9.
const IntegerValue rhs = IntegerValue(9);
std::vector<IntegerVariable> vars = {x0, x1};
std::vector<IntegerValue> coeffs = {IntegerValue(6), IntegerValue(-4)};
std::vector<double> lp_values = {7.5, 4.5};
CutData cut;
std::vector<IntegerValue> lbs;
std::vector<IntegerValue> ubs;
auto* integer_trail = model.Get<IntegerTrail>();
for (int i = 0; i < vars.size(); ++i) {
lbs.push_back(integer_trail->LowerBound(vars[i]));
ubs.push_back(integer_trail->UpperBound(vars[i]));
}
cut.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs);
cut.ComplementForSmallerLpValues();
// 6 (I0' + 7) - 4 (I1' - 3) <= 9
ASSERT_EQ(cut.terms.size(), 2);
EXPECT_EQ(cut.rhs, 9 - 4 * 3 - 6 * 7);
EXPECT_EQ(cut.terms[0].coeff, 6);
EXPECT_EQ(cut.terms[0].lp_value, 0.5);
EXPECT_EQ(cut.terms[0].bound_diff, 3);
EXPECT_EQ(cut.terms[1].coeff, -4);
EXPECT_EQ(cut.terms[1].lp_value, 7.5);
EXPECT_EQ(cut.terms[1].bound_diff, 23);
// Lets complement.
const absl::int128 old_rhs = cut.rhs;
cut.terms[0].Complement(&cut.rhs);
EXPECT_EQ(cut.rhs, old_rhs - 3 * 6);
EXPECT_EQ(cut.terms[0].coeff, -6);
EXPECT_EQ(cut.terms[0].lp_value, 3 - 0.5);
EXPECT_EQ(cut.terms[0].bound_diff, 3);
// Encode back.
LinearConstraint new_constraint;
CutDataBuilder builder;
EXPECT_TRUE(builder.ConvertToLinearConstraint(cut, &new_constraint));
// We have a division by GCD in there.
const IntegerValue gcd = 2;
EXPECT_EQ(vars.size(), new_constraint.num_terms);
for (int i = 0; i < new_constraint.num_terms; ++i) {
EXPECT_EQ(vars[i], new_constraint.vars[i]);
EXPECT_EQ(coeffs[i] / gcd, new_constraint.coeffs[i]);
}
}
TEST(SumOfAllDiffLowerBounderTest, ContinuousVariables) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10));
SumOfAllDiffLowerBounder helper;
helper.Add(x1, 3, *integer_trail);
helper.Add(x2, 3, *integer_trail);
helper.Add(x3, 3, *integer_trail);
EXPECT_EQ(3, helper.size());
EXPECT_EQ(6, helper.SumOfMinDomainValues());
EXPECT_EQ(6, helper.SumOfDifferentMins());
std::string suffix;
EXPECT_EQ(6, helper.GetBestLowerBound(suffix));
EXPECT_EQ("e", suffix);
helper.Clear();
EXPECT_EQ(0, helper.size());
}
TEST(SumOfAllDiffLowerBounderTest, DisjointVariables) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10));
SumOfAllDiffLowerBounder helper;
helper.Add(x1, 3, *integer_trail);
helper.Add(x2, 3, *integer_trail);
helper.Add(AffineExpression(x3, 1, 10), 3, *integer_trail);
EXPECT_EQ(3, helper.size());
EXPECT_EQ(6, helper.SumOfMinDomainValues());
EXPECT_EQ(14, helper.SumOfDifferentMins());
std::string suffix;
EXPECT_EQ(14, helper.GetBestLowerBound(suffix));
EXPECT_EQ("a", suffix);
}
TEST(SumOfAllDiffLowerBounderTest, DiscreteDomains) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10));
IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10));
SumOfAllDiffLowerBounder helper;
helper.Add(AffineExpression(x1, 3, 0), 3, *integer_trail);
helper.Add(AffineExpression(x2, 3, 0), 3, *integer_trail);
helper.Add(AffineExpression(x3, 3, 0), 3, *integer_trail);
EXPECT_EQ(3, helper.size());
EXPECT_EQ(18, helper.SumOfMinDomainValues());
EXPECT_EQ(12, helper.SumOfDifferentMins());
std::string suffix;
EXPECT_EQ(18, helper.GetBestLowerBound(suffix));
EXPECT_EQ("d", suffix);
}
} // namespace
} // namespace sat
} // namespace operations_research