Files
ortools-clone/ortools/sat/pb_constraint_test.cc

782 lines
30 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/pb_constraint.h"
#include <cstdint>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_vector.h"
#include "ortools/sat/enforcement.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_base.h"
#include "ortools/util/strong_integers.h"
namespace operations_research {
namespace sat {
namespace {
using ::testing::ContainerEq;
template <typename... Args>
auto LiteralsAre(Args... literals) {
return ::testing::ElementsAre(Literal(literals)...);
}
std::vector<LiteralWithCoeff> MakePb(
absl::Span<const std::pair<int, Coefficient>> input) {
std::vector<LiteralWithCoeff> result;
result.reserve(input.size());
for (const auto p : input) {
result.push_back({Literal(p.first), p.second});
}
return result;
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, RemoveZeroCoefficient) {
Coefficient bound_shift, max_value;
auto cst = MakePb({{+1, 4}, {+2, 0}, {+3, 4}, {+5, 0}});
const auto result = MakePb({{+1, 4}, {+3, 4}});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, 0);
EXPECT_EQ(max_value, 8);
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, MakeAllCoefficientPositive) {
Coefficient bound_shift, max_value;
auto cst = MakePb({{+1, 4}, {+2, -3}, {+3, 4}, {+5, -1}});
const auto result = MakePb({{-5, 1}, {-2, 3}, {+1, 4}, {+3, 4}});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, 4);
EXPECT_EQ(max_value, 12);
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase1) {
Coefficient bound_shift, max_value;
// 4x -3(1-x) +4(1-x) -x is the same as to 2x + 1
auto cst = MakePb({{+1, 4}, {-1, -3}, {-1, 4}, {+1, -1}});
const auto result = MakePb({{+1, 2}});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, -1);
EXPECT_EQ(max_value, 2);
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase2) {
Coefficient bound_shift, max_value;
// 4x -3(1-x) +4(1-x) -5x is the same as to -2x + 1
// which is expressed as 2(1-x) -2 +1
auto cst = MakePb({{+1, 4}, {-1, -3}, {-1, 4}, {+1, -5}});
const auto result = MakePb({{-1, 2}});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, 1);
EXPECT_EQ(max_value, 2);
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase3) {
Coefficient bound_shift, max_value;
// Here the last variable will disappear completely
auto cst = MakePb({{+1, 4}, {+2, -3}, {+2, 4}, {+2, -1}});
const auto result = MakePb({{+1, 4}});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, 0);
EXPECT_EQ(max_value, 4);
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, Overflow) {
Coefficient bound_shift, max_value;
auto cst = MakePb({{+1, -kCoefficientMax}, {+2, -kCoefficientMax}});
EXPECT_FALSE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
}
TEST(ComputeBooleanLinearExpressionCanonicalForm, BigIntCase) {
Coefficient bound_shift, max_value;
auto cst = MakePb({{+1, -kCoefficientMax}, {-1, -kCoefficientMax}});
const auto result = MakePb({});
EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift,
&max_value));
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, kCoefficientMax);
EXPECT_EQ(max_value, 0);
}
TEST(ApplyLiteralMappingTest, BasicTest) {
Coefficient bound_shift, max_value;
// This is needed to initialize the ITIVector below.
std::vector<LiteralIndex> temp{
kTrueLiteralIndex, kFalseLiteralIndex, // var1 fixed to true.
Literal(-1).Index(), Literal(+1).Index(), // var2 mapped to not(var1)
Literal(+2).Index(), Literal(-2).Index(), // var3 mapped to var2
kFalseLiteralIndex, kTrueLiteralIndex, // var4 fixed to false
Literal(+2).Index(), Literal(-2).Index()}; // var5 mapped to var2
util_intops::StrongVector<LiteralIndex, LiteralIndex> mapping(temp.begin(),
temp.end());
auto cst = MakePb({{+1, 4}, {+3, -3}, {+2, 4}, {+4, 7}, {+5, 5}});
EXPECT_TRUE(ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value));
const auto result = MakePb({{+2, 2}, {-1, 4}});
EXPECT_THAT(cst, ContainerEq(result));
EXPECT_EQ(bound_shift, -4);
EXPECT_EQ(max_value, 6);
}
TEST(SimplifyCanonicalBooleanLinearConstraint, CoefficientsLargerThanRhs) {
auto cst = MakePb({{+1, 4}, {+2, 5}, {+3, 6}, {-4, 7}});
Coefficient rhs(10);
SimplifyCanonicalBooleanLinearConstraint(&cst, &rhs);
EXPECT_THAT(cst, ContainerEq(cst));
rhs = Coefficient(5);
SimplifyCanonicalBooleanLinearConstraint(&cst, &rhs);
const auto result = MakePb({{+1, 4}, {+2, 5}, {+3, 6}, {-4, 6}});
EXPECT_THAT(cst, ContainerEq(result));
}
TEST(CanonicalBooleanLinearProblem, BasicTest) {
auto cst = MakePb({{+1, 4}, {+2, -5}, {+3, 6}, {-4, 7}});
CanonicalBooleanLinearProblem problem;
problem.AddLinearConstraint(true, Coefficient(-5), true, Coefficient(5),
&cst);
// We have just one constraint because the >= -5 is always true.
EXPECT_EQ(1, problem.NumConstraints());
const auto result0 = MakePb({{+1, 4}, {-2, 5}, {+3, 6}, {-4, 7}});
EXPECT_EQ(problem.Rhs(0), 10);
EXPECT_THAT(problem.Constraint(0), ContainerEq(result0));
// So lets restrict it and only use the lower bound
// Note that the API destroy the input so we have to reconstruct it.
cst = MakePb({{+1, 4}, {+2, -5}, {+3, 6}, {-4, 7}});
problem.AddLinearConstraint(true, Coefficient(-4), false,
/*unused*/ Coefficient(-10), &cst);
// Now we have another constraint corresponding to the >= -4 constraint.
EXPECT_EQ(2, problem.NumConstraints());
const auto result1 = MakePb({{-1, 4}, {+2, 5}, {-3, 6}, {+4, 7}});
EXPECT_EQ(problem.Rhs(1), 21);
EXPECT_THAT(problem.Constraint(1), ContainerEq(result1));
}
TEST(CanonicalBooleanLinearProblem, BasicTest2) {
auto cst = MakePb({{+1, 1}, {+2, 2}});
CanonicalBooleanLinearProblem problem;
problem.AddLinearConstraint(true, Coefficient(2), false,
/*unused*/ Coefficient(0), &cst);
EXPECT_EQ(1, problem.NumConstraints());
const auto result = MakePb({{-1, 1}, {-2, 2}});
EXPECT_EQ(problem.Rhs(0), 1);
EXPECT_THAT(problem.Constraint(0), ContainerEq(result));
}
TEST(CanonicalBooleanLinearProblem, OverflowCases) {
auto cst = MakePb({});
CanonicalBooleanLinearProblem problem;
for (int i = 0; i < 2; ++i) {
std::vector<LiteralWithCoeff> reference;
if (i == 0) {
// This is a constraint with a "bound shift" of 10.
reference = MakePb({{+1, -10}, {+2, 10}});
} else {
// This is a constraint with a "bound shift" of -10 since its domain value
// is actually [10, 10].
reference = MakePb({{+1, 10}, {-1, 10}});
}
// All these constraints are trivially satisfiable, so no new constraints
// should be added.
cst = reference;
EXPECT_TRUE(problem.AddLinearConstraint(true, -kCoefficientMax, true,
kCoefficientMax, &cst));
cst = reference;
EXPECT_TRUE(problem.AddLinearConstraint(true, -kCoefficientMax - 1, true,
kCoefficientMax, &cst));
cst = reference;
EXPECT_TRUE(problem.AddLinearConstraint(true, Coefficient(-10), true,
Coefficient(10), &cst));
// These are trivially unsat, and all AddLinearConstraint() should return
// false.
cst = reference;
EXPECT_FALSE(problem.AddLinearConstraint(true, kCoefficientMax, true,
kCoefficientMax, &cst));
cst = reference;
EXPECT_FALSE(problem.AddLinearConstraint(true, -kCoefficientMax, true,
-kCoefficientMax, &cst));
cst = reference;
EXPECT_FALSE(problem.AddLinearConstraint(
true, -kCoefficientMax, true, -kCoefficientMax - Coefficient(1), &cst));
}
// No constraints were actually added.
EXPECT_EQ(problem.NumConstraints(), 0);
}
// Constructs a vector from the current trail, so we can use LiteralsAre().
std::vector<Literal> TrailToVector(const Trail& trail) {
std::vector<Literal> output;
for (int i = 0; i < trail.Index(); ++i) output.push_back(trail[i]);
return output;
}
TEST(UpperBoundedLinearConstraintTest, ConstructionAndBasicPropagation) {
Coefficient threshold;
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
Trail trail;
trail.Resize(10);
UpperBoundedLinearConstraint cst(
/*enforcement_literals=*/{},
MakePb({{+1, 4}, {+2, 4}, {-3, 5}, {+4, 10}}));
cst.InitializeRhs(EnforcementStatus::IS_ENFORCED, /*enforcement_literals=*/{},
Coefficient(7), 0, &threshold, &trail, &helper);
EXPECT_EQ(threshold, 2);
EXPECT_THAT(TrailToVector(trail), LiteralsAre(-4));
trail.Enqueue(Literal(-3), AssignmentType::kSearchDecision);
threshold -= 5; // The coeff of -3 in cst.
EXPECT_TRUE(cst.Propagate(trail.Info(Literal(-3).Variable()).trail_index,
&threshold, &trail, EnforcementStatus::IS_ENFORCED,
/*enforcement_literals=*/{}, &helper));
EXPECT_EQ(threshold, 2);
EXPECT_THAT(TrailToVector(trail), LiteralsAre(-4, -3, -1, -2));
// Untrail.
trail.Untrail(0);
threshold += 5;
cst.Untrail(&threshold, 0);
EXPECT_EQ(threshold, 2);
}
TEST(UpperBoundedLinearConstraintTest, Conflict) {
Coefficient threshold;
Trail trail;
trail.Resize(10);
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
// At most one constraint.
UpperBoundedLinearConstraint cst(
/*enforcement_literals=*/{},
MakePb({{+1, 1}, {+2, 1}, {+3, 1}, {+4, 1}}));
cst.InitializeRhs(EnforcementStatus::IS_ENFORCED, /*enforcement_literals=*/{},
Coefficient(1), 0, &threshold, &trail, &helper);
EXPECT_EQ(threshold, 0);
// Two assignment from other part of the solver.
trail.EnqueueSearchDecision(Literal(+1));
trail.EnqueueSearchDecision(Literal(+2));
// We propagate only +1.
threshold -= 1;
EXPECT_FALSE(cst.Propagate(trail.Info(Literal(+1).Variable()).trail_index,
&threshold, &trail, EnforcementStatus::IS_ENFORCED,
/*enforcement_literals=*/{}, &helper));
EXPECT_THAT(helper.conflict, LiteralsAre(-1, -2));
}
TEST(UpperBoundedLinearConstraintTest, CompactReason) {
Coefficient threshold;
Trail trail;
trail.Resize(10);
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
UpperBoundedLinearConstraint cst(
/*enforcement_literals=*/{},
MakePb({{+1, 1}, {+2, 2}, {+3, 3}, {+4, 4}}));
cst.InitializeRhs(EnforcementStatus::IS_ENFORCED, /*enforcement_literals=*/{},
Coefficient(7), 0, &threshold, &trail, &helper);
EXPECT_EQ(threshold, 3);
// Two assignment from other part of the solver.
trail.EnqueueSearchDecision(Literal(+1));
trail.EnqueueSearchDecision(Literal(+2));
trail.EnqueueSearchDecision(Literal(+3));
// We propagate when +3 is processed.
threshold = -3;
const int source_trail_index = trail.Info(Literal(+3).Variable()).trail_index;
EXPECT_TRUE(cst.Propagate(source_trail_index, &threshold, &trail,
EnforcementStatus::IS_ENFORCED,
/*enforcement_literals=*/{}, &helper));
EXPECT_EQ(trail.Index(), 4);
EXPECT_EQ(trail[3], Literal(-4));
// -2 do not need to be in the reason since {-3, -1} propagates exactly
// the same way.
cst.FillReason(trail, source_trail_index, /*enforcement_literals=*/{},
Literal(-4).Variable(), &helper.temporary_tuples,
&helper.conflict);
EXPECT_THAT(helper.conflict, LiteralsAre(-3, -1));
}
TEST(UpperBoundedLinearConstraintTest, ConflictAfterEnforcementStatusChange) {
Coefficient threshold;
Trail trail;
trail.Resize(10);
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
std::vector<Literal> enforcement_literals = {Literal(+9)};
UpperBoundedLinearConstraint cst(
enforcement_literals, MakePb({{+1, 1}, {+2, 2}, {+3, 3}, {+4, 4}}));
cst.InitializeRhs(EnforcementStatus::IS_FALSE, enforcement_literals,
Coefficient(7), 0, &threshold, &trail, &helper);
EXPECT_EQ(threshold, 3);
// Some assignments from other parts of the solver.
trail.EnqueueSearchDecision(Literal(+1));
trail.EnqueueSearchDecision(Literal(+2));
trail.EnqueueSearchDecision(Literal(+3));
trail.EnqueueSearchDecision(Literal(+4));
trail.EnqueueSearchDecision(Literal(+9));
// We detect a conflict when +9 is processed.
threshold = -7;
const int source_trail_index = trail.Info(Literal(+9).Variable()).trail_index;
EXPECT_FALSE(cst.Propagate(source_trail_index, &threshold, &trail,
EnforcementStatus::IS_ENFORCED,
enforcement_literals, &helper));
// -2 do not need to be in the reason since {-4, -3, -1} propagates exactly
// the same way.
EXPECT_THAT(helper.conflict, LiteralsAre(-9, -4, -3, -1));
}
TEST(UpperBoundedLinearConstraintTest, PropagateEnforcementAfterStatusChange) {
Coefficient threshold;
Trail trail;
trail.Resize(10);
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
std::vector<Literal> enforcement_literals = {Literal(+8), Literal(+9)};
UpperBoundedLinearConstraint cst(
enforcement_literals, MakePb({{+1, 1}, {+2, 2}, {+3, 3}, {+4, 4}}));
cst.InitializeRhs(EnforcementStatus::IS_FALSE, enforcement_literals,
Coefficient(7), 0, &threshold, &trail, &helper);
EXPECT_EQ(threshold, 3);
// Some assignments from other parts of the solver.
trail.EnqueueSearchDecision(Literal(+1));
trail.EnqueueSearchDecision(Literal(+2));
trail.EnqueueSearchDecision(Literal(+3));
trail.EnqueueSearchDecision(Literal(+4));
trail.EnqueueSearchDecision(Literal(+9));
// We should propagate -8 when +9 is processed.
threshold = -7;
const int source_trail_index = trail.Info(Literal(+9).Variable()).trail_index;
EXPECT_TRUE(cst.Propagate(source_trail_index, &threshold, &trail,
EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT,
enforcement_literals, &helper));
EXPECT_EQ(trail.Index(), 6);
EXPECT_EQ(trail[5], Literal(-8));
// -2 do not need to be in the reason since {-4, -3, -1} propagates exactly
// the same way.
const PbConstraintsEnqueueHelper::ReasonInfo& reason = helper.reasons[5];
cst.FillReason(trail, reason.source_trail_index, enforcement_literals,
Literal(-8).Variable(), &helper.temporary_tuples,
&helper.conflict);
EXPECT_THAT(helper.conflict, LiteralsAre(-9, -4, -3, -1));
}
TEST(UpperBoundedLinearConstraintTest,
PropagateEnforcementAfterTermAssignment) {
Coefficient threshold;
Trail trail;
trail.Resize(10);
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
// At most one constraint with enforcement literal.
std::vector<Literal> enforcement_literals = {Literal(+9)};
UpperBoundedLinearConstraint cst(
enforcement_literals, MakePb({{+1, 1}, {+2, 1}, {+3, 1}, {+4, 1}}));
cst.InitializeRhs(EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT,
enforcement_literals, Coefficient(1), 0, &threshold, &trail,
&helper);
EXPECT_EQ(threshold, 0);
// Some assignments from other parts of the solver.
trail.EnqueueSearchDecision(Literal(+1));
trail.EnqueueSearchDecision(Literal(+2));
// We should propagate -9 when +2 is processed.
const int source_trail_index = trail.Info(Literal(+1).Variable()).trail_index;
threshold = -1;
EXPECT_TRUE(cst.Propagate(source_trail_index, &threshold, &trail,
EnforcementStatus::CAN_PROPAGATE_ENFORCEMENT,
enforcement_literals, &helper));
EXPECT_EQ(trail.Index(), 3);
EXPECT_EQ(trail[2], Literal(-9));
const PbConstraintsEnqueueHelper::ReasonInfo& reason = helper.reasons[2];
cst.FillReason(trail, reason.source_trail_index, enforcement_literals,
Literal(-9).Variable(), &helper.temporary_tuples,
&helper.conflict);
EXPECT_THAT(helper.conflict, LiteralsAre(-2, -1));
}
TEST(PbConstraintsTest, Duplicates) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
csts.Resize(10);
CHECK_EQ(csts.NumberOfConstraints(), 0);
csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20),
&trail);
csts.AddConstraint(MakePb({{-1, 1}, {-2, 3}, {+3, 7}}), Coefficient(20),
&trail);
CHECK_EQ(csts.NumberOfConstraints(), 2);
// Adding the same constraints will do nothing.
csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20),
&trail);
CHECK_EQ(csts.NumberOfConstraints(), 2);
CHECK_EQ(trail.Index(), 0);
// Over constraining it will fix the 3 literals.
csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(6),
&trail);
CHECK_EQ(csts.NumberOfConstraints(), 2);
EXPECT_THAT(TrailToVector(trail), LiteralsAre(+1, +2, -3));
}
TEST(PbConstraintsTest, BasicPropagation) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
trail.EnqueueSearchDecision(Literal(-1));
csts.Resize(10);
csts.AddConstraint(MakePb({{-1, 1}, {+2, 1}}), Coefficient(1), &trail);
csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20),
&trail);
csts.AddConstraint(MakePb({{-1, 1}, {-2, 1}, {-3, 1}, {+4, 1}}),
Coefficient(3), &trail);
EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2));
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2, -3, -4));
// Test the reason for each assignment.
EXPECT_THAT(trail.Reason(Literal(-2).Variable()), LiteralsAre(+1));
EXPECT_THAT(trail.Reason(Literal(-3).Variable()), LiteralsAre(+1, +2));
EXPECT_THAT(trail.Reason(Literal(-4).Variable()), LiteralsAre(+1, +2, +3));
// Untrail, and repropagate everything.
csts.Untrail(trail, 0);
trail.Untrail(0);
trail.EnqueueSearchDecision(Literal(-1));
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2, -3, -4));
}
TEST(PbConstraintsTest, BasicDeletion) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
PbConstraintsEnqueueHelper helper;
helper.reasons.resize(10);
trail.Resize(10);
csts.Resize(10);
csts.AddConstraint(MakePb({{-1, 1}, {+2, 1}}), Coefficient(1), &trail);
csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20),
&trail);
csts.AddConstraint(MakePb({{-1, 1}, {-2, 1}, {-3, 1}, {+4, 1}}),
Coefficient(3), &trail);
// Delete the first constraint.
EXPECT_EQ(3, csts.NumberOfConstraints());
csts.DeleteConstraint(0);
EXPECT_EQ(2, csts.NumberOfConstraints());
// The constraint 1 is deleted, so enqueuing -1 shouldn't propagate.
trail.Enqueue(Literal(-1), AssignmentType::kSearchDecision);
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_EQ("-1", trail.DebugString());
// But also enqueuing -2 should.
trail.Enqueue(Literal(-2), AssignmentType::kSearchDecision);
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_EQ("-1 -2 -3 -4", trail.DebugString());
// Let's bactrack.
trail.Untrail(1);
csts.Untrail(trail, 1);
// Let's delete one more constraint.
csts.DeleteConstraint(0);
EXPECT_EQ(1, csts.NumberOfConstraints());
// Now, if we enqueue -2 again, nothing is propagated.
trail.Enqueue(Literal(-2), AssignmentType::kSearchDecision);
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_EQ("-1 -2", trail.DebugString());
// We need to also enqueue -3 for -4 to be propagated.
trail.Enqueue(Literal(-3), AssignmentType::kSearchDecision);
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
EXPECT_EQ("-1 -2 -3 -4", trail.DebugString());
// Deleting everything doesn't crash.
csts.DeleteConstraint(0);
EXPECT_EQ(0, csts.NumberOfConstraints());
}
TEST(PbConstraintsTest, UnsatAtConstruction) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
trail.EnqueueSearchDecision(Literal(+1));
trail.Enqueue(Literal(+2), AssignmentType::kUnitReason);
trail.Enqueue(Literal(+3), AssignmentType::kUnitReason);
csts.Resize(10);
EXPECT_TRUE(
csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}}), Coefficient(2), &trail));
while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail));
// We need to propagate before adding this constraint for the AddConstraint()
// to notice that it is unsat. Otherwise, it will be noticed at propagation
// time.
EXPECT_FALSE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 1}}),
Coefficient(2), &trail));
EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+4, 1}}),
Coefficient(2), &trail));
}
TEST(PbConstraintsTest, AddConstraintWithLevel0Propagation) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
csts.Resize(10);
EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 3}, {+3, 7}}),
Coefficient(2), &trail));
EXPECT_EQ(trail.Index(), 2);
EXPECT_EQ(trail[0], Literal(-2));
EXPECT_EQ(trail[1], Literal(-3));
}
TEST(PbConstraintsTest, AddConstraintUMR) {
const auto cst = MakePb({{+3, 7}});
UpperBoundedLinearConstraint c(/*enforcement_literals=*/{}, cst);
// Calling hashing on c generates an UMR that is triggered during the hash_map
// lookup below.
const uint64_t ct_hash = c.hash();
absl::flat_hash_map<uint64_t, std::vector<int>> store;
std::vector<int>& vec = store[ct_hash];
EXPECT_EQ(vec.size(), 0);
}
TEST(PbConstraintsDeathTest, AddConstraintWithLevel0PropagationInSearch) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
trail.EnqueueSearchDecision(Literal(+10));
csts.Resize(10);
// If the decision level is not 0, this will fail.
ASSERT_DEATH(csts.AddConstraint(MakePb({{+1, 1}, {+2, 3}, {+3, 7}}),
Coefficient(2), &trail),
"var should have been propagated at an earlier level.");
}
TEST(PbConstraintsDeathTest, AddConstraintPrecondition) {
Model model;
PbConstraints& csts = *(model.GetOrCreate<PbConstraints>());
Trail& trail = *(model.GetOrCreate<Trail>());
trail.Resize(10);
trail.EnqueueSearchDecision(Literal(+1));
trail.Enqueue(Literal(+2), AssignmentType::kUnitReason);
trail.EnqueueSearchDecision(Literal(+4)); // dummy.
trail.Enqueue(Literal(+3), AssignmentType::kUnitReason);
csts.Resize(10);
// We can't add this constraint since it is conflicting under the current
// assignment.
EXPECT_FALSE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 1}}),
Coefficient(2), &trail));
trail.Untrail(trail.Index() - 1); // Remove the +3.
EXPECT_EQ(trail.Index(), 3);
csts.Untrail(trail, 3);
// Adding this one at a decision level of 2 will also fail because it will
// propagate 3 from decision level 1.
ASSERT_DEATH(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 2}}),
Coefficient(3), &trail),
"var should have been propagated at an earlier level.");
// However, adding the same constraint while the decision level is 1 is ok.
// It will propagate -3 at the correct decision level.
trail.Untrail(trail.PrepareBacktrack(1));
EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 2}}),
Coefficient(3), &trail));
EXPECT_EQ(trail.Index(), 3);
EXPECT_EQ(trail[2], Literal(-3));
}
TEST(MutableUpperBoundedLinearConstraintTest, LinearAddition) {
MutableUpperBoundedLinearConstraint cst_a;
cst_a.ClearAndResize(5);
cst_a.AddTerm(Literal(+1), Coefficient(3));
cst_a.AddTerm(Literal(+2), Coefficient(4));
cst_a.AddTerm(Literal(+3), Coefficient(5));
cst_a.AddTerm(Literal(+4), Coefficient(1));
cst_a.AddTerm(Literal(+5), Coefficient(1));
cst_a.AddToRhs(Coefficient(10));
// The result of cst_a + cst_b is describes in the comments.
MutableUpperBoundedLinearConstraint cst_b;
cst_b.ClearAndResize(5);
cst_b.AddTerm(Literal(+1), Coefficient(3)); // 3x + 3x = 6x
cst_b.AddTerm(Literal(-2), Coefficient(3)); // 4x + 3(1-x) = x + 3
cst_b.AddTerm(Literal(+3), Coefficient(3)); // 5x + 3x = 8x
cst_b.AddTerm(Literal(-4), Coefficient(6)); // x + 6(1-x) = 5(1-x) + 1
cst_b.AddTerm(Literal(+5), Coefficient(5)); // x + 5x = 6x
cst_b.AddToRhs(Coefficient(10));
for (BooleanVariable var : cst_b.PossibleNonZeros()) {
cst_a.AddTerm(cst_b.GetLiteral(var), cst_b.GetCoefficient(var));
}
cst_a.AddToRhs(cst_b.Rhs());
EXPECT_EQ(cst_a.DebugString(), "6[+1] + 1[+2] + 8[+3] + 5[-4] + 6[+5] <= 16");
}
TEST(MutableUpperBoundedLinearConstraintTest, ReduceCoefficients) {
MutableUpperBoundedLinearConstraint cst;
cst.ClearAndResize(100);
Coefficient max_value(0);
for (int i = 1; i <= 10; ++i) {
max_value += Coefficient(i);
cst.AddTerm(Literal(BooleanVariable(i), true), Coefficient(i));
}
cst.AddToRhs(max_value - 3);
// The constraint is equivalent to sum i * Literal(i, false) >= 3,
// So we can reduce any coeff > 3 to 3 and change the rhs accordingly.
cst.ReduceCoefficients();
for (BooleanVariable var : cst.PossibleNonZeros()) {
EXPECT_LE(cst.GetCoefficient(var), 3);
}
EXPECT_EQ(cst.Rhs(), 1 + 2 + 3 * 8 - 3);
}
TEST(MutableUpperBoundedLinearConstraintTest, ComputeSlackForTrailPrefix) {
MutableUpperBoundedLinearConstraint cst;
cst.ClearAndResize(100);
cst.AddTerm(Literal(+1), Coefficient(3));
cst.AddTerm(Literal(+2), Coefficient(4));
cst.AddTerm(Literal(+3), Coefficient(5));
cst.AddTerm(Literal(+4), Coefficient(6));
cst.AddTerm(Literal(+5), Coefficient(7));
cst.AddToRhs(Coefficient(10));
Trail trail;
trail.Resize(10);
trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(-2), AssignmentType::kUnitReason);
trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(-5), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(+4), AssignmentType::kSearchDecision);
EXPECT_EQ(Coefficient(10), cst.ComputeSlackForTrailPrefix(trail, 0));
EXPECT_EQ(Coefficient(10 - 3), cst.ComputeSlackForTrailPrefix(trail, 1));
EXPECT_EQ(Coefficient(10 - 3), cst.ComputeSlackForTrailPrefix(trail, 2));
EXPECT_EQ(Coefficient(10 - 3 - 5), cst.ComputeSlackForTrailPrefix(trail, 3));
EXPECT_EQ(Coefficient(10 - 3 - 5), cst.ComputeSlackForTrailPrefix(trail, 4));
EXPECT_EQ(Coefficient(10 - 14), cst.ComputeSlackForTrailPrefix(trail, 5));
EXPECT_EQ(Coefficient(10 - 14), cst.ComputeSlackForTrailPrefix(trail, 50));
}
TEST(MutableUpperBoundedLinearConstraintTest, ReduceSlackToZero) {
MutableUpperBoundedLinearConstraint cst;
cst.ClearAndResize(100);
cst.AddTerm(Literal(+1), Coefficient(3));
cst.AddTerm(Literal(+2), Coefficient(1));
cst.AddTerm(Literal(+3), Coefficient(5));
cst.AddTerm(Literal(+4), Coefficient(6));
cst.AddTerm(Literal(+5), Coefficient(7));
cst.AddToRhs(Coefficient(10));
Trail trail;
trail.Resize(10);
trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(-2), AssignmentType::kUnitReason);
trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(+5), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(+4), AssignmentType::kSearchDecision);
// +1, -2 and +3 gives a slack of 2.
EXPECT_EQ(Coefficient(2), cst.ComputeSlackForTrailPrefix(trail, 3));
// It also propagate -4 and -5, to have the same propagation but with a slack
// of zero, we can call ReduceSlackToZero().
cst.ReduceSlackTo(trail, 3, Coefficient(2), Coefficient(0));
// +1 and +3 have the same coeff.
EXPECT_EQ(cst.GetCoefficient(BooleanVariable(0)), Coefficient(3));
EXPECT_EQ(cst.GetCoefficient(BooleanVariable(2)), Coefficient(5));
// the variable 1 disappeared.
EXPECT_EQ(cst.GetCoefficient(BooleanVariable(1)), Coefficient(0));
// The propagated variable coeff has been reduced by the slack.
EXPECT_EQ(cst.GetCoefficient(BooleanVariable(3)), Coefficient(6 - 2));
EXPECT_EQ(cst.GetCoefficient(BooleanVariable(4)), Coefficient(7 - 2));
// The rhs has been reduced by slack, and the slack is now 0.
EXPECT_EQ(cst.Rhs(), Coefficient(10 - 2));
EXPECT_EQ(Coefficient(0), cst.ComputeSlackForTrailPrefix(trail, 3));
}
} // namespace
} // namespace sat
} // namespace operations_research