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

985 lines
38 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/precedences.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/parse_test_proto.h"
#include "ortools/sat/cp_model_mapping.h"
#include "ortools/sat/cp_model_solver_helpers.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_base.h"
#include "ortools/sat/integer_search.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_base.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/sat/sat_solver.h"
#include "ortools/util/sorted_interval_list.h"
namespace operations_research {
namespace sat {
namespace {
using ::google::protobuf::contrib::parse_proto::ParseTestProto;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;
// A simple macro to make the code more readable.
// TODO(user): move that in a common place. test_utils?
#define EXPECT_BOUNDS_EQ(var, lb, ub) \
EXPECT_EQ(integer_trail->LowerBound(var), lb); \
EXPECT_EQ(integer_trail->UpperBound(var), ub)
// All the tests here uses 10 integer variables initially in [0, 100].
std::vector<IntegerVariable> AddVariables(IntegerTrail* integer_trail) {
std::vector<IntegerVariable> vars;
const int num_variables = 10;
const IntegerValue lower_bound(0);
const IntegerValue upper_bound(100);
for (int i = 0; i < num_variables; ++i) {
vars.push_back(integer_trail->AddIntegerVariable(lower_bound, upper_bound));
}
return vars;
}
TEST(PrecedenceRelationsTest, BasicAPI) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
const std::vector<IntegerVariable> vars = AddVariables(integer_trail);
// Note that odd indices are for the negation.
IntegerVariable a(0), b(2), c(4), d(6);
PrecedenceRelations precedences(&model);
precedences.Add(a, b, 10);
precedences.Add(d, c, 7);
precedences.Add(b, d, 5);
precedences.Build();
EXPECT_EQ(precedences.GetOffset(a, b), 10);
EXPECT_EQ(precedences.GetOffset(NegationOf(b), NegationOf(a)), 10);
EXPECT_EQ(precedences.GetOffset(a, c), 22);
EXPECT_EQ(precedences.GetOffset(NegationOf(c), NegationOf(a)), 22);
EXPECT_EQ(precedences.GetOffset(a, d), 15);
EXPECT_EQ(precedences.GetOffset(NegationOf(d), NegationOf(a)), 15);
EXPECT_EQ(precedences.GetOffset(d, a), kMinIntegerValue);
// Once built, we can update the offsets.
// Note however that this would not propagate through the precedence graphs.
precedences.Add(a, b, 15);
EXPECT_EQ(precedences.GetOffset(a, b), 15);
EXPECT_EQ(precedences.GetOffset(NegationOf(b), NegationOf(a)), 15);
}
TEST(PrecedenceRelationsTest, CornerCase1) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
const std::vector<IntegerVariable> vars = AddVariables(integer_trail);
// Note that odd indices are for the negation.
IntegerVariable a(0), b(2), c(4), d(6);
PrecedenceRelations precedences(&model);
precedences.Add(a, b, 10);
precedences.Add(b, c, 7);
precedences.Add(b, d, 5);
precedences.Add(NegationOf(b), a, 5);
precedences.Build();
EXPECT_EQ(precedences.GetOffset(NegationOf(b), a), 5);
EXPECT_EQ(precedences.GetOffset(NegationOf(b), b), 15);
EXPECT_EQ(precedences.GetOffset(NegationOf(b), c), 22);
EXPECT_EQ(precedences.GetOffset(NegationOf(b), d), 20);
}
TEST(PrecedenceRelationsTest, CornerCase2) {
Model model;
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
const std::vector<IntegerVariable> vars = AddVariables(integer_trail);
// Note that odd indices are for the negation.
IntegerVariable a(0), b(2), c(4), d(6);
PrecedenceRelations precedences(&model);
precedences.Add(NegationOf(a), a, 10);
precedences.Add(a, b, 7);
precedences.Add(a, c, 5);
precedences.Add(a, d, 2);
precedences.Build();
EXPECT_EQ(precedences.GetOffset(NegationOf(a), a), 10);
EXPECT_EQ(precedences.GetOffset(NegationOf(a), b), 17);
EXPECT_EQ(precedences.GetOffset(NegationOf(a), c), 15);
EXPECT_EQ(precedences.GetOffset(NegationOf(a), d), 12);
}
TEST(PrecedenceRelationsTest, ConditionalRelations) {
Model model;
auto* sat_solver = model.GetOrCreate<SatSolver>();
auto* integer_trail = model.GetOrCreate<IntegerTrail>();
const std::vector<IntegerVariable> vars = AddVariables(integer_trail);
const Literal l(model.Add(NewBooleanVariable()), true);
EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(l));
// Note that odd indices are for the negation.
IntegerVariable a(0), b(2);
PrecedenceRelations precedences(&model);
precedences.PushConditionalRelation({l}, a, b, 15);
precedences.PushConditionalRelation({l}, a, b, 20);
// We only keep the best one.
EXPECT_EQ(precedences.GetConditionalOffset(a, NegationOf(b)), -15);
EXPECT_THAT(precedences.GetConditionalEnforcements(a, NegationOf(b)),
ElementsAre(l));
// Backtrack works.
EXPECT_TRUE(sat_solver->ResetToLevelZero());
EXPECT_EQ(precedences.GetConditionalOffset(a, NegationOf(b)),
kMinIntegerValue);
EXPECT_THAT(precedences.GetConditionalEnforcements(a, NegationOf(b)),
ElementsAre());
}
TEST(PrecedencesPropagatorTest, Empty) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_TRUE(propagator->Propagate(trail));
propagator->Untrail(*trail, 0);
}
TEST(PrecedencesPropagatorTest, BasicPropagationTest) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4));
propagator->AddPrecedenceWithOffset(vars[0], vars[2], IntegerValue(8));
propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(10));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_BOUNDS_EQ(vars[0], 0, 86);
EXPECT_BOUNDS_EQ(vars[1], 4, 90);
EXPECT_BOUNDS_EQ(vars[2], 14, 100);
// Lets now move vars[1] lower bound.
std::vector<Literal> lr;
std::vector<IntegerLiteral> ir;
EXPECT_TRUE(integer_trail->Enqueue(
IntegerLiteral::GreaterOrEqual(vars[1], IntegerValue(20)), lr, ir));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_BOUNDS_EQ(vars[1], 20, 90);
EXPECT_BOUNDS_EQ(vars[2], 30, 100);
}
TEST(PrecedencesPropagatorTest, PropagationTestWithVariableOffset) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]);
// Make var[2] >= 10 and propagate
std::vector<Literal> lr;
std::vector<IntegerLiteral> ir;
EXPECT_TRUE(integer_trail->Enqueue(
IntegerLiteral::GreaterOrEqual(vars[2], IntegerValue(10)), lr, ir));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_BOUNDS_EQ(vars[0], 0, 90);
EXPECT_BOUNDS_EQ(vars[1], 10, 100);
// Change the lower bound to 40 and propagate again.
EXPECT_TRUE(integer_trail->Enqueue(
IntegerLiteral::GreaterOrEqual(vars[2], IntegerValue(40)), lr, ir));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_BOUNDS_EQ(vars[0], 0, 60);
EXPECT_BOUNDS_EQ(vars[1], 40, 100);
}
TEST(PrecedencesPropagatorTest, BasicPropagation) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
trail->Resize(10);
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4));
propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(8));
propagator->AddPrecedenceWithOffset(vars[0], vars[3], IntegerValue(90));
// These arcs are not possible, because the upper bound of vars[0] is 10.
propagator->AddConditionalPrecedenceWithOffset(vars[1], vars[0],
IntegerValue(7), Literal(+1));
propagator->AddConditionalPrecedenceWithOffset(vars[2], vars[0],
IntegerValue(-1), Literal(+2));
// These are is ok.
propagator->AddConditionalPrecedenceWithOffset(vars[1], vars[0],
IntegerValue(6), Literal(+3));
propagator->AddConditionalPrecedenceWithOffset(vars[2], vars[0],
IntegerValue(-2), Literal(+4));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+1)));
EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+2)));
EXPECT_FALSE(trail->Assignment().VariableIsAssigned(Literal(+3).Variable()));
EXPECT_FALSE(trail->Assignment().VariableIsAssigned(Literal(+4).Variable()));
}
TEST(PrecedencesPropagatorTest, PropagateOnVariableOffset) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
trail->Resize(10);
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]);
propagator->AddPrecedenceWithOffset(vars[1], vars[3], IntegerValue(50));
EXPECT_TRUE(propagator->Propagate(trail));
EXPECT_BOUNDS_EQ(vars[0], 0, 50);
EXPECT_BOUNDS_EQ(vars[1], 0, 50);
EXPECT_BOUNDS_EQ(vars[2], 0, 50);
}
TEST(PrecedencesPropagatorTest, Cycles) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
trail->Resize(10);
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4));
propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(8));
propagator->AddConditionalPrecedenceWithOffset(
vars[2], vars[3], IntegerValue(-10), Literal(+1));
propagator->AddConditionalPrecedenceWithOffset(vars[3], vars[0],
IntegerValue(-2), Literal(+2));
propagator->AddConditionalPrecedence(vars[3], vars[0], Literal(+3));
// This one will force the upper bound of vars[0] to be 50, so we can
// check that the cycle is detected before the lower bound of var[0] crosses
// this bound.
propagator->AddConditionalPrecedenceWithOffset(vars[0], vars[4],
IntegerValue(50), Literal(+4));
// If we add this one, the cycle will be detected using the integer bound and
// not the graph cycle. TODO(user): Maybe this is a bad thing? but it seems
// difficult to avoid it without extra computations.
propagator->AddConditionalPrecedenceWithOffset(vars[0], vars[4],
IntegerValue(99), Literal(+5));
EXPECT_TRUE(propagator->Propagate(trail));
// Cycle of weight zero is fine.
trail->SetDecisionLevel(1);
EXPECT_TRUE(integer_trail->Propagate(trail));
trail->Enqueue(Literal(+1), AssignmentType::kUnitReason);
trail->Enqueue(Literal(+2), AssignmentType::kUnitReason);
trail->Enqueue(Literal(+4), AssignmentType::kUnitReason);
EXPECT_TRUE(propagator->Propagate(trail));
// But a cycle of positive length is not!
trail->Enqueue(Literal(+3), AssignmentType::kUnitReason);
EXPECT_FALSE(propagator->Propagate(trail));
EXPECT_THAT(trail->FailingClause(),
UnorderedElementsAre(Literal(-1), Literal(-3)));
// Test the untrail.
trail->SetDecisionLevel(0);
integer_trail->Untrail(*trail, 0);
propagator->Untrail(*trail, 0);
trail->Untrail(0);
EXPECT_TRUE(propagator->Propagate(trail));
// Still fine here.
trail->SetDecisionLevel(1);
EXPECT_TRUE(integer_trail->Propagate(trail));
trail->Enqueue(Literal(+5), AssignmentType::kUnitReason);
EXPECT_TRUE(propagator->Propagate(trail));
// But fail there with a different and longer reason.
trail->Enqueue(Literal(+1), AssignmentType::kUnitReason);
trail->Enqueue(Literal(+3), AssignmentType::kUnitReason);
EXPECT_FALSE(propagator->Propagate(trail));
EXPECT_THAT(trail->FailingClause(),
UnorderedElementsAre(Literal(-1), Literal(-3), Literal(-5)));
}
// This test a tricky situation:
//
// vars[0] + (offset = vars[2]) <= var[1]
// vars[1] <= vars[2] !!
TEST(PrecedencesPropagatorTest, TrickyCycle) {
Model model;
Trail* trail = model.GetOrCreate<Trail>();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
PrecedencesPropagator* propagator =
model.GetOrCreate<PrecedencesPropagator>();
trail->Resize(10);
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]);
propagator->AddPrecedence(vars[1], vars[2]);
// This will cause an infinite cycle.
propagator->AddConditionalPrecedenceWithOffset(vars[3], vars[0],
IntegerValue(1), Literal(+1));
// So far so good.
EXPECT_TRUE(propagator->Propagate(trail));
trail->SetDecisionLevel(1);
EXPECT_TRUE(integer_trail->Propagate(trail));
// Conflict.
trail->Enqueue(Literal(+1), AssignmentType::kUnitReason);
EXPECT_FALSE(propagator->Propagate(trail));
EXPECT_THAT(trail->FailingClause(), ElementsAre(Literal(-1)));
// Test that the code detected properly a positive cycle in the dependency
// graph instead of just pushing the bounds until the upper bound is reached.
EXPECT_LT(integer_trail->num_enqueues(), 10);
}
TEST(PrecedencesPropagatorTest, ZeroWeightCycleOnDiscreteDomain) {
Model model;
IntegerVariable a = model.Add(
NewIntegerVariable(Domain::FromValues({2, 5, 7, 15, 16, 17, 20, 32})));
IntegerVariable b = model.Add(
NewIntegerVariable(Domain::FromValues({3, 6, 9, 14, 16, 18, 20, 35})));
// Add the fact that a == b with two inequalities.
model.Add(LowerOrEqual(a, b));
model.Add(LowerOrEqual(b, a));
// After propagation, we should detect that the only common values fall in
// [16, 20].
EXPECT_TRUE(model.GetOrCreate<SatSolver>()->Propagate());
// The integer_trail is only used in the macros below.
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
EXPECT_BOUNDS_EQ(a, 16, 20);
EXPECT_BOUNDS_EQ(b, 16, 20);
}
// This was failing before CL 135903015.
TEST(PrecedencesPropagatorTest, ConditionalPrecedencesOnFixedLiteral) {
Model model;
// To trigger the old bug, we need to add some precedences.
IntegerVariable x = model.Add(NewIntegerVariable(0, 100));
IntegerVariable y = model.Add(NewIntegerVariable(50, 100));
model.Add(LowerOrEqual(x, y));
// We then add a Boolean variable and fix it.
// This will trigger a propagation.
BooleanVariable b = model.Add(NewBooleanVariable());
model.Add(ClauseConstraint({Literal(b, true)})); // Fix b To true.
// We now add a conditional precedences using the fixed variable.
// This used to not be taken into account.
model.Add(ConditionalLowerOrEqualWithOffset(y, x, 0, Literal(b, true)));
EXPECT_EQ(SatSolver::FEASIBLE, SolveIntegerProblemWithLazyEncoding(&model));
EXPECT_EQ(model.Get(Value(x)), model.Get(Value(y)));
}
#undef EXPECT_BOUNDS_EQ
TEST(PrecedenceRelationsTest, CollectPrecedences) {
Model model;
auto* integer_trail = model.GetOrCreate<IntegerTrail>();
auto* relations = model.GetOrCreate<PrecedenceRelations>();
std::vector<IntegerVariable> vars = AddVariables(integer_trail);
relations->Add(vars[0], vars[2], IntegerValue(1));
relations->Add(vars[0], vars[5], IntegerValue(1));
relations->Add(vars[1], vars[2], IntegerValue(1));
relations->Add(vars[2], vars[4], IntegerValue(1));
relations->Add(vars[3], vars[4], IntegerValue(1));
relations->Add(vars[4], vars[5], IntegerValue(1));
std::vector<PrecedenceRelations::PrecedenceData> p;
relations->CollectPrecedences({vars[0], vars[2], vars[3]}, &p);
// Note that we do not return precedences with just one variable.
std::vector<int> indices;
std::vector<IntegerVariable> variables;
for (const auto precedence : p) {
indices.push_back(precedence.index);
variables.push_back(precedence.var);
}
EXPECT_EQ(indices, (std::vector<int>{1, 2}));
EXPECT_EQ(variables, (std::vector<IntegerVariable>{vars[4], vars[4]}));
// Same with NegationOf() and also test that p is cleared.
relations->CollectPrecedences({NegationOf(vars[0]), NegationOf(vars[4])}, &p);
EXPECT_TRUE(p.empty());
}
TEST(BinaryRelationRepositoryTest, Build) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable z = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true);
BinaryRelationRepository repository;
repository.Add(lit_a, {NegationOf(x), 1}, {y, 1}, 2, 8);
repository.Add(Literal(kNoLiteralIndex), {x, 2}, {y, -2}, 0, 10);
repository.Add(lit_a, {x, -3}, {NegationOf(y), 2}, 1, 15);
repository.Add(lit_b, {x, -3}, {kNoIntegerVariable, 0}, 3, 5);
repository.Add(Literal(kNoLiteralIndex), {x, 3}, {y, -1}, 5, 15);
repository.Add(Literal(kNoLiteralIndex), {x, 1}, {z, -1}, 0, 10);
repository.AddPartialRelation(lit_b, x, z);
repository.Build();
EXPECT_EQ(repository.size(), 7);
EXPECT_EQ(repository.relation(0), (Relation{lit_a, {x, -1}, {y, 1}, 2, 8}));
EXPECT_EQ(repository.relation(1),
(Relation{Literal(kNoLiteralIndex), {x, 2}, {y, -2}, 0, 10}));
EXPECT_EQ(repository.relation(2), (Relation{lit_a, {x, -3}, {y, -2}, 1, 15}));
EXPECT_EQ(repository.relation(3),
(Relation{lit_b, {x, -3}, {kNoIntegerVariable, 0}, 3, 5}));
EXPECT_EQ(repository.relation(6), (Relation{lit_b, {x, 1}, {z, 1}, 0, 0}));
EXPECT_THAT(repository.IndicesOfRelationsEnforcedBy(lit_a),
UnorderedElementsAre(0, 2));
EXPECT_THAT(repository.IndicesOfRelationsEnforcedBy(lit_b),
UnorderedElementsAre(3, 6));
EXPECT_THAT(repository.IndicesOfRelationsContaining(x),
UnorderedElementsAre(1, 4, 5));
EXPECT_THAT(repository.IndicesOfRelationsContaining(y),
UnorderedElementsAre(1, 4));
EXPECT_THAT(repository.IndicesOfRelationsContaining(z),
UnorderedElementsAre(5));
EXPECT_THAT(repository.IndicesOfRelationsBetween(x, y),
UnorderedElementsAre(1, 4));
EXPECT_THAT(repository.IndicesOfRelationsBetween(y, x),
UnorderedElementsAre(1, 4));
EXPECT_THAT(repository.IndicesOfRelationsBetween(x, z),
UnorderedElementsAre(5));
EXPECT_THAT(repository.IndicesOfRelationsBetween(z, y), IsEmpty());
}
std::vector<Relation> GetRelations(Model& model) {
const BinaryRelationRepository& repository =
*model.GetOrCreate<BinaryRelationRepository>();
std::vector<Relation> relations;
for (int i = 0; i < repository.size(); ++i) {
Relation r = repository.relation(i);
if (r.a.coeff < 0) {
r = Relation({r.enforcement,
{r.a.var, -r.a.coeff},
{r.b.var, -r.b.coeff},
-r.rhs,
-r.lhs});
}
relations.push_back(r);
}
return relations;
}
TEST(BinaryRelationRepositoryTest, LoadCpModelAddUnaryAndBinaryRelations) {
const CpModelProto model_proto = ParseTestProto(R"pb(
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 1 ] }
constraints {
enforcement_literal: [ 0 ]
linear {
vars: [ 2, 3 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: [ 1 ]
linear {
vars: [ 2 ]
coeffs: [ 1 ]
domain: [ 5, 10 ]
}
}
constraints {
enforcement_literal: [ 0, 1, 4 ]
linear {
vars: [ 3 ]
coeffs: [ 1 ]
domain: [ 5, 10 ]
}
}
constraints {
linear {
vars: [ 2, 3 ]
coeffs: [ 3, -2 ]
domain: [ -10, 10 ]
}
}
constraints {
linear {
vars: [ 0, 1, 2, 3 ]
coeffs: [ 1, 1, 1, 1 ]
domain: [ -5, 5 ]
}
}
)pb");
Model model;
LoadCpModel(model_proto, &model);
const CpModelMapping& mapping = *model.GetOrCreate<CpModelMapping>();
EXPECT_THAT(GetRelations(model),
UnorderedElementsAre(Relation{mapping.Literal(0),
{mapping.Integer(2), 1},
{mapping.Integer(3), -1},
0,
10},
Relation{mapping.Literal(1),
{mapping.Integer(2), 1},
{kNoIntegerVariable, 0},
5,
10},
Relation{Literal(kNoLiteralIndex),
{mapping.Integer(2), 3},
{mapping.Integer(3), -2},
-10,
10}));
}
TEST(BinaryRelationRepositoryTest,
LoadCpModelAddsUnaryRelationsEnforcedByTwoLiterals_Case1) {
// x in [10, 90] and "a and b => x in [20, 90]".
const CpModelProto model_proto = ParseTestProto(R"pb(
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 1 ] }
variables { domain: [ 10, 90 ] }
constraints {
enforcement_literal: [ 0, 1 ]
linear {
vars: [ 2 ]
coeffs: [ 1 ]
domain: [ 20, 90 ]
}
}
)pb");
Model model;
// This is needed to get an integer view of all the Boolean variables.
model.GetOrCreate<SatParameters>()->set_linearization_level(2);
LoadCpModel(model_proto, &model);
const CpModelMapping& mapping = *model.GetOrCreate<CpModelMapping>();
const IntegerEncoder& encoder = *model.GetOrCreate<IntegerEncoder>();
const IntegerVariable a = encoder.GetLiteralView(mapping.Literal(0));
const IntegerVariable b = encoder.GetLiteralView(mapping.Literal(1));
const IntegerVariable x = mapping.Integer(2);
// Two binary relations enforced by only one literal should be added:
// - a => x - 10.b in [10, 90]
// - b => x - 10.a in [10, 90]
EXPECT_THAT(GetRelations(model),
UnorderedElementsAre(
Relation{mapping.Literal(0), {x, 1}, {b, -10}, 10, 90},
Relation{mapping.Literal(1), {x, 1}, {a, -10}, 10, 90}));
}
TEST(BinaryRelationRepositoryTest,
LoadCpModelAddsUnaryRelationsEnforcedByTwoLiterals_Case2) {
// x in [10, 90] and "a and b => x in [10, 80]".
const CpModelProto model_proto = ParseTestProto(R"pb(
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 1 ] }
variables { domain: [ 10, 90 ] }
constraints {
enforcement_literal: [ 0, 1 ]
linear {
vars: [ 2 ]
coeffs: [ 1 ]
domain: [ 10, 80 ]
}
}
)pb");
Model model;
// This is needed to get an integer view of all the Boolean variables.
model.GetOrCreate<SatParameters>()->set_linearization_level(2);
LoadCpModel(model_proto, &model);
const CpModelMapping& mapping = *model.GetOrCreate<CpModelMapping>();
const IntegerEncoder& encoder = *model.GetOrCreate<IntegerEncoder>();
const IntegerVariable a = encoder.GetLiteralView(mapping.Literal(0));
const IntegerVariable b = encoder.GetLiteralView(mapping.Literal(1));
const IntegerVariable x = mapping.Integer(2);
// Two binary relations enforced by only one literal should be added:
// - a => x + 10.b in [10, 90]
// - b => x + 10.a in [10, 90]
EXPECT_THAT(GetRelations(model),
UnorderedElementsAre(
Relation{mapping.Literal(0), {x, 1}, {b, 10}, 10, 90},
Relation{mapping.Literal(1), {x, 1}, {a, 10}, 10, 90}));
}
TEST(BinaryRelationRepositoryTest,
LoadCpModelAddsUnaryRelationsEnforcedByTwoLiterals_Case3) {
// x in [10, 90] and "a and not(b) => x in [20, 90]".
const CpModelProto model_proto = ParseTestProto(R"pb(
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 1 ] }
variables { domain: [ 10, 90 ] }
constraints {
enforcement_literal: [ 0, -2 ]
linear {
vars: [ 2 ]
coeffs: [ 1 ]
domain: [ 20, 90 ]
}
}
)pb");
Model model;
// This is needed to get an integer view of all the Boolean variables.
model.GetOrCreate<SatParameters>()->set_linearization_level(2);
LoadCpModel(model_proto, &model);
const CpModelMapping& mapping = *model.GetOrCreate<CpModelMapping>();
const IntegerEncoder& encoder = *model.GetOrCreate<IntegerEncoder>();
const IntegerVariable a = encoder.GetLiteralView(mapping.Literal(0));
const IntegerVariable b = encoder.GetLiteralView(mapping.Literal(1));
const IntegerVariable x = mapping.Integer(2);
// Two binary relations enforced by only one literal should be added:
// - a => x + 10.b in [20, 100] (<=> a => x - (10-10.b) in [10, 90])
// - b => x - 10.a in [10, 90]
EXPECT_THAT(
GetRelations(model),
UnorderedElementsAre(
Relation{mapping.Literal(0), {x, 1}, {b, 10}, 20, 100},
Relation{mapping.Literal(1).Negated(), {x, 1}, {a, -10}, 10, 90}));
}
TEST(BinaryRelationRepositoryTest,
LoadCpModelAddsUnaryRelationsEnforcedByTwoLiterals_Case4) {
// x in [10, 90] and "a and not(b) => x in [10, 80]".
const CpModelProto model_proto = ParseTestProto(R"pb(
variables { domain: [ 0, 1 ] }
variables { domain: [ 0, 1 ] }
variables { domain: [ 10, 90 ] }
constraints {
enforcement_literal: [ 0, -2 ]
linear {
vars: [ 2 ]
coeffs: [ 1 ]
domain: [ 10, 80 ]
}
}
)pb");
Model model;
// This is needed to get an integer view of all the Boolean variables.
model.GetOrCreate<SatParameters>()->set_linearization_level(2);
LoadCpModel(model_proto, &model);
const CpModelMapping& mapping = *model.GetOrCreate<CpModelMapping>();
const IntegerEncoder& encoder = *model.GetOrCreate<IntegerEncoder>();
const IntegerVariable a = encoder.GetLiteralView(mapping.Literal(0));
const IntegerVariable b = encoder.GetLiteralView(mapping.Literal(1));
const IntegerVariable x = mapping.Integer(2);
// Two binary relations enforced by only one literal should be added:
// - a => x - 10.b in [0, 80] (<=> a => x + (10-10.b) in [10, 90])
// - b => x + 10.a in [10, 90]
EXPECT_THAT(
GetRelations(model),
UnorderedElementsAre(
Relation{mapping.Literal(0), {x, 1}, {b, -10}, 0, 80},
Relation{mapping.Literal(1).Negated(), {x, 1}, {a, 10}, 10, 90}));
}
TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedRelation) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
BinaryRelationRepository repository;
repository.Add(lit_a, {x, -1}, {y, 1}, 2, 10); // lit_a => y => x + 2
repository.Build();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
absl::flat_hash_map<IntegerVariable, IntegerValue> input = {{x, 3}};
absl::flat_hash_map<IntegerVariable, IntegerValue> output;
const bool result =
repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output);
EXPECT_TRUE(result);
EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8),
std::make_pair(y, 5)));
}
TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_UnenforcedRelation) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
const Literal kNoLiteral = Literal(kNoLiteralIndex);
BinaryRelationRepository repository;
repository.Add(lit_a, {x, -1}, {y, 1}, -5, 10); // lit_a => y => x - 5
repository.Add(kNoLiteral, {x, -1}, {y, 1}, 2, 10); // y => x + 2
repository.Build();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
absl::flat_hash_map<IntegerVariable, IntegerValue> input = {{x, 3}};
absl::flat_hash_map<IntegerVariable, IntegerValue> output;
const bool result =
repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output);
EXPECT_TRUE(result);
EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8),
std::make_pair(y, 5)));
}
TEST(BinaryRelationRepositoryTest,
PropagateLocalBounds_EnforcedBoundSmallerThanLevelZeroBound) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true);
BinaryRelationRepository repository;
repository.Add(lit_a, {x, -1}, {y, 1}, -5, 10); // lit_a => y => x - 5
repository.Add(lit_b, {x, -1}, {y, 1}, 2, 10); // lit_b => y => x + 2
repository.Build();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
absl::flat_hash_map<IntegerVariable, IntegerValue> input = {{x, 3}};
absl::flat_hash_map<IntegerVariable, IntegerValue> output;
const bool result =
repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output);
EXPECT_TRUE(result);
EXPECT_THAT(output, IsEmpty());
}
TEST(BinaryRelationRepositoryTest,
PropagateLocalBounds_EnforcedBoundSmallerThanOutputBound) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
BinaryRelationRepository repository;
repository.Add(lit_a, {x, -1}, {y, 1}, 2, 10); // lit_a => y => x + 2
repository.Build();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
absl::flat_hash_map<IntegerVariable, IntegerValue> input = {{x, 3}};
absl::flat_hash_map<IntegerVariable, IntegerValue> output = {{y, 8}};
const bool result =
repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output);
EXPECT_TRUE(result);
EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8),
std::make_pair(y, 8)));
}
TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_Infeasible) {
Model model;
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
BinaryRelationRepository repository;
repository.Add(lit_a, {x, -1}, {y, 1}, 8, 10); // lit_a => y => x + 8
repository.Build();
IntegerTrail* integer_trail = model.GetOrCreate<IntegerTrail>();
absl::flat_hash_map<IntegerVariable, IntegerValue> input = {{x, 3}};
absl::flat_hash_map<IntegerVariable, IntegerValue> output;
const bool result =
repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output);
EXPECT_FALSE(result);
EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -2),
std::make_pair(y, 11)));
}
TEST(GreaterThanAtLeastOneOfDetectorTest, AddGreaterThanAtLeastOneOf) {
Model model;
const IntegerVariable a = model.Add(NewIntegerVariable(2, 10));
const IntegerVariable b = model.Add(NewIntegerVariable(5, 10));
const IntegerVariable c = model.Add(NewIntegerVariable(3, 10));
const IntegerVariable d = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_c = Literal(model.Add(NewBooleanVariable()), true);
model.Add(ClauseConstraint({lit_a, lit_b, lit_c}));
auto* repository = model.GetOrCreate<BinaryRelationRepository>();
repository->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2
repository->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1
repository->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c
repository->Build();
auto* detector = model.GetOrCreate<GreaterThanAtLeastOneOfDetector>();
auto* solver = model.GetOrCreate<SatSolver>();
EXPECT_TRUE(solver->Propagate());
EXPECT_EQ(model.Get(LowerBound(d)), 0);
EXPECT_EQ(1, detector->AddGreaterThanAtLeastOneOfConstraints(&model));
EXPECT_TRUE(solver->Propagate());
EXPECT_EQ(model.Get(LowerBound(d)), std::min({2 + 2, 5 - 1, 3 + 0}));
}
TEST(GreaterThanAtLeastOneOfDetectorTest,
AddGreaterThanAtLeastOneOfWithAutoDetect) {
Model model;
const IntegerVariable a = model.Add(NewIntegerVariable(2, 10));
const IntegerVariable b = model.Add(NewIntegerVariable(5, 10));
const IntegerVariable c = model.Add(NewIntegerVariable(3, 10));
const IntegerVariable d = model.Add(NewIntegerVariable(0, 10));
const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true);
const Literal lit_c = Literal(model.Add(NewBooleanVariable()), true);
model.Add(ClauseConstraint({lit_a, lit_b, lit_c}));
auto* repository = model.GetOrCreate<BinaryRelationRepository>();
repository->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2
repository->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1
repository->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c
repository->Build();
auto* detector = model.GetOrCreate<GreaterThanAtLeastOneOfDetector>();
auto* solver = model.GetOrCreate<SatSolver>();
EXPECT_TRUE(solver->Propagate());
EXPECT_EQ(model.Get(LowerBound(d)), 0);
EXPECT_EQ(1, detector->AddGreaterThanAtLeastOneOfConstraints(
&model, /*auto_detect_clauses=*/true));
EXPECT_TRUE(solver->Propagate());
EXPECT_EQ(model.Get(LowerBound(d)), std::min({2 + 2, 5 - 1, 3 + 0}));
}
TEST(PrecedencesPropagatorTest, ComputeFullPrecedencesIfCycle) {
Model model;
std::vector<IntegerVariable> vars(10);
for (int i = 0; i < vars.size(); ++i) {
vars[i] = model.Add(NewIntegerVariable(0, 10));
}
// Even if the weight are compatible, we will fail here.
model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2));
model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2));
model.Add(LowerOrEqualWithOffset(vars[2], vars[1], -10));
model.Add(LowerOrEqualWithOffset(vars[0], vars[2], 5));
std::vector<FullIntegerPrecedence> precedences;
model.GetOrCreate<PrecedenceRelations>()->ComputeFullPrecedences(
{vars[0], vars[1]}, &precedences);
EXPECT_TRUE(precedences.empty());
}
TEST(PrecedencesPropagatorTest, BasicFiltering) {
Model model;
std::vector<IntegerVariable> vars(10);
for (int i = 0; i < vars.size(); ++i) {
vars[i] = model.Add(NewIntegerVariable(0, 10));
}
// 1
// / \
// 0 2 -- 4
// \ /
// 3
model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2));
model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2));
model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1));
model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2));
model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2));
std::vector<FullIntegerPrecedence> precedences;
model.GetOrCreate<PrecedenceRelations>()->ComputeFullPrecedences(
{vars[0], vars[1], vars[3]}, &precedences);
// We only output size at least 2, and "relevant" precedences.
// So here only vars[2].
ASSERT_EQ(precedences.size(), 1);
EXPECT_EQ(precedences[0].var, vars[2]);
EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2));
EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2));
}
TEST(PrecedencesPropagatorTest, BasicFiltering2) {
Model model;
std::vector<IntegerVariable> vars(10);
for (int i = 0; i < vars.size(); ++i) {
vars[i] = model.Add(NewIntegerVariable(0, 10));
}
// 1
// / \
// 0 2 -- 4
// \ / /
// 3 5
model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2));
model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2));
model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1));
model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2));
model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2));
model.Add(LowerOrEqualWithOffset(vars[5], vars[4], 7));
std::vector<FullIntegerPrecedence> precedences;
model.GetOrCreate<PrecedenceRelations>()->ComputeFullPrecedences(
{vars[0], vars[1], vars[3]}, &precedences);
// Same as before here.
ASSERT_EQ(precedences.size(), 1);
EXPECT_EQ(precedences[0].var, vars[2]);
EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2));
EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2));
// But if we ask for 5, we will get two results.
precedences.clear();
model.GetOrCreate<PrecedenceRelations>()->ComputeFullPrecedences(
{vars[0], vars[1], vars[3], vars[5]}, &precedences);
ASSERT_EQ(precedences.size(), 2);
EXPECT_EQ(precedences[0].var, vars[2]);
EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2));
EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2));
EXPECT_EQ(precedences[1].var, vars[4]);
EXPECT_THAT(precedences[1].offsets, ElementsAre(6, 4, 4, 7));
EXPECT_THAT(precedences[1].indices, ElementsAre(0, 1, 2, 3));
}
} // namespace
} // namespace sat
} // namespace operations_research