From c06d62f0cbb10a150b681f354a0459dfa672497a Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Thu, 3 Apr 2025 16:12:45 +0200 Subject: [PATCH] rename internal sat method; more tests --- ortools/bop/bop_fs.cc | 2 +- ortools/bop/bop_lns.cc | 18 +- ortools/bop/bop_ls.cc | 2 +- ortools/bop/bop_ls.h | 2 +- ortools/bop/bop_util.cc | 2 +- ortools/graph/BUILD.bazel | 27 + ortools/graph/graph_test.cc | 1319 +++ ortools/graph/iterators_test.cc | 175 + ortools/graph/k_shortest_paths.h | 10 + ortools/graph/k_shortest_paths_test.cc | 36 +- ortools/sat/2d_orthogonal_packing_test.cc | 518 ++ ortools/sat/BUILD.bazel | 158 +- ortools/sat/cp_model_presolve_test.cc | 7926 +++++++++++++++++ ortools/sat/cp_model_solver_test.cc | 4592 ++++++++++ ortools/sat/cp_model_utils_test.cc | 361 + ortools/sat/opb_reader.h | 66 +- ...ean_problem_test.cc => opb_reader_test.cc} | 17 +- ortools/sat/sat_solver.h | 7 +- ortools/sat/simplification.cc | 2 +- ortools/sat/simplification.h | 2 +- ortools/sat/synchronization_test.cc | 1133 +++ ortools/set_cover/set_cover_model.cc | 4 +- ortools/set_cover/set_cover_model.h | 5 +- 23 files changed, 16294 insertions(+), 90 deletions(-) create mode 100644 ortools/graph/graph_test.cc create mode 100644 ortools/graph/iterators_test.cc create mode 100644 ortools/sat/2d_orthogonal_packing_test.cc create mode 100644 ortools/sat/cp_model_presolve_test.cc create mode 100644 ortools/sat/cp_model_solver_test.cc create mode 100644 ortools/sat/cp_model_utils_test.cc rename ortools/sat/{boolean_problem_test.cc => opb_reader_test.cc} (93%) create mode 100644 ortools/sat/synchronization_test.cc diff --git a/ortools/bop/bop_fs.cc b/ortools/bop/bop_fs.cc index e60685c664..ae67e79c60 100644 --- a/ortools/bop/bop_fs.cc +++ b/ortools/bop/bop_fs.cc @@ -345,7 +345,7 @@ BopOptimizerBase::Status BopRandomFirstSolutionGenerator::Optimize( } // This can be proved during the call to RestoreSolverToAssumptionLevel(). - if (sat_propagator_->IsModelUnsat()) { + if (sat_propagator_->ModelIsUnsat()) { // The solution is proved optimal (if any). learned_info->lower_bound = best_cost; return best_cost == std::numeric_limits::max() diff --git a/ortools/bop/bop_lns.cc b/ortools/bop/bop_lns.cc index 4912f7e791..32460a3c73 100644 --- a/ortools/bop/bop_lns.cc +++ b/ortools/bop/bop_lns.cc @@ -110,7 +110,7 @@ BopOptimizerBase::Status BopCompleteLNSOptimizer::SynchronizeIfNeeded( /*use_lower_bound=*/false, sat::Coefficient(0), /*use_upper_bound=*/true, sat::Coefficient(num_relaxed_vars), &cst); - if (sat_solver_->IsModelUnsat()) return BopOptimizerBase::ABORT; + if (sat_solver_->ModelIsUnsat()) return BopOptimizerBase::ABORT; // It sounds like a good idea to force the solver to find a similar solution // from the current one. On another side, this is already somewhat enforced by @@ -255,7 +255,7 @@ BopOptimizerBase::Status BopAdaptiveLNSOptimizer::Optimize( const double initial_dt = sat_propagator_->deterministic_time(); auto sat_propagator_cleanup = ::absl::MakeCleanup([initial_dt, this, &learned_info, &time_limit]() { - if (!sat_propagator_->IsModelUnsat()) { + if (!sat_propagator_->ModelIsUnsat()) { sat_propagator_->SetAssumptionLevel(0); sat_propagator_->RestoreSolverToAssumptionLevel(); ExtractLearnedInfoFromSatSolver(sat_propagator_, learned_info); @@ -289,7 +289,7 @@ BopOptimizerBase::Status BopAdaptiveLNSOptimizer::Optimize( << problem_state.original_problem().num_variables(); // Special case if the difficulty is too high. - if (!sat_propagator_->IsModelUnsat()) { + if (!sat_propagator_->ModelIsUnsat()) { if (sat_propagator_->CurrentDecisionLevel() == 0) { VLOG(2) << "Nothing fixed!"; adaptive_difficulty_.DecreaseParameter(); @@ -300,7 +300,7 @@ BopOptimizerBase::Status BopAdaptiveLNSOptimizer::Optimize( // Since everything is already set-up, we try the sat_propagator_ with // a really low conflict limit. This allow to quickly skip over UNSAT // cases without the costly new problem setup. - if (!sat_propagator_->IsModelUnsat()) { + if (!sat_propagator_->ModelIsUnsat()) { sat::SatParameters params; params.set_max_number_of_conflicts( local_parameters.max_number_of_conflicts_for_quick_check()); @@ -329,13 +329,13 @@ BopOptimizerBase::Status BopAdaptiveLNSOptimizer::Optimize( // propagator_ will be used to construct the local problem below. // Note that calling RestoreSolverToAssumptionLevel() might actually prove // the infeasibility. It is important to check the UNSAT status afterward. - if (!sat_propagator_->IsModelUnsat()) { + if (!sat_propagator_->ModelIsUnsat()) { sat_propagator_->RestoreSolverToAssumptionLevel(); } // Check if the problem is proved UNSAT, by previous the search or the // RestoreSolverToAssumptionLevel() call above. - if (sat_propagator_->IsModelUnsat()) { + if (sat_propagator_->ModelIsUnsat()) { return problem_state.solution().IsFeasible() ? BopOptimizerBase::OPTIMAL_SOLUTION_FOUND : BopOptimizerBase::INFEASIBLE; @@ -466,7 +466,7 @@ void ObjectiveBasedNeighborhood::GenerateNeighborhood( break; } sat_propagator->EnqueueDecisionAndBacktrackOnConflict(literal); - if (sat_propagator->IsModelUnsat()) return; + if (sat_propagator->ModelIsUnsat()) return; } } @@ -515,7 +515,7 @@ void ConstraintBasedNeighborhood::GenerateNeighborhood( for (const sat::Literal literal : to_fix) { if (variable_is_relaxed[literal.Variable().value()]) continue; sat_propagator->EnqueueDecisionAndBacktrackOnConflict(literal); - if (sat_propagator->IsModelUnsat()) return; + if (sat_propagator->ModelIsUnsat()) return; } } @@ -595,7 +595,7 @@ void RelationGraphBasedNeighborhood::GenerateNeighborhood( } } } - if (sat_propagator->IsModelUnsat()) return; + if (sat_propagator->ModelIsUnsat()) return; } VLOG(2) << "target:" << target << " relaxed:" << num_relaxed << " actual:" << num_variables - sat_propagator->LiteralTrail().Index(); diff --git a/ortools/bop/bop_ls.cc b/ortools/bop/bop_ls.cc index 6a29c4a929..b2d5e7620c 100644 --- a/ortools/bop/bop_ls.cc +++ b/ortools/bop/bop_ls.cc @@ -665,7 +665,7 @@ int SatWrapper::ApplyDecision(sat::Literal decision_literal, const int old_decision_level = sat_solver_->CurrentDecisionLevel(); const int new_trail_index = sat_solver_->EnqueueDecisionAndBackjumpOnConflict(decision_literal); - if (sat_solver_->IsModelUnsat()) { + if (sat_solver_->ModelIsUnsat()) { return old_decision_level + 1; } diff --git a/ortools/bop/bop_ls.h b/ortools/bop/bop_ls.h index 2154a29f83..4ee2d60ea2 100644 --- a/ortools/bop/bop_ls.h +++ b/ortools/bop/bop_ls.h @@ -75,7 +75,7 @@ class SatWrapper { // the SAT solver is not able to prove it; After some decisions / learned // conflicts, the SAT solver might be able to prove UNSAT and so this will // return true. - bool IsModelUnsat() const { return sat_solver_->IsModelUnsat(); } + bool IsModelUnsat() const { return sat_solver_->ModelIsUnsat(); } // Return the current solver VariablesAssignment. const sat::VariablesAssignment& SatAssignment() const { diff --git a/ortools/bop/bop_util.cc b/ortools/bop/bop_util.cc index 0381468f33..7d0dddfe8d 100644 --- a/ortools/bop/bop_util.cc +++ b/ortools/bop/bop_util.cc @@ -112,7 +112,7 @@ void ExtractLearnedInfoFromSatSolver(sat::SatSolver* solver, CHECK(nullptr != info); // This should never be called if the problem is UNSAT. - CHECK(!solver->IsModelUnsat()); + CHECK(!solver->ModelIsUnsat()); // Fixed variables. info->fixed_literals.clear(); diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index cac7ee0c9a..9c6e2d8532 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -44,6 +44,22 @@ cc_library( ], ) +cc_test( + name = "graph_test", + size = "small", + srcs = ["graph_test.cc"], + deps = [ + ":graph", + "//ortools/base:gmock_main", + "//ortools/base:intops", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", + "@com_google_benchmark//:benchmark", + ], +) + cc_library( name = "flow_graph", hdrs = ["flow_graph.h"], @@ -835,6 +851,17 @@ cc_library( hdrs = ["iterators.h"], ) +cc_test( + name = "iterators_test", + size = "small", + srcs = ["iterators_test.cc"], + deps = [ + ":iterators", + "//ortools/base:gmock_main", + "//ortools/base:int_type", + ], +) + cc_library( name = "random_graph", srcs = ["random_graph.cc"], diff --git a/ortools/graph/graph_test.cc b/ortools/graph/graph_test.cc new file mode 100644 index 0000000000..e599d39af4 --- /dev/null +++ b/ortools/graph/graph_test.cc @@ -0,0 +1,1319 @@ +// 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/graph/graph.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_int.h" + +namespace util { + +using testing::Pair; +using testing::UnorderedElementsAre; + +DEFINE_STRONG_INT_TYPE(StrongNodeId, int32_t); +DEFINE_STRONG_INT_TYPE(StrongArcId, int32_t); + +// Iterators. +#if __cplusplus >= 202002L +static_assert(std::forward_iterator::OutgoingArcIterator>); +static_assert(std::forward_iterator::OutgoingHeadIterator>); +static_assert( + std::forward_iterator::OutgoingArcIterator>); +static_assert( + std::forward_iterator::IncomingArcIterator>); +static_assert(std::input_iterator< + ReverseArcListGraph<>::OutgoingOrOppositeIncomingArcIterator>); +static_assert( + std::forward_iterator::OutgoingHeadIterator>); +#endif // __cplusplus >= 202002L + +// GraphTraits. +static_assert( + std::is_same_v>::NodeIndex, + int32_t>); +static_assert( + std::is_same_v< + typename GraphTraits>::NodeIndex, + int16_t>); +static_assert(std::is_same_v< + typename GraphTraits>::NodeIndex, + uint32_t>); +static_assert( + std::is_same_v< + typename GraphTraits>::NodeIndex, + StrongNodeId>); +static_assert( + std::is_same_v< + typename GraphTraits>>::NodeIndex, int>); + +// Check that the OutgoingArcs() returns exactly the same arcs as the verifier. +// This also test Head(), Tail(), and OutDegree(). +template +void CheckOutgoingArcIterator( + const GraphType& graph, + absl::Span> verifier) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + std::vector node_seen(verifier.size(), 0); + for (int i = 0; i < verifier.size(); ++i) { + for (int j = 0; j < verifier[i].size(); ++j) { + // We have to use int because there can be multiple arcs. + node_seen[static_cast(verifier[i][j])]++; + } + int outgoing_arc_number = 0; + for (const ArcIndex arc : graph.OutgoingArcs(NodeIndex(i))) { + const int head = static_cast(graph.Head(arc)); + const int tail = static_cast(graph.Tail(arc)); + EXPECT_GE(head, 0); + EXPECT_LT(head, verifier.size()); + EXPECT_GT(node_seen[head], 0); + node_seen[head]--; + EXPECT_EQ(i, tail); + EXPECT_EQ(arc, + *(graph.OutgoingArcsStartingFrom(NodeIndex(i), arc).begin())); + ++outgoing_arc_number; + } + // If this is true, then node_seen must have been cleaned. + EXPECT_EQ(verifier[i].size(), outgoing_arc_number); + EXPECT_EQ(ArcIndex(verifier[i].size()), graph.OutDegree(NodeIndex(i))); + } +} + +// Check that the operator[] returns exactly the same nodes as the verifier. +template +void CheckOutgoingHeadIterator( + const GraphType& graph, + absl::Span> verifier) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + std::vector node_seen(verifier.size(), 0); + for (int i = 0; i < verifier.size(); ++i) { + for (int j = 0; j < verifier[i].size(); ++j) { + // We have to use int because there can be multiple arcs. + node_seen[static_cast(verifier[i][j])]++; + } + int outgoing_head_number = 0; + for (const NodeIndex node : graph[NodeIndex(i)]) { + const int node_id = static_cast(node); + EXPECT_GE(node_id, 0); + EXPECT_LT(node_id, verifier.size()); + EXPECT_GT(node_seen[node_id], 0); + node_seen[node_id]--; + ++outgoing_head_number; + } + // If this is true, then node_seen must have been cleaned. + EXPECT_EQ(verifier[i].size(), outgoing_head_number); + EXPECT_EQ(ArcIndex(verifier[i].size()), graph.OutDegree(NodeIndex(i))); + } +} + +// Check that the heads of OutgoingArcs() + the tails of IncomingArcs() are the +// same as the heads of OutgoingOrOppositeIncomingArcs(). Also perform +// various checks on the arcs. +template +void CheckReverseArcIterator(const GraphType& graph) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + ArcIndex total_arc_number(0); + std::vector node_seen(static_cast(graph.num_nodes()), 0); + for (const NodeIndex node : graph.AllNodes()) { + ArcIndex num_incident_arcs(0); + for (const ArcIndex arc : graph.OutgoingOrOppositeIncomingArcs(node)) { + EXPECT_EQ(node, graph.Tail(arc)); + EXPECT_EQ(arc, + *(graph.OutgoingOrOppositeIncomingArcsStartingFrom(node, arc) + .begin())); + node_seen[static_cast(graph.Head(arc))]++; + ++num_incident_arcs; + } + total_arc_number += num_incident_arcs; + ArcIndex num_outgoing_arcs(0); + for (const ArcIndex arc : graph.OutgoingArcs(node)) { + EXPECT_GE(arc, ArcIndex(0)); + EXPECT_EQ(node, graph.Tail(arc)); + EXPECT_EQ(arc, *(graph.OutgoingArcsStartingFrom(node, arc).begin())); + const size_t head = static_cast(graph.Head(arc)); + EXPECT_GE(node_seen[head], 0); + node_seen[head]--; + ++num_outgoing_arcs; + } + EXPECT_EQ(num_outgoing_arcs, graph.OutDegree(node)); + ArcIndex num_incoming_arcs(0); + for (const ArcIndex arc : graph.IncomingArcs(node)) { + EXPECT_GE(arc, ArcIndex(0)); + EXPECT_EQ(node, graph.Head(arc)); + EXPECT_EQ(arc, *(graph.IncomingArcsStartingFrom(node, arc).begin())); + const size_t tail = static_cast(graph.Tail(arc)); + node_seen[tail]--; + EXPECT_GE(node_seen[tail], 0); + ++num_incoming_arcs; + } + EXPECT_EQ(num_incoming_arcs, graph.InDegree(node)); + // If this is true, then node_seen must have been cleaned. + EXPECT_EQ(num_incident_arcs, num_outgoing_arcs + num_incoming_arcs); + } + EXPECT_EQ(2 * graph.num_arcs(), total_arc_number); +} + +// Check that the arcs returned by OppositeIncomingArcs() are exactly the +// reverse of the arcs returned by IncomingArcs(). +template +void CheckOppositeIncomingArcs(const GraphType& graph) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + std::vector arcs; + std::vector opposite_arcs; + for (const NodeIndex node : graph.AllNodes()) { + arcs.clear(); + opposite_arcs.clear(); + for (const ArcIndex arc : graph.IncomingArcs(node)) { + arcs.push_back(arc); + } + for (const ArcIndex arc : graph.OppositeIncomingArcs(node)) { + opposite_arcs.push_back(arc); + } + ASSERT_EQ(arcs.size(), opposite_arcs.size()); + for (int a = 0; a < arcs.size(); ++a) { + ASSERT_EQ(opposite_arcs[a], graph.OppositeArc(arcs[a])); + } + } +} + +template +void CheckReverseArc(const GraphType& graph) {} + +template +void CheckReverseArc( + const ReverseArcListGraph& graph) { + CheckReverseArcIterator(graph); + CheckOppositeIncomingArcs(graph); +} + +template +void CheckReverseArc( + const ReverseArcStaticGraph& graph) { + CheckReverseArcIterator(graph); + CheckOppositeIncomingArcs(graph); +} + +// Check that arc annotation can be permuted properly. This is achieved +// by "annotating" the original arc index with the head and tail information +// and checking that after permutation the annotation of a given arc index +// matches is actual head and tail in the graph. +template +void CheckArcIndexPermutation( + const GraphType& graph, + const std::vector& permutation, + const std::vector& heads, + const std::vector& tails) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + std::vector annotation_h(heads); + std::vector annotation_t(tails); + Permute(permutation, &annotation_h); + Permute(permutation, &annotation_t); + for (ArcIndex arc : graph.AllForwardArcs()) { + CHECK_EQ(annotation_h[static_cast(arc)], graph.Head(arc)); + CHECK_EQ(annotation_t[static_cast(arc)], graph.Tail(arc)); + } +} + +template +void ConstructAndCheckGraph( + const typename GraphType::NodeIndex num_nodes, + const typename GraphType::ArcIndex num_arcs, + const std::vector& heads, + const std::vector& tails, bool reserve, + bool test_permutation) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + std::unique_ptr graph; + if (reserve) { + graph.reset(new GraphType(num_nodes, num_arcs)); + } else { + graph.reset(new GraphType()); + } + std::vector> verifier(static_cast(num_nodes)); + + for (ArcIndex i(0); i < num_arcs; ++i) { + NodeIndex head = heads[static_cast(i)]; + NodeIndex tail = tails[static_cast(i)]; + EXPECT_EQ(i, graph->AddArc(tail, head)); + verifier[static_cast(tail)].push_back(head); + } + std::vector permutation; + if (test_permutation) { + graph->Build(&permutation); + } else { + graph->Build(); + } + + EXPECT_EQ(num_nodes, graph->num_nodes()); + EXPECT_EQ(num_nodes, graph->size()); + EXPECT_EQ(num_arcs, graph->num_arcs()); + CheckOutgoingArcIterator(*graph, verifier); + CheckOutgoingHeadIterator(*graph, verifier); + if (test_permutation) { + CheckArcIndexPermutation(*graph, permutation, heads, tails); + } + CheckReverseArc(*graph); +} + +// Return the size of the memory block allocated by malloc when asking for x +// bytes. +inline int UpperBoundOfMallocBlockSizeOf(int x) { + // Note(user): as of 2012-09, the rule seems to be: round x up to the + // next multiple of 16. + // WARNING: This may change, and may already be wrong for small values. + return 16 * ((x + 15) / 16); +} + +TEST(SVectorTest, DynamicGrowth) { + internal::SVector v; + EXPECT_EQ(0, v.size()); + EXPECT_EQ(0, v.capacity()); + for (int i = 0; i < 100; i++) { + v.grow(-i, i); + } + EXPECT_EQ(100, v.size()); + EXPECT_GE(v.capacity(), 100); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); + for (int i = 0; i < 100; i++) { + EXPECT_EQ(-i, v[~i]); + EXPECT_EQ(i, v[i]); + } +} + +TEST(SVectorTest, Reserve) { + internal::SVector v; + v.reserve(100); + EXPECT_EQ(0, v.size()); + EXPECT_GE(v.capacity(), 100); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); + for (int i = 0; i < 100; i++) { + v.grow(-i, i); + } + EXPECT_EQ(100, v.size()); + EXPECT_GE(v.capacity(), 100); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); + for (int i = 0; i < 10; i++) { + EXPECT_EQ(-i, v[~i]); + EXPECT_EQ(i, v[i]); + } +} + +TEST(SVectorTest, Resize) { + internal::SVector v; + v.resize(100); + EXPECT_EQ(100, v.size()); + EXPECT_GE(v.capacity(), 100); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); + for (int i = 0; i < 100; i++) { + EXPECT_EQ(0, v[-i - 1]); + EXPECT_EQ(0, v[i]); + } +} + +TEST(SVectorTest, ResizeToZero) { + internal::SVector s; + s.resize(1); + s.resize(0); + EXPECT_EQ(0, s.size()); +} + +TEST(SVectorTest, Swap) { + internal::SVector s; + internal::SVector t; + s.resize(1); + s[0] = 's'; + s[-1] = 's'; + t.resize(2); + for (int i = -2; i <= 1; ++i) { + t[i] = 't'; + } + s.swap(t); + EXPECT_EQ(1, t.size()); + EXPECT_EQ('s', t[-1]); + EXPECT_EQ('s', t[0]); + EXPECT_EQ(2, s.size()); + EXPECT_EQ('t', s[-2]); + EXPECT_EQ('t', s[-1]); + EXPECT_EQ('t', s[0]); + EXPECT_EQ('t', s[1]); +} + +TEST(SVectorTest, SwapAndDestroy) { + internal::SVector s; + { + internal::SVector t; + t.resize(2); + t[-2] = 42; + t.swap(s); + } + EXPECT_EQ(2, s.size()); + EXPECT_EQ(42, s[-2]); + EXPECT_EQ(0, s[1]); +} + +// Use a more complex type to better check the invocations of +// constructors/destructors. +TEST(SVectorStringTest, DynamicSize) { + internal::SVector s; + s.resize(10); + for (int i = 0; i < 10; ++i) { + s[i] = "Right"; + s[~i] = "Left"; + } + ASSERT_LT(s.capacity(), 50); + for (int i = 0; i < 50; ++i) s.grow("NewLeft", "NewRight"); + s.resize(10); + for (int i = 0; i < 50; ++i) s.grow("NewNewLeft", "NewNewRight"); + for (int i = 0; i < 10; ++i) { + EXPECT_EQ("Left", s[-i - 1]); + EXPECT_EQ("Right", s[i]); + } + for (int i = 10; i < 10 + 50; ++i) { + EXPECT_EQ("NewNewLeft", s[-i - 1]); + EXPECT_EQ("NewNewRight", s[i]); + } +} + +// An object that supports moves but not copies. It also has non-trivial +// default constructor, and a non-trivial destructor, and makes various internal +// consistency checks that help flush out bugs (double destruction, failure to +// destruct, etc.). +class MoveOnlyObject { + public: + MoveOnlyObject() : id_(std::make_unique(sequence_++)) { + ++object_count_; + Validate(); + } + ~MoveOnlyObject() { + Validate(); + --object_count_; + CHECK_GE(object_count_, 0); + } + MoveOnlyObject(const MoveOnlyObject&) = delete; + MoveOnlyObject(MoveOnlyObject&& other) : id_(std::move(other.id_)) { + ++object_count_; + other.id_ = std::make_unique(sequence_++); + Validate(); + other.Validate(); + } + MoveOnlyObject& operator=(const MoveOnlyObject&) = delete; + MoveOnlyObject& operator=(MoveOnlyObject&& other) { + using std::swap; + swap(id_, other.id_); + Validate(); + other.Validate(); + return *this; + } + + static int GetObjectCount() { return object_count_; } + + private: + void Validate() { + // Every MoveOnlyObject, even after it has been moved from, has a valid + // non-null id_. + EXPECT_TRUE(id_ != nullptr); + if (id_ != nullptr) { + EXPECT_GT(*id_, 0); + EXPECT_LT(*id_, sequence_); + } + } + + static int sequence_; + static int object_count_; + std::unique_ptr id_; +}; + +int MoveOnlyObject::sequence_ = 1; +int MoveOnlyObject::object_count_ = 0; + +TEST(SVectorTest, MoveWithMoveOnlyObject) { + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); + internal::SVector a; + a.resize(10); + EXPECT_EQ(10, a.size()); + EXPECT_EQ(20, MoveOnlyObject::GetObjectCount()); + + internal::SVector b = std::move(a); + EXPECT_EQ(10, b.size()); + EXPECT_EQ(20, MoveOnlyObject::GetObjectCount()); + // Suppress the bugprone-use-after-move clang-tidy warning on `a` + EXPECT_EQ(0, a.size()); // NOLINT +} + +TEST(SVectorTest, ShrinkWithMoveOnlyObject) { + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); + { + internal::SVector a; + a.resize(10); + EXPECT_EQ(20, MoveOnlyObject::GetObjectCount()); + a.resize(5); + EXPECT_EQ(10, MoveOnlyObject::GetObjectCount()); + } + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); +} + +TEST(SVectorTest, GrowMoveOnlyObject) { + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); + { + internal::SVector a; + a.resize(10); + EXPECT_EQ(a.size() * 2, MoveOnlyObject::GetObjectCount()); + + // Grow to the point where the vector reallocates. + MoveOnlyObject* const original_data = a.data(); + while (original_data == a.data()) { + a.resize(a.size() + 1); + EXPECT_EQ(a.size() * 2, MoveOnlyObject::GetObjectCount()); + } + } + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); +} + +TEST(SVectorTest, ReserveMoveOnlyObject) { + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); + { + internal::SVector a; + a.resize(10); + EXPECT_EQ(a.size() * 2, MoveOnlyObject::GetObjectCount()); + + // Reserve to the point where the vector reallocates. + MoveOnlyObject* const original_data = a.data(); + while (original_data == a.data()) { + a.reserve(a.size() * 2); + EXPECT_EQ(a.size() * 2, MoveOnlyObject::GetObjectCount()); + } + } + EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); +} + +struct TrackedObject { + static int num_constructions; + static int num_destructions; + static int num_moves; + static int num_copies; + static void ResetCounters() { + num_constructions = 0; + num_destructions = 0; + num_moves = 0; + num_copies = 0; + } + static std::string Counters() { + return absl::StrCat("constructions: ", num_constructions, + ", destructions: ", num_destructions, + ", moves: ", num_moves, ", copies: ", num_copies); + } + + TrackedObject() { ++num_constructions; } + ~TrackedObject() { ++num_destructions; } + TrackedObject(TrackedObject&&) { ++num_moves; } + TrackedObject(const TrackedObject&) { ++num_copies; } + TrackedObject& operator=(const TrackedObject&) { + ++num_copies; + return *this; + } + TrackedObject& operator=(TrackedObject&&) { + ++num_moves; + return *this; + } +}; + +int TrackedObject::num_constructions = 0; +int TrackedObject::num_destructions = 0; +int TrackedObject::num_moves = 0; +int TrackedObject::num_copies = 0; + +TEST(SVectorTest, CopyConstructor) { + TrackedObject::ResetCounters(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + auto v = std::make_unique>(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + v->resize(5); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 0"); + auto v_copy(*v); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 10"); + v.reset(nullptr); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 10, moves: 0, copies: 10"); + ASSERT_EQ(v_copy.size(), 5); +} + +TEST(SVectorTest, AssignmentOperator) { + TrackedObject::ResetCounters(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + auto v = std::make_unique>(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + v->resize(5); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 0"); + internal::SVector other; + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 0"); + other = *v; + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 10"); + v.reset(nullptr); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 10, moves: 0, copies: 10"); + ASSERT_EQ(other.size(), 5); +} + +TEST(SVectorTest, CopyConstructorIntegralType) { + auto v = internal::SVector(); + v.resize(3); + v[-3] = 1; + v[-2] = 2; + v[-1] = 3; + v[0] = 1; + v[1] = 2; + v[2] = 3; + + auto other = internal::SVector(v); + + ASSERT_EQ(v.size(), other.size()); + for (int i = -v.size(); i < v.size(); i++) { + ASSERT_EQ(v[i], other[i]); + } +} + +TEST(SVectorTest, AssignmentOperatorIntegralType) { + internal::SVector other; + auto v = internal::SVector(); + v.resize(3); + v[-3] = 1; + v[-2] = 2; + v[-1] = 3; + v[0] = 1; + v[1] = 2; + v[2] = 3; + + other = v; + + ASSERT_EQ(v.size(), other.size()); + for (int i = -v.size(); i < v.size(); i++) { + ASSERT_EQ(v[i], other[i]); + } +} + +TEST(SVectorTest, MoveConstructor) { + TrackedObject::ResetCounters(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + internal::SVector a; + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + a.resize(5); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 0"); + internal::SVector b = std::move(a); + // We don't expect any moves of the individual elements, because the + // containers just swap their memory buffers. + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 10, destructions: 0, moves: 0, copies: 0"); + ASSERT_EQ(b.size(), 5); +} + +TEST(SVectorTest, MoveAssignmentOperator) { + TrackedObject::ResetCounters(); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 0, destructions: 0, moves: 0, copies: 0"); + internal::SVector a; + a.resize(3); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 6, destructions: 0, moves: 0, copies: 0"); + { + internal::SVector b; + b.resize(5); + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 16, destructions: 0, moves: 0, copies: 0"); + a = std::move(b); + // Ditto: the containers swap themselves. But we do trigger the destruction + // of the underlying elements of the destination vector immediately. + ASSERT_EQ(TrackedObject::Counters(), + "constructions: 16, destructions: 6, moves: 0, copies: 0"); + } + ASSERT_EQ(a.size(), 5); +} + +template +class GenericGraphInterfaceTest : public ::testing::Test {}; + +typedef ::testing::Types< + ListGraph, ListGraph, + ListGraph, ListGraph, + ListGraph, ReverseArcListGraph, + ReverseArcListGraph, + ReverseArcListGraph, + ReverseArcListGraph, + ReverseArcListGraph, + ReverseArcStaticGraph, + ReverseArcStaticGraph, + ReverseArcStaticGraph, + ReverseArcStaticGraph, + ReverseArcStaticGraph, + StaticGraph, StaticGraph, + StaticGraph, StaticGraph, + StaticGraph> + GraphType; + +TYPED_TEST_SUITE(GenericGraphInterfaceTest, GraphType); + +TYPED_TEST(GenericGraphInterfaceTest, EmptyGraph) { + using NodeIndex = typename TypeParam::NodeIndex; + using ArcIndex = typename TypeParam::ArcIndex; + TypeParam graph; + graph.Build(); + EXPECT_EQ(NodeIndex(0), graph.num_nodes()); + EXPECT_EQ(NodeIndex(0), graph.size()); + EXPECT_EQ(ArcIndex(0), graph.num_arcs()); +} + +TYPED_TEST(GenericGraphInterfaceTest, EmptyGraphAlternateSyntax) { + using NodeIndex = typename TypeParam::NodeIndex; + using ArcIndex = typename TypeParam::ArcIndex; + TypeParam graph(NodeIndex(0), ArcIndex(0)); + graph.Build(); + EXPECT_EQ(NodeIndex(0), graph.num_nodes()); + EXPECT_EQ(NodeIndex(0), graph.size()); + EXPECT_EQ(ArcIndex(0), graph.num_arcs()); +} + +TYPED_TEST(GenericGraphInterfaceTest, GraphWithNodesButNoArc) { + using NodeIndex = typename TypeParam::NodeIndex; + using ArcIndex = typename TypeParam::ArcIndex; + const NodeIndex kNodes(1000); + TypeParam graph(kNodes, ArcIndex(0)); + graph.Build(); + EXPECT_EQ(kNodes, graph.num_nodes()); + EXPECT_EQ(kNodes, graph.size()); + EXPECT_EQ(ArcIndex(0), graph.num_arcs()); + int count = 0; + for (const NodeIndex node : graph.AllNodes()) { + for (const __attribute__((unused)) ArcIndex arc : + graph.OutgoingArcs(node)) { + ++count; + } + } + EXPECT_EQ(0, count); + for (const __attribute__((unused)) ArcIndex arc : graph.AllForwardArcs()) { + ++count; + } + EXPECT_EQ(0, count); +} + +TYPED_TEST(GenericGraphInterfaceTest, BuildWithRandomArc) { + using NodeIndex = typename TypeParam::NodeIndex; + using ArcIndex = typename TypeParam::ArcIndex; + const int kNodes = 1000; + const int kArcs = 5 * kNodes; + std::vector heads(kArcs); + std::vector tails(kArcs); + + std::mt19937 randomizer(42); + for (int i = 0; i < kArcs; ++i) { + heads[i] = NodeIndex(absl::Uniform(randomizer, 0, kNodes)); + tails[i] = NodeIndex(absl::Uniform(randomizer, 0, kNodes)); + } + for (int i = 0; i < 4; ++i) { + const bool reserve = i % 2; + const bool test_permutation = i < 2; + ConstructAndCheckGraph(NodeIndex(kNodes), ArcIndex(kArcs), heads, + tails, reserve, test_permutation); + } +} + +// This exercise the arc index permutation a bit differently, it also +// test for node with 0 outgoing arcs. +TYPED_TEST(GenericGraphInterfaceTest, BuildWithOrderedArc) { + using NodeIndex = typename TypeParam::NodeIndex; + using ArcIndex = typename TypeParam::ArcIndex; + const int kNodes = 10000; + const int kDegree = 2; + const int kArcs = kDegree * kNodes; + std::vector heads(kArcs); + std::vector tails(kArcs); + + std::mt19937 randomizer(42); + int index = 0; + for (int i = 0; i < kNodes; ++i) { + for (int j = 0; j < kDegree; ++j) { + heads[index] = NodeIndex(absl::Uniform(randomizer, 0, kNodes)); + tails[index] = NodeIndex(i); + index++; + } + } + for (int i = 0; i < 4; ++i) { + const bool reserve = i % 2; + const bool test_permutation = i < 2; + ConstructAndCheckGraph(NodeIndex(kNodes), ArcIndex(kArcs), heads, + tails, reserve, test_permutation); + } +} + +TYPED_TEST(GenericGraphInterfaceTest, PastTheEndIterators) { + using NodeIndex = typename TypeParam::NodeIndex; + TypeParam graph; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(0), NodeIndex(2)); + graph.AddArc(NodeIndex(0), NodeIndex(3)); + graph.AddArc(NodeIndex(3), NodeIndex(4)); + graph.AddArc(NodeIndex(1), NodeIndex(4)); + graph.Build(); + for (NodeIndex i(0); i < NodeIndex(4); ++i) { + EXPECT_EQ(graph.OutgoingArcsStartingFrom(i, TypeParam::kNilArc).end(), + graph.OutgoingArcs(i).end()); + if constexpr (TypeParam::kHasNegativeReverseArcs) { + EXPECT_EQ(graph.IncomingArcsStartingFrom(i, TypeParam::kNilArc).end(), + graph.IncomingArcs(i).end()); + EXPECT_EQ( + graph.OppositeIncomingArcsStartingFrom(i, TypeParam::kNilArc).end(), + graph.OppositeIncomingArcs(i).end()); + EXPECT_EQ( + graph + .OutgoingOrOppositeIncomingArcsStartingFrom(i, TypeParam::kNilArc) + .end(), + typename TypeParam::OutgoingOrOppositeIncomingArcIterator( + graph, i, TypeParam::kNilArc)); + } + } +} + +TEST(StaticGraphTest, HeadAndTailBeforeAndAfterBuild) { + for (const bool poll_in_the_middle_of_construction : {false, true}) { + for (const bool build : {false, true}) { + SCOPED_TRACE(absl::StrCat( + "Polling in the middle of construction: ", + poll_in_the_middle_of_construction, + ", Calling Build() at the end of the construction: ", build)); + StaticGraph<> graph; + graph.AddArc(1, 3); + graph.AddArc(2, 1); + graph.AddArc(4, 6); + if (poll_in_the_middle_of_construction) { + ASSERT_EQ(1, graph.Tail(0)); + ASSERT_EQ(3, graph.Head(0)); + ASSERT_EQ(2, graph.Tail(1)); + ASSERT_EQ(1, graph.Head(1)); + ASSERT_EQ(4, graph.Tail(2)); + ASSERT_EQ(6, graph.Head(2)); + ASSERT_EQ(3, graph.num_arcs()); + } + graph.AddArc(2, 1); + graph.AddArc(0, 0); + graph.AddArc(7, 7); + if (build) { + graph.Build(); + std::vector arcs; + for (int i = 0; i < graph.num_arcs(); ++i) { + arcs.push_back(absl::StrCat(graph.Tail(i), "->", graph.Head(i))); + } + EXPECT_THAT(arcs, UnorderedElementsAre("1->3", "2->1", "4->6", "2->1", + "0->0", "7->7")); + } else { + ASSERT_EQ(1, graph.Tail(0)); + ASSERT_EQ(3, graph.Head(0)); + ASSERT_EQ(2, graph.Tail(1)); + ASSERT_EQ(1, graph.Head(1)); + ASSERT_EQ(4, graph.Tail(2)); + ASSERT_EQ(6, graph.Head(2)); + ASSERT_EQ(2, graph.Tail(3)); + ASSERT_EQ(1, graph.Head(3)); + ASSERT_EQ(0, graph.Tail(4)); + ASSERT_EQ(0, graph.Head(4)); + ASSERT_EQ(7, graph.Tail(5)); + ASSERT_EQ(7, graph.Head(5)); + ASSERT_EQ(6, graph.num_arcs()); + } + } + } +} + +TEST(StaticGraphTest, FromArcs) { + const std::vector> arcs = {{1, 2}, {1, 0}}; + StaticGraph<> graph = StaticGraph<>::FromArcs(3, arcs); + EXPECT_EQ(3, graph.num_nodes()); + EXPECT_EQ(3, graph.size()); + EXPECT_EQ(2, graph.num_arcs()); + std::vector> read_arcs; + for (const auto arc : graph.AllForwardArcs()) { + read_arcs.push_back({graph.Tail(arc), graph.Head(arc)}); + } + EXPECT_THAT(read_arcs, UnorderedElementsAre(Pair(1, 2), Pair(1, 0))); +} + +TEST(CompleteGraphTest, EmptyGraph) { + CompleteGraph<> graph(0); + EXPECT_EQ(0, graph.num_nodes()); + EXPECT_EQ(0, graph.size()); + EXPECT_EQ(0, graph.num_arcs()); + for (const auto arc : graph.AllForwardArcs()) { + EXPECT_TRUE(false); + EXPECT_TRUE(graph.IsArcValid(arc)); + } +} + +TEST(CompleteGraphTest, OneNodeGraph) { + CompleteGraph<> graph(1); + EXPECT_EQ(1, graph.num_nodes()); + EXPECT_EQ(1, graph.size()); + EXPECT_EQ(1, graph.num_arcs()); + EXPECT_EQ(graph.Head(0), 0); + EXPECT_EQ(graph.Tail(0), 0); +} + +TEST(CompleteGraphTest, NonEmptyGraph) { + static const int kNumNodes = 5; + CompleteGraph<> graph(kNumNodes); + EXPECT_EQ(kNumNodes, graph.num_nodes()); + EXPECT_EQ(kNumNodes, graph.size()); + EXPECT_EQ(kNumNodes * kNumNodes, graph.num_arcs()); + int count = 0; + for (const auto arc : graph.AllForwardArcs()) { + ++count; + EXPECT_TRUE(graph.IsArcValid(arc)); + } + EXPECT_EQ(kNumNodes * kNumNodes, count); + for (const auto node : graph.AllNodes()) { + EXPECT_EQ(kNumNodes, graph.OutDegree(node)); + EXPECT_TRUE(graph.IsNodeValid(node)); + count = 0; + for (const auto arc : graph.OutgoingArcs(node)) { + EXPECT_EQ(node, graph.Tail(arc)); + ++count; + for (const auto arc_from_tail : + graph.OutgoingArcsStartingFrom(node, arc)) { + EXPECT_EQ(arc_from_tail, arc); + break; + } + } + EXPECT_EQ(kNumNodes, count); + count = 0; + for (const auto head : graph[node]) { + ++count; + EXPECT_TRUE(graph.IsNodeValid(head)); + } + EXPECT_EQ(kNumNodes, count); + } +} + +TEST(CompleteBipartiteGraphTest, EmptyGraph) { + CompleteBipartiteGraph<> graph(0, 0); + EXPECT_EQ(0, graph.num_nodes()); + EXPECT_EQ(0, graph.size()); + EXPECT_EQ(0, graph.num_arcs()); + for (const auto arc : graph.AllForwardArcs()) { + EXPECT_TRUE(false); + EXPECT_TRUE(graph.IsArcValid(arc)); + } +} + +TEST(CompleteBipartiteGraphTest, OneRightNodeGraph) { + CompleteBipartiteGraph<> graph(3, 1); + EXPECT_EQ(4, graph.num_nodes()); + EXPECT_EQ(4, graph.size()); + EXPECT_EQ(3, graph.num_arcs()); + EXPECT_EQ(graph.Head(0), 3); + EXPECT_EQ(graph.Head(1), 3); + EXPECT_EQ(graph.Head(2), 3); + EXPECT_EQ(graph.Tail(0), 0); + EXPECT_EQ(graph.Tail(1), 1); + EXPECT_EQ(graph.Tail(2), 2); +} + +TEST(CompleteBipartiteGraphTest, NonEmptyGraph) { + static const int kNumRightNodes = 5; + static const int kNumLeftNodes = 3; + CompleteBipartiteGraph<> graph(kNumLeftNodes, kNumRightNodes); + EXPECT_EQ(kNumLeftNodes + kNumRightNodes, graph.num_nodes()); + EXPECT_EQ(graph.num_nodes(), graph.size()); + EXPECT_EQ(kNumLeftNodes * kNumRightNodes, graph.num_arcs()); + int count = 0; + for (const auto arc : graph.AllForwardArcs()) { + ++count; + EXPECT_TRUE(graph.IsArcValid(arc)); + } + EXPECT_EQ(kNumLeftNodes * kNumRightNodes, count); + for (const auto node : graph.AllNodes()) { + EXPECT_EQ(node < kNumLeftNodes ? kNumRightNodes : 0, graph.OutDegree(node)); + EXPECT_TRUE(graph.IsNodeValid(node)); + count = 0; + for (const auto arc : graph.OutgoingArcs(node)) { + EXPECT_EQ(node, graph.Tail(arc)); + EXPECT_EQ(kNumLeftNodes + count, graph.Head(arc)); + ++count; + for (const auto arc_from_tail : + graph.OutgoingArcsStartingFrom(node, arc)) { + EXPECT_EQ(arc_from_tail, arc); + break; + } + } + EXPECT_EQ(node < kNumLeftNodes ? kNumRightNodes : 0, count); + count = 0; + for (const auto head : graph[node]) { + EXPECT_EQ(head, kNumLeftNodes + count); + EXPECT_TRUE(graph.IsNodeValid(head)); + ++count; + } + EXPECT_EQ(node < kNumLeftNodes ? kNumRightNodes : 0, count); + } + for (const auto arc : graph.AllForwardArcs()) { + EXPECT_EQ(graph.GetArc(graph.Tail(arc), graph.Head(arc)), arc); + } +} + +TEST(CompleteBipartiteGraphTest, Overflow) { + using NodeIndex = uint32_t; + using ArcIndex = uint64_t; + using Graph = CompleteBipartiteGraph; + constexpr NodeIndex kNumNodes = std::numeric_limits::max() / 2; + Graph graph(kNumNodes, kNumNodes); + EXPECT_EQ(2 * kNumNodes, graph.num_nodes()); + EXPECT_EQ(graph.num_nodes(), graph.size()); + EXPECT_EQ(kNumNodes * kNumNodes, graph.num_arcs()); + constexpr uint64_t kLeft = kNumNodes - 3; + constexpr uint64_t kRight = kNumNodes + kNumNodes - 2; + + EXPECT_EQ(graph.GetArc(kLeft, kRight), + kLeft * kNumNodes + (kRight - kNumNodes)); +} + +TEST(SVector, NoHeapCheckerFalsePositive) { + static const internal::SVector* const kVector = []() { + auto* vector = new internal::SVector(); + vector->resize(5000); + return vector; + }(); + EXPECT_EQ(kVector->size(), 5000); +} + +template +static void BM_RandomArcs(benchmark::State& state) { + const int kRandomSeed = 0; + const int kNodes = 10 * 1000 * 1000; + const int kArcs = 5 * kNodes; + int items_processed = 0; + for (auto _ : state) { + GraphType graph; + if (reserve) { + graph.Reserve(kNodes, kArcs); + } + std::mt19937 randomizer(kRandomSeed); + for (int i = 0; i < kArcs; ++i) { + graph.AddArc(absl::Uniform(randomizer, 0, kNodes), + absl::Uniform(randomizer, 0, kNodes)); + } + graph.Build(); + items_processed += kArcs; + } + state.SetItemsProcessed(items_processed); +} + +template +static void BM_OrderedArcs(benchmark::State& state) { + const int kRandomSeed = 0; + const int kNodes = 10 * 1000 * 1000; + const int kDegree = 5; + const int kArcs = kDegree * kNodes; + int items_processed = 0; + for (auto _ : state) { + GraphType graph; + if (reserve) { + graph.Reserve(kNodes, kArcs); + } + std::mt19937 randomizer(kRandomSeed); + for (int i = 0; i < kNodes; ++i) { + for (int j = 0; j < kDegree; ++j) { + graph.AddArc(i, absl::Uniform(randomizer, 0, kNodes)); + } + } + graph.Build(); + items_processed += kArcs; + } + state.SetItemsProcessed(items_processed); +} + +// This is just here to get some timing on the AddArc() function to see how the +// GraphType building time is split between the AddArc() calls and the actual +// Build() call. It is not usefull for all type of graph. +template +static void BM_RandomArcsBeforeBuild(benchmark::State& state) { + const int kRandomSeed = 0; + const int kNodes = 10 * 1000 * 1000; + const int kArcs = 5 * kNodes; + int items_processed = 0; + for (auto _ : state) { + GraphType graph; + if (reserve) { + graph.Reserve(kNodes, kArcs); + } + std::mt19937 randomizer(kRandomSeed); + for (int i = 0; i < kArcs; ++i) { + graph.AddArc(absl::Uniform(randomizer, 0, kNodes), + absl::Uniform(randomizer, 0, kNodes)); + } + items_processed += kArcs; + } + state.SetItemsProcessed(items_processed); +} + +// A basic vector> graph implementation that many people uses. It is +// quite slower and use more memory than a static graph, except maybe during +// construction. +class VectorVectorGraph { + public: + VectorVectorGraph() {} + void Reserve(int32_t num_nodes, int32_t num_arcs) { + // We could only reserve the space, but AddArc() need to be smarter then. + graph_.resize(num_nodes); + } + void Build() {} + void AddArc(int32_t tail, int32_t head) { graph_[tail].push_back(head); } + + private: + std::vector> graph_; +}; + +#define RESERVE true +#define NO_RESERVE false +BENCHMARK_TEMPLATE2(BM_RandomArcsBeforeBuild, StaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcsBeforeBuild, StaticGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcsBeforeBuild, ReverseArcStaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcsBeforeBuild, ReverseArcStaticGraph<>, + NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ListGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ListGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, StaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, StaticGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, VectorVectorGraph, RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ReverseArcListGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ReverseArcListGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ReverseArcStaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_OrderedArcs, ReverseArcStaticGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ListGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ListGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, StaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, StaticGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, VectorVectorGraph, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ReverseArcListGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ReverseArcListGraph<>, NO_RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ReverseArcStaticGraph<>, RESERVE); +BENCHMARK_TEMPLATE2(BM_RandomArcs, ReverseArcStaticGraph<>, NO_RESERVE); +#undef RESERVE +#undef NO_RESERVE + +template +void BuildGraphForIterationsBenchmarks(GraphType* graph) { + const int kRandomSeed = 0; + const int kNodes = 10 * 1000 * 1000; + const int kArcs = 5 * kNodes; + graph->Reserve(kNodes, kArcs); + std::mt19937 randomizer(kRandomSeed); + for (int i = 0; i < kArcs; ++i) { + graph->AddArc(absl::Uniform(randomizer, 0, kNodes), + absl::Uniform(randomizer, 0, kNodes)); + } + graph->Build(); +} + +template +static void BM_OutgoingIterations(benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + int64_t num_arcs = 0; + int64_t some_work = 0; + for (auto _ : state) { + for (int node = 0; node < graph.num_nodes(); ++node) { + for (const int arc : graph.OutgoingArcs(node)) { + some_work += graph.Head(arc); + ++num_arcs; + } + } + } + CHECK_GT(some_work, 0); + state.SetItemsProcessed(num_arcs); +} + +BENCHMARK_TEMPLATE(BM_OutgoingIterations, ListGraph<>); +BENCHMARK_TEMPLATE(BM_OutgoingIterations, StaticGraph<>); +BENCHMARK_TEMPLATE(BM_OutgoingIterations, ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_OutgoingIterations, ReverseArcStaticGraph<>); + +template +static void BM_IncomingIterations(benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + int64_t num_arcs = 0; + int64_t some_work = 0; + for (auto _ : state) { + for (int node = 0; node < graph.num_nodes(); ++node) { + for (const int arc : graph.IncomingArcs(node)) { + some_work += graph.Tail(arc); + ++num_arcs; + } + } + } + CHECK_GT(some_work, 0); + state.SetItemsProcessed(num_arcs); +} + +BENCHMARK_TEMPLATE(BM_IncomingIterations, ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_IncomingIterations, ReverseArcStaticGraph<>); + +template +static void BM_OppositeIncomingIterations(benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + int64_t num_arcs = 0; + int64_t some_work = 0; + for (auto _ : state) { + for (int node = 0; node < graph.num_nodes(); ++node) { + for (const int arc : graph.OppositeIncomingArcs(node)) { + some_work += graph.Head(arc); + ++num_arcs; + } + } + } + CHECK_GT(some_work, 0); + state.SetItemsProcessed(num_arcs); +} + +BENCHMARK_TEMPLATE(BM_OppositeIncomingIterations, ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_OppositeIncomingIterations, ReverseArcStaticGraph<>); + +template +static void BM_OutgoingOrOppositeIncomingIterations(benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + int64_t num_arcs = 0; + for (auto _ : state) { + for (int node = 0; node < graph.num_nodes(); ++node) { + for (const int arc : graph.OutgoingOrOppositeIncomingArcs(node)) { + auto head = graph.Head(arc); + benchmark::DoNotOptimize(head); + } + } + } + state.SetItemsProcessed(state.iterations() * num_arcs); +} + +BENCHMARK_TEMPLATE(BM_OutgoingOrOppositeIncomingIterations, + ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_OutgoingOrOppositeIncomingIterations, + ReverseArcStaticGraph<>); + +// It's bit sad, but having two loops to iterate over opposite incoming and +// outgoing arcs is much faster than using `OutgoingOrOppositeIncomingArcs`. +template +static void BM_OutgoingOrOppositeIncomingIterationsTwoLoops( + benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + for (auto _ : state) { + for (int node = 0; node < graph.num_nodes(); ++node) { + const auto work = [&graph](int arc) { + auto head = graph.Head(arc); + benchmark::DoNotOptimize(head); + }; + for (const int arc : graph.OppositeIncomingArcs(node)) { + work(arc); + } + for (const int arc : graph.OutgoingArcs(node)) { + work(arc); + } + } + } + state.SetItemsProcessed(state.iterations() * graph.num_arcs()); +} +BENCHMARK_TEMPLATE(BM_OutgoingOrOppositeIncomingIterationsTwoLoops, + ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_OutgoingOrOppositeIncomingIterationsTwoLoops, + ReverseArcStaticGraph<>); + +template +static void BM_IntegralTypeCopy(benchmark::State& state) { + GraphType graph; + BuildGraphForIterationsBenchmarks(&graph); + + for (auto s : state) { + GraphType copied; + benchmark::DoNotOptimize(copied = GraphType(graph)); + } +} + +BENCHMARK_TEMPLATE(BM_IntegralTypeCopy, ListGraph<>); +BENCHMARK_TEMPLATE(BM_IntegralTypeCopy, StaticGraph<>); +BENCHMARK_TEMPLATE(BM_IntegralTypeCopy, ReverseArcListGraph<>); +BENCHMARK_TEMPLATE(BM_IntegralTypeCopy, ReverseArcStaticGraph<>); + +template +void TailHeadBenchmark(benchmark::State& state, const GraphType& graph) { + // Prevent constant folding. Weird construct due to b/284459966. + auto* graph_ptr = &graph; + benchmark::DoNotOptimize(graph_ptr); + for (auto s : state) { + const auto num_arcs = graph.num_arcs(); + for (int arc = 0; arc < num_arcs; ++arc) { + auto head = graph.Head(arc); + auto tail = graph.Tail(arc); + benchmark::DoNotOptimize(head); + benchmark::DoNotOptimize(tail); + } + } +} + +template +static void BM_CompleteGraphTailHead(benchmark::State& state) { + constexpr int kNumNodes = 100; + CompleteGraph graph(kNumNodes); + TailHeadBenchmark(state, graph); +} +BENCHMARK_TEMPLATE(BM_CompleteGraphTailHead, int32_t); +BENCHMARK_TEMPLATE(BM_CompleteGraphTailHead, int16_t); + +template +static void BM_CompleteBipartiteGraphTailHead(benchmark::State& state) { + constexpr int kNumLeft = 100; + CompleteBipartiteGraph graph(kNumLeft, kNumLeft); + TailHeadBenchmark(state, graph); +} +BENCHMARK_TEMPLATE(BM_CompleteBipartiteGraphTailHead, int32_t); +BENCHMARK_TEMPLATE(BM_CompleteBipartiteGraphTailHead, int16_t); + +} // namespace util diff --git a/ortools/graph/iterators_test.cc b/ortools/graph/iterators_test.cc new file mode 100644 index 0000000000..4f469de963 --- /dev/null +++ b/ortools/graph/iterators_test.cc @@ -0,0 +1,175 @@ +// 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/graph/iterators.h" + +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/int_type.h" + +namespace util { +namespace { + +DEFINE_INT_TYPE(TestIndex, int64_t); + +#if __cplusplus >= 202002L +static_assert(std::random_access_iterator>); +static_assert(std::random_access_iterator>); +#endif // __cplusplus >= 202002L + +TEST(IntegerRangeTest, VariousEmptyRanges) { + bool went_inside = false; + for ([[maybe_unused]] const int i : IntegerRange(0, 0)) { + went_inside = true; + } + for ([[maybe_unused]] const int i : IntegerRange(10, 10)) { + went_inside = true; + } + for ([[maybe_unused]] const int i : IntegerRange(-10, -10)) { + went_inside = true; + } + EXPECT_FALSE(went_inside); +} + +TEST(IntegerRangeTest, NormalBehavior) { + int reference_index = 0; + for (const int i : IntegerRange(0, 100)) { + EXPECT_EQ(i, reference_index); + ++reference_index; + } + EXPECT_EQ(reference_index, 100); +} + +TEST(IntegerRangeTest, NormalBehaviorWithIntType) { + TestIndex reference_index(0); + for (const TestIndex i : + IntegerRange(TestIndex(0), TestIndex(100))) { + EXPECT_EQ(i, reference_index); + ++reference_index; + } + EXPECT_EQ(reference_index, TestIndex(100)); +} + +TEST(IntegerRangeTest, AssignToVector) { + static const int kRangeSize = 100; + IntegerRange range(0, kRangeSize); + ASSERT_EQ(kRangeSize, range.size()); + std::vector vector_from_range(range.begin(), range.end()); + ASSERT_EQ(kRangeSize, vector_from_range.size()); + for (int i = 0; i < kRangeSize; ++i) { + EXPECT_EQ(i, vector_from_range[i]); + } +} + +TEST(ChasingIteratorTest, ChasingIterator) { + static constexpr int kSentinel = 42; + struct Tag {}; + using Iterator = ChasingIterator; + const auto end = Iterator{}; +#if __cplusplus >= 202002L + static_assert(std::forward_iterator); +#endif + // There are two chains: 0->1->3 and 4->2. + const int next[] = {1, 3, kSentinel, kSentinel, 2}; + { + Iterator it(0, next); + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 0); + ++it; + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 1); + ++it; + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 3); + ++it; + ASSERT_TRUE(it == end); + } + { + Iterator it(1, next); + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 1); + ++it; + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 3); + ++it; + ASSERT_TRUE(it == end); + } + { + Iterator it(2, next); + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 2); + ++it; + ASSERT_TRUE(it == end); + } + { + Iterator it(3, next); + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 3); + ++it; + ASSERT_TRUE(it == end); + } + { + Iterator it(4, next); + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 4); + ++it; + ASSERT_FALSE(it == end); + EXPECT_EQ(*it, 2); + ++it; + ASSERT_TRUE(it == end); + } +} + +TEST(IntegerRangeTest, AssignToVectorOfIntType) { + static const int kRangeSize = 100; + IntegerRange range(TestIndex(0), TestIndex(kRangeSize)); + std::vector vector_from_range(range.begin(), range.end()); + ASSERT_EQ(kRangeSize, vector_from_range.size()); + for (int i = 0; i < kRangeSize; ++i) { + EXPECT_EQ(TestIndex(i), vector_from_range[i]); + } +} + +TEST(ReverseTest, EmptyVector) { + std::vector test_vector; + bool went_inside = false; + for ([[maybe_unused]] const int value : Reverse(test_vector)) { + went_inside = true; + } + EXPECT_FALSE(went_inside); +} + +TEST(ReverseTest, ReverseOfAVector) { + const int kSize = 10000; + std::vector test_vector; + for (int i = 0; i < kSize; ++i) { + test_vector.push_back(5 * i + 5); + } + int index = 0; + for (int value : Reverse(test_vector)) { + EXPECT_EQ(test_vector[kSize - 1 - index], value); + index++; + } + // Same with references. + index = 0; + for (const int& value : Reverse(test_vector)) { + EXPECT_EQ(test_vector[kSize - 1 - index], value); + index++; + } +} + +} // namespace +} // namespace util diff --git a/ortools/graph/k_shortest_paths.h b/ortools/graph/k_shortest_paths.h index 941ec0a053..94fbc1e443 100644 --- a/ortools/graph/k_shortest_paths.h +++ b/ortools/graph/k_shortest_paths.h @@ -25,6 +25,16 @@ // Loopless path: path not going through the same node more than once. Also // called simple path. // +// The implementations do not support multigraphs, i.e. graphs with several +// arcs between the same source and destination nodes. One way to work around +// this limitation is to create dummy nodes between the source and destination +// nodes, with one of the edges carrying the weight and the other having a zero +// weight. This transformation slightly increases the size of the graph. +// If you have n edges between the nodes s and t, with the weights w_i, do the +// following: create n - 1 nodes (d_i for i from 2 to n), create n - 1 edges +// between s and d_i with weight w_i, create n - 1 edges between d_i and t with +// weight 0. +// // // Design choices // ============== diff --git a/ortools/graph/k_shortest_paths_test.cc b/ortools/graph/k_shortest_paths_test.cc index 77ef234f96..8ae1b9c957 100644 --- a/ortools/graph/k_shortest_paths_test.cc +++ b/ortools/graph/k_shortest_paths_test.cc @@ -205,7 +205,7 @@ TEST(KShortestPathsYenTest, ReturnsTheRightNumberOfPaths) { // This test verifies that the algorithm returns the shortest path from the // candidate paths produced at each spur. -TEST(DISABLED_KShortestPathsYenTest, ShortestPathSelectedFromCandidates) { +TEST(KShortestPathsYenTest, ShortestPathSelectedFromCandidates) { // Topology: // // 0 ---- 3 ---- 6 Arcs length @@ -247,20 +247,30 @@ TEST(DISABLED_KShortestPathsYenTest, ShortestPathSelectedFromCandidates) { const KShortestPaths> paths = YenKShortestPaths(graph, lengths, /*source=*/0, - /*destination=*/6, /*k=*/10); + /*destination=*/6, /*k=*/14); - EXPECT_THAT(paths.paths, ElementsAre(std::vector{0, 2, 6}, // - std::vector{0, 3, 6}, - std::vector{0, 2, 1, 3, 6}, - std::vector{0, 3, 1, 2, 6}, - std::vector{0, 2, 7, 3, 6}, - std::vector{0, 3, 7, 2, 6}, - std::vector{0, 2, 7, 5, 1, 3, 6}, - std::vector{0, 3, 7, 5, 1, 2, 6}, - std::vector{0, 2, 4, 5, 1, 3, 6}, - std::vector{0, 3, 7, 5, 4, 2, 6})); EXPECT_THAT(paths.distances, - ElementsAre(200, 200, 400, 400, 400, 400, 600, 600, 600, 600)); + ElementsAre(200, 200, // + 400, 400, 400, 400, // + 600, 600, 600, 600, 600, 600, 600, 600)); + + EXPECT_THAT( + paths.paths, + testing::UnorderedElementsAre( + // 200 + std::vector{0, 2, 6}, std::vector{0, 3, 6}, + // 400 + std::vector{0, 2, 1, 3, 6}, std::vector{0, 3, 1, 2, 6}, + std::vector{0, 2, 7, 3, 6}, std::vector{0, 3, 7, 2, 6}, + // 600 + std::vector{0, 2, 7, 5, 1, 3, 6}, + std::vector{0, 3, 7, 5, 1, 2, 6}, + std::vector{0, 2, 4, 5, 1, 3, 6}, + std::vector{0, 3, 7, 5, 4, 2, 6}, + std::vector{0, 2, 4, 5, 7, 3, 6}, + std::vector{0, 2, 8, 5, 1, 3, 6}, + std::vector{0, 3, 7, 5, 8, 2, 6}, + std::vector{0, 2, 8, 5, 7, 3, 6})); } namespace internal { diff --git a/ortools/sat/2d_orthogonal_packing_test.cc b/ortools/sat/2d_orthogonal_packing_test.cc new file mode 100644 index 0000000000..a9fdab2725 --- /dev/null +++ b/ortools/sat/2d_orthogonal_packing_test.cc @@ -0,0 +1,518 @@ +// 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/2d_orthogonal_packing.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/algorithms/binary_search.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/2d_orthogonal_packing_testing.h" +#include "ortools/sat/diffn_util.h" +#include "ortools/sat/integer_base.h" +#include "ortools/sat/synchronization.h" + +namespace operations_research { +namespace sat { +namespace { + +// Alternative way of computing Dff.LowestInverse(). +template +IntegerValue ComputeDffInverse(const DffClass& dff, IntegerValue max, + IntegerValue value) { + DCHECK_EQ(dff(0), 0); + if (value <= 0) return 0; + return BinarySearch( + max, 0, [&dff, value](IntegerValue x) { return dff(x) >= value; }); +} + +template +void TestMaximalDff(absl::BitGenRef random, int64_t num_values_to_check, + int64_t max, Args&&... args) { + DffClass dff(max, std::forward(args)...); + + num_values_to_check = std::min(num_values_to_check, max); + const int64_t step_size = max / num_values_to_check; + for (int64_t i = 0; i < max; i += step_size) { + int64_t value_to_check; + if (step_size > 1) { + value_to_check = + absl::Uniform(random, i, std::min(i + step_size - 1, max)); + } else { + value_to_check = i; + } + + for (int64_t j = 0; j < max; j += step_size) { + int64_t value_to_check2; + if (step_size > 1) { + value_to_check2 = + absl::Uniform(random, j, std::min(j + step_size - 1, max)); + } else { + value_to_check2 = j; + } + + const IntegerValue f = dff(value_to_check); + const IntegerValue f2 = dff(value_to_check2); + // f is nondecreasing + if (f != f2) { + CHECK_EQ(f < f2, value_to_check < value_to_check2); + } + // f is superadditive, i.e., f(x) + f(y) <= f(x + y) + if (value_to_check + value_to_check2 <= max) { + CHECK_LE(f + f2, dff(value_to_check + value_to_check2)); + } + // f is symmetric, i.e., f(x) + f(C - x) = f(C) + CHECK_EQ(dff(value_to_check) + dff(max - value_to_check), dff(max)); + CHECK_EQ(dff(value_to_check2) + dff(max - value_to_check2), dff(max)); + + // Inverse works + CHECK_EQ(dff(dff.LowestInverse(f)), f); + // Lowest inverse is indeed the lowest + CHECK_LE(dff.LowestInverse(f), value_to_check); + if (dff.LowestInverse(f) > 0) { + CHECK_NE(dff(dff.LowestInverse(f) - 1), f); + } + + // Inverse works outside domain + if (value_to_check <= dff(max)) { + CHECK_GE(dff(dff.LowestInverse(value_to_check)), value_to_check); + if (dff.LowestInverse(value_to_check) > 0) { + CHECK_LT(dff(dff.LowestInverse(value_to_check) - 1), value_to_check); + } + CHECK_EQ(dff.LowestInverse(value_to_check), + ComputeDffInverse(dff, max, value_to_check)); + } + } + } + // f(0) = 0 + CHECK_EQ(dff(0), 0); +} + +TEST(DualFeasibilityFunctionTest, Dff2IsMaximal) { + absl::BitGen random; + for (int k = 1; k < 50; k++) { + TestMaximalDff( + random, 100, 100 + absl::Uniform(random, 0, 1), k); + } + for (int k = 100; 2 * k <= std::numeric_limits::max(); k += 200) { + TestMaximalDff(random, 200, 2 * k, k); + } +} + +TEST(DualFeasibilityFunctionTest, DffPowerOfTwo) { + absl::BitGen random; + for (int k = 0; k < 61; k++) { + TestMaximalDff( + random, 100, std::numeric_limits::max() / 2, k); + } +} + +TEST(DualFeasibilityFunctionTest, Dff0IsMaximal) { + absl::BitGen random; + for (int k = 1; k < 50; k++) { + TestMaximalDff(random, 100, 100, k); + } + for (int k = 100; 2 * k <= std::numeric_limits::max(); k += 200) { + TestMaximalDff(random, 200, 2 * k, k); + } +} + +TEST(DualFeasibilityFunctionTest, Composed) { + absl::BitGen random; + for (int k_f0 = 1; k_f0 < 50; k_f0++) { + for (int k_f2 = 2; k_f2 < 50; k_f2++) { + TestMaximalDff(random, 100, 100, k_f0, k_f2); + } + } +} + +using DetectorStatus = OrthogonalPackingResult::Status; + +struct OppProblem { + std::vector items_x_sizes; + std::vector items_y_sizes; + std::pair bb_sizes; + + template + friend void AbslStringify(Sink& sink, const OppProblem& p) { + absl::Format(&sink, + "items_x_sizes={%s}, items_y_sizes={%s}, bb_sizes={%i, %i}", + absl::StrJoin(p.items_x_sizes, ", "), + absl::StrJoin(p.items_y_sizes, ", "), p.bb_sizes.first.value(), + p.bb_sizes.second.value()); + } +}; + +OppProblem CreateFeasibleOppProblem(absl::BitGenRef random, int max_size) { + std::vector problem = MakeItemsFromRectangles( + GenerateNonConflictingRectangles( + absl::Uniform(random, max_size - 1, max_size), random), + 0, random); + + OppProblem result; + Rectangle bounding_box; + bounding_box = {.x_min = std::numeric_limits::max(), + .x_max = std::numeric_limits::min(), + .y_min = std::numeric_limits::max(), + .y_max = std::numeric_limits::min()}; + std::vector& items_x_sizes = result.items_x_sizes; + std::vector& items_y_sizes = result.items_y_sizes; + for (const auto& item : problem) { + items_x_sizes.push_back(item.x_size); + items_y_sizes.push_back(item.y_size); + + bounding_box.x_min = std::min(bounding_box.x_min, item.bounding_area.x_min); + bounding_box.x_max = std::max(bounding_box.x_max, item.bounding_area.x_max); + bounding_box.y_min = std::min(bounding_box.y_min, item.bounding_area.y_min); + bounding_box.y_max = std::max(bounding_box.y_max, item.bounding_area.y_max); + } + result.bb_sizes = {bounding_box.SizeX(), bounding_box.SizeY()}; + return result; +} + +OppProblem CreateRandomOppProblem(absl::BitGenRef random, int size) { + OppProblem result; + std::vector& items_x_sizes = result.items_x_sizes; + std::vector& items_y_sizes = result.items_y_sizes; + const int num_items = absl::Uniform(random, 1, 20); + items_x_sizes.clear(); + items_y_sizes.clear(); + IntegerValue area = 0; + for (int i = 0; i < num_items; ++i) { + const IntegerValue x_size = absl::Uniform(random, 1, size); + const IntegerValue y_size = absl::Uniform(random, 1, size); + items_x_sizes.push_back(x_size); + items_y_sizes.push_back(y_size); + area += x_size * y_size; + } + const IntegerValue box_x_size = absl::Uniform(random, size, 4 * size); + const IntegerValue box_y_size = + std::max(IntegerValue(size), (area + box_x_size - 1) / box_x_size); + result.bb_sizes = {box_x_size, box_y_size}; + return result; +} + +TEST(DualFeasibilityTest, NoConflictWhenItDoesNotExist) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + constexpr int num_runs = 400; + for (int k = 0; k < num_runs; k++) { + auto problem = CreateFeasibleOppProblem(random, 30); + CHECK(opp_solver + .TestFeasibility(problem.items_x_sizes, problem.items_y_sizes, + problem.bb_sizes) + .GetResult() != DetectorStatus::INFEASIBLE) + << "problem: " << absl::StrCat(problem); + } +} + +DetectorStatus NaiveCheckPairwiseFeasibility( + absl::Span sizes_x, + absl::Span sizes_y, + const std::pair& bounding_box_size) { + for (int i = 0; i < sizes_x.size(); i++) { + for (int j = i + 1; j < sizes_x.size(); j++) { + if (sizes_x[i] + sizes_x[j] > bounding_box_size.first && + sizes_y[i] + sizes_y[j] > bounding_box_size.second) { + return DetectorStatus::INFEASIBLE; + } + } + } + return DetectorStatus::UNKNOWN; +} + +DetectorStatus NaiveCheckFeasibilityWithDualFunction( + absl::Span sizes_x, + absl::Span sizes_y, + const std::pair& bounding_box_size) { + const IntegerValue bb_area = + bounding_box_size.first * bounding_box_size.second; + for (IntegerValue k = 0; 2 * k <= bounding_box_size.first; k++) { + DualFeasibleFunctionF0 dff0_x(bounding_box_size.first, k); + for (IntegerValue l = 0; 2 * l <= bounding_box_size.second; l++) { + DualFeasibleFunctionF0 dff0_y(bounding_box_size.second, l); + IntegerValue area = 0; + for (int i = 0; i < sizes_x.size(); i++) { + area += dff0_x(sizes_x[i]) * dff0_y(sizes_y[i]); + } + if (area > bb_area) { + return DetectorStatus::INFEASIBLE; + } + } + } + return DetectorStatus::UNKNOWN; +} + +TEST(DualFeasibilityTest, Random) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + constexpr int num_runs = 400; + constexpr int size = 20; + for (int k = 0; k < num_runs; k++) { + const OppProblem problem = CreateRandomOppProblem(random, size); + const auto naive_result = NaiveCheckFeasibilityWithDualFunction( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes); + const auto naive_pairwise = NaiveCheckPairwiseFeasibility( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes); + + const auto result = opp_solver.TestFeasibility( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes, + OrthogonalPackingOptions{.use_pairwise = true, + .use_dff_f0 = true, + .use_dff_f2 = false, + .brute_force_threshold = 0}); + CHECK_EQ(result.GetResult() == DetectorStatus::INFEASIBLE, + naive_result == DetectorStatus::INFEASIBLE || + naive_pairwise == DetectorStatus::INFEASIBLE); + CHECK_EQ(result.GetItemsParticipatingOnConflict().size() == 2, + naive_pairwise == DetectorStatus::INFEASIBLE); + } +} + +// Try f_2^k(f_0^l(x)) for all values of k and l. +DetectorStatus NaiveFeasibilityWithDualFunction2( + absl::Span sizes_x, + absl::Span sizes_y, + std::pair bounding_box_size) { + for (IntegerValue l = 0; 2 * l <= bounding_box_size.first; l++) { + DualFeasibleFunctionF0 dff0(bounding_box_size.first, l); + for (IntegerValue k = 1; 2 * k < bounding_box_size.first; k++) { + const RoundingDualFeasibleFunction dff2(bounding_box_size.first, k); + const IntegerValue c_k = bounding_box_size.first / k; + const IntegerValue bb_area = 2 * c_k * bounding_box_size.second; + IntegerValue area = 0; + for (int i = 0; i < sizes_x.size(); i++) { + // First apply f_0 + IntegerValue x_size = dff0(sizes_x[i]); + // Now apply f_2 + if (2 * x_size > bounding_box_size.first) { + x_size = 2 * (c_k - (bounding_box_size.first - x_size) / k); + } else if (2 * x_size == bounding_box_size.first) { + x_size = c_k; + } else { + x_size = 2 * (x_size / k); + } + CHECK_EQ(x_size, dff2(dff0(sizes_x[i]))); + IntegerValue y_size = sizes_y[i]; + area += x_size * y_size; + } + if (area > bb_area) { + return DetectorStatus::INFEASIBLE; + } + } + } + return DetectorStatus::UNKNOWN; +} + +TEST(DualFeasibility2Test, Random) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + constexpr int num_runs = 40000; + const int size = absl::Uniform(random, 10, 30); + for (int k = 0; k < num_runs; k++) { + const OppProblem problem = CreateRandomOppProblem(random, size); + const DetectorStatus naive_result1 = NaiveFeasibilityWithDualFunction2( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes); + const DetectorStatus naive_result2 = NaiveFeasibilityWithDualFunction2( + problem.items_y_sizes, problem.items_x_sizes, + {problem.bb_sizes.second, problem.bb_sizes.first}); + + const auto result = opp_solver.TestFeasibility( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes, + OrthogonalPackingOptions{.use_pairwise = false, + .use_dff_f0 = true, + .use_dff_f2 = true, + .brute_force_threshold = 0}); + CHECK_EQ(naive_result1 == DetectorStatus::INFEASIBLE || + naive_result2 == DetectorStatus::INFEASIBLE, + result.GetResult() == DetectorStatus::INFEASIBLE) + << " naive result x: " << (naive_result1 == DetectorStatus::INFEASIBLE) + << " Naive result y: " << (naive_result2 == DetectorStatus::INFEASIBLE) + << " Result: " << (result.GetResult() == DetectorStatus::INFEASIBLE) + << " problem:" << absl::StrCat(problem); + } +} + +TEST(OrthogonalPackingTest, SlackDoesNotChangeFeasibility) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + constexpr int num_runs = 400; + constexpr int size = 20; + for (int k = 0; k < num_runs; k++) { + OppProblem problem = CreateRandomOppProblem(random, size); + // Add one more item since `CreateRandomOppProblem()` does not generate + // trivially infeasible problems. + problem.items_x_sizes.push_back(absl::Uniform(random, 1, size)); + problem.items_y_sizes.push_back(absl::Uniform(random, 1, size)); + auto result = opp_solver.TestFeasibility( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes); + if (result.GetResult() != DetectorStatus::INFEASIBLE) { + continue; + } + const std::vector& + items_participating_on_conflict = + result.GetItemsParticipatingOnConflict(); + for (int i = 0; i < items_participating_on_conflict.size(); ++i) { + if (absl::Bernoulli(random, 0.5)) { + result.TryUseSlackToReduceItemSize( + i, OrthogonalPackingResult::Coord::kCoordX, 0); + result.TryUseSlackToReduceItemSize( + i, OrthogonalPackingResult::Coord::kCoordY, 0); + } else { + result.TryUseSlackToReduceItemSize( + i, OrthogonalPackingResult::Coord::kCoordY, 0); + result.TryUseSlackToReduceItemSize( + i, OrthogonalPackingResult::Coord::kCoordX, 0); + } + } + OppProblem modified_problem; + modified_problem.bb_sizes = problem.bb_sizes; + for (const auto& item : result.GetItemsParticipatingOnConflict()) { + modified_problem.items_x_sizes.push_back(item.size_x); + modified_problem.items_y_sizes.push_back(item.size_y); + } + EXPECT_TRUE(opp_solver + .TestFeasibility(modified_problem.items_x_sizes, + modified_problem.items_y_sizes, + modified_problem.bb_sizes) + .GetResult() == DetectorStatus::INFEASIBLE) + << "problem: " << absl::StrCat(problem); + } +} + +void BM_OrthogonalPackingInfeasibilityDetector(benchmark::State& state) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + std::vector problems; + for (int i = 0; i < 10; ++i) { + problems.push_back(CreateFeasibleOppProblem(random, state.range(0))); + } + int index = 0; + for (auto s : state) { + const auto& problem = problems[index]; + CHECK(opp_solver + .TestFeasibility(problem.items_x_sizes, problem.items_y_sizes, + problem.bb_sizes) + .GetResult() != DetectorStatus::INFEASIBLE); + ++index; + if (index == 10) { + index = 0; + } + } +} + +BENCHMARK(BM_OrthogonalPackingInfeasibilityDetector) + ->Arg(5) + ->Arg(10) + ->Arg(20) + ->Arg(30) + ->Arg(40) + ->Arg(80) + ->Arg(100) + ->Arg(200) + ->Arg(1000) + ->Arg(10000); + +MATCHER_P3(ItemIs, index, size_x, size_y, "") { + return arg.index == index && arg.size_x == size_x && arg.size_y == size_y; +} + +TEST(OrthogonalPacking, UseSlack) { + using Item = OrthogonalPackingResult::Item; + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + auto result = opp_solver.TestFeasibility( + {10, 10, 10}, {10, 10, 10}, {11, 20}, + OrthogonalPackingOptions{// Detect only trivial conflicts + .use_pairwise = false, + .use_dff_f0 = false, + .use_dff_f2 = false, + .brute_force_threshold = 0}); + CHECK(result.GetResult() == DetectorStatus::INFEASIBLE); + EXPECT_THAT(result.GetItemsParticipatingOnConflict(), + testing::UnorderedElementsAre( + ItemIs(0, 10, 10), ItemIs(1, 10, 10), ItemIs(2, 10, 10))); + + int position_of_index_zero = + std::find_if(result.GetItemsParticipatingOnConflict().begin(), + result.GetItemsParticipatingOnConflict().end(), + [](const Item& item) { return item.index == 0; }) - + result.GetItemsParticipatingOnConflict().begin(); + + result.TryUseSlackToReduceItemSize( + position_of_index_zero, OrthogonalPackingResult::Coord::kCoordX, 9); + EXPECT_THAT(result.GetItemsParticipatingOnConflict(), + testing::Contains(ItemIs(0, 9, 10))); + result.TryUseSlackToReduceItemSize(position_of_index_zero, + OrthogonalPackingResult::Coord::kCoordX); + // 2*10 + 10*10 + 10*10 = 220 = 11 * 20. So (2, 10) would fit. + EXPECT_THAT(result.GetItemsParticipatingOnConflict(), + testing::Contains(ItemIs(0, 3, 10))); + + auto result2 = opp_solver.TestFeasibility( + {10, 10, 10}, {11, 10, 10}, {11, 20}, + OrthogonalPackingOptions{// Detect only trivial conflicts + .use_pairwise = false, + .use_dff_f0 = false, + .use_dff_f2 = false, + .brute_force_threshold = 0}); + position_of_index_zero = + std::find_if(result2.GetItemsParticipatingOnConflict().begin(), + result2.GetItemsParticipatingOnConflict().end(), + [](const Item& item) { return item.index == 0; }) - + result2.GetItemsParticipatingOnConflict().begin(); + result2.TryUseSlackToReduceItemSize(position_of_index_zero, + OrthogonalPackingResult::Coord::kCoordX); + // 2*11 + 10*10 + 10*10 = 222 > 11 * 20 = 220. So (2, 11) does not fit. + EXPECT_THAT(result2.GetItemsParticipatingOnConflict(), + testing::Contains(ItemIs(0, 2, 11))); +} + +TEST(OrthogonalPacking, InvertDFF) { + absl::BitGen random; + SharedStatistics stats; + OrthogonalPackingInfeasibilityDetector opp_solver(random, &stats); + auto result = opp_solver.TestFeasibility( + {4, 4, 8, 8}, {6, 6, 5, 5}, {13, 10}, + OrthogonalPackingOptions{.use_pairwise = false, + .use_dff_f0 = false, + .use_dff_f2 = true, + .brute_force_threshold = 0}); + CHECK(result.GetResult() == DetectorStatus::INFEASIBLE); + EXPECT_THAT(result.GetItemsParticipatingOnConflict(), + testing::UnorderedElementsAre(ItemIs(0, 3, 6), ItemIs(1, 3, 6), + ItemIs(2, 8, 5), ItemIs(3, 8, 5))); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index eba29fa24c..f83ed847a7 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -219,6 +219,21 @@ cc_library( ], ) +cc_test( + name = "cp_model_utils_test", + size = "small", + srcs = ["cp_model_utils_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_utils", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/base:stl_util", + "//ortools/port:proto_utils", + "@abseil-cpp//absl/container:flat_hash_set", + ], +) + cc_library( name = "synchronization", srcs = ["synchronization.cc"], @@ -265,6 +280,24 @@ cc_library( ], ) +cc_test( + name = "synchronization_test", + size = "small", + srcs = ["synchronization_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":integer_base", + ":model", + ":synchronization", + ":util", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:random_engine", + "@abseil-cpp//absl/time", + "@abseil-cpp//absl/types:span", + ], +) + cc_library( name = "cp_model_checker", srcs = ["cp_model_checker.cc"], @@ -709,6 +742,27 @@ cc_library( ], ) +cc_test( + name = "cp_model_solver_test", + size = "medium", + srcs = ["cp_model_solver_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_checker", + ":cp_model_solver", + ":cp_model_test_utils", + ":lp_utils", + ":model", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/util:logging", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + ], +) + cc_library( name = "cp_model_mapping", hdrs = ["cp_model_mapping.h"], @@ -1042,6 +1096,36 @@ cc_library( ], ) +cc_test( + name = "cp_model_presolve_test", + size = "small", + srcs = ["cp_model_presolve_test.cc"], + tags = ["noautofuzz"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_checker", + ":cp_model_presolve", + ":cp_model_solver", + ":cp_model_utils", + ":lp_utils", + ":model", + ":presolve_context", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/lp_data", + "//ortools/lp_data:lp_parser", + "//ortools/lp_data:proto_utils", + "//ortools/port:proto_utils", + "//ortools/util:logging", + "//ortools/util:sorted_interval_list", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + ], +) + cc_test( name = "cp_model_presolve_random_test", size = "medium", @@ -2373,32 +2457,6 @@ cc_library( ], ) -cc_test( - name = "boolean_problem_test", - size = "small", - srcs = [ - "boolean_problem_test.cc", - ], - deps = [ - ":boolean_problem", - ":cp_model_cc_proto", - ":cp_model_checker", - ":cp_model_symmetries", - ":opb_reader", - ":sat_parameters_cc_proto", - "//ortools/algorithms:sparse_permutation", - "//ortools/base", - "//ortools/base:file", - "//ortools/base:gmock_main", - "//ortools/base:path", - "//ortools/util:filelineiter", - "//ortools/util:logging", - "//ortools/util:time_limit", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/strings", - ], -) - cc_library( name = "linear_relaxation", srcs = ["linear_relaxation.cc"], @@ -2780,6 +2838,7 @@ cc_library( ":cuts", ":integer", ":integer_base", + ":intervals", ":linear_constraint", ":linear_constraint_manager", ":model", @@ -2795,7 +2854,6 @@ cc_library( "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/types:span", @@ -3312,6 +3370,27 @@ cc_library( ], ) +cc_test( + name = "2d_orthogonal_packing_test", + srcs = ["2d_orthogonal_packing_test.cc"], + deps = [ + ":2d_orthogonal_packing", + ":2d_orthogonal_packing_testing", + ":diffn_util", + ":integer_base", + ":synchronization", + "//ortools/algorithms:binary_search", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + "@abseil-cpp//absl/random:distributions", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", + "@com_google_benchmark//:benchmark", + ], +) + cc_library( name = "2d_try_edge_propagator", srcs = ["2d_try_edge_propagator.cc"], @@ -3762,6 +3841,31 @@ cc_library( ], ) +cc_test( + name = "opb_reader_test", + size = "small", + srcs = [ + "opb_reader_test.cc", + ], + deps = [ + ":cp_model_cc_proto", + ":cp_model_checker", + ":cp_model_symmetries", + ":opb_reader", + ":sat_parameters_cc_proto", + "//ortools/algorithms:sparse_permutation", + "//ortools/base", + "//ortools/base:file", + "//ortools/base:gmock_main", + "//ortools/base:path", + "//ortools/util:filelineiter", + "//ortools/util:logging", + "//ortools/util:time_limit", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + ], +) + cc_binary( name = "sat_runner", srcs = [ diff --git a/ortools/sat/cp_model_presolve_test.cc b/ortools/sat/cp_model_presolve_test.cc new file mode 100644 index 0000000000..05275d282f --- /dev/null +++ b/ortools/sat/cp_model_presolve_test.cc @@ -0,0 +1,7926 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_presolve.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/lp_data/lp_data.h" +#include "ortools/lp_data/lp_parser.h" +#include "ortools/lp_data/proto_utils.h" +#include "ortools/port/proto_utils.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_checker.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/lp_utils.h" +#include "ortools/sat/model.h" +#include "ortools/sat/presolve_context.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/logging.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::EqualsProto; + +MATCHER_P(ModelEqualsIgnoringConstraintsOrder, expected, "") { + CpModelProto arg_no_ct = arg; + arg_no_ct.clear_constraints(); + CpModelProto expected_no_ct = expected; + expected_no_ct.clear_constraints(); + if (!ExplainMatchResult(EqualsProto(expected_no_ct), arg_no_ct, + result_listener)) { + return false; + } + std::vector> expected_constraints; + for (const ConstraintProto& constraint : expected.constraints()) { + expected_constraints.push_back(EqualsProto(constraint)); + } + return ExplainMatchResult( + testing::UnorderedElementsAreArray(expected_constraints), + arg.constraints(), result_listener); +} + +std::vector RandomPermutation(int num_variables, absl::BitGenRef random) { + std::vector permutation(num_variables); + std::iota(permutation.begin(), permutation.end(), 0); + std::shuffle(permutation.begin(), permutation.end(), random); + return permutation; +} + +// Generate a triangular clause system with a known random solution, and fix the +// "singleton" variables so that the full solution can be found by pure +// propagation. +// +// TODO(user): do the same with a linear system. +CpModelProto RandomTrivialSatProblem(int num_variables, + absl::BitGenRef random) { + CpModelProto result; + result.set_name("Random trivial SAT"); + std::vector solution_literals; + for (int i = 0; i < num_variables; ++i) { + solution_literals.push_back(absl::Bernoulli(random, 1.0 / 2) ? i : -i - 1); + IntegerVariableProto* var = result.add_variables(); + var->add_domain(0); + var->add_domain(1); + } + const std::vector perm_a = RandomPermutation(num_variables, random); + const std::vector perm_b = RandomPermutation(num_variables, random); + for (int i = 0; i < num_variables; ++i) { + ConstraintProto* ct = result.add_constraints(); + for (int j = 0; j <= perm_a[i]; ++j) { + ct->mutable_bool_or()->add_literals(solution_literals[perm_b[j]]); + } + } + return result; +} + +CpModelProto PresolveForTest( + CpModelProto initial_model, SatParameters extra_params = SatParameters(), + CpSolverStatus expected_status = CpSolverStatus::UNKNOWN) { + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + Model model; + model.GetOrCreate()->EnableLogging(true); + model.GetOrCreate()->SetLogToStdOut(true); + auto* params = model.GetOrCreate(); + params->set_permute_variable_randomly(false); + params->set_cp_model_probing_level(0); + params->set_convert_intervals(false); + params->MergeFrom(extra_params); + PresolveContext context(&model, &presolved_model, &mapping_model); + CpModelPresolver presolver(&context, &mapping); + EXPECT_EQ(presolver.Presolve(), expected_status); + return presolved_model; +} + +// This expects the presolve to remove everything and return the mapping model. +CpModelProto GetMappingModel(CpModelProto initial_model, + SatParameters extra_params = SatParameters()) { + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + Model model; + auto* params = model.GetOrCreate(); + params->set_permute_variable_randomly(false); + params->set_cp_model_probing_level(0); + params->set_convert_intervals(false); + params->MergeFrom(extra_params); + PresolveContext context(&model, &presolved_model, &mapping_model); + CpModelPresolver presolver(&context, &mapping); + presolver.Presolve(); + EXPECT_THAT(CpModelProto(), testing::EqualsProto(presolved_model)); + return mapping_model; +} + +// Return a proto with reduced domain after presolve. +CpModelProto GetReducedDomains(CpModelProto initial_model) { + const int num_vars = initial_model.variables().size(); + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + Model model; + auto* params = model.GetOrCreate(); + params->set_keep_all_feasible_solutions_in_presolve(true); + params->set_permute_variable_randomly(false); + params->set_cp_model_probing_level(0); + params->set_convert_intervals(false); + PresolveContext context(&model, &presolved_model, &mapping_model); + CpModelPresolver presolver(&context, &mapping); + presolver.Presolve(); + + // Only keep variable domain, and erase extra ones. + mapping_model.clear_constraints(); + mapping_model.mutable_variables()->DeleteSubrange( + num_vars, mapping_model.mutable_variables()->size() - num_vars); + return mapping_model; +} + +void ExpectInfeasibleDuringPresolve(CpModelProto initial_model) { + PresolveForTest(initial_model, SatParameters(), CpSolverStatus::INFEASIBLE); +} + +CpModelProto PresolveOneConstraint(const CpModelProto& initial_model, + const int constraint_index) { + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + Model model; + model.GetOrCreate() + ->set_keep_all_feasible_solutions_in_presolve(true); + PresolveContext context(&model, &presolved_model, &mapping_model); + CpModelPresolver presolver(&context, &mapping); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + presolver.PresolveOneConstraint(constraint_index); + presolver.RemoveEmptyConstraints(); + for (int i = 0; i < presolved_model.variables_size(); ++i) { + FillDomainInProto(context.DomainOf(i), + presolved_model.mutable_variables(i)); + } + return presolved_model; +} + +TEST(PresolveCpModelTest, BoolAndWithDuplicate) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1, 2 ] + bool_and { literals: [ 2, 3, 4 ] } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 2, 1, 0 ] + bool_and { literals: [ 3, 4 ] } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BoolAndWithNegatedDuplicate) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 1, 2 ] + bool_and { literals: [ -3, 3, 4 ] } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_or { literals: [ -3, -2, -1 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, EmptyPresolvedProblem) { + std::mt19937 random(12345); + const CpModelProto initial_model = RandomTrivialSatProblem(100, random); + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + Model model; + std::vector mapping; + PresolveContext context(&model, &presolved_model, &mapping_model); + PresolveCpModel(&context, &mapping); + EXPECT_EQ(presolved_model.variables_size(), 0); + EXPECT_TRUE(mapping.empty()); + { + Model tmp_model; + tmp_model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse r = SolveCpModel(presolved_model, &tmp_model); + EXPECT_EQ(r.status(), CpSolverStatus::OPTIMAL); + } + + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(mapping_model, &model); + std::vector solution(response.solution().begin(), + response.solution().end()); + EXPECT_TRUE(SolutionIsFeasible(initial_model, solution)); +} + +TEST(PresolveCpModelTest, SimplifyRemovableConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + name: "celar" + variables { domain: [ 16, 792 ] } + variables { domain: [ 16, 792 ] } + variables { domain: [ 16, 792 ] } + variables { domain: [ -776, 776 ] } + variables { domain: [ 0, 776 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ -776, 776 ] } + variables { domain: [ 0, 776 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ -238, 238 ] } + variables { domain: [ 238, 238 ] } + constraints { + name: "int_lin_eq" + linear { + vars: 0 + vars: 1 + vars: 3 + coeffs: 1 + coeffs: -1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + constraints { + name: "int_abs" + lin_max { + target: { vars: 4 coeffs: 1 } + exprs: { vars: 3 coeffs: 1 } + exprs: { vars: 3 coeffs: -1 } + } + } + constraints { + name: "int_le_reif" + enforcement_literal: 5 + linear { vars: 4 coeffs: 1 domain: -9223372036854775808 domain: 59 } + } + constraints { + name: "int_le_reif (negated)" + enforcement_literal: -6 + linear { vars: 4 coeffs: 1 domain: 60 domain: 9223372036854775807 } + } + constraints { + name: "int_lin_eq" + linear { + vars: 0 + vars: 2 + vars: 6 + coeffs: 1 + coeffs: -1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + constraints { + name: "int_abs" + lin_max { + target: { vars: 7 coeffs: 1 } + exprs: { vars: 6 coeffs: 1 } + exprs: { vars: 6 coeffs: -1 } + } + } + constraints { + name: "int_le_reif" + enforcement_literal: 8 + linear { vars: 7 coeffs: 1 domain: -9223372036854775808 domain: 186 } + } + constraints { + name: "int_le_reif (negated)" + enforcement_literal: -9 + linear { vars: 7 coeffs: 1 domain: 187 domain: 9223372036854775807 } + } + constraints { + name: "int_lin_eq" + linear { + vars: 1 + vars: 2 + vars: 9 + coeffs: 1 + coeffs: -1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + constraints { + name: "int_abs" + lin_max { + target: { vars: 10 coeffs: 1 } + exprs: { vars: 9 coeffs: 1 } + exprs: { vars: 9 coeffs: -1 } + } + } + )pb"); + // This model is FEASIBLE, but before the CL, trying to solve it was crashing. + // because of the encoding of the variable was created after the var was + // marked as removable. It was then in both presolved and mapping models, and + // the postsolve phase was failing. + Model model; + const CpSolverResponse response = SolveCpModel(initial_model, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, BasicLinearConstraintPresolve) { + // Note(user): I tried a random small problem. Note that the conversion to LP + // put artificial large bounds to x and y which allow to start the round of + // propagations that reduces the domains of the variables. + // + // When removing z, this is: y = 3 + 2x, 0 <= x + y <= 2 and there is actually + // only one solution (x = -1). The presolve simplify everything. + const std::string text_lp = + "y - 2 x + z = 6;" + "x + y + z <= 5;" + "x + y >= 0;" + "z = 3 ;"; + glop::LinearProgram lp; + CHECK(ParseLp(text_lp, &lp)); + MPModelProto mp_model; + LinearProgramToMPModelProto(lp, &mp_model); + CpModelProto initial_model; + SolverLogger logger; + ConvertMPModelProtoToCpModelProto(SatParameters(), mp_model, &initial_model, + &logger); + + const CpModelProto mapping_model = GetMappingModel(initial_model); + + // By default we clear the names. + EXPECT_EQ(mapping_model.variables(1).name(), ""); + EXPECT_EQ(mapping_model.variables(1).domain(0), -1); + EXPECT_EQ(mapping_model.variables(1).domain(1), -1); +} + +// This test used to fail before CL 180337997. +TEST(PresolveCpModelTest, LinearConstraintCornerCasePresolve) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 0, 1, 2 ] + coeffs: [ 1, -1, 1, 1 ] + domain: [ 5, 10 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 1, 2 ] + domain: [ 3, 3 ] + } + } + )pb"); + + // This model is UNSAT, but before the CL, trying to solve it was crashing. + // because of the duplicate singleton var 0 in the first constraint. + Model model; + const CpSolverResponse response = SolveCpModel(initial_model, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +// This test show how we extract simple bool => bound encoding from a big-M +// encoding. +TEST(PresolveCpModelTest, LinearConstraintSplitting) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 10, 1 ] + domain: [ 3, 15 ] + } + } + )pb"); + + // The model is equivalent to var0 => var1 <= 5 + // not(var0) => var1 >= 3 + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + constraints { + enforcement_literal: 0 + linear { + vars: 1 + coeffs: [ 1 ] + domain: [ 0, 5 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: 1 + coeffs: [ 1 ] + domain: [ 3, 10 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, + ExtractEnforcementLiteralFromLinearConstraintPositiveMax) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 7, 1 ] + domain: [ 0, 10 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 0 + enforcement_literal: 1 + linear { + vars: 2 + coeffs: 1 + domain: [ 0, 1 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, + ExtractEnforcementLiteralFromLinearConstraintNegativeMax) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, -3, 1 ] + domain: [ -10, 1 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: -2 + linear { + vars: 2 + coeffs: 1 + domain: [ 0, 1 ] + } + } + constraints { + enforcement_literal: -2 + bool_and { literals: -1 } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, + ExtractEnforcementLiteralFromLinearConstraintPositiveMin) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 1 ] + domain: [ 3, 100 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: -2 + linear { + vars: 2 + coeffs: 1 + domain: [ 1, 2 ] + } + } + constraints { + enforcement_literal: -2 + bool_and { literals: 0 } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, + ExtractEnforcementLiteralFromLinearConstraintNegativeMin) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, -3, 1 ] + domain: [ 0, 100 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 1 + linear { + vars: 2 + coeffs: 1 + domain: [ 1, 2 ] + } + } + constraints { + enforcement_literal: -1 + bool_and { literals: -2 } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, + ExtractEnforcementLiteralFromLinearConstraintMultiple) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + name: "r0" + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 1 ] + domain: [ 2, 100 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + name: "r0" + enforcement_literal: -1 + enforcement_literal: -2 + linear { + vars: 2 + coeffs: 1 + domain: [ 2, 2 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BasicLinMaxPresolve) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 7, 12 ] } + variables { domain: [ -2, 4 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 7, 12 ] } + variables { domain: [ -2, 4 ] } + variables { domain: [ 7, 12 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, MoreAdvancedPresolve) { + // We can remove variable zero from the max since it do not change the + // outcome. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 10, 12 ] } + variables { domain: [ 10, 13 ] } + variables { domain: [ 10, 20 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 10, 12 ] } + variables { domain: [ 10, 13 ] } + variables { domain: [ 10, 13 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ConvertToEquality) { + // We can infer that the target is necessarily equal to the second variable. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -4, 0 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ -2, 0 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -4, 0 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ -2, 0 ] } + variables { domain: [ 0, 12 ] } + constraints { + linear { + vars: [ 3, 1 ] + coeffs: [ 1, -1 ] + domain: [ 0, 0 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ConvertToEqualityDoNotWork) { + // Compared to ConvertToEquality, here we can't because the min of that + // variable is too low. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -4, 0 ] } + variables { domain: [ -3, 12 ] } + variables { domain: [ -2, 0 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -4, 0 ] } + variables { domain: [ -3, 12 ] } + variables { domain: [ -2, 0 ] } + variables { domain: [ 0, 12 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinMaxExprEqualTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: -16777224 domain: 1 } + constraints { + lin_max { + target { vars: -1 coeffs: 1 } + exprs { vars: -1 coeffs: 1 } + exprs { vars: 0 coeffs: -10 offset: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BasicLinAbsPresolveVarToAbs) { + // Note that we use duplicate constraints otherwise, the presolve will + // solve the problem for us because m appear in only one constraint. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -2, 12 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + constraints { dummy_constraint { vars: [ 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BasicLinAbsPresolveAbsToVar) { + // Note that we use duplicate constraints otherwise, the presolve will + // solve the problem for us because m appear in only one constraint. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -20, 20 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + constraints { dummy_constraint { vars: [ 1, 2, 3 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -12, 12 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BasicLinAbsPresolveFixedTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -20, 20 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ -10, 10 ] } + constraints { + lin_max { + target { offset: 5 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + constraints { + lin_max { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 5 ] } + constraints { + lin_max { + target: { vars: 2 coeffs: 1 } + exprs: { vars: 0 coeffs: 10 offset: -5 } + exprs: { vars: 1 coeffs: 1 } + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveAbsFromUnaryLinear) { + // Make sure we can only remove the varibale 1 here. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -20, 20 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ 0, 1 ] } + constraints { dummy_constraint { vars: [ 0, 2 ] } } + constraints { + lin_max { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + constraints { + enforcement_literal: 2 + linear { + vars: 1 + coeffs: 1 + domain: [ 3, 5 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -12, 12 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 1 + linear { vars: 0 coeffs: 1 domain: -5 domain: -3 domain: 3 domain: 5 } + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinMaxBasicPresolveSingleVar) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 7, 12 ] } + variables { domain: [ -2, 4 ] } + variables { domain: [ 0, 20 ] } + constraints { + lin_max { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { + lin_max { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 7, 12 ] } + variables { domain: [ -2, 4 ] } + variables { domain: [ 7, 12 ] } + constraints { + lin_max { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinMaxBasicPresolveExprs) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ -2, -1 ] } + variables { domain: [ -3, 0 ] } + constraints { + lin_max { + target { vars: 3 coeffs: 1 } + exprs { + vars: [ 0, 1 ] + coeffs: [ 2, 3 ] + offset: -5 + } + exprs { + vars: [ 1, 2 ] + coeffs: [ 2, -5 ] + offset: -6 + } + exprs { + vars: [ 0, 2 ] + coeffs: [ -2, 3 ] + } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ -2, -1 ] } + variables { domain: [ -1, 0 ] } + constraints { + lin_max { + target { vars: 3 coeffs: 1 } + exprs { + vars: [ 0, 1 ] + coeffs: [ 2, 3 ] + offset: -5 + } + exprs { + vars: [ 1, 2 ] + coeffs: [ 2, -5 ] + offset: -6 + } + } + } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlapRemovedRedundantIntervals) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 3, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 5, 10 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 6, 12 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + size { vars: 7 coeffs: 1 } + end { vars: 8 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 3, 4, 5 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 6, 7, 8 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 3, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 4, 10 ] } + variables { domain: [ 5, 10 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 6, 12 ] } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + size { vars: 7 coeffs: 1 } + end { vars: 8 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 3, 4, 5 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 6, 7, 8 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { no_overlap { intervals: [ 0, 1 ] } } + )pb"); + + SatParameters params; + params.set_convert_intervals(true); + params.set_cp_model_probing_level(2); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, + ModelEqualsIgnoringConstraintsOrder(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlapMergeFixedIntervals) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 3, 26 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 3, 26 ] } + constraints { + interval { + start { offset: 0 } + size { offset: 5 } + end { offset: 5 } + } + } + constraints { + interval { + start { offset: 6 } + size { offset: 3 } + end { offset: 9 } + } + } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2, 3 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 3, 26 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 3, 26 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { + interval { + start {} + end { offset: 9 } + size { offset: 9 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + SatParameters params; + params.set_convert_intervals(true); + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlapNoMergingOfFixedIntervals) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 1, 6 ] } + variables { domain: [ 1, 26 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 3, 26 ] } + constraints { + interval { + start { offset: 0 } + size { offset: 5 } + end { offset: 5 } + } + } + constraints { + interval { + start { offset: 6 } + size { offset: 3 } + end { offset: 9 } + } + } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2, 3 ] } } + )pb"); + + SatParameters params; + params.set_convert_intervals(true); + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(initial_model)); +} + +TEST(PresolveCpModelTest, RemoveIsolatedFixedIntervalsBefore) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + constraints { + interval { + start { offset: 0 } + size { offset: 5 } + end { offset: 5 } + } + } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: 0 intervals: 1 } } + )pb"); + SatParameters params; + params.set_convert_intervals(true); + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveIsolatedFixedIntervalsAfter) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + constraints { + interval { + start { offset: 26 } + size { offset: 5 } + end { offset: 31 } + } + } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + variables { domain: [ 5, 20 ] } + variables { domain: [ 3, 6 ] } + variables { domain: [ 8, 26 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: 0 intervals: 1 } } + )pb"); + SatParameters params; + params.set_convert_intervals(true); + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, SplitNoOverlap) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 5, 13 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 6, 14 ] } + variables { domain: [ 14, 20 ] } + variables { domain: [ 19, 25 ] } + variables { domain: [ 18, 22 ] } + variables { domain: [ 23, 27 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 5 } + end { vars: 1 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 5 } + end { vars: 3 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + size { offset: 5 } + end { vars: 5 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + size { offset: 5 } + end { vars: 7 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2, 3 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 5, 13 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 6, 14 ] } + variables { domain: [ 14, 20 ] } + variables { domain: [ 19, 25 ] } + variables { domain: [ 18, 22 ] } + variables { domain: [ 23, 27 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 5 } + end { vars: 1 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 5 } + end { vars: 3 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + size { offset: 5 } + end { vars: 5 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + size { offset: 5 } + end { vars: 7 coeffs: 1 } + } + } + constraints { no_overlap { intervals: 0 intervals: 1 } } + constraints { no_overlap { intervals: 2 intervals: 3 } } + )pb"); + SatParameters params; + params.set_convert_intervals(true); + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, + ModelEqualsIgnoringConstraintsOrder(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlapDuplicateNonZeroSizedInterval) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + constraints { + interval { + start { offset: 1 } + end { offset: 1 } + size { vars: 0 coeffs: 5 offset: 3 } + } + } + constraints { no_overlap { intervals: 0 intervals: 0 } } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = + PresolveForTest(initial_model, params, CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, NoOverlapDuplicatePossiblyZeroSizedInterval) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + constraints { + interval { + start { offset: 1 } + end { offset: 1 } + size { vars: 0 coeffs: 5 } + } + } + constraints { no_overlap { intervals: 0 intervals: 0 } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 0 } + )pb"); + + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlapDuplicateOptionalPossiblyZeroSizedInterval) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 1 + interval { + start { offset: 1 } + end { offset: 1 } + size { vars: 0 coeffs: 5 } + } + } + constraints { no_overlap { intervals: 0 intervals: 0 } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 1 + linear { vars: 0 coeffs: 1 domain: 0 domain: 0 } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeWithNoInterval) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 5, 5 ] } + constraints { + cumulative { + intervals: [] + demands: [] + capacity { offset: 0 } + } + } + )pb"); + + CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_EQ(presolved_model.constraints_size(), 0); +} + +TEST(PresolveCpModelTest, CumulativeWithUnperformedIntervals) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 1 + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + intervals: [ 0, 1 ] + demands { offset: 2 } + demands { offset: 3 } + capacity { offset: 4 } + } + } + constraints { + linear { + vars: 1 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: 2 + coeffs: 1 + domain: [ 0, 0 ] + } + } + )pb"); + + CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_EQ(presolved_model.constraints_size(), 0); +} + +TEST(PresolveCpModelTest, SplitCumulative) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 13, 20 ] } + variables { domain: [ 18, 22 ] } + variables { domain: [ 16, 30 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 5 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2, 3, 4, 5 ], + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + capacity: { offset: 2 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 13, 20 ] } + variables { domain: [ 18, 22 ] } + variables { domain: [ 16, 30 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 5 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ], + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + capacity { offset: 2 } + } + } + constraints { + cumulative { + intervals: [ 3, 5, 4 ], + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + capacity: { offset: 2 } + } + } + )pb"); + SatParameters params; + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeZeroDemands) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 2, 4 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ], + demands: + [ { offset: 0 } + , { offset: 2 } + , { vars: 3 coeffs: 1 }], + capacity: { offset: 4 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 2, 4 ] } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + cumulative { + intervals: [ 0, 1 ], + demands: + [ { offset: 2 } + , { vars: 3 coeffs: 1 }], + capacity: { offset: 4 } + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeDemandsDoNotFit) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 2, 8 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 3 } + size { offset: 3 } + } + } + constraints { + cumulative { + intervals: [ 0, 1 ], + demands: + [ { vars: 2 coeffs: 1 } + , { vars: 3 coeffs: 1 }], + capacity: { offset: 4 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ 2, 8 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 3 } + size { offset: 3 } + } + } + constraints { + cumulative { + intervals: [ 0, 1 ], + demands: + [ { vars: 2 coeffs: 1 } + , { vars: 3 coeffs: 1 }], + capacity: { offset: 4 } + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeDemandsDoNotFitSizeMinZero) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 3, 6, 0 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 0 ] + coeffs: [ 1 ] + } + end { + vars: [ 3 ] + coeffs: [ 1 ] + } + size { + vars: [ 6 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 4, 7, 1 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 1 ] + coeffs: [ 1 ] + } + end { + vars: [ 4 ] + coeffs: [ 1 ] + } + size { + vars: [ 7 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 5, 8, 2 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 2 ] + coeffs: [ 1 ] + } + end { + vars: [ 5 ] + coeffs: [ 1 ] + } + size { + vars: [ 8 ] + coeffs: [ 1 ] + } + } + } + constraints { + cumulative { + capacity { offset: 1 } + intervals: [ 1, 3, 5 ] + demands { offset: 1 } + demands { offset: 5 } + demands { offset: 1 } + } + } + )pb"); + + EXPECT_EQ(Solve(initial_model).status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CumulativeRemoveIncompativeDemands) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ 5, 8 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + enforcement_literal: 5 + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 3 } + size { offset: 3 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ], + demands: { vars: 3 coeffs: 1 } + demands: { vars: 3 coeffs: 1 } + demands: { vars: 4 coeffs: 1 } + capacity: { offset: 4 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ 5, 8 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + cumulative { + intervals: [ 0, 1 ], + demands: { vars: 3 coeffs: 1 } + demands: { vars: 3 coeffs: 1 } + capacity: { offset: 4 } + } + } + )pb"); + SatParameters params; + // This will force all variables to be kept, even if unused. + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeGcdDemands) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 3 } + size { offset: 3 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ], + demands: { offset: 2 } + demands: { offset: 2 } + demands: { offset: 2 } + capacity: { offset: 4 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 1, 9 ] } + variables { domain: [ 2, 7 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 3 } + size { offset: 3 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ], + demands: { offset: 1 } + demands: { offset: 1 } + demands: { offset: 1 } + capacity: { offset: 2 } + } + } + )pb"); + SatParameters params; + + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlap2DOneBox) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 5, 5 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 5, 5 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 4 coeffs: 1 } + size { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap_2d { x_intervals: 0 y_intervals: 1 } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 5, 5 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 5, 5 ] } + )pb"); + + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlap2DRemoveInactiveBoxes) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 0 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + enforcement_literal: 6 + interval { + start { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1, 2 ] + y_intervals: [ 0, 1, 2 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1 ] + y_intervals: [ 0, 1 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlap2DNoRemoveNullAreaBoxes) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + size { offset: 5 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size {} + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1, 2 ] + y_intervals: [ 0, 1, 3 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(initial_model)); +} + +TEST(PresolveCpModelTest, NoOverlap2DSplitBoxes) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } # 0: start 0 + variables { domain: [ 2, 4 ] } # 3: start 1 + variables { domain: [ 8, 12 ] } # 6: start 2 + variables { domain: [ 9, 13 ] } # 9: start 3 + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1, 2, 3 ] + y_intervals: [ 0, 1, 2, 3 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ 8, 12 ] } + variables { domain: [ 9, 13 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + no_overlap_2d { + x_intervals: 0 + x_intervals: 1 + y_intervals: 0 + y_intervals: 1 + } + } + constraints { + no_overlap_2d { + x_intervals: 2 + x_intervals: 3 + y_intervals: 2 + y_intervals: 3 + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoOverlap2DSplitSingletonBoxes) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ 8, 12 ] } # Disjoint from the other two + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1, 2 ] + y_intervals: [ 0, 1, 2 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 2, 4 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1 ] + y_intervals: [ 0, 1 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdWithLeftConstant) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 10, 12 ] + } + variables { + name: 'y' + domain: [ 2, 2 ] + } + variables { + name: 'p' + domain: [ 0, 100 ] + } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 10, 12 ] + } + variables { + name: 'y' + domain: [ 2, 2 ] + } + variables { + name: 'p' + domain: [ 20, 24 ] + } + constraints { + linear { + vars: 2 + vars: 0 + coeffs: 1 + coeffs: -2 + domain: [ 0, 0 ] + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdWithRightConstant) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 10, 14 ] + } + variables { + name: 'y' + domain: [ 2, 2 ] + } + variables { + name: 'p' + domain: [ 0, 100 ] + } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 10, 14 ] + } + variables { + name: 'y' + domain: [ 2, 2 ] + } + variables { + name: 'p' + domain: [ 20, 28 ] + } + constraints { + linear { + vars: 2 + vars: 0 + coeffs: 1 + coeffs: -2 + domain: [ 0, 0 ] + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdWithXEqualTwoX) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 20 ] } + variables { domain: [ 2, 2 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 2, 2 ] } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +TEST(PresolveCpModelTest, IntProdWithConstantProduct) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2000 ] } + variables { domain: [ 2, 2 ] } + variables { domain: [ 5, 5 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ 10, 10 ] } + variables { domain: [ 2, 2 ] } + variables { domain: [ 5, 5 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { offset: 2 } + exprs { offset: 5 } + } + } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +TEST(PresolveCpModelTest, IntProdWithOverflow) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -100000000000, 100000000000 ] } + variables { domain: [ 0, 0, 100000000000, 100000000000 ] } + variables { domain: [ 0, 0, 100000000000, 100000000000 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 0, 100000000000, 100000000000 ] } + variables { domain: [ 0, 0, 100000000000, 100000000000 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 3 + bool_and { literals: -5 } + } + constraints { + linear { + vars: 1 + vars: 3 + coeffs: 1 + coeffs: -100000000000 + domain: 0 + domain: 0 + } + } + constraints { + linear { + vars: 2 + vars: 4 + coeffs: 1 + coeffs: -100000000000 + domain: 0 + domain: 0 + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdWithOverflowLargeConstantFactor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 266 } + constraints { + int_prod { + target { offset: 1862270976 } + exprs { offset: 1862270975 } + exprs { vars: 0 coeffs: 250970374144 offset: 1 } + } + } + )pb"); + + EXPECT_EQ(Solve(initial_model).status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, IntProdWithOverflowLargeNegativeConstantFactor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 266 } + constraints { + int_prod { + target { offset: -1862270976 } + exprs { offset: -1862270975 } + exprs { vars: 0 coeffs: 250970374144 offset: 1 } + } + } + )pb"); + + EXPECT_EQ(Solve(initial_model).status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, IntProdWithIdentity) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 20 ] } + variables { domain: [ 1, 1 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 0 } + variables { domain: 1 domain: 1 } + )pb"); + EXPECT_THAT(mapping_model, testing::EqualsProto(expected_mapping_model)); +} + +TEST(PresolveCpModelTest, IntProdWithXEqualX2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 20 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntSquareDomainReduction) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -3, 5 ] } + variables { domain: [ -30, 30 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -3, 5 ] } + variables { domain: [ 0, 1, 4, 4, 9, 9, 16, 16, 25, 25 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntSquareLargeDomainReduction) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -20, 110 ] } + variables { domain: [ -200000, 200000 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -20, 110 ] } + variables { domain: [ 0, 12100 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntSquareExprDomainReduction) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -20, 110 ] } + variables { domain: [ -9000, 9000 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -20, 94 ] } + variables { domain: [ 0, 9000 ] } + constraints { + int_prod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdWithAffineRelation) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 20 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 0, 3, 3, 6, 6, 9, 9 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + # Add this just to avoid triggering the rule of unused target variable. + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + } + )pb"); + + // The variable 2 is detected to be of the form 3 * new_var1. Subsequently, + // the product target is detected to be a multiple of 3, so its target is + // replaced by new_var2. The domain are computed accordingly. + CpModelProto presolved_model = PresolveForTest(initial_model); + presolved_model.clear_objective(); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 6 ] } # This is old_var_0 / 3. + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 3 ] } # This is old_var_2 / 3. + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(presolved_model, EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdCoeffDividesTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 3, 9 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 1000 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 10 offset: 20 } + exprs { vars: 0 coeffs: 1 offset: 3 } + exprs { vars: 1 coeffs: 5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 3, 9 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 1, 58 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 2 offset: 4 } + exprs { vars: 0 coeffs: 1 offset: 3 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = + PresolveOneConstraint(initial_model, /*constraint_index=*/0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntProdGlobalGcd) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 3, 9 ] } + variables { domain: [ 1, 10 ] } + variables { domain: [ 0, 200 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 9 offset: 18 } + exprs { vars: 0 coeffs: 2 offset: 4 } + exprs { vars: 1 coeffs: 6 offset: -6 } + } + } + )pb"); + + // The gcd is 12 ! + // So we have 9 * target + 18 is a multiple of 12, so target can be for + // instance written 4 * new_target + 2. + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 3, 9 ] } + variables { domain: [ 1, 10 ] } + variables { + domain: [ 0, 10, 12, 14, 16, 16, 18, 20, 22, 22, 25, 25, 28, 28, 31, 31 ] + } # We divide by 4 + constraints { + int_prod { + target { vars: 2 coeffs: 3 offset: 6 } + exprs { vars: 0 coeffs: 1 offset: 2 } + exprs { vars: 1 coeffs: 1 offset: -1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NullProduct) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 20 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 0 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 0, 1, 2 ] } } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 5 ] } # Many possible values here. + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, BooleanProduct) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: -1 offset: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 4 coeffs: -1 offset: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + bool_or { literals: -4 literals: -2 literals: 0 literals: 2 literals: 4 } + } + constraints { + enforcement_literal: -2 + bool_and { literals: -1 } + } + constraints { + enforcement_literal: 0 + bool_and { literals: -3 literals: 3 literals: -5 } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_permute_variable_randomly(false); + params.set_cp_model_probing_level(0); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, AffineBooleanProduct) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 30 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 2 offset: 3 } + exprs { vars: 2 coeffs: 3 offset: 2 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 6, 6, 10, 10, 15, 15, 24, 25 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: -2 + linear { vars: 0 vars: 2 coeffs: 1 coeffs: -9 domain: 6 domain: 6 } + } + constraints { + enforcement_literal: 1 + linear { vars: 0 vars: 2 coeffs: 1 coeffs: -15 domain: 10 domain: 10 } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_permute_variable_randomly(false); + params.set_cp_model_probing_level(0); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntDivSimplification) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 3, 20 ] } + variables { domain: [ -5, 5 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 3, 20 ] } + variables { domain: [ 1, 1 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntDivSingleVariable) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + constraints { + int_div { + target { vars: 0 coeffs: -6 offset: 12 } + exprs { offset: 12 } + exprs { vars: 0 coeffs: 1 offset: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntDivSimplificationOpp) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 3, 20 ] } + variables { domain: [ -5, 5 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 3, 20 ] } + variables { domain: [ -1, -1 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, PositiveFixedTargetAndPositiveDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: 3 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 15, 19 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ZeroFixedTargetAndPositiveDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: 0 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -4, 4 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NegativeFixedTargetAndPositiveDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: -3 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -19, -15 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, PositiveFixedTargetAndNegativeDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: 3 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: -5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -19, -15 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ZeroFixedTargetAndNegativeDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: 0 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: -5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -4, 4 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NegativeFixedTargetAndNegativeDivisor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { offset: -3 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: -5 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 15, 19 ] } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, TargetFixedToPositiveValue) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 210, 288 ] } + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 100 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 210, 288 ] } + variables { domain: [ 2, 2 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, TargetFixedToZeroValue) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -55, 75 ] } + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 100 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ -55, 75 ] } + variables { domain: [ 0, 0 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, TargetFixedToNegativeValue) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 210, 288 ] } + variables { domain: [ -30, 30 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: -100 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 210, 288 ] } + variables { domain: [ -2, -2 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, TargetFixedThenExprPropagated) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 110, 288 ] } + variables { domain: [ 2, 30 ] } + constraints { + int_div { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 100 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 200, 288 ] } + variables { domain: [ 2, 2 ] } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntModFixesTargetToZero) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 20 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ -3, 0 ] } + constraints { + int_mod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 20 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_div { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntModReduceTargetDomain) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 0, 8 ] } + constraints { + int_mod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 2, 7 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 20 ] } + constraints { + int_div { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + int_prod { + target { vars: 4 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + linear { + vars: 0 + vars: 2 + vars: 4 + coeffs: 1 + coeffs: -1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntModFixedTargetAndMod) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 20 ] } + variables { domain: [ -5, 11 ] } + variables { domain: [ -17, 8 ] } + constraints { + int_mod { + target { offset: 2 } + exprs { vars: 0 coeffs: 1 } + exprs { offset: 5 } + } + } + constraints { + int_mod { + target { offset: 2 } + exprs { vars: 1 coeffs: 1 } + exprs { offset: 5 } + } + } + constraints { + int_mod { + target { offset: -2 } + exprs { vars: 2 coeffs: 1 } + exprs { offset: 5 } + } + })pb"); + + // We get a representative for each int_mod. + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 3 } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinearConstraintWithGcd) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: 0 + coeffs: 100 + vars: 1 + coeffs: 200 + domain: [ 320, 999 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + EXPECT_EQ(presolved_model.variables_size(), 2); + ASSERT_EQ(1, presolved_model.constraints_size()); + const LinearConstraintProto& lin = presolved_model.constraints(0).linear(); + EXPECT_EQ(0, lin.vars(0)); + EXPECT_EQ(1, lin.vars(1)); + EXPECT_EQ(1, lin.coeffs(0)); + EXPECT_EQ(2, lin.coeffs(1)); + EXPECT_EQ(4, lin.domain(0)); + EXPECT_EQ(9, lin.domain(1)); +} + +TEST(PresolveCpModelTest, RemoveNonUsefulTerms) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 10, 10, 4, 3 ] + domain: [ 0, 29 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 2 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveNonUsefulTerms2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 9, 9, 4, 3 ] + domain: [ 0, 26 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 2 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveNonUsefulTerms3) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 10, 7 ] + domain: [ 0, 17 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, DetectApproximateGCD) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 100 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1001, 999 ] + domain: [ 0, 28500 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 28 ] } + variables { domain: [ 0, 28 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 28 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinearConstraintWithGcdInfeasible) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 4, 4 ] + domain: [ 9, 9 ] + } + } + )pb"); + + EXPECT_EQ(Solve(initial_model).status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, + LinearConstraintWithGcdFalseConstraintWithEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 2 + linear { + vars: [ 0, 1 ] + coeffs: [ 4, 4 ] + domain: [ 9, 9 ] + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 0 ] } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IntervalPresolveNegativeSize) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -7, -7, 0, 0 ] } + constraints { + interval { + start { offset: 0 } + end { vars: 0 coeffs: 1 } + size { vars: 0 coeffs: 1 } + } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + )pb"); + const CpModelProto presolved_model = PresolveOneConstraint(initial_model, 0); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +// TODO(user): really stop testing the full presolve, we always have to add +// irrelevant constraint so that stuff are not presolved away. +TEST(PresolveCpModelTest, BasicIntervalPresolve) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 3, 10 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 3, 10 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 4 coeffs: 1 } + size { vars: 5 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, -1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 3, 4, 5 ] + coeffs: [ 1, -1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1 ] + y_intervals: [ 0, 1 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 12 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 3, 10 ] } + variables { domain: [ 0, 12 ] } + variables { domain: [ 5, 15 ] } + variables { domain: [ 3, 10 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 4 coeffs: 1 } + size { vars: 5 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, -1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 3, 4, 5 ] + coeffs: [ 1, -1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + no_overlap_2d { + x_intervals: [ 0, 1 ] + y_intervals: [ 0, 1 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ExpandMinimizeObjective) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -4611686018427387903, 4611686018427387903 ] } + constraints { dummy_constraint { vars: [ 0, 1 ] } } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 2 domain: -10 domain: 10 } + } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 2 + coeffs: -1 + domain: 3 + domain: 3 + } + } + objective { vars: 2 coeffs: 2 offset: 1 } + )pb"); + + // We both expand the objective and merge it with other parallel constraint. + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + offset: -2.5 + scaling_factor: 2 + integer_before_offset: -3 + integer_scaling_factor: 2 + domain: [ -10, 10 ] + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, ExpandMinimizeObjectiveWithOppositeCoeff) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -4611686018427387903, 4611686018427387903 ] } + constraints { dummy_constraint { vars: [ 0, 1 ] } } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 2 domain: -10 domain: 10 } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 3, 3 ] + } + } + objective { vars: 2 coeffs: 2 offset: 1 } + )pb"); + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + domain: [ -10, 10 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ -1, -2 ] + offset: 3.5 + scaling_factor: 2 + integer_before_offset: 3 + integer_scaling_factor: 2 + domain: [ -30, 30 ] + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, ExpandMaximizeObjective) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -4611686018427387903, 4611686018427387903 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 1 domain: -10 domain: 10 } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, -1 ] + domain: [ 3, 3 ] + } + } + objective { vars: -3 coeffs: 2 scaling_factor: -1 offset: 1 } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_EQ(2, presolved_model.objective().vars_size()); + EXPECT_EQ(0, presolved_model.objective().vars(0)); + EXPECT_EQ(-1, presolved_model.objective().coeffs(0)); + EXPECT_EQ(1, presolved_model.objective().vars(1)); + EXPECT_EQ(-2, presolved_model.objective().coeffs(1)); + EXPECT_EQ(3.5, presolved_model.objective().offset()); + EXPECT_EQ(-2, presolved_model.objective().scaling_factor()); +} + +TEST(PresolveCpModelTest, ExpandMaximizeObjectiveWithOppositeCoeff) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -4611686018427387903, 4611686018427387903 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 3, 3 ] + } + } + objective { vars: -3 coeffs: 2 scaling_factor: -1 offset: 1 } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_EQ(2, presolved_model.objective().vars_size()); + EXPECT_EQ(0, presolved_model.objective().vars(0)); + EXPECT_EQ(1, presolved_model.objective().coeffs(0)); + EXPECT_EQ(1, presolved_model.objective().vars(1)); + EXPECT_EQ(2, presolved_model.objective().coeffs(1)); + EXPECT_EQ(-2.5, presolved_model.objective().offset()); + EXPECT_EQ(-2, presolved_model.objective().scaling_factor()); +} + +TEST(PresolveCpModelTest, ExpandMinimizeObjectiveWithLimitingLinearEquation) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -8, 7 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, -1 ] + domain: [ 3, 3 ] + } + } + objective { vars: 2 coeffs: 2 offset: 1 } + )pb"); + + // The objective domain without offset above (and after moving the coeff to + // the scaling) is [-8, 7], and when doing the transformation new_expression = + // old_obj + 3, the domain of the new expression is [-5, 10]. + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ -10, 9 ] } + variables { domain: [ -7, 3 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + offset: -2.5 + scaling_factor: 2 + integer_before_offset: -3 + integer_scaling_factor: 2 + domain: [ -5, 10 ] + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, ExpandMinimizeObjectiveWithLimitingLinearEquation2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -8, 7 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 3, 3 ] + } + } + objective { vars: 2 coeffs: 2 offset: 1 } + )pb"); + + // This time, we have new_obj = old_obj - 3. + // Note that the variable #2 is removed, but this do not remove any feasible + // solution since its value will be uniquely determined via the removed + // constraint x0 + 2x1 + x2 = 3. The objective domain constrains x0 + 2x1 + // to take feasible value for x3. + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -7, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ -1, -2 ] + offset: 3.5 + scaling_factor: 2 + integer_before_offset: 3 + integer_scaling_factor: 2 + domain: [ -11, 4 ] + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, ExpandObjectiveInfeasible) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -10, 10 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 30, 40 ] + } + )pb"); + + Model tmp_model; + EXPECT_EQ(SolveCpModel(initial_model, &tmp_model).status(), + CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, ExpandObjectiveWithLimitedPresolve) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { vars: 0 vars: 1 coeffs: -1 coeffs: 1 domain: -1 domain: -1 } + } + objective { vars: 1 coeffs: 1 })pb"); + + SatParameters params; + params.set_max_presolve_iterations(0); + params.set_log_search_progress(true); + EXPECT_EQ(SolveWithParameters(initial_model, params).status(), + CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CircuitConstraint) { + // A rho shape. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 0 + heads: 0 + literals: 0 # needed not to be unsat. + tails: 0 + heads: 1 + literals: 1 + tails: 1 + heads: 2 + literals: 2 + tails: 2 + heads: 3 + literals: 3 + tails: 3 + heads: 1 + literals: 4 + } + } + )pb"); + + // There is just one possible solution, detected by the presolve. + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +// Fully specified circuit. This used to remove the constraint instead of +// dedecting infeasibility since some mandatory node are not in the 0 <-> 1 +// circuit. +TEST(PresolveCpModelTest, FixedButIncompleteCircuitConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: [ 0, 1, 1, 2, 1, 3, 2, 3 ] + heads: [ 1, 0, 2, 1, 3, 1, 3, 2 ] + literals: [ 0, 1, 2, 3, 4, 5, 6, 7 ] + } + } + )pb"); + ExpectInfeasibleDuringPresolve(initial_model); +} + +TEST(PresolveCpModelTest, CircuitConstraintWithDuplicateLiteral) { + // A rho shape. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 0 + heads: 0 + literals: 0 # set at true + tails: 0 + heads: 1 + literals: 1 # will be false + + tails: 1 + heads: 2 + literals: 2 + tails: 2 + heads: 3 + literals: 3 + tails: 3 + heads: 1 + literals: 4 + + tails: 1 + heads: 1 + literals: 1 + tails: 2 + heads: 2 + literals: 1 + tails: 3 + heads: 3 + literals: 1 + } + } + )pb"); + + // There is just one possible solution, detected by the presolve. + SatParameters params; + params.set_max_presolve_iterations(1); + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +TEST(PresolveCpModelTest, RouteConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 1 ] } + constraints { + routes { + tails: [ 0, 0, 1, 1, 2, 2 ] + heads: [ 1, 2, 0, 2, 1, 0 ] + literals: [ 0, 1, 2, 3, 4, 5 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + constraints { + routes { + tails: [ 0, 1, 2 ] + heads: [ 1, 2, 0 ] + literals: [ 0, 0, 0 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +// The presolve used to fail by removing all arcs incident to node 2 and thus +// node 2 was no longer considered as unreachable. +TEST(PresolveCpModelTest, RouteConstraintWithUnreachableNode) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 0 ] } + constraints { + routes { + tails: [ 0, 0, 2, 1 ] + heads: [ 1, 2, 1, 0 ] + literals: [ 0, 1, 1, 0 ] + } + } + )pb"); + ExpectInfeasibleDuringPresolve(initial_model); +} + +TEST(PresolveCpModelTest, CircuitConstraintWithDegree2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 0 + heads: 0 + literals: 0 + tails: 1 + heads: 1 + literals: 1 + tails: 0 + heads: 1 + literals: 2 + tails: 1 + heads: 0 + literals: 3 + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 0 + heads: 0 + literals: 0 + tails: 1 + heads: 1 + literals: 0 + tails: 0 + heads: 1 + literals: -1 + tails: 1 + heads: 0 + literals: -1 + } + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, UsedToCrash) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 2, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + linear { + vars: 1 + vars: 0 + coeffs: 1 + coeffs: -1 + domain: [ 1, 9223372036854775807 ] + } + } + constraints { linear { vars: 1 coeffs: 1 domain: 1 domain: 1 } } + )pb"); + ExpectInfeasibleDuringPresolve(initial_model); +} + +TEST(PresolveCpModelTest, FixedAllDifferent) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 50 ] } + variables { domain: [ 3, 3 ] } + variables { domain: [ 1, 50 ] } + variables { domain: [ 1, 50 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 2, 4, 50 ] } + variables { domain: [ 1, 2, 4, 50 ] } + variables { domain: [ 1, 2, 4, 50 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + + SatParameters extra_params; + extra_params.set_symmetry_level(0); + const CpModelProto presolved_model = + PresolveForTest(initial_model, extra_params); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, AllDifferentWithExpressionsSharingVariable) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 50 ] } + variables { domain: [ 2, 20 ] } + variables { domain: [ 1, 50 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + exprs { vars: 1 coeffs: 2 offset: -3 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 50 ] } + variables { domain: [ 2, 2, 4, 20 ] } + variables { domain: [ 1, 50 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + exprs { vars: 1 coeffs: 2 offset: -3 } + } + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, DetectDifferentVariablesAndAddNoOverlap) { + CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 91, 905 ] } + variables { domain: [ 638, 937 ] } + variables { domain: [ 0, 69 ] } + variables { domain: [ 575, 930 ] } + constraints { + linear { + vars: [ 3, 4 ] + coeffs: [ 1, -1 ] + domain: [ -863, -506 ] + } + } + constraints { + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ 569, 909 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 1, -1 ] + domain: [ -846, -32 ] + } + } + constraints { + linear { + vars: [ 1, 3 ] + coeffs: [ -1, 1 ] + domain: [ -868, -22 ] + } + } + constraints { + enforcement_literal: 0 + linear { + vars: [ 1, 4 ] + coeffs: [ 1, -1 ] + domain: [ -839, -300 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 1, 4 ] + coeffs: [ 1, -1 ] + domain: [ 310, 330 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 2, 4 ] + coeffs: [ -1, 1 ] + domain: [ 275, 292 ] + } + } + constraints { + enforcement_literal: 0 + linear { + vars: [ 2, 4 ] + coeffs: [ -1, 1 ] + domain: [ -362, -123 ] + } + } + solution_hint { + vars: [ 0, 1, 2, 3, 4 ] + values: [ 1, 397, 836, 69, 713 ] + } + )pb"); + ASSERT_TRUE(SolutionIsFeasible(cp_model, cp_model.solution_hint().values())); + + Model model; + CpModelProto mapping_model; + PresolveContext context(&model, &cp_model, &mapping_model); + std::vector mapping; + CpModelPresolver presolver(&context, &mapping); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + presolver.DetectDifferentVariables(); + context.WriteVariableDomainsToProto(); + + bool has_no_overlap_constraint = false; + for (const ConstraintProto& constraint : cp_model.constraints()) { + if (constraint.has_no_overlap()) { + has_no_overlap_constraint = true; + break; + } + } + EXPECT_TRUE(has_no_overlap_constraint); + EXPECT_TRUE(SolutionIsFeasible(cp_model, cp_model.solution_hint().values())); + EXPECT_EQ(Solve(cp_model).status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, PermutationMandatoryValues) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 4 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + + Model model; + model.GetOrCreate()->set_expand_alldiff_constraints(false); + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + PresolveContext context(&model, &presolved_model, &mapping_model); + PresolveCpModel(&context, &mapping); + + const IntegerVariableProto expected_var = ParseTestProto(R"pb( + domain: [ 4, 4 ])pb"); + EXPECT_THAT(expected_var, testing::EqualsProto(mapping_model.variables(3))); +} + +TEST(PresolveCpModelTest, CircuitCornerCase1) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 1 + tails: 2 + tails: 0 + heads: 2 + heads: 0 + heads: 1 + literals: 0 + literals: 1 + literals: 2 + } + } + )pb"); + + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_EQ(0, presolved_model.constraints_size()); +} + +TEST(PresolveCpModelTest, CircuitCornerCase2) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + tails: 0 + heads: 1 + literals: 0 + tails: 1 + heads: 1 + literals: 1 + tails: 0 + heads: 2 + literals: 2 + tails: 2 + heads: 2 + literals: 3 + } + } + )pb"); + + Model tmp_model; + EXPECT_EQ(SolveCpModel(initial_model, &tmp_model).status(), + CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, ObjectiveWithLargeCoefficient) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective: { + vars: [ -1, -2, -3 ] + scaling_factor: -1.0 + coeffs: [ 194833170077, 3656800, 19394221124 ] + } + )pb"); + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective: { + vars: [ 0, 1, 2 ] + scaling_factor: -1.0 + coeffs: [ -194833170077, -3656800, -19394221124 ] + + # We simplify the domain. + domain: -214231048001 + domain: 0 + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, MipSimplificationExample) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 4 ] + domain: [ 4, 4 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 4 ] + domain: [ 4, 4 ] + } + } + )pb"); + const CpModelProto mapping_model = GetMappingModel(initial_model); + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +TEST(PresolveCpModelTest, TriviallyUnsatCumulative) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 2, 2 ] } # size + variables { domain: [ 0, 9 ] } # end + variables { domain: [ 2, 2 ] } # capacity + variables { domain: [ 5, 5 ] } # demand + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + cumulative { + capacity: { offset: 2 } + demands: { offset: 5 } + intervals: [ 0 ] + } + } + )pb"); + ExpectInfeasibleDuringPresolve(initial_model); +} + +TEST(PresolveCpModelTest, ZeroDemandCumulative) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + demands { offset: 1 } + demands { offset: 2 } + demands { offset: 0 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + demands { offset: 1 } + demands { offset: 2 } + intervals: [ 0, 1 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CapacityExceedsDemands) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 1 ] } # optional literal + variables { domain: [ 7, 8 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { vars: 4 coeffs: 1 } + demands { offset: 1 } + demands { offset: 2 } + demands { offset: 4 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 1 ] } # optional literal + variables { domain: [ 0, 1 ] } # capacity representative + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeDivideByGcd) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 1 ] } # optional literal + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 13 } + demands { offset: 3 } + demands { offset: 6 } + demands { offset: 9 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 1 ] } # optional literal + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 4 } + demands { offset: 1 } + demands { offset: 2 } + demands { offset: 3 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, CumulativeDivideByGcdBug) { + const CpModelProto initial_model = ParseTestProto(R"pb( + name: "Multi-device peak-memory minimization." + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 13 ] } + variables { domain: [ 13, 13 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + enforcement_literal: 7 + interval { + start { vars: 8 coeffs: 1 } + end { vars: 10 coeffs: 1 } + size { vars: 9 coeffs: 1 } + } + } + constraints { + enforcement_literal: 7 + linear { + vars: [ 8, 9, 10 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { linear { vars: 3 coeffs: 1 domain: 1 domain: 1 } } + constraints { + enforcement_literal: 3 + linear { vars: 0 vars: 4 coeffs: 1 coeffs: -1 domain: 0 domain: 0 } + } + constraints { + linear { + vars: 3 + vars: 7 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + enforcement_literal: 3 + linear { + vars: 8 + vars: 0 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + enforcement_literal: 3 + linear { + vars: 11 + vars: 10 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + enforcement_literal: 7 + linear { + vars: 8 + vars: 11 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: -1 + } + } + constraints { + enforcement_literal: 3 + linear { + vars: 8 + vars: 4 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + enforcement_literal: 3 + linear { + vars: 6 + vars: 10 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + cumulative { + capacity { vars: 12 coeffs: 1 } + intervals: 2 + demands { vars: 13 coeffs: 1 } + } + } + objective { vars: 12 coeffs: 1 } + )pb"); + const CpSolverResponse response = Solve(initial_model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(13.0, response.objective_value()); +} + +TEST(PresolveCpModelTest, NonConflictingDemands) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } # start 0 + variables { domain: [ 2, 6 ] } # start 1 + variables { domain: [ 4, 8 ] } # start 2 + variables { domain: [ 8, 10 ] } # start 3 + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + # Fixed interval that creates the potential overload. + interval { + start { offset: 5 } + end { offset: 7 } + size { offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + demands: { offset: 1 } + demands: { offset: 1 } + demands: { offset: 1 } + demands: { offset: 1 } + demands: { offset: 1 } + intervals: [ 0, 1, 2, 3, 4 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 2, 6 ] } + variables { domain: [ 4, 8 ] } + variables { domain: [ 8, 10 ] } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { offset: 5 } + end { offset: 7 } + size { offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + intervals: [ 0, 1, 2 ] + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NonConflictingDemandsInTheMiddle) { + // Initially, all intervals are connected through overlap. + // Then interval 3 should be removed as it can never cause an overlap. + // Then the cumulative should be split in 2. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } # start 0 + variables { domain: [ 0, 4 ] } # start 1 + variables { domain: [ 0, 5 ] } # start 2 + variables { domain: [ 6, 9 ] } # start 3 + variables { domain: [ 10, 15 ] } # start 4 + variables { domain: [ 11, 15 ] } # start 5 + variables { domain: [ 11, 15 ] } # start 6 + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 5 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + end { vars: 6 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + intervals: [ 0, 1, 2, 3, 4, 5, 6 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 6, 9 ] } + variables { domain: [ 10, 15 ] } + variables { domain: [ 11, 15 ] } + variables { domain: [ 11, 15 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 2 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 5 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + interval { + start { vars: 6 coeffs: 1 } + end { vars: 6 coeffs: 1 offset: 2 } + size { offset: 2 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + intervals: [ 0, 1, 2 ] + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + } + } + constraints { + cumulative { + capacity { offset: 2 } + intervals: [ 3, 4, 5 ] + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 1 } + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ConvertToNoOverlap) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 7 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { vars: 3 coeffs: 1 } + demands { offset: 4 } + demands { offset: 4 } + demands { offset: 4 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 4, 7 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ConvertToNoOverlapVariableDemand) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 4, 6 ] } # variable demand + variables { domain: [ 0, 7 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { vars: 4 coeffs: 1 } + demands { vars: 3 coeffs: 1 } + demands { vars: 3 coeffs: 1 } + demands { vars: 3 coeffs: 1 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 4, 6 ] } # variable demand + variables { domain: [ 4, 7 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + linear { vars: 3 vars: 4 coeffs: 1 coeffs: -1 domain: -3 domain: 0 } + } + constraints { no_overlap { intervals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, NoConvertToNoOverlap) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 7, 8 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { vars: 3 coeffs: 1 } + demands { offset: 4 } + demands { offset: 4 } + demands { offset: 4 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + SatParameters extra_params; + extra_params.set_symmetry_level(0); + const CpModelProto presolved_model = + PresolveForTest(initial_model, extra_params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 9 ] } # start + variables { domain: [ 0, 1 ] } # capacity + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 2 } + end { vars: 0 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 1 coeffs: 1 } + size { offset: 2 } + end { vars: 1 coeffs: 1 offset: 2 } + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + capacity { vars: 3 coeffs: 1 offset: 7 } + demands { offset: 4 } + demands { offset: 4 } + demands { offset: 4 } + intervals: [ 0, 1, 2 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, ConversionToBoolOr) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 3 ] + domain: [ 1, 100 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 2 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 3 ] + scaling_factor: 1 + } + constraints { bool_or { literals: [ 0, 1, 2 ] } } + constraints { bool_or { literals: [ -3, -2, -1 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, ConversionToAtMostOnePositive) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 2, 3 ] + domain: [ 0, 3 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 3 ] + scaling_factor: 1 + } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, ConversionToAtMostOneNegative) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 2, 3 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + // Note that the order of the literal in the constraint do not change. + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 3 ] + scaling_factor: 1 + } + constraints { at_most_one { literals: [ -3, -2, -1 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, ExtractAtMostOne) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 3, 2, 1 ] + domain: [ 1, 3 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 3 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 5 ] + scaling_factor: 1 + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 3, 2, 1 ] + domain: [ 1, 3 ] + } + } + constraints { + enforcement_literal: 0 + bool_and { literals: -2 } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, DuplicateLiteralsBoolOr) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + constraints { bool_or { literals: [ 0, 1, 0, 2 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, FalseLiteralBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2, 3 ] } } + )pb"); + SatParameters params; + params.set_symmetry_level(0); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, TrueLiteralBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2, 3 ] } } + )pb"); + SatParameters params; + params.set_symmetry_level(0); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 1, 2, 3, 0 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, TwoTrueLiteralBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2, 3, 4 ] } } + )pb"); + SatParameters params; + params.set_symmetry_level(0); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, OneActiveLiteralToFalseBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1 ] } } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 0 ] } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, OneActiveLiteralToTrueBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2 ] } } + )pb"); + const CpModelProto presolved_model = GetReducedDomains(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, TwoActiveLiteralsAndTrueBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2 ] } } + )pb"); + SatParameters params; + params.set_symmetry_level(0); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, TwoActiveLiteralsAndFalseBoolXor) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2 ] } } + )pb"); + SatParameters params; + params.set_symmetry_level(0); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, SetPPCRedundentConstraints) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + constraints { bool_or { literals: [ 0, 1, 2 ] } } + constraints { bool_or { literals: [ 0, 1, 2 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, SetPPCDominatedConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + constraints { at_most_one { literals: [ 0, 1, 2, 3 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2, 3 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, SetPPCFixVariables) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_or { literals: [ 0, 1, 2 ] } } + constraints { at_most_one { literals: [ 0, 1, 2, 3 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, DuplicateInAtMostOne) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2, 3, 2 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, CanonicalBinaryVarAndTable) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ -1, -1, 1, 1 ] } + constraints { + table { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + values: [ 0, -1, 1, -1, 2, -1, 2, 1 ] + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + constraints { + table { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + values: [ 0, 0, 1, 0, 2, 0, 2, 1 ] + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, DuplicateVariablesInTable) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 0 coeffs: -1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + values: [ + 0, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0, 0, 1, -1, 1, 1, 2, -2, 2, 2 + ] + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + values: [ 0, 0, 1, 0, 1, 1, 2, 2 ] + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, CanonicalAffineVar) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0, 2, 2, 4, 4 ] } + variables { domain: [ 1, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + domain: [ 3, 1000 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 1, 10 ] } + constraints { + linear { + vars: [ 1, 0 ] + coeffs: [ 1, 1 ] + domain: [ 2, 12 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, IdempotentElement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1, 3, 4 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 5, 5 ] + } + } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 0 coeffs: 1 } + exprs { offset: 1 } + exprs { offset: 1 } + exprs { offset: 3 } + exprs { offset: 3 } + exprs { offset: 4 } + exprs { offset: 12 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, AffineElement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 7 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 1 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 4 } + exprs { offset: 5 } + exprs { offset: 6 } + } + } + constraints { dummy_constraint { vars: [ 0, 1 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 1, 6 ] } + constraints { + linear { + vars: [ 1, 0 ] + coeffs: [ 1, -1 ] + domain: [ 1, 1 ] + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, AffineElementWithScaledBooleanIndex) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0, 3, 3 ] } + variables { domain: [ 0, 2 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 2 } + exprs { offset: 0 } + exprs { offset: 0 } + exprs { offset: 0 } + } + } + constraints { dummy_constraint { vars: [ 0, 1 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0, 3, 3 ] } + variables { domain: [ 0, 0, 2, 2 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { vars: 0 vars: 2 coeffs: 1 coeffs: -3 domain: 0 domain: 0 } + } + constraints { + linear { vars: 1 vars: 2 coeffs: 1 coeffs: 2 domain: 2 domain: 2 } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, AffineElementWithNonIntegerSlope) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 6 ] } + variables { domain: [ -2, 2 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: 1 } + exprs { offset: 2 } + exprs { offset: 5 } + exprs { offset: 6 } + exprs { offset: 0 } + exprs { offset: 7 } + exprs { offset: 8 } + exprs { offset: -2 } + } + } + constraints { dummy_constraint { vars: [ 0, 1 ] } } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 0, 3, 3, 6, 6 ] } + variables { domain: [ -2, -2, 0, 0, 2, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { vars: 0 vars: 2 coeffs: 1 coeffs: 3 domain: 6 domain: 6 } + } + constraints { + linear { vars: 1 vars: 2 coeffs: 1 coeffs: -2 domain: -2 domain: -2 } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, ReduceDomainsInAutomaton) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1, 3, 3, 6, 10 ] } + variables { domain: [ 1, 1, 3, 3, 6, 6 ] } + variables { domain: [ 1, 3, 6, 6 ] } + constraints { + automaton { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + transition_tail: [ 4, 1, 1, 1, 2, 3, 4 ] + transition_head: [ 4, 2, 3, 4, 2, 3, 4 ] + transition_label: [ 4, 1, 3, 6, 1, 3, 6 ] + starting_state: 1 + final_states: [ 2, 3, 4 ] + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1, 3, 3, 6, 6 ] } + variables { domain: [ 1, 1, 3, 3, 6, 6 ] } + variables { domain: [ 1, 1, 3, 3, 6, 6 ] } + constraints { + automaton { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + transition_tail: [ 4, 1, 1, 1, 2, 3, 4 ] + transition_head: [ 4, 2, 3, 4, 2, 3, 4 ] + transition_label: [ 4, 1, 3, 6, 1, 3, 6 ] + starting_state: 1 + final_states: [ 2, 3, 4 ] + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, UnsatIntegerLinearConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 7, 6, 5 ] + domain: [ 1, 4 ] + } + } + )pb"); + ExpectInfeasibleDuringPresolve(initial_model); +} + +TEST(PresolveCpModelTest, LinMaxCanBeRemoved) { + // The target variable is not constraining after simple propagation and not + // used anywhere else. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -15, 8 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -4, 11 ] } + constraints { + lin_max { + target: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 2 coeffs: 1 } + } + } + constraints { dummy_constraint { vars: [ 1, 2 ] } } + )pb"); + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + Model model; + auto* params = model.GetOrCreate(); + params->set_permute_variable_randomly(false); + params->set_cp_model_probing_level(0); + PresolveContext context(&model, &presolved_model, &mapping_model); + CpModelPresolver presolver(&context, &mapping); + presolver.Presolve(); + + const CpModelProto expected_mapping_model = ParseTestProto(R"pb( + variables { domain: [ -2, 8 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -4, 8 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(expected_mapping_model, testing::EqualsProto(mapping_model)); +} + +TEST(PresolveCpModelTest, LinMaxCannotBeRemoved) { + // Almost the same as above, but the target of the int_max might constraint + // the other variable via its lower bound, so we cannot remove it. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -4, 11 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -4, 8 ] } + constraints { + lin_max { + target: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, LinMaxCannotBeRemovedWithHoles) { + // Almost the same as above, but the target does not contains the infered + // domain. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -4, 2, 5, 11 ] } + variables { domain: [ 0, 2, 4, 8 ] } + variables { domain: [ -2, 1, 4, 7 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2, 5, 8 ] } + variables { domain: [ 0, 2, 4, 8 ] } + variables { domain: [ -2, 1, 4, 7 ] } + constraints { + lin_max { + target: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 2 coeffs: 1 } + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, DetectVarValueEncoding) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 3 ] } + constraints { + enforcement_literal: 0 + linear { + vars: 1 + coeffs: 1 + domain: [ 2, 2 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: 1 + coeffs: 1 + domain: [ 1, 1, 3, 3 ] + } + } + )pb"); + Model model; + model.GetOrCreate() + ->set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = initial_model; + CpModelProto mapping_model; + std::vector mapping; + PresolveContext context(&model, &presolved_model, &mapping_model); + PresolveCpModel(&context, &mapping); + int encoding_literal = -1; + EXPECT_TRUE(context.HasVarValueEncoding(1, int64_t{2}, &encoding_literal)); + EXPECT_EQ(encoding_literal, 0); +} + +TEST(FindDuplicateConstraintsTest, BasicTest) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -4, 11 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 2 } + } + } + constraints { + name: "name are ignored" + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 2 } + } + } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 2 } + } + } + constraints { + lin_max { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 2 } + } + } + )pb"); + + std::vector> duplicates = FindDuplicateConstraints(model); + EXPECT_THAT(duplicates, + ::testing::ElementsAre(std::make_pair(1, 0), std::make_pair(2, 0), + std::make_pair(3, 0))); +} + +TEST(FindDuplicateConstraintsTest, LinearConstraintParallelToObjective) { + const CpModelProto model = ParseTestProto(R"pb( + constraints { + name: "name are ignored" + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 3, 3, 7 ] + } + } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 3, 3, 7 ] + } + )pb"); + + std::vector> duplicates = FindDuplicateConstraints(model); + EXPECT_THAT(duplicates, + ::testing::ElementsAre(std::make_pair(0, kObjectiveConstraint))); +} + +TEST(DetectDuplicateConstraintsTest, DifferentRedundantEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 4 ] + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 1 ] + domain: [ 0, 6 ] + } + } + constraints { + enforcement_literal: [ 5 ] + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 1 ] + domain: [ 0, 6 ] + } + } + constraints { + enforcement_literal: [ 4, 5 ] + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 1 ] + domain: [ 0, 6 ] + } + } + constraints { bool_or { literals: [ -5, 5 ] } } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 5 ] + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 1 ] + domain: [ 0, 6 ] + } + } + constraints { + enforcement_literal: -6 + bool_and { literals: -5 } + })pb"); + + SatParameters params; + params.set_cp_model_probing_level(2); + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveTest, EncodingIssue) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 0, 2, 2 ] } + variables { domain: [ 0, 0, 3, 3 ] } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target: { offset: 1 } + exprs { offset: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + constraints { + linear { + vars: [ 1, 2, 3 ] + coeffs: [ 1, 1, 1 ] + domain: [ 1, 1 ] + } + } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 4 coeffs: 1 } + exprs { vars: 4 coeffs: 1 } + exprs { offset: 2 } + exprs { offset: 0 } + exprs { offset: 0 } + } + } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 5 coeffs: 1 } + exprs { vars: 5 coeffs: 1 } + exprs { offset: 0 } + exprs { offset: 3 } + exprs { offset: 0 } + } + } + )pb"); + + SatParameters params; + params.set_log_search_progress(true); + const CpSolverResponse response = SolveWithParameters(model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +// This test was failing with the wrong optimal. +TEST(PresolveCpModelTest, FailedRandomTest) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 7, 7 ] } + variables { domain: [ -5, 8 ] } + variables { domain: [ -6, -2 ] } + variables { domain: [ -8, -2 ] } + variables { domain: [ -7, -4 ] } + variables { domain: [ -4, 7 ] } + constraints { + linear { + vars: 0 + vars: 2 + vars: 4 + vars: 5 + coeffs: -8 + coeffs: 7 + coeffs: -35 + coeffs: 80 + domain: -42 + domain: -21 + } + } + constraints { + linear { + vars: 0 + vars: 2 + vars: 4 + vars: 5 + coeffs: 40 + coeffs: -35 + coeffs: 175 + coeffs: -400 + domain: 105 + domain: 105 + domain: 110 + domain: 110 + domain: 115 + domain: 115 + domain: 120 + domain: 120 + domain: 125 + domain: 125 + domain: 130 + domain: 130 + domain: 135 + domain: 135 + domain: 140 + domain: 140 + domain: 145 + domain: 145 + domain: 150 + domain: 150 + domain: 155 + domain: 155 + domain: 160 + domain: 160 + domain: 165 + domain: 165 + domain: 170 + domain: 170 + domain: 175 + domain: 175 + domain: 180 + domain: 180 + domain: 185 + domain: 185 + domain: 190 + domain: 190 + domain: 195 + domain: 195 + domain: 200 + domain: 200 + domain: 205 + domain: 205 + domain: 210 + domain: 210 + } + } + constraints { + linear { + vars: 0 + vars: 2 + vars: 4 + vars: 5 + coeffs: -8 + coeffs: 7 + coeffs: -35 + coeffs: 80 + domain: -42 + domain: -21 + } + } + constraints { + linear { + vars: 0 + vars: 2 + vars: 4 + vars: 5 + coeffs: 40 + coeffs: -35 + coeffs: 175 + coeffs: -400 + domain: 105 + domain: 105 + domain: 110 + domain: 110 + domain: 115 + domain: 115 + domain: 120 + domain: 120 + domain: 125 + domain: 125 + domain: 130 + domain: 130 + domain: 135 + domain: 135 + domain: 140 + domain: 140 + domain: 145 + domain: 145 + domain: 150 + domain: 150 + domain: 155 + domain: 155 + domain: 160 + domain: 160 + domain: 165 + domain: 165 + domain: 170 + domain: 170 + domain: 175 + domain: 175 + domain: 180 + domain: 180 + domain: 185 + domain: 185 + domain: 190 + domain: 190 + domain: 195 + domain: 195 + domain: 200 + domain: 200 + domain: 205 + domain: 205 + domain: 210 + domain: 210 + } + } + objective { vars: 3 vars: 1 vars: 2 coeffs: 37 coeffs: -18 coeffs: -7 } + )pb"); + SatParameters params; + params.set_log_search_progress(true); + const CpSolverResponse response = SolveWithParameters(model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.objective_value(), -419); +} + +TEST(PresolveCpModelTest, DetectDuplicateVarEqValueEncoding) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 9 ] } + constraints { + enforcement_literal: 0 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 6, 6 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 0, 5, 7, 9 ] + } + } + constraints { + enforcement_literal: 1 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 6, 6 ] + } + } + constraints { + enforcement_literal: -2 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 0, 5, 7, 9 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 9 ] } + constraints { + enforcement_literal: 0 + linear { + vars: [ 1 ] + coeffs: [ 1 ] + domain: [ 6, 6 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: [ 1 ] + coeffs: [ 1 ] + domain: [ 0, 5, 7, 9 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, EqualityWithOnlyTwoOddBooleans) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 99 ] } + variables { domain: [ 0, 99 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 1, 3, 4, 4 ] + domain: [ 60, 60 ] + } + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 15 ] } + variables { domain: [ 0, 15 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 15, 15 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, DualEquality) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 99 ] } + variables { domain: [ 0, 99 ] } + constraints { + enforcement_literal: 0 + bool_and { literals: 1 } + } + # Anything that we don't know how to presolve. + # TODO(user): could be nice to had a "unknown" constraint for this purpose. + constraints { + all_diff { + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ -1, 1, 1, 1 ] + } + )pb"); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 99 ] } + variables { domain: [ 0, 99 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + objective { + vars: [ 1, 2 ] + coeffs: [ 1, 1 ] + scaling_factor: 1 + domain: [ 0, 198 ] + } + )pb"); + + CpModelProto presolved_model = PresolveForTest(initial_model); + EXPECT_THAT(presolved_model, EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, EmptyProduct) { + // A rho shape. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + constraints { int_prod { target: { vars: 0 coeffs: 1 } } } + constraints { dummy_constraint { vars: [ 0 ] } } + )pb"); + CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, ElementWithTargetEqualIndex) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 1, 1 ] } # 0 + variables { domain: [ 0, 4 ] } # 1 - ok + variables { domain: [ 3, 7 ] } # 2 + variables { domain: [ 3, 3 ] } # 3 - ok + variables { domain: [ 4, 9 ] } # 4 - ok + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 4 coeffs: 1 } + exprs { vars: 5 coeffs: 1 } + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: 1 domain: 1 domain: 3 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 3 domain: 7 } + variables { domain: 4 domain: 9 } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 0 coeffs: 1 } + exprs {} + exprs { vars: 1 coeffs: 1 } + exprs {} + exprs { offset: 3 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, ReduceDomainsInInverse) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + inverse { + f_direct: [ 0, 1, 2 ] + f_inverse: [ 3, 4, 5 ] + } + } + )pb"); + const CpModelProto domains = GetReducedDomains(initial_model); + const CpModelProto expected_domains = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 1, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 0, 2, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + )pb"); + EXPECT_THAT(expected_domains, testing::EqualsProto(domains)); +} + +TEST(PresolveCpModelTest, RemoveZeroEventsFromReservoir) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 11 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + reservoir { + min_level: 0 + max_level: 10 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + active_literals: [ 4, 4, 5, 6 ] + level_changes: { offset: 3 } + level_changes: { offset: 0 } + level_changes: { offset: 3 } + level_changes: { offset: -2 } + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 11 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + reservoir { + min_level: 0 + max_level: 6 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + active_literals: [ 3, 4, 5 ] + level_changes: { offset: 3 } + level_changes: { offset: 3 } + level_changes: { offset: -2 } + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveInactiveEventsFromReservoir) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 11 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + reservoir { + min_level: 0 + max_level: 10 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + active_literals: [ 4, 4, 5, 6 ] + level_changes: { offset: 3 } + level_changes: { offset: -1 } + level_changes: { offset: 3 } + level_changes: { offset: -2 } + } + } + )pb"); + SatParameters params; + params.set_disable_constraint_expansion(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 11 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + reservoir { + min_level: 0 + max_level: 3 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + active_literals: [ 2, 3 ] + level_changes: { offset: 3 } + level_changes: { offset: -2 } + } + } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveUnusedEncoding) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 3 ] } + constraints { dummy_constraint { vars: [ 0, 1, 2 ] } } + constraints { + enforcement_literal: 0 + linear { + vars: 3 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: 1 + linear { + vars: 3 + coeffs: 1 + domain: [ 1, 1 ] + } + } + constraints { + enforcement_literal: 2 + linear { + vars: 3 + coeffs: 1 + domain: [ 2, 2 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, RemoveUnusedEncodingWithObjective) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + objective { + vars: [ 3 ] + coeffs: [ 1 ] + } + constraints { dummy_constraint { vars: [ 0, 1, 2 ] } } + constraints { + enforcement_literal: 0 + linear { + vars: 3 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: 1 + linear { + vars: 3 + coeffs: 1 + domain: [ 1, 1 ] + } + } + constraints { + enforcement_literal: 2 + linear { + vars: 3 + coeffs: 1 + domain: [ 2, 2 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { exactly_one { literals: [ 3, 4, 5 ] } } + constraints { + enforcement_literal: -4 + bool_and { literals: -1 } + } + constraints { + enforcement_literal: -5 + bool_and { literals: -2 } + } + constraints { + enforcement_literal: -6 + bool_and { literals: -3 } + } + objective: { + vars: [ 3, 5 ] + coeffs: [ -1, 1 ] + scaling_factor: 1 + offset: 1 + integer_before_offset: 1 + domain: [ -1, 1 ] + } + )pb"); + EXPECT_THAT(presolved_model, + ModelEqualsIgnoringConstraintsOrder(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, RemovableEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { dummy_constraint { vars: [ 1, 2, 3 ] } } + constraints { + enforcement_literal: 0 + linear { + vars: 1 + coeffs: 1 + domain: [ 0, 5 ] + } + } + constraints { + enforcement_literal: -1 + linear { + vars: 1 + coeffs: 1 + domain: [ 4, 7 ] + } + } + )pb"); + const CpModelProto presolved_model = PresolveForTest(initial_model); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + )pb"); + EXPECT_THAT(expected_presolved_model, testing::EqualsProto(presolved_model)); +} + +TEST(PresolveCpModelTest, LinearAndExactlyOne) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 5 ] } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 1 ] + domain: [ 0, 6 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + constraints { + linear { + vars: [ 1, 2, 3 ] + coeffs: [ 1, 2, 1 ] + domain: [ 0, 4 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinearAndAtMostOnePropagation) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 50 ] } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, -1 ] + domain: [ 0, 10 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + // Not only we need to consider the pair of constraint to know that the + // last variable is <= 4, but once we know that, we can extract the variable + // with coefficient 4 as an enforcement literal. + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } + constraints { + enforcement_literal: -3 + linear { + vars: [ 0, 1, 3 ] + coeffs: [ 2, 3, -1 ] + domain: [ 0, 5 ] + } + } + constraints { at_most_one { literals: [ 0, 1, 2 ] } } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, LinMaxWithBoolean) { + const int num_vars = 5; + const int max_value = 4; + absl::BitGen random; + + for (int runs = 0; runs < 1000; ++runs) { + // Create num_vars variable that are either fixed or have two values. + CpModelProto cp_model; + for (int i = 0; i < num_vars; ++i) { + FillDomainInProto( + Domain::FromValues({absl::Uniform(random, 0, max_value + 1), + absl::Uniform(random, 0, max_value + 1)}), + cp_model.add_variables()); + } + + // We randomize the variable to have duplicates. + LinearArgumentProto* lin_max = + cp_model.add_constraints()->mutable_lin_max(); + lin_max->mutable_target()->add_vars( + absl::Uniform(random, 0, num_vars)); + lin_max->mutable_target()->add_coeffs(1); + for (int i = 0; i < absl::Uniform(random, 0, num_vars); ++i) { + LinearExpressionProto* expr = lin_max->add_exprs(); + expr->add_vars(absl::Uniform(random, 0, num_vars)); + expr->add_coeffs(1); + } + + int num_solutions_without_presolve = 0; + { + Model model; + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + parameters.set_keep_all_feasible_solutions_in_presolve(true); + parameters.set_cp_model_presolve(false); + parameters.set_log_search_progress(true); + model.Add(NewSatParameters(parameters)); + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + num_solutions_without_presolve++; + })); + SolveCpModel(cp_model, &model); + } + + int num_solutions_with_presolve = 0; + { + Model model; + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + parameters.set_keep_all_feasible_solutions_in_presolve(true); + parameters.set_log_search_progress(true); + model.Add(NewSatParameters(parameters)); + model.Add(NewFeasibleSolutionObserver( + [&](const CpSolverResponse& r) { num_solutions_with_presolve++; })); + SolveCpModel(cp_model, &model); + } + + // Note that the solution are checked by the checker, so there is not really + // any need to compare that we get exactly the same ones. + ASSERT_EQ(num_solutions_with_presolve, num_solutions_without_presolve) + << ProtobufDebugString(cp_model); + } +} + +TEST(PresolveCpModelTest, Bug174584992) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -288230376151711744, 262144 ] } + variables { domain: [ 0, 5 ] } + constraints { + name: "T" + linear { vars: 1 vars: 0 coeffs: 1 coeffs: 2 } + } + )pb"); + + Model tmp_model; + EXPECT_EQ(SolveCpModel(initial_model, &tmp_model).status(), + CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveTest, DetectInfeasibilityDuringMerging) { + ExpectInfeasibleDuringPresolve(ParseTestProto(R"pb( + variables { domain: [ -100, 100 ] } + variables { domain: [ -100, 100 ] } + variables { domain: [ -100, 100 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 3 ] + domain: [ 0, 10 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 3 ] + domain: [ 11, 20 ] + } + } + )pb")); +} + +TEST(PresolveTest, DetectEncodingFromLinear) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ -100, 100 ] } + constraints { exactly_one { literals: [ 0, 1, 2, 3, 4 ] } } + constraints { + linear { + vars: [ 0, 1, 3, 4, 5 ] + coeffs: [ 1, 7, -2, 4, 1 ] + domain: [ 10, 10 ] + } + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + // The values are 10, 10-1, 10-7, 10+2, and 10-4. + EXPECT_EQ(ReadDomainFromProto(presolved_model.variables(5)).ToString(), + "[3][6][9,10][12]"); +} + +TEST(PresolveTest, OrToolsIssue2924) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1000 ] } # This lower bound caused issues. + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 1 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 80, 100, 120, -1 ] + domain: [ 95, 95 ] + } + } + objective { + vars: [ 3 ] + coeffs: [ 1 ] + } + )pb"); + SatParameters params; + params.set_log_search_progress(true); + EXPECT_EQ(SolveWithParameters(initial_model, params).status(), + CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, AtMostOneAndLinear) { + // Using the at most one, the linear constraint will be always true. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } # variable 4 + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 4 ] + coeffs: [ 1, 1, 1, 1 ] + domain: [ 0, 5 ] + } + } + constraints { at_most_one { literals: [ 0, 1, 2, 5 ] } } + )pb"); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 0, 1, 2, 5 ] } } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, AtMostOneWithSingleton) { + // Using the at most one, the linear constraint will be always true. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3, 4 ] + coeffs: [ 1534, 5646, 4564, 145, 178 ] + domain: [ 47888, 53888 ] + } + } + constraints { at_most_one { literals: [ 3, 4, 5 ] } } + objective { + vars: [ 0, 1, 2, 3, 4, 5 ] + coeffs: [ 1534, 5646, 4564, -878, -787, -874 ] + } + )pb"); + + // We transform the at most one to exactly one and then shift the cost to + // the other variable so we can remove a singleton. + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 32 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 11 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3, 4 ] + coeffs: [ 1534, 5646, 4564, 145, 178 ] + domain: [ 47888, 53888 ] + } + } + constraints { + enforcement_literal: 3 + bool_and { literals: [ -5 ] } + } + objective { + scaling_factor: 1 + offset: -874 + integer_before_offset: -874 + vars: [ 0, 1, 2, 3, 4 ] + coeffs: [ 1534, 5646, 4564, -4, 87 ] + domain: [ -4, 150193 ] + } + )pb"); + + SatParameters params; + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, PresolveDiophantinePreservesSolutionHint) { + // Diophantine equation: https://miplib.zib.de/instance_details_ej.html. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 10000000 ] } + variables { domain: [ 0, 10000000 ] } + variables { domain: [ 0, 10000000 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 31013, -41014, -51015 ] + domain: [ 0, 0 ] + } + } + objective { + vars: [ 0 ] + coeffs: [ 1 ] + } + solution_hint { + vars: [ 0, 1, 2 ] + values: [ 25508, 1, 15506 ] + } + )pb"); + + SatParameters params; + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + ASSERT_EQ(presolved_model.solution_hint().vars_size(), + presolved_model.variables_size()); + std::vector solution_hint(presolved_model.variables_size()); + for (int i = 0; i < presolved_model.solution_hint().vars_size(); ++i) { + solution_hint[presolved_model.solution_hint().vars(i)] = + presolved_model.solution_hint().values(i); + } + EXPECT_TRUE(SolutionIsFeasible(presolved_model, solution_hint)); +} + +TEST(PresolveCpModelTest, SolveDiophantine) { + // Diophantine equation: https://miplib.zib.de/instance_details_ej.html. + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 10000000 ] } + variables { domain: [ 0, 10000000 ] } + variables { domain: [ 0, 10000000 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 31013, -41014, -51015 ] + domain: [ 0, 0 ] + } + } + objective { + vars: [ 0 ] + coeffs: [ 1 ] + } + )pb"); + + SatParameters params; + params.set_cp_model_presolve(true); + // Should solve in < .01 second. Note that deterministic time is not + // completely accurate. + params.set_max_deterministic_time(.001); + const CpSolverResponse response_with = + SolveWithParameters(model_proto, params); + + EXPECT_EQ(response_with.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response_with.solution(0), 25508); + + // Does not solve without presolving. + params.set_cp_model_presolve(false); + const CpSolverResponse response_without = + SolveWithParameters(model_proto, params); + EXPECT_NE(response_without.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, IncompatibleLinear) { + // a <=> x <= y + // b <=> x >= y + // a => not(b) + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 0, 6 ] } + constraints { + enforcement_literal: [ 0 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ -6, 0 ] + } + } + constraints { + enforcement_literal: [ -1 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ 1, 6 ] + } + } + constraints { + enforcement_literal: [ 1 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ 0, 6 ] + } + } + constraints { + enforcement_literal: [ -2 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ -6, -1 ] + } + } + constraints { + enforcement_literal: 0 + bool_and { literals: [ -2 ] } + } + )pb"); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 0, 6 ] } + constraints { + enforcement_literal: [ 0 ] + linear { + vars: [ 1, 2 ] + coeffs: [ 1, -1 ] + domain: [ -6, -1 ] + } + } + constraints { + enforcement_literal: [ -1 ] + linear { + vars: [ 1, 2 ] + coeffs: [ 1, -1 ] + domain: [ 1, 6 ] + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(initial_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, SearchStrategySurvivePresolve) { + const CpModelProto proto = ParseTestProto(R"pb( + variables { + name: "x" + domain: [ 1, 10 ] + } + variables { + name: "y" + domain: [ 3, 8 ] + } + search_strategy { + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 0 coeffs: -1 } + domain_reduction_strategy: SELECT_MAX_VALUE + } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(proto, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(proto)); +} + +TEST(PresolveCpModelTest, AmoRectangle) { + // We need a large rectangle, so we generate this by hand. + CpModelProto model; + for (int i = 0; i < 100; ++i) { + auto* var = model.add_variables(); + var->add_domain(0); + var->add_domain(1); + } + for (int i = 0; i < 10; ++i) { + auto* var = model.add_variables(); + var->add_domain(0); + var->add_domain(5); + } + { + auto* amo = model.add_constraints()->mutable_at_most_one(); + for (int i = 0; i < 100; ++i) amo->add_literals(i); + } + { + auto* linear = model.add_constraints()->mutable_linear(); + for (int i = 0; i < 100; ++i) { + linear->add_vars(i); + linear->add_coeffs(1); + } + linear->add_vars(100); + linear->add_coeffs(1); + linear->add_vars(101); + linear->add_coeffs(1); + linear->add_domain(0); + linear->add_domain(5); + } + { + auto* linear = model.add_constraints()->mutable_linear(); + for (int i = 0; i < 100; ++i) { + linear->add_vars(i); + linear->add_coeffs(3); + } + linear->add_vars(102); + linear->add_coeffs(1); + linear->add_vars(103); + linear->add_coeffs(1); + linear->add_domain(0); + linear->add_domain(5); + } + { + auto* linear = model.add_constraints()->mutable_linear(); + for (int i = 0; i < 100; ++i) { + linear->add_vars(i); + linear->add_coeffs(-2); + } + linear->add_vars(104); + linear->add_coeffs(1); + linear->add_vars(105); + linear->add_coeffs(1); + linear->add_domain(0); + linear->add_domain(5); + } + + CpModelProto expected_presolved_model; + for (int i = 0; i < 100; ++i) { + auto* var = expected_presolved_model.add_variables(); + var->add_domain(0); + var->add_domain(1); + } + for (int i = 0; i < 10; ++i) { + auto* var = expected_presolved_model.add_variables(); + var->add_domain(0); + var->add_domain(5); + } + { + // New new variable. + auto* var = expected_presolved_model.add_variables(); + var->add_domain(0); + var->add_domain(1); + } + { + auto* linear = expected_presolved_model.add_constraints()->mutable_linear(); + linear->add_vars(100); + linear->add_coeffs(1); + linear->add_vars(101); + linear->add_coeffs(1); + linear->add_vars(110); + linear->add_coeffs(1); + linear->add_domain(0); + linear->add_domain(5); + } + { + auto* linear = expected_presolved_model.add_constraints()->mutable_linear(); + linear->add_vars(102); + linear->add_coeffs(1); + linear->add_vars(103); + linear->add_coeffs(1); + linear->add_vars(110); + linear->add_coeffs(3); + linear->add_domain(0); + linear->add_domain(5); + } + { + auto* linear = expected_presolved_model.add_constraints()->mutable_linear(); + linear->add_vars(104); + linear->add_coeffs(1); + linear->add_vars(105); + linear->add_coeffs(1); + linear->add_vars(110); + linear->add_coeffs(-2); + linear->add_domain(0); + linear->add_domain(5); + } + { + auto* exo = + expected_presolved_model.add_constraints()->mutable_exactly_one(); + exo->add_literals(NegatedRef(110)); + for (int i = 0; i < 100; ++i) exo->add_literals(i); + } + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + const CpModelProto presolved_model = PresolveForTest(model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_presolved_model)); +} + +TEST(PresolveCpModelTest, PreserveHints) { + const CpModelProto input_model = ParseTestProto(R"pb( + variables { domain: [ 1, 1, 4, 4 ] } + variables { domain: [ 0, 0, 3, 3, 9, 9 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 1, 10 ] + } + } + solution_hint { + vars: [ 0, 1 ] + values: [ 1, 9 ] + } + )pb"); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1, 3, 3 ] } + constraints { + enforcement_literal: 0 + linear { vars: 1 coeffs: 1 domain: 0 domain: 1 } + } + solution_hint { vars: 0 vars: 1 values: 0 values: 3 } + )pb"); + + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + CpModelProto presolved_model = PresolveForTest(input_model, params); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, DuplicateColumns) { + CpModelProto presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 1, 2, 3 ] } } + constraints { + linear { + vars: [ 0, 2, 3 ] + coeffs: [ 1, 2, 2 ] + domain: [ 1, 10 ] + } + } + )pb"); + + Model model; + CpModelProto mapping_model; + PresolveContext context(&model, &presolved_model, &mapping_model); + std::vector mapping; + CpModelPresolver presolver(&context, &mapping); + + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + presolver.DetectDuplicateColumns(); + context.WriteVariableDomainsToProto(); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { at_most_one { literals: [ 1, 4 ] } } + constraints { + linear { + vars: [ 0, 4 ] + coeffs: [ 1, 2 ] + domain: [ 1, 10 ] + } + } + )pb"); + EXPECT_THAT(presolved_model, testing::EqualsProto(expected_model)); +} + +TEST(PresolveCpModelTest, TrivialAfterPresolveWithVariousOffsets) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + floating_point_objective { + vars: [ 0, 2 ] + coeffs: [ 1, 1 ] + maximize: true + } + )pb"); + + const CpSolverResponse response = Solve(initial_model); + + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.objective_value(), 2); +} + +TEST(PresolveCpModelTest, EmptyDomain) { + // The model checker doesn't allow empty domains, but we still might generate + // them in LNS. + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [] } + )pb"); + + PresolveForTest(initial_model, SatParameters(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, CanonicalizeAndRemapRoutesConstraintNodeVariables) { + // A complete graph with 3 nodes and the following arcs: + // 0 --l0-> 1 --l2-> 2 --l4-> 0 + // 0 <-l1-- 1 <-l3-- 2 <-l5-- 0 + // + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + # unused, should be removed. + variables { domain: [ 0, 0 ] } + # fixed value, should be removed. + variables { domain: [ 5, 5 ] } + variables { domain: [ 0, 10 ] } + # should be replaced with an affine representative in [0, 4] + variables { domain: [ 0, 0, 2, 2, 4, 4, 6, 6, 8, 8 ] } + constraints { + routes { + tails: [ 0, 1, 1, 2, 2, 0 ] + heads: [ 1, 0, 2, 1, 0, 2 ] + literals: [ 0, 1, 2, 3, 4, 5 ] + dimensions: { + exprs { + vars: [ 7 ] + coeffs: [ 1 ] + } + exprs { + vars: [ 8 ] + coeffs: [ 1 ] + } + exprs { + vars: [ 9 ] + coeffs: [ 1 ] + } + } + } + } + constraints { + enforcement_literal: 0 + linear { + vars: [ 7, 8 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + constraints { + enforcement_literal: 1 + linear { + vars: [ 8, 7 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + constraints { + enforcement_literal: 2 + linear { + vars: [ 8, 9 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + constraints { + enforcement_literal: 3 + linear { + vars: [ 9, 8 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + constraints { + enforcement_literal: 4 + linear { + vars: [ 9, 7 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + constraints { + enforcement_literal: 5 + linear { + vars: [ 7, 9 ] + coeffs: [ 1, -1 ] + domain: [ 0, 10 ] + } + } + )pb"); + + SatParameters params; + const CpModelProto presolved_model = PresolveForTest(initial_model, params); + + const CpModelProto expected_presolved_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 4 ] } + constraints { + routes { + tails: [ 0, 1, 1, 2, 2, 0 ] + heads: [ 1, 0, 2, 1, 0, 2 ] + literals: [ 0, 1, 2, 3, 4, 5 ] + dimensions: { + exprs { offset: 5 } + exprs { + vars: [ 6 ] + coeffs: [ 1 ] + } + exprs { + vars: [ 7 ] + coeffs: [ 2 ] + } + } + } + } + # ... more constraints (omitted) ... + )pb"); + EXPECT_THAT(presolved_model.variables(), + testing::Pointwise(testing::EqualsProto(), + expected_presolved_model.variables())); + EXPECT_THAT(presolved_model.constraints(0), + testing::EqualsProto(expected_presolved_model.constraints(0))); +} + +TEST(PresolveCpModelTest, InnerObjectiveLowerBound) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 10 ] } + variables { domain: [ -1647, 504, 3054, 3054 ] } + constraints { + linear { + vars: 0 + vars: 1 + coeffs: 2 + coeffs: 1 + domain: [ 10, 10 ] + } + } + objective { + vars: 1 + coeffs: 2 + domain: [ 8, 10 ] + } + )pb"); + + const CpSolverResponse r = Solve(initial_model); + EXPECT_EQ(r.inner_objective_lower_bound(), 8); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_solver_test.cc b/ortools/sat/cp_model_solver_test.cc new file mode 100644 index 0000000000..1769afc4ed --- /dev/null +++ b/ortools/sat/cp_model_solver_test.cc @@ -0,0 +1,4592 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_solver.h" + +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/strings/str_join.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_checker.h" +#include "ortools/sat/cp_model_test_utils.h" +#include "ortools/sat/lp_utils.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/logging.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::AnyOf; +using ::testing::Eq; +using ::testing::UnorderedElementsAre; + +int AddVariable(int64_t lb, int64_t ub, CpModelProto* model) { + const int index = model->variables_size(); + sat::IntegerVariableProto* var = model->add_variables(); + var->add_domain(lb); + var->add_domain(ub); + return index; +} + +int AddInterval(int64_t start, int64_t size, int64_t end, CpModelProto* model) { + const int index = model->constraints_size(); + IntervalConstraintProto* interval = + model->add_constraints()->mutable_interval(); + interval->mutable_start()->add_vars(AddVariable(start, end - size, model)); + interval->mutable_start()->add_coeffs(1); + interval->mutable_size()->set_offset(size); + *interval->mutable_end() = interval->start(); + interval->mutable_end()->set_offset(size); + return index; +} + +int AddOptionalInterval(int64_t start, int64_t size, int64_t end, + int existing_enforcement_variable, + CpModelProto* model) { + const int index = model->constraints_size(); + ConstraintProto* constraint = model->add_constraints(); + constraint->add_enforcement_literal(existing_enforcement_variable); + IntervalConstraintProto* interval = constraint->mutable_interval(); + interval->mutable_start()->add_vars(AddVariable(start, end - size, model)); + interval->mutable_start()->add_coeffs(1); + interval->mutable_size()->set_offset(size); + *interval->mutable_end() = interval->start(); + interval->mutable_end()->set_offset(size); + return index; +} + +TEST(LoadCpModelTest, PureSatProblem) { + const CpModelProto model_proto = Random3SatProblem(100, 3); + LOG(INFO) << CpModelStats(model_proto); + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + LOG(INFO) << CpSolverResponseStats(response); +} + +TEST(LoadCpModelTest, PureSatProblemWithLimit) { + const CpModelProto model_proto = Random3SatProblem(500); + LOG(INFO) << CpModelStats(model_proto); + Model model; + model.Add(NewSatParameters("max_deterministic_time:0.00001")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::UNKNOWN); + LOG(INFO) << CpSolverResponseStats(response); +} + +TEST(LoadCpModelTest, BooleanLinearOptimizationProblem) { + const CpModelProto model_proto = RandomLinearProblem(20, 5); + LOG(INFO) << CpModelStats(model_proto); + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + LOG(INFO) << CpSolverResponseStats(response); +} + +TEST(StopAfterFirstSolutionTest, BooleanLinearOptimizationProblem) { + const CpModelProto model_proto = RandomLinearProblem(100, 100); + LOG(INFO) << CpModelStats(model_proto); + + Model model; + SatParameters params; + params.set_num_search_workers(8); + params.set_stop_after_first_solution(true); + + int num_solutions = 0; + model.Add(NewFeasibleSolutionObserver( + [&num_solutions](const CpSolverResponse& /*r*/) { num_solutions++; })); + model.Add(NewSatParameters(params)); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::FEASIBLE); + EXPECT_GE(num_solutions, 1); + + // Because we have 8 threads and we currently report all solution as we found + // them, we might report more than one the time every subsolver is + // terminated. This happens 8% of the time as of March 2020. + EXPECT_LE(num_solutions, 2); + LOG(INFO) << CpSolverResponseStats(response); +} + +TEST(RelativeGapLimitTest, BooleanLinearOptimizationProblem) { + const CpModelProto model_proto = RandomLinearProblem(100, 100); + LOG(INFO) << CpModelStats(model_proto); + + Model model; + SatParameters params; + params.set_relative_gap_limit(1e10); // Should stop at the first solution! + + int num_solutions = 0; + model.Add(NewFeasibleSolutionObserver( + [&num_solutions](const CpSolverResponse& /*r*/) { num_solutions++; })); + model.Add(NewSatParameters(params)); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + + // We reported OPTIMAL, but there is indeed a gap. + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_LT(response.best_objective_bound() + 1e-6, response.objective_value()); + EXPECT_EQ(1, num_solutions); + LOG(INFO) << CpSolverResponseStats(response); +} + +TEST(LoadCpModelTest, InvalidProblem) { + CpModelProto model_proto; + model_proto.add_variables(); // No domain. + Model model; + EXPECT_EQ(SolveCpModel(model_proto, &model).status(), + CpSolverStatus::MODEL_INVALID); +} + +TEST(LoadCpModelTest, UnsatProblem) { + CpModelProto model_proto; + for (int i = 0; i < 2; ++i) { + AddVariable(i, i, &model_proto); + } + auto* ct = model_proto.add_constraints()->mutable_linear(); + ct->add_domain(0); + ct->add_domain(0); + ct->add_vars(0); + ct->add_coeffs(1); + ct->add_vars(1); + ct->add_coeffs(1); + Model model; + EXPECT_EQ(SolveCpModel(model_proto, &model).status(), + CpSolverStatus::INFEASIBLE); +} + +TEST(LoadCpModelTest, SimpleCumulative) { + CpModelProto model_proto; + AddInterval(0, 2, 4, &model_proto); + AddInterval(1, 2, 4, &model_proto); + ConstraintProto* ct = model_proto.add_constraints(); + ct->mutable_cumulative()->add_intervals(0); + ct->mutable_cumulative()->add_demands()->set_offset(3); + ct->mutable_cumulative()->add_intervals(1); + ct->mutable_cumulative()->add_demands()->set_offset(4); + ct->mutable_cumulative()->mutable_capacity()->set_offset(6); + + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.solution(0), 0); // start_1 + EXPECT_EQ(response.solution(1), 2); // start_2 +} + +TEST(SolverCpModelTest, EmptyModel) { + const CpModelProto cp_model = ParseTestProto("solution_hint {}"); + + SatParameters params; + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, SimpleInterval) { + CpModelProto model_proto; + const int deadline = 6; + const int i1 = AddInterval(0, 3, deadline, &model_proto); + const int i3 = AddInterval(3, 3, deadline, &model_proto); + NoOverlapConstraintProto* no_overlap = + model_proto.add_constraints()->mutable_no_overlap(); + no_overlap->add_intervals(i1); + no_overlap->add_intervals(i3); + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, SimpleOptionalIntervalFeasible) { + CpModelProto model_proto; + const int deadline = 6; + const int i1_enforcement = AddVariable(0, 1, &model_proto); + const int i1 = + AddOptionalInterval(0, 3, deadline, i1_enforcement, &model_proto); + + const int i2_enforcement = AddVariable(0, 1, &model_proto); + const int i2 = + AddOptionalInterval(2, 2, deadline, i2_enforcement, &model_proto); + + const int i3 = AddInterval(3, 3, deadline, &model_proto); + + NoOverlapConstraintProto* no_overlap = + model_proto.add_constraints()->mutable_no_overlap(); + no_overlap->add_intervals(i1); + no_overlap->add_intervals(i2); + no_overlap->add_intervals(i3); + + BoolArgumentProto* one_of_intervals_on = + model_proto.add_constraints()->mutable_bool_xor(); + one_of_intervals_on->add_literals(i1_enforcement); + one_of_intervals_on->add_literals(i2_enforcement); + + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, SimpleOptionalIntervalInfeasible) { + CpModelProto model_proto; + const int deadline = 6; + const int i1_enforcement = AddVariable(0, 1, &model_proto); + const int i1 = + AddOptionalInterval(0, 3, deadline, i1_enforcement, &model_proto); + + const int i2_enforcement = AddVariable(0, 1, &model_proto); + const int i2 = + AddOptionalInterval(2, 2, deadline, i2_enforcement, &model_proto); + + const int i3 = AddInterval(3, 3, deadline, &model_proto); + + NoOverlapConstraintProto* no_overlap = + model_proto.add_constraints()->mutable_no_overlap(); + no_overlap->add_intervals(i1); + no_overlap->add_intervals(i2); + no_overlap->add_intervals(i3); + + BoolArgumentProto* one_of_intervals_on = + model_proto.add_constraints()->mutable_bool_and(); + one_of_intervals_on->add_literals(i1_enforcement); + one_of_intervals_on->add_literals(i2_enforcement); + + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(SolveCpModelTest, NonInstantiatedVariables) { + CpModelProto model_proto; + const int a = AddVariable(0, 10, &model_proto); + const int b = AddVariable(0, 10, &model_proto); + auto* linear_constraint = model_proto.add_constraints()->mutable_linear(); + linear_constraint->add_vars(a); + linear_constraint->add_vars(b); + linear_constraint->add_coeffs(1); + linear_constraint->add_coeffs(1); + linear_constraint->add_domain(4); + linear_constraint->add_domain(5); + + // We need to fix the first one, otherwise the lower bound will not be + // enough for the second. + model_proto.add_search_strategy()->add_variables(0); + + Model model; + SatParameters params; + params.set_instantiate_all_variables(false); + params.set_search_branching(SatParameters::FIXED_SEARCH); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + + const CpSolverResponse response = SolveCpModel(model_proto, &model); + + // Because we didn't try to instantiate the variables, we just did one round + // of propagation. Note that this allows to use the solve as a simple + // propagation engine with no search decision (modulo the binary variable that + // will be instantiated anyway)! + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + ASSERT_EQ(2, response.solution_size()); + EXPECT_EQ(response.solution(0), 0); + + // Note that this one was not instantiated, but we used its lower bound. + EXPECT_EQ(response.solution(1), 4); +} + +// When there is nothing to do, we had a bug that didn't copy the solution +// with the core based solver, this simply test this corner case. +TEST(SolveCpModelTest, TrivialModelWithCore) { + CpModelProto model_proto; + const int a = AddVariable(1, 1, &model_proto); + model_proto.mutable_objective()->add_vars(a); + model_proto.mutable_objective()->add_coeffs(1); + Model model; + SatParameters params; + params.set_optimize_with_core(true); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); +} + +TEST(SolveCpModelTest, TrivialLinearTranslatedModel) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: -10 domain: 10 } + variables { domain: -10 domain: 10 } + variables { domain: -461168601842738790 domain: 461168601842738790 } + constraints { + linear { + vars: 0 + vars: 1 + coeffs: 1 + coeffs: 1 + domain: -4611686018427387903 + domain: 4611686018427387903 + } + } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 2 + coeffs: -1 + domain: 0 + domain: 0 + } + } + objective { vars: 2 coeffs: -1 scaling_factor: -1 } + )pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); +} + +TEST(ConvertMPModelProtoToCpModelProtoTest, SimpleLinearExampleWithMaximize) { + const MPModelProto mp_model = ParseTestProto(R"pb( + maximize: true + objective_offset: 0 + variable { + lower_bound: -10 + upper_bound: 10 + objective_coefficient: 1 + is_integer: true + } + variable { + lower_bound: -10 + upper_bound: 10 + objective_coefficient: 2 + is_integer: true + } + constraint { + lower_bound: -100 + upper_bound: 100 + var_index: 0 + var_index: 1 + coefficient: 1 + coefficient: 1 + } + )pb"); + CpModelProto cp_model; + SolverLogger logger; + ConvertMPModelProtoToCpModelProto(SatParameters(), mp_model, &cp_model, + &logger); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(cp_model, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_TRUE(SolutionIsFeasible( + cp_model, std::vector(response.solution().begin(), + response.solution().end()))); +} + +TEST(SolveCpModelTest, SmallDualConnectedComponentsModel) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 2 domain: 0 domain: 8 } + } + constraints { + linear { vars: 2 vars: 3 coeffs: 1 coeffs: 2 domain: 0 domain: 6 } + } + objective { + vars: 0 + vars: 1 + vars: 2 + vars: 3 + coeffs: -1 + coeffs: -2 + coeffs: -3 + coeffs: -4 + scaling_factor: -1 + } + )pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); +} + +TEST(SolveCpModelTest, DualConnectedComponentsModel) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 2 domain: 0 domain: 8 } + } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 1 domain: 2 domain: 20 } + } + constraints { + linear { vars: 2 vars: 3 coeffs: 1 coeffs: 2 domain: 0 domain: 6 } + } + constraints { + linear { vars: 2 vars: 3 coeffs: 1 coeffs: 1 domain: 2 domain: 20 } + } + objective { + vars: 0 + vars: 1 + vars: 2 + vars: 3 + coeffs: -1 + coeffs: -2 + coeffs: -3 + coeffs: -4 + scaling_factor: -1 + } + )pb"); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_TRUE(SolutionIsFeasible( + model_proto, std::vector(response.solution().begin(), + response.solution().end()))); +} + +TEST(SolveCpModelTest, EnumerateAllSolutions) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 24); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsBis) { + const std::string model_str = R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 1 domain: 6 domain: 6 } + } + )pb"; + const CpModelProto model_proto = ParseTestProto(model_str); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + // Test the response was correctly filled. + EXPECT_NE(0, response.num_branches()); + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 5); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsDomainsWithHoleInVar) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 1 domain: 2 domain: 4 domain: 5 } + constraints { + enforcement_literal: 0 + enforcement_literal: 1 + linear { vars: 2 coeffs: 1 domain: 2 domain: 2 } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 3 * 4 + 1); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsDomainsWithHoleInEnforcedLinear1) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 1 domain: 5 } + constraints { + enforcement_literal: 0 + enforcement_literal: 1 + linear { + vars: 2 + coeffs: 1 + domain: [ 1, 2, 4, 4 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 5 * 3 + 3); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsDomainsWithHoleInEnforcedLinear2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + constraints { + enforcement_literal: 0 + enforcement_literal: 1 + linear { + vars: 2 + coeffs: 1 + vars: 3 + coeffs: 1 + domain: [ 0, 1, 3, 4 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 9 * 3 + (9 - 3)); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsDomainsWithHoleInLinear2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + constraints { + linear { + vars: 0 + coeffs: 1 + vars: 1 + coeffs: 1 + domain: [ 0, 1, 3, 4 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 9 - 3); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsAndCopyToResponse) { + const std::string model_str = R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + constraints { + linear { vars: 0 vars: 1 coeffs: 1 coeffs: 1 domain: 6 domain: 6 } + } + )pb"; + const CpModelProto model_proto = ParseTestProto(model_str); + + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_fill_additional_solutions_in_response(true); + params.set_solution_pool_size(1000); // A big enough value. + + const CpSolverResponse response = SolveWithParameters(model_proto, params); + std::vector> additional_solutions; + for (const auto& solution : response.additional_solutions()) { + additional_solutions.push_back(std::vector( + solution.values().begin(), solution.values().end())); + } + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(additional_solutions, + UnorderedElementsAre( + UnorderedElementsAre(1, 5), UnorderedElementsAre(2, 4), + UnorderedElementsAre(3, 3), UnorderedElementsAre(4, 2), + UnorderedElementsAre(5, 1))); + + // Not setting the solution_pool_size high enough gives partial result. + // Because we randomize variable order, we don't know which solution will be + // in the pool deterministically. + params.set_solution_pool_size(3); + const CpSolverResponse response2 = SolveWithParameters(model_proto, params); + EXPECT_EQ(response2.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response2.additional_solutions().size(), 3); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsOfEmptyModel) { + const std::string model_str = R"pb( + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + )pb"; + const CpModelProto model_proto = ParseTestProto(model_str); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 27); +} + +TEST(SolveCpModelTest, SolutionsAreCorrectlyPostsolvedInTheObserver) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 1 } + variables { domain: 3 domain: 3 } + variables { domain: 1 domain: 4 } + )pb"); + Model model; + model.Add(NewFeasibleSolutionObserver([](const CpSolverResponse& response) { + EXPECT_EQ(response.solution_size(), 4); + LOG(INFO) << absl::StrJoin(response.solution(), " "); + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, ObjectiveDomainLowerBound) { + // y = 10 - 2x. + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + constraints { + linear { vars: 0 vars: 1 coeffs: 2 coeffs: 1 domain: 10 domain: 10 } + } + objective { vars: 1 coeffs: 1 domain: 1 domain: 10 } + )pb"); + for (int lb = 1; lb <= 8; ++lb) { + model_proto.mutable_objective()->set_domain(0, lb); + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.objective_value(), lb % 2 ? lb + 1 : lb); + } +} + +TEST(SolveCpModelTest, LinMaxObjectiveDomainLowerBoundInfeasible) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 3 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 1 ] + } + } + constraints { + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 2, 9223372036854775807 ] + } + } + constraints { + lin_max { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + objective { vars: 2 coeffs: 1 } + )pb"); + + Model model; + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(SolveCpModelTest, LinMaxUniqueTargetLowerBoundInfeasible) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 3 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 1 ] + } + } + constraints { + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 2, 9223372036854775807 ] + } + } + constraints { + lin_max { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(SolveCpModelTest, LinMaxUniqueTarget) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 4 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 1 ] + } + } + constraints { + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 0, 4 ] + } + } + constraints { + lin_max { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, HintWithCore) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 2, 8 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + scaling_factor: 1 + } + solution_hint { + vars: [ 0, 1 ] + values: [ 2, 3 ] + } + )pb"); + Model model; + model.Add(NewSatParameters("optimize_with_core:true, linearization_level:0")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(2.0, response.objective_value()); +} + +TEST(SolveCpModelTest, BadHintWithCore) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 2 domain: 8 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + objective { vars: 2 scaling_factor: 1 coeffs: 1 } + solution_hint { vars: 0 vars: 1 values: 4 values: 5 } + )pb"); + Model model; + model.Add(NewSatParameters("optimize_with_core:true, linearization_level:0")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(2.0, response.objective_value()); +} + +TEST(SolveCpModelTest, ForcedBadHint) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 2 domain: 8 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + objective { vars: 2 scaling_factor: 1 coeffs: 1 } + solution_hint { vars: 0 vars: 1 values: 4 values: 5 } + )pb"); + Model model; + model.Add(NewSatParameters( + "fix_variables_to_their_hinted_value:true, linearization_level:0")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(SolveCpModelTest, UnforcedBadHint) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 2 domain: 8 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 1 + coeffs: -1 + domain: 0 + domain: 0 + } + } + objective { vars: 2 scaling_factor: 1 coeffs: 1 } + solution_hint { vars: 0 vars: 1 values: 4 values: 5 } + )pb"); + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, HintWithNegativeRef) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + solution_hint { vars: -1 values: 1 } + )pb"); + Model model; + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(SolveCpModelTest, SolutionHintBasicTest) { + SatParameters params; + params.set_cp_model_presolve(false); + for (int loop = 0; loop < 50; ++loop) { + CpModelProto model_proto; + + // Because the random problem might be UNSAT, we loop a few times until we + // have a SAT one. + for (;;) { + model_proto = Random3SatProblem(200, 3); + + // Find a solution. + Model model; + model.Add(NewSatParameters(params)); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + if (response.status() != CpSolverStatus::OPTIMAL) continue; + if (response.num_conflicts() == 0) continue; + + // Copy the solution to the hint. + for (int i = 0; i < response.solution_size(); ++i) { + model_proto.mutable_solution_hint()->add_vars(i); + model_proto.mutable_solution_hint()->add_values(response.solution(i)); + } + break; + } + + // Now solve again, we should have no conflict! + { + Model model; + int num_solution = 0; + model.Add(NewSatParameters(params)); + model.Add(NewFeasibleSolutionObserver( + [&num_solution](const CpSolverResponse& /*r*/) { num_solution++; })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.num_conflicts(), 0); + EXPECT_EQ(num_solution, 1); + } + } +} + +TEST(SolveCpModelTest, SolutionHintRepairTest) { + SatParameters params; + params.set_cp_model_presolve(false); + params.set_num_workers(1); + + // NOTE(user): This test doesn't ensure that the hint is repaired. It only + // makes sure that the solver doesn't crash if the hint is perturbed. + CpModelProto model_proto; + + // Because the random problem might be UNSAT, we loop a few times until we + // have a SAT one. + for (;;) { + model_proto = Random3SatProblem(200, 3); + + // Find a solution. + Model model; + model.Add(NewSatParameters(params)); + + const CpSolverResponse response = SolveCpModel(model_proto, &model); + if (response.status() != CpSolverStatus::OPTIMAL) continue; + if (response.num_conflicts() == 0) continue; + + // Copy the solution to the hint with small perturbation. + model_proto.mutable_solution_hint()->add_vars(0); + model_proto.mutable_solution_hint()->add_values(response.solution(0) ^ 1); + for (int i = 1; i < response.solution_size(); ++i) { + model_proto.mutable_solution_hint()->add_vars(i); + model_proto.mutable_solution_hint()->add_values(response.solution(i)); + } + break; + } + + // Now solve again. + { + Model model; + params.set_repair_hint(true); + model.Add(NewSatParameters(params)); + int num_solution = 0; + model.Add(NewFeasibleSolutionObserver( + [&num_solution](const CpSolverResponse& /*r*/) { num_solution++; })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(num_solution, 1); + } +} + +TEST(SolveCpModelTest, SolutionHintMinimizeL1DistanceTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 1, 1, 1, 1 ] + domain: [ 1, 1 ] + } + } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 1, 2, 4, 8 ] + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 0, 1, 0, 1 ] + } + )pb"); + + // TODO(user): Instead, we might change the presolve to always try to keep the + // given hint feasible. + Model model; + model.Add( + NewSatParameters("repair_hint:true, stop_after_first_solution:true, " + "keep_all_feasible_solutions_in_presolve:true")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_THAT(response.status(), + AnyOf(Eq(CpSolverStatus::OPTIMAL), Eq(CpSolverStatus::FEASIBLE))); + EXPECT_THAT(response.objective_value(), AnyOf(Eq(8), Eq(2))); +} + +TEST(SolveCpModelTest, SolutionHintObjectiveTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 1, 2, 3, 4 ] + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 1, 0, 0, 1 ] + } + )pb"); + Model model; + std::vector solutions; + SatParameters* parameters = model.GetOrCreate(); + parameters->set_cp_model_presolve(false); + parameters->set_log_search_progress(true); + model.Add( + NewFeasibleSolutionObserver([&solutions](const CpSolverResponse& r) { + solutions.push_back(r.objective_value()); + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + ASSERT_GE(solutions.size(), 2); + EXPECT_EQ(solutions[0], 5.0); + EXPECT_EQ(solutions.back(), 0.0); +} + +TEST(SolveCpModelTest, SolutionHintOptimalObjectiveTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ -1, 2, 3, -4 ] + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 1, 0, 0, 1 ] + } + )pb"); + Model model; + std::vector solutions; + model.Add( + NewFeasibleSolutionObserver([&solutions](const CpSolverResponse& r) { + solutions.push_back(r.objective_value()); + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + ASSERT_EQ(solutions.size(), 1); + EXPECT_EQ(solutions[0], -5.0); +} + +TEST(SolveCpModelTest, SolutionHintEnumerateTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { name: "x" domain: 0 domain: 10 } + variables { name: "y" domain: 0 domain: 10 } + constraints { + linear { vars: 1 vars: 0 coeffs: 1 coeffs: 1 domain: 10 domain: 10 } + } + solution_hint { vars: 0 values: -1 } + )pb"); + Model model; + SatParameters parameters; + parameters.set_cp_model_presolve(false); + parameters.set_enumerate_all_solutions(true); + model.Add(NewSatParameters(parameters)); + int num_solutions = 0; + model.Add(NewFeasibleSolutionObserver( + [&num_solutions](const CpSolverResponse& /*r*/) { num_solutions++; })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(num_solutions, 11); +} + +TEST(SolveCpModelTest, SolutionHintAndAffineRelation) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 4, 4, 8, 8, 12, 12 ] } + variables { domain: [ 2, 2, 4, 4, 6, 6 ] } + solution_hint { + vars: [ 0, 1 ] + values: [ 8, 4 ] + } + )pb"); + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_stop_after_first_solution(true); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::FEASIBLE); + EXPECT_EQ(response.solution(0), 8); + EXPECT_EQ(response.solution(1), 4); + EXPECT_EQ(response.num_conflicts(), 0); +} + +TEST(SolveCpModelTest, MultipleEnforcementLiteral) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + constraints { + enforcement_literal: [ 0, 1 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ 0, 0 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 25 + 25 + 25 + /*when enforced*/ 5); +} + +TEST(SolveCpModelTest, TightenedDomains) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 2 + coeffs: 3 + domain: 0 + domain: 7 + } + } + )pb"); + SatParameters params; + params.set_fill_tightened_domains_in_response(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpSolverResponse response = SolveWithParameters(model_proto, params); + CpSolverResponse response_with_domains_only; + *response_with_domains_only.mutable_tightened_variables() = + response.tightened_variables(); + + const CpSolverResponse expected_domains = ParseTestProto(R"pb( + tightened_variables { domain: 0 domain: 5 } + tightened_variables { domain: 1 domain: 3 } + tightened_variables { domain: 0 domain: 1 } + )pb"); + EXPECT_THAT(expected_domains, + testing::EqualsProto(response_with_domains_only)); +} + +TEST(SolveCpModelTest, TightenedDomainsAfterPresolve) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 2 + coeffs: 3 + domain: 0 + domain: 7 + } + } + )pb"); + SatParameters params; + params.set_fill_tightened_domains_in_response(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_stop_after_presolve(true); + + const CpSolverResponse response = SolveWithParameters(model_proto, params); + CpSolverResponse response_with_domains_only; + *response_with_domains_only.mutable_tightened_variables() = + response.tightened_variables(); + + const CpSolverResponse expected_domains = ParseTestProto(R"pb( + tightened_variables { domain: 0 domain: 5 } + tightened_variables { domain: 1 domain: 3 } + tightened_variables { domain: 0 domain: 1 } + )pb"); + EXPECT_THAT(expected_domains, + testing::EqualsProto(response_with_domains_only)); +} + +TEST(SolveCpModelTest, TightenedDomains2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 100 } + constraints { + enforcement_literal: 0 + linear { vars: 1 coeffs: 1 domain: 90 domain: 100 } + } + constraints { + enforcement_literal: -1 + linear { vars: 1 coeffs: 1 domain: 0 domain: 10 } + } + )pb"); + SatParameters params; + params.set_fill_tightened_domains_in_response(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + + const CpSolverResponse response = SolveWithParameters(model_proto, params); + CpSolverResponse response_with_domains_only; + *response_with_domains_only.mutable_tightened_variables() = + response.tightened_variables(); + + const CpSolverResponse expected_domains = ParseTestProto(R"pb( + tightened_variables { domain: 0 domain: 1 } + tightened_variables { domain: 0 domain: 10 domain: 90 domain: 100 } + )pb"); + EXPECT_THAT(expected_domains, + testing::EqualsProto(response_with_domains_only)); +} + +TEST(SolveCpModelTest, TightenedDomainsIfInfeasible) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 1 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + linear { + vars: 0 + vars: 1 + vars: 2 + coeffs: 1 + coeffs: 2 + coeffs: 3 + domain: 80 + domain: 87 + } + } + )pb"); + SatParameters params; + params.set_fill_tightened_domains_in_response(true); + + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(CpSolverStatus::INFEASIBLE, response.status()); + EXPECT_TRUE(response.tightened_variables().empty()); +} + +TEST(SolveCpModelTest, PermutedObjectiveNoPresolve) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 7 domain: 10 } + variables { domain: 4 domain: 10 } + variables { domain: 5 domain: 10 } + objective { + vars: [ 2, 1, 0 ] + coeffs: [ 1, 2, 3 ] + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); +} + +TEST(SolveCpModelTest, TriviallyInfeasibleAssumptions) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 0 } + assumptions: [ 0, 1 ] + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + EXPECT_THAT(response.sufficient_assumptions_for_infeasibility(), + testing::ElementsAre(1)); +} + +TEST(SolveCpModelTest, TriviallyInfeasibleNegatedAssumptions) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 1 domain: 1 } + assumptions: [ 0, -2 ] + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + EXPECT_THAT(response.sufficient_assumptions_for_infeasibility(), + testing::ElementsAre(-2)); +} + +TEST(SolveCpModelTest, AssumptionsAndInfeasibility) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 3 } + constraints { + enforcement_literal: 0 + linear { + vars: [ 1 ] + coeffs: [ 1 ] + domain: [ 4, 4 ] + } + } + assumptions: [ 0 ] + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + EXPECT_THAT(response.sufficient_assumptions_for_infeasibility(), + testing::ElementsAre(0)); +} + +TEST(SolveCpModelTest, AssumptionsAndInfeasibility2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 0 + linear { + vars: [ 1 ] + coeffs: [ 1 ] + domain: [ 4, 4 ] + } + } + assumptions: [ 3, 0, 2 ] + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + EXPECT_THAT(response.sufficient_assumptions_for_infeasibility(), + testing::ElementsAre(0)); +} + +TEST(SolveCpModelTest, AssumptionsAndInfeasibility3) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "a" + domain: [ 0, 1 ] + } + variables { + name: "b" + domain: [ 0, 1 ] + } + variables { + name: "i1" + domain: [ 0, 1 ] + } + variables { + name: "i2" + domain: [ 0, 1 ] + } + variables { + name: "i3" + domain: [ 0, 1 ] + } + variables { + name: "i4" + domain: [ 0, 1 ] + } + constraints { + enforcement_literal: 2 + bool_or { literals: [ -1, 1 ] } + } + constraints { + enforcement_literal: 3 + bool_or { literals: [ 0, 1 ] } + } + constraints { + enforcement_literal: 4 + bool_or { literals: [ -2, -1 ] } + } + constraints { + enforcement_literal: 5 + bool_or { literals: [ -2 ] } + } + assumptions: [ 2, 3, 4, 5 ] + )pb"); + + SatParameters params; + params.set_log_search_progress(true); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + EXPECT_THAT(response.sufficient_assumptions_for_infeasibility(), + testing::ElementsAre(2, 3, 5)); +} + +TEST(SolveCpModelTest, RegressionTest) { + // This used to wrongly return UNSAT. + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: -2 + bool_or { literals: -1 } + } + )pb"); + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +// This used to crash because of how nodes with no arc were handled. +TEST(SolveCpModelTest, RouteConstraintRegressionTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + routes { + tails: [ 0, 1, 3 ] + heads: [ 1, 3, 0 ] + literals: [ 0, 2, 1 ] + } + } + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(SolveCpModelTest, ObjectiveInnerObjectiveBasic) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 2, 10 ] } + variables { domain: [ 2, 10 ] } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + scaling_factor: 10 + offset: 5 + } + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.objective_value(), 10 * (6 + 5)); + EXPECT_EQ(response.best_objective_bound(), 10 * (6 + 5)); + EXPECT_EQ(response.inner_objective_lower_bound(), 6); +} + +TEST(SolveCpModelTest, ObjectiveDomainWithCore) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 6, 100 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + scaling_factor: 1 + domain: [ 3, 4, 8, 10 ] + } + )pb"); + Model model; + SatParameters params; + params.set_optimize_with_core(true); + params.set_linearization_level(0); + params.set_log_search_progress(true); + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(8.0, response.objective_value()); +} + +TEST(SolveCpModelTest, ObjectiveDomainWithCore2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 6, 8 ] + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + scaling_factor: 1 + domain: [ 3, 4, 9, 10 ] + } + )pb"); + Model model; + SatParameters params; + params.set_optimize_with_core(true); + params.set_linearization_level(0); + params.set_log_search_progress(true); + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(SolveCpModelTest, EnumerateAllSolutionsReservoir) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: -1 } + level_changes: { offset: 3 } + level_changes: { offset: -3 } + min_level: 0 + max_level: 3 + } + } + )pb"); + + // We can have (var0 <= var1) <= (var2 <= var3) or the other way. + for (const bool encode : {true, false}) { + SatParameters params; + params.set_enumerate_all_solutions(true); + params.set_expand_reservoir_constraints(encode); + Model model; + model.Add(NewSatParameters(params)); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 89); + } +} + +TEST(SolveCpModelTest, EmptyModel) { + const CpModelProto model_proto = ParseTestProto(R"pb()pb"); + SatParameters params; + params.set_log_search_progress(true); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, EmptyOptimizationModel) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective { offset: 0 } + )pb"); + SatParameters params; + params.set_log_search_progress(true); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, EmptyOptimizationModelBuggy) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective { offset: 0 } + )pb"); + SatParameters params; + params.set_num_workers(1); + params.set_log_search_progress(true); + + // This causes the inner solver to abort before finding the empty solution! + params.set_max_number_of_conflicts(0); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::UNKNOWN); +} + +TEST(SolveCpModelTest, EmptyOptimizationModelMultiThread) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective { offset: 0 } + )pb"); + SatParameters params; + params.set_log_search_progress(true); + + // This causes the inner solver to abort before finding the empty solution! + // In non-interleave mode, everyone aborts and we finish with UNKNOWN. + params.set_max_number_of_conflicts(0); + params.set_num_workers(8); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(SolveCpModelTest, EmptyOptimizationModelBuggyInterleave) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective { offset: 0 } + )pb"); + SatParameters params; + params.set_log_search_progress(true); + + // This cause each chunk to abort right away with UNKNOWN. But because we are + // in chunked mode, we always reschedule full solver and we never finish if + // there is no time limit. + // + // TODO(user): Fix this behavior by not rescheduling in this case? + params.set_max_number_of_conflicts(0); + params.set_num_workers(8); + params.set_interleave_search(true); + params.set_use_feasibility_jump(false); + params.set_interleave_batch_size(10); + params.set_max_time_in_seconds(1); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + + // The feasibility jump solver does not care about max_number_of_conflicts(), + // so it finds the empty solution. But it is disabled in interleaved search. + EXPECT_THAT(response.status(), testing::Eq(CpSolverStatus::UNKNOWN)); +} + +TEST(PresolveCpModelTest, Issue4068) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 2 ] } + variables { domain: [ 1, 2 ] } + constraints { + no_overlap_2d { + x_intervals: [ 1, 2 ] + y_intervals: [ 3, 4 ] + } + } + constraints { + interval { + start {} + end { + vars: [ 1 ] + coeffs: [ 1 ] + } + size { + vars: [ 1 ] + coeffs: [ 1 ] + } + } + } + constraints { + interval { + start {} + end { offset: 1 } + size { offset: 1 } + } + } + constraints { + interval { + start { + vars: [ 2 ] + coeffs: [ 1 ] + } + end { + vars: [ 2 ] + coeffs: [ 1 ] + offset: 2 + } + size { offset: 2 } + } + } + constraints { + interval { + start { offset: 2 } + end { + vars: [ 0 ] + coeffs: [ 1 ] + offset: 2 + } + size { + vars: [ 0 ] + coeffs: [ 1 ] + } + } + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(cp_model, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 2); +} + +TEST(PresolveCpModelTest, EmptyExactlyOne) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + constraints { exactly_one {} } + )pb"); + Model model; + const CpSolverResponse response = SolveCpModel(cp_model, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, EmptyConstantProduct) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints { int_prod { target { offset: 2 } } })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, EmptyElement) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints { element {} })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, EmptyCumulativeNegativeCapacity) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints { cumulative { capacity { offset: -1 } } })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, BadAutomaton) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints { + automaton { + transition_tail: -2 + transition_head: -1 + transition_label: 1 + exprs { coeffs: 1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, ConstantEnforcementLiteral) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 0 } + constraints { + enforcement_literal: -1 + bool_xor {} + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, EmptySearchStrategyExpr) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints {} + search_strategy { + domain_reduction_strategy: SELECT_UPPER_HALF + exprs {} + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, ConstantSearchStrategyExpr) { + const CpModelProto cp_model = ParseTestProto(R"pb( + constraints {} + search_strategy { + domain_reduction_strategy: SELECT_UPPER_HALF + exprs { offset: 1 } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NegativeElement) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { element { target: -1 vars: -1 } } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, NegativeAutomaton) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + constraints { + automaton { + final_states: 3 + transition_tail: 0 + transition_head: 0 + transition_label: 0 + vars: [ -1 ] + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, ImpossibleInterval) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 1 domain: 10 } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end {} + size {} + } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, BadCumulative) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 1 domain: 10 } + constraints { cumulative { capacity { vars: 0 coeffs: -1 } } })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, NegatedStrategy) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 1 domain: 4617263143898057573 } + variables { domain: 1 domain: 1 } + search_strategy { variables: -1 } + assumptions: 1)pb"); + SatParameters params; + params.set_cp_model_presolve(false); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, CumulativeWithOverflow) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 6 } + variables { domain: 3 domain: 3 } + variables { domain: 0 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + constraints { + cumulative { + intervals: 0 + demands { offset: 4402971607593202523 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CumulativeWithOverflow2) { + const CpModelProto cp_model = + ParseTestProto( + R"pb( + variables { domain: 1 domain: 10 } + variables { domain: 1 domain: 10 } + constraints { + cumulative { capacity { vars: 0 coeffs: 0 offset: -1 } } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, NoOverlap2dCornerCase) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + constraints { + enforcement_literal: 2 + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { offset: 3 } + } + } + constraints { + enforcement_literal: 2 + interval { + start { vars: 3 coeffs: 1 } + end { vars: 2 coeffs: 1 } + size { offset: 2 } + } + } + constraints { no_overlap_2d { x_intervals: 0 y_intervals: 1 } } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, BadDivision) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + constraints { + int_div { + target { vars: 1 coeffs: 0 } + exprs { offset: 1 } + exprs { vars: 1 coeffs: 0 offset: 1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, CumulativeWithNegativeCapacity) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + variables { domain: 2 domain: 2 } + variables { domain: 2 domain: 6 } + constraints { + enforcement_literal: 1 + interval { + start { vars: 2 coeffs: 1 } + end { vars: 4 coeffs: 1 } + size { vars: 3 coeffs: 1 } + } + } + constraints { + cumulative { + capacity { offset: -1 } + intervals: 0 + demands { vars: 0 coeffs: -1 offset: 1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, TrivialTableNegated) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + constraints { + table { + values: [ 0, 1 ] + negated: true + exprs { offset: 1 } + exprs { vars: 0 coeffs: 1 } + } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, TrivialTable) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + constraints { + table { + values: [ 0, 1 ] + exprs { offset: 1 } + exprs { vars: 0 coeffs: 1 } + } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(NoOverlap2dCpModelTest, RequiresLns) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables: { + name: "x_0" + domain: [ 0, 80 ] + } + variables: { + name: "y_0" + domain: [ 0, 40 ] + } + variables: { + name: "x_1" + domain: [ 0, 80 ] + } + variables: { + name: "y_1" + domain: [ 0, 60 ] + } + variables: { + name: "x_2" + domain: [ 0, 90 ] + } + variables: { + name: "y_2" + domain: [ 0, 50 ] + } + variables: { domain: [ 1, 1 ] } + variables: { domain: [ 0, 200 ] } + variables: { domain: [ 0, 200 ] } + variables: { domain: [ 0, 200 ] } + variables: { domain: [ 0, 200 ] } + variables: { domain: [ 0, 200 ] } + variables: { domain: [ 0, 200 ] } + constraints: { + no_overlap_2d: { + x_intervals: [ 1, 3, 5 ] + y_intervals: [ 2, 4, 6 ] + } + } + constraints: { + name: "x_interval_0" + enforcement_literal: 6 + interval: { + start: { vars: 0 coeffs: 1 } + end: { vars: 0 coeffs: 1 offset: 20 } + size: { offset: 20 } + } + } + constraints: { + name: "y_interval_0" + enforcement_literal: 6 + interval: { + start: { vars: 1 coeffs: 1 } + end: { vars: 1 coeffs: 1 offset: 60 } + size: { offset: 60 } + } + } + constraints: { + name: "x_interval_1" + enforcement_literal: 6 + interval: { + start: { vars: 2 coeffs: 1 } + end: { vars: 2 coeffs: 1 offset: 20 } + size: { offset: 20 } + } + } + constraints: { + name: "y_interval_1" + enforcement_literal: 6 + interval: { + start: { vars: 3 coeffs: 1 } + end: { vars: 3 coeffs: 1 offset: 40 } + size: { offset: 40 } + } + } + constraints: { + name: "x_interval_2" + enforcement_literal: 6 + interval: { + start: { vars: 4 coeffs: 1 } + end: { vars: 4 coeffs: 1 offset: 10 } + size: { offset: 10 } + } + } + constraints: { + name: "y_interval_2" + enforcement_literal: 6 + interval: { + start: { vars: 5 coeffs: 1 } + end: { vars: 5 coeffs: 1 offset: 50 } + size: { offset: 50 } + } + } + constraints: { + linear: { + vars: [ 7, 0, 2 ] + coeffs: [ 1, -2, 2 ] + domain: [ 0, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 7, 0, 2 ] + coeffs: [ 1, 2, -2 ] + domain: [ 0, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 8, 0, 4 ] + coeffs: [ 1, -2, 2 ] + domain: [ 10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 8, 0, 4 ] + coeffs: [ 1, 2, -2 ] + domain: [ -10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 9, 2, 4 ] + coeffs: [ 1, -2, 2 ] + domain: [ 10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 9, 2, 4 ] + coeffs: [ 1, 2, -2 ] + domain: [ -10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 10, 1, 3 ] + coeffs: [ 1, -2, 2 ] + domain: [ 20, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 10, 1, 3 ] + coeffs: [ 1, 2, -2 ] + domain: [ -20, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 11, 1, 5 ] + coeffs: [ 1, -2, 2 ] + domain: [ 10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 11, 1, 5 ] + coeffs: [ 1, 2, -2 ] + domain: [ -10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 12, 3, 5 ] + coeffs: [ 1, -2, 2 ] + domain: [ -10, 9223372036854775807 ] + } + } + constraints: { + linear: { + vars: [ 12, 3, 5 ] + coeffs: [ 1, 2, -2 ] + domain: [ 10, 9223372036854775807 ] + } + } + objective: { + vars: [ 7, 8, 9, 10, 11, 12 ] + coeffs: [ 1, 1, 1, 1, 1, 1 ] + } + )pb"); + SatParameters params; + params.set_num_workers(16); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.objective_value(), 120); +} +TEST(PresolveCpModelTest, TableWithOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -6055696632510658248 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + table { vars: 1 vars: 0 values: 2 values: 0 negated: true } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, ProdOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + constraints { + int_prod { + target { offset: -3652538342751591977 } + exprs { offset: -3 } + exprs { vars: 0 coeffs: 0 offset: -3243792610144686519 } + exprs {} + exprs { offset: -1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, ModuloNotCanonical) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 10 } + variables { domain: -4299172082820395165 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + int_mod { + target { vars: 1 coeffs: 1 offset: -4 } + exprs { vars: 0 coeffs: 0 } + exprs { offset: 3 } + } + } + search_strategy { variables: 0 variables: 1 })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CumulativeWithOverflow3) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 4 } + variables { domain: 2 domain: 2 } + variables { domain: 0 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 2 domain: 33554434 } + variables { domain: 0 domain: 4 } + variables { domain: 3 domain: 3 } + variables { domain: 4 domain: 4 } + variables { domain: 6 domain: 18014398509481990 } + constraints { + interval { + start {} + end { vars: 2 coeffs: 1 } + size { vars: 4 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 5 coeffs: 1 } + size { vars: 4 coeffs: 1 } + } + } + constraints { + cumulative { + capacity { vars: 8 coeffs: 129 } + intervals: 0 + intervals: 1 + demands { vars: 2 coeffs: 1 offset: 1 } + demands { vars: 7 coeffs: 1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CumulativeWithOverflow4) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 4 } + variables { domain: 2 domain: 2 } + variables { domain: 0 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 2 domain: 33554434 } + variables { domain: 0 domain: 4 } + variables { domain: 3 domain: 3 } + variables { domain: 4 domain: 32772 } + variables { domain: 6 domain: 3848116990577877790 } + constraints { + interval { + start {} + end { vars: 2 coeffs: 1 } + size { vars: 4 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 5 coeffs: 1 } + size { vars: 4 coeffs: 1 } + } + } + constraints { + cumulative { + capacity { vars: 8 coeffs: 1 } + intervals: 0 + intervals: 1 + demands { vars: 6 coeffs: 1 offset: 1 } + demands { vars: 7 coeffs: 1 offset: 1 } + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, FoundByFuzzing) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1024 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 1, 4 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 1, 4 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 1, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 512 ] } + variables { domain: [ 0, 512 ] } + variables { domain: [ 0, 2048 ] } + variables { domain: [ 0, 512 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 2, 1 ] + coeffs: [ 1, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + linear { + vars: [ 3, 1 ] + coeffs: [ 1, -1 ] + domain: [ 1, 9223372036854775807 ] + } + } + constraints { + linear { + vars: [ 5, 4 ] + coeffs: [ 1, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + linear { + vars: [ 6, 4 ] + coeffs: [ 1, -1 ] + domain: [ 1, 9223372036854775807 ] + } + } + constraints {} + constraints { + linear { + vars: [ 9, 7 ] + coeffs: [ 1, -1 ] + domain: [ 1, 9223372036854775807 ] + } + } + constraints { + linear { + vars: [ 3, 1, 10 ] + coeffs: [ -1, 1, 1 ] + domain: [ -1, -1 ] + } + } + constraints { + interval { + start { + vars: [ 1 ] + coeffs: [ 1 ] + offset: 1 + } + end { + vars: [ 3 ] + coeffs: [ 1 ] + } + size { + vars: [ 10 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 1, 2, 11 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 2 ] + coeffs: [ 1 ] + } + end { + vars: [ 1 ] + coeffs: [ 1 ] + } + size { + vars: [ 11 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 6, 4, 12 ] + coeffs: [ -1, 1, 1 ] + domain: [ -1, -1 ] + } + } + constraints { + interval { + start { + vars: [ 4 ] + coeffs: [ 1 ] + offset: 1 + } + end { + vars: [ 6 ] + coeffs: [ 1 ] + offset: 1 + } + size { + vars: [ 12 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 4, 5, 13 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 5 ] + coeffs: [ 1 ] + } + end { + vars: [ 4 ] + coeffs: [ 1 ] + } + size { + vars: [ 13 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 9, 7, 14 ] + coeffs: [ -1, 1, 1 ] + domain: [ -1, -1 ] + } + } + constraints { + interval { + start { + vars: [ 7 ] + coeffs: [ 1 ] + offset: 1 + } + end { + vars: [ 9 ] + coeffs: [ 1 ] + } + size { + vars: [ 14 ] + coeffs: [ 1 ] + } + } + } + constraints { + linear { + vars: [ 7, 8, 15 ] + coeffs: [ -1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + interval { + start { + vars: [ 8 ] + coeffs: [ 1 ] + } + end { + vars: [ 7 ] + coeffs: [ 1 ] + } + size { + vars: [ 15 ] + coeffs: [ 1 ] + } + } + } + constraints {} + constraints { + linear { + vars: [ 6, 1 ] + coeffs: [ 1, -1 ] + domain: [ 1, 9223372036854775807 ] + } + } + constraints { + enforcement_literal: [ -17 ] + linear { + vars: [ 4, 1 ] + coeffs: [ 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: [ 16 ] + interval { + start { + vars: [ 1 ] + coeffs: [ 1 ] + } + end { + vars: [ 1 ] + coeffs: [ 1 ] + offset: 1 + } + size { offset: 1 } + } + } + constraints { + interval { + start { + vars: [ 4 ] + coeffs: [ 1 ] + } + end { + vars: [ 4 ] + coeffs: [ 1 ] + offset: 1 + } + size { offset: 1 } + } + } + constraints { + linear { + vars: [ 8, 4 ] + coeffs: [ 1, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + linear { + vars: [ 9, 4 ] + coeffs: [ 1, -1 ] + domain: [ 1, 9223372036854775807 ] + } + } + constraints { + enforcement_literal: [ -18 ] + linear { + vars: [ 7, 4 ] + coeffs: [ 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + enforcement_literal: [ 17 ] + interval { + start { + vars: [ 4 ] + coeffs: [ 1 ] + } + end { + vars: [ 4 ] + coeffs: [ 1 ] + offset: 1 + } + size { offset: 1 } + } + } + constraints { + linear { + vars: [ 7 ] + coeffs: [ 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 1 ] + coeffs: [ 1 ] + domain: [ 1, 1 ] + } + } + constraints { + linear { + vars: [ 18, 0 ] + coeffs: [ 2, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + cumulative { + capacity { + vars: [ 18 ] + coeffs: [ 1 ] + } + intervals: [ 7, 11, 15 ] + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + linear { + vars: [ 19, 0 ] + coeffs: [ 2, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + cumulative { + capacity { + vars: [ 19 ] + coeffs: [ 1 ] + } + intervals: [ 9, 13, 17 ] + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + cumulative { + capacity { + vars: [ 20 ] + coeffs: [ 1 ] + } + intervals: [ 21, 26 ] + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + linear { + vars: [ 21, 0 ] + coeffs: [ 2, -1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + cumulative { + capacity { + vars: [ 21 ] + coeffs: [ 1 ] + } + intervals: [ 22 ] + demands { offset: 1 } + } + } + constraints { + enforcement_literal: [ 22 ] + linear { + vars: [ 2, 3 ] + coeffs: [ 1, -1 ] + domain: [ -1, -1 ] + } + } + constraints { + enforcement_literal: [ 23 ] + linear { + vars: [ 5, 6 ] + coeffs: [ 1, -1 ] + domain: [ -1, -1 ] + } + } + constraints { + enforcement_literal: [ 24 ] + linear { + vars: [ 8, 9 ] + coeffs: [ 1, -1 ] + domain: [ -1, -1 ] + } + } + objective { + vars: [ 0 ] + coeffs: [ 1 ] + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, AllDifferentNotCanonical) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 1, 4294967306 ] } + variables { domain: [ 1, 6 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 1, 10000000 ] } + constraints { + all_diff { + exprs { vars: 1 coeffs: 0 } + exprs {} + exprs { vars: 1 coeffs: 2 } + exprs { vars: 0 coeffs: -1 } + } + })pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, HintGetBrokenByPresolve) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ -1 ] + table { vars: 1 } + } + constraints { + enforcement_literal: [ -1 ] + table { + values: [ 9223372036854775807, 1 ] + exprs { + vars: [ 0 ] + coeffs: [ 1 ] + offset: 3562345932446661909 + } + exprs { + vars: [ 1 ] + coeffs: [ 1 ] + } + } + } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 2 ] + } + solution_hint { + vars: [ 0, 1 ] + values: [ 1, 0 ] + })pb"); + SatParameters params; + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, DisjunctiveFromFuzzing) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 3, 140737488355331 ] } + variables { domain: [ 0, 6 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 2, 6 ] } + variables { domain: [ 2, 2 ] } + variables { domain: [ 2, 6 ] } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: -1 } + size { vars: 2 coeffs: -1 offset: 2199023255554 } + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size { vars: 6 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 0, 1, 1 ] } } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, PresolveChangesFeasibility) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ 1, 1 ] } + constraints { cumulative { capacity { vars: 1 coeffs: -1 } } } + solution_hint { vars: 1 values: 6277701416517650879 } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, PresolveChangesFeasibility2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: -1 + bool_and { literals: 0 } + } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ -1, 2, 3, -4 ] + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 1, 0, 0, 1 ] + } + assumptions: -1 + )pb"); + SatParameters params; + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, HintContradictsAssumptions) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { name: "x" domain: 0 domain: 1 } + variables { name: "y" domain: 0 domain: 1 } + constraints { bool_or { literals: 0 } } + constraints { bool_or { literals: -1 literals: -2 } } + solution_hint { vars: 1 values: 1 } + assumptions: 1 + )pb"); + CpSolverResponse response = SolveWithParameters(cp_model, SatParameters()); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, CumulativeOutOfBoundsRead) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { cumulative { capacity { vars: 0 coeffs: -1 } } } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, InverseCrash) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 0 } + variables { domain: 1 domain: 1 } + constraints { inverse { f_direct: 1 f_inverse: 1 } } + solution_hint { vars: 1 values: -1 })pb"); + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, CumulativeOutOfBoundsReadFixedDemand) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 4 } + variables { domain: 2 domain: 2 } + variables { domain: 0 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 2 domain: 2 } + variables { domain: 0 domain: 4 } + variables { domain: 3 domain: 3 } + variables { domain: 4 domain: 4 } + variables { domain: 6 domain: 6 } + constraints { + interval { + start {} + end { vars: 2 coeffs: 1 offset: 1 } + size { vars: 2 coeffs: 1 offset: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + end { vars: 5 coeffs: 1 } + size { vars: 4 coeffs: 1 } + } + } + constraints { + cumulative { + capacity { vars: 8 coeffs: 36028797018963969 } + intervals: 0 + intervals: 1 + demands { vars: 6 coeffs: 1 offset: -3 } + demands { vars: 8 coeffs: 1 } + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, PresolveChangesFeasibilityMultiplication) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: [ 2327064070896255483, 2327067369431138070 ] } + variables { domain: [ 257, 1099511627786 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 offset: -6 } + exprs { vars: 1 coeffs: 3 offset: 2327064070896254706 } + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, BadNoOverlap2d) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 2 } + variables { domain: 2 domain: 6 } + constraints { + enforcement_literal: 0 + bool_or {} + } + constraints { + enforcement_literal: 1 + interval { + start { vars: 0 coeffs: 1 } + end { vars: 3 coeffs: 1 offset: -4607772983994847345 } + size { vars: 2 coeffs: 1 } + } + } + constraints { no_overlap_2d { x_intervals: 1 y_intervals: 1 } } + )pb"); + SatParameters params; + + params.set_use_timetabling_in_no_overlap_2d(true); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NoOverlapLinearizationOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + variables { domain: -3659321530269907407 domain: 3496689482055784131 } + variables { domain: 2 domain: 7 } + constraints { + enforcement_literal: 0 + linear { + vars: [ 1, 2, 3 ] + coeffs: [ 1, 1, -1 ] + domain: [ 0, 0 ] + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, TableHintBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 576460752303423489 } + variables { domain: 0 domain: 268435457 } + variables { domain: 0 domain: 576460752303423489 } + constraints { + table { vars: 1 values: 17179869184 values: 1 negated: true } + } + solution_hint { + vars: 0 + vars: 1 + vars: 2 + vars: 3 + values: 1 + values: 0 + values: 0 + values: 1 + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_debug_crash_if_presolve_breaks_hint(true); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, LinearizationBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -4040617518406929344 domain: 10 } + variables { domain: 6 domain: 10 } + variables { domain: 0 domain: 10 } + variables { domain: 1 domain: 10000000 } + constraints { + all_diff { + exprs { + vars: 1 + coeffs: 18014398509481986 + offset: -1252623043085079047 + } + exprs { vars: 0 coeffs: -1 } + exprs { vars: 0 coeffs: -1 offset: -7 } + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_debug_crash_if_presolve_breaks_hint(true); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, DetectDuplicateColumnsOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -4611686018427387903 domain: 0 } + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + objective { + vars: 0 + vars: 1 + coeffs: 1 + coeffs: 1 + domain: 1 + domain: 7666432986417144262 + })pb"); + SatParameters params; + + params.set_log_search_progress(true); + params.set_cp_model_presolve(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, TableExpandPreservesSolutionHint) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 18014398509481985 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: -1 + table { + values: 9223372036854775807 + values: 1 + values: 0 + exprs { vars: 1 coeffs: 1 } + } + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 1, 0, 0, 1 ] + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, TableExpandPreservesSolutionHint2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: [ 0, 1, 18014398509481985, 18014398509481985 ] } + variables { domain: [ 0, 18014398509481985 ] } + variables { domain: [ 0, 18014398509481985 ] } + constraints { + enforcement_literal: -1 + table { + values: [ 9223372036854775807, 1, 0 ] + exprs { vars: 1 coeffs: 1 } + } + } + constraints { + enforcement_literal: 0 + table { + values: [ 9223372036854775807, 1, 0 ] + exprs { vars: 1 coeffs: 1 } + } + } + solution_hint { + vars: [ 0, 1, 2, 3 ] + values: [ 1, 0, 0, 1 ] + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, PresolveOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 4611686018427387903 } + variables { domain: 0 domain: 1 } + variables { + domain: 8 + domain: 12 + domain: 2986687222969572620 + domain: 2986687222969572620 + } + variables { domain: 2 domain: 2 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 1 coeffs: 1 offset: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, InverseBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -3744721377111001386 domain: 0 } + variables { domain: 0 domain: 3 } + variables { + domain: -1 + domain: 0 + domain: 4611686018427387903 + domain: 4611686018427387903 + } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + constraints { + inverse { + f_direct: 0 + f_direct: 2 + f_direct: 4 + f_direct: 6 + f_inverse: 1 + f_inverse: 3 + f_inverse: 5 + f_inverse: 7 + } + } + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(true); + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, FuzzerCrash3) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1024 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 1 domain: 4 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 1 domain: 4 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 1 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 4 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 512 } + variables { domain: 0 domain: 512 } + variables { domain: 0 domain: 2048 } + variables { domain: 0 domain: 512 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + linear { + vars: 2 + vars: 1 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + linear { + vars: 3 + vars: 1 + coeffs: 1 + coeffs: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints {} + constraints {} + constraints { + linear { + vars: 8 + vars: 7 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + linear { + vars: 9 + vars: 7 + coeffs: 1 + coeffs: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints {} + constraints { + interval { + start { vars: 1 coeffs: 1 offset: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 10 coeffs: 1 } + } + } + constraints { + linear { + vars: 1 + vars: 2 + vars: 11 + coeffs: -1 + coeffs: 1 + coeffs: 1 + domain: 0 + domain: 0 + } + } + constraints { + interval { + start { vars: 2 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { vars: 11 coeffs: 1 } + } + } + constraints { + linear { + vars: 6 + vars: 4 + vars: 12 + coeffs: -1 + coeffs: 1 + coeffs: 1 + domain: -1 + domain: -1 + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 offset: 1 } + end { vars: 6 coeffs: 1 } + size { vars: 12 coeffs: 1 } + } + } + constraints { + linear { + vars: 4 + vars: 5 + vars: 13 + coeffs: -1 + coeffs: 1 + coeffs: 1 + domain: 0 + domain: 0 + } + } + constraints { + interval { + start { vars: 5 coeffs: 1 } + end { vars: 4 coeffs: 1 } + size { vars: 13 coeffs: 1 } + } + } + constraints {} + constraints { + interval { + start { vars: 7 coeffs: 1 offset: 1 } + end { vars: 9 coeffs: 1 } + size { vars: 14 coeffs: 1 } + } + } + constraints { + linear { + vars: 7 + vars: 8 + vars: 15 + coeffs: -1 + coeffs: 1 + coeffs: 1 + domain: 0 + domain: 0 + } + } + constraints { + interval { + start { vars: 8 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size { vars: 15 coeffs: 1 } + } + } + constraints { + linear { + vars: 5 + vars: 1 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + linear { + vars: 6 + vars: 1 + coeffs: 1 + coeffs: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: -17 + linear { vars: 4 vars: 1 coeffs: 1 coeffs: -1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: 16 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + linear { + vars: 8 + vars: 4 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + linear { + vars: 9 + vars: 4 + coeffs: 1 + coeffs: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: -18 + linear { vars: 7 vars: 4 coeffs: 1 coeffs: -1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: 17 + interval { + start { vars: 4 coeffs: 1 } + end { vars: 4 coeffs: 1 offset: 4175356038966811637 } + size { offset: 1 } + } + } + constraints { linear { vars: 7 coeffs: 1 domain: 0 domain: 0 } } + constraints {} + constraints { + linear { + vars: 18 + vars: 0 + coeffs: 2 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + cumulative { + capacity { vars: 18 coeffs: 1 } + intervals: 7 + intervals: 11 + intervals: 15 + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + linear { + vars: 19 + vars: 0 + coeffs: 2 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + cumulative { + capacity { vars: 19 coeffs: 1 } + intervals: 9 + intervals: 13 + intervals: 17 + demands { offset: 1 } + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + linear { + vars: 20 + vars: 0 + coeffs: 1 + coeffs: -2 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + cumulative { + capacity { vars: 20 coeffs: 1 } + intervals: 21 + intervals: 26 + demands { offset: 1 } + demands { offset: 2 } + } + } + constraints { + linear { + vars: 21 + vars: 0 + coeffs: 2 + coeffs: -1 + domain: -9223372036854775808 + domain: 0 + } + } + constraints { + cumulative { + capacity { vars: 21 coeffs: 1 } + intervals: 22 + demands { offset: 1 } + } + } + constraints { + enforcement_literal: 22 + linear { vars: 2 vars: 3 coeffs: 1 coeffs: -1 domain: -1 domain: -1 } + } + constraints { + enforcement_literal: 23 + linear { vars: 5 vars: 6 coeffs: 1 coeffs: -1 domain: -1 domain: -1 } + } + constraints { + enforcement_literal: 24 + linear { vars: 8 vars: 9 coeffs: 1 coeffs: -1 domain: -1 domain: -1 } + } + objective { vars: 0 coeffs: 1 } + )pb"); + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, PotentialOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { + domain: 2 + domain: 2447234766972268842 + domain: 3535826881723299506 + domain: 4050838349900690071 + } + variables { + domain: -2798048574462918627 + domain: 2251799813685248 + domain: 357364299240879354 + domain: 1499017952464168848 + } + variables { domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + reservoir { + max_level: 2 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + active_literals: 2 + active_literals: 3 + level_changes { offset: -1 } + level_changes { offset: 1 } + } + } + )pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, LinearizationOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { + domain: 0 + domain: 6 + domain: 1495197974356070066 + domain: 1495197974356070067 + } + variables { domain: 0 domain: 50 } + constraints { + linear { + vars: 0 + vars: 2 + vars: 3 + coeffs: 2 + coeffs: 4 + coeffs: -1 + domain: -896813501530156794 + domain: 6343756879353628413 + domain: 9223372036854775807 + domain: 9223372036854775807 + } + } + )pb"); + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, LinearizationOverflow2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -1 domain: 0 } + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 2 domain: 8 } + constraints { + linear { + vars: 1 + vars: 2 + coeffs: 1 + coeffs: -1 + domain: 3 + domain: 1387315275818938588 + domain: 9223372036854775807 + domain: 9223372036854775807 + } + } + )pb"); + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, IntervalThatCanOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { + domain: -2700435943562583052 + domain: 122393683034791 + domain: 1153604922529384902 + domain: 1153604922529384903 + } + variables { domain: 2 domain: 2 } + variables { + domain: 5 + domain: 8198 + domain: 502515202656425278 + domain: 3082664781292582538 + } + variables { domain: 3 domain: 6 } + variables { domain: 3 domain: 3 } + variables { domain: 3 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + assumptions: -1 + )pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_debug_crash_if_presolve_breaks_hint(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::MODEL_INVALID); +} + +TEST(PresolveCpModelTest, ProdPotentialOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: -4611686018427387903 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 2 } + variables { domain: 0 domain: 100 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + assumptions: 0 + )pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, CumulativeCornerCase) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { cumulative { capacity { vars: 1 coeffs: -1 } } } + objective { vars: 1 offset: 1 coeffs: 1 } + solution_hint {} + assumptions: 1 + assumptions: -1)pb"); + SatParameters params; + + params.set_linearization_level(2); + + params.set_cp_model_presolve(false); + params.set_debug_crash_if_presolve_breaks_hint(true); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, ProdPotentialOverflow2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -2547768298502951547 domain: 0 } + variables { domain: 2 domain: 2 } + variables { domain: -4611686018427387903 domain: 1 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + } + } + solution_hint {})pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NoOverlap2dBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 6 } + variables { domain: -2 domain: 19 domain: 2185 domain: 2185 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 808 } + variables { domain: 3 domain: 6 } + variables { domain: 3 domain: 3 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 4 coeffs: 1 } + end { vars: 6 coeffs: 1 } + size { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap_2d { x_intervals: 0 y_intervals: 1 } } + assumptions: 0 + )pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NoOverlap2dBug2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 6 } + variables { domain: -58 domain: 11 domain: 3523 domain: 3524 } + variables { domain: 0 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 } + } + } + constraints { no_overlap_2d { x_intervals: 0 y_intervals: 0 } } + assumptions: 0)pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, LinMaxBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 4096 } + variables { domain: -3013 domain: 516 domain: 680 domain: 681 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + variables { domain: 1 domain: 4 } + constraints { + lin_max { + exprs { vars: 0 vars: 0 coeffs: 1 coeffs: -1 offset: -4032 } + } + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, InverseBug2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + variables { domain: 0 domain: 3 } + constraints { + lin_max { + target { vars: 0 coeffs: -1 } + exprs { vars: 1 coeffs: 1 offset: -1 } + } + } + constraints { + inverse { + f_direct: 0 + f_direct: 2 + f_direct: 4 + f_direct: 6 + f_inverse: 1 + f_inverse: 3 + f_inverse: 5 + f_inverse: 7 + } + })pb"); + + SatParameters params; + + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, ElementBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 5 } + variables { domain: -1298 domain: -1 domain: 4095 domain: 4095 } + constraints { + element { + linear_index { vars: 0 coeffs: 1 } + linear_target { vars: 1 coeffs: -3 } + exprs { offset: 1 } + exprs { offset: 2 } + exprs { offset: 3 } + exprs { offset: 4 } + exprs { offset: 5 } + exprs { offset: 6 } + } + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, AutomatonBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: -1 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + automaton { + final_states: 3 + transition_tail: [ 0, 0, 1, 2, 1, 2 ] + transition_head: [ 1, 2, 1, 2, 3, 3 ] + transition_label: [ 0, 1, 0, 1, 1, 0 ] + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 1 coeffs: -1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + } + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NoOverlapBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 domain: 2 domain: 2 } + constraints { + enforcement_literal: 0 + interval { + start {} + end { vars: 0 coeffs: 1 } + size { offset: 1 } + } + } + constraints { + enforcement_literal: 0 + interval { + start {} + end {} + size { vars: 6 coeffs: 1 } + } + } + constraints { + no_overlap { intervals: 1 intervals: 0 intervals: 1 intervals: 0 } + })pb"); + + SatParameters params; + + params.set_debug_crash_if_presolve_breaks_hint(true); + + // Enable all fancy heuristics. + params.set_linearization_level(2); + params.set_use_try_edge_reasoning_in_no_overlap_2d(true); + params.set_exploit_all_precedences(true); + params.set_use_hard_precedences_in_cumulative(true); + params.set_max_num_intervals_for_timetable_edge_finding(1000); + params.set_use_overload_checker_in_cumulative(true); + params.set_use_strong_propagation_in_disjunctive(true); + params.set_use_timetable_edge_finding_in_cumulative(true); + params.set_max_pairs_pairwise_reasoning_in_no_overlap_2d(50000); + params.set_use_timetabling_in_no_overlap_2d(true); + params.set_use_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_area_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_conservative_scale_overload_checker(true); + params.set_use_dual_scheduling_heuristics(true); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, NoOverlapBug2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: -1558 domain: 2 domain: 2476 domain: 3080 } + variables { domain: -3998 domain: 5 domain: 3175 domain: 3527 } + variables { domain: 3 domain: 38 domain: 2329 domain: 2922 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + variables { domain: 5 domain: 18 domain: 402 domain: 1493 } + variables { domain: 258 domain: 1534 domain: 2025 domain: 2026 } + variables { domain: -4096 domain: 1962 domain: 2394 domain: 3458 } + variables { domain: 3 domain: 3 } + variables { domain: 3 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 5 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size { vars: 6 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 8 coeffs: 1 } + end { vars: 10 coeffs: 1 } + size { vars: 9 coeffs: 1 } + } + } + constraints { + no_overlap { intervals: 1 intervals: 0 intervals: 1 intervals: 2 } + } + constraints { bool_xor { literals: 0 literals: 4 } } + floating_point_objective { vars: 1 coeffs: 1 offset: 2 } + )pb"); + + SatParameters params; + + params.set_debug_crash_if_presolve_breaks_hint(true); + + // Enable all fancy heuristics. + params.set_linearization_level(2); + params.set_use_try_edge_reasoning_in_no_overlap_2d(true); + params.set_exploit_all_precedences(true); + params.set_use_hard_precedences_in_cumulative(true); + params.set_max_num_intervals_for_timetable_edge_finding(1000); + params.set_use_overload_checker_in_cumulative(true); + params.set_use_strong_propagation_in_disjunctive(true); + params.set_use_timetable_edge_finding_in_cumulative(true); + params.set_max_pairs_pairwise_reasoning_in_no_overlap_2d(50000); + params.set_use_timetabling_in_no_overlap_2d(true); + params.set_use_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_area_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_conservative_scale_overload_checker(true); + params.set_use_dual_scheduling_heuristics(true); + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, LinMaxBug2) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 2 } + constraints { + lin_max { + target { vars: 0 coeffs: -1 } + exprs { vars: 0 coeffs: -1 offset: -1 } + } + })pb"); + + SatParameters params; + params.set_cp_model_presolve(false); + + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(PresolveCpModelTest, NoOverlap2dBug3) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 6 } + variables { domain: 3 domain: 3 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + variables { domain: -3 domain: 3 domain: 3033 domain: 3033 } + variables { domain: 2 domain: 6 } + variables { domain: 3 domain: 6 } + variables { domain: 0 domain: 0 } + variables { domain: 3 domain: 3 } + variables { domain: 3 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 5 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size { vars: 6 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 8 coeffs: 1 } + end { vars: 10 coeffs: 1 } + size { vars: 9 coeffs: 1 } + } + } + constraints { + no_overlap_2d { + x_intervals: 1 + x_intervals: 1 + y_intervals: 1 + y_intervals: 1 + } + } + constraints { bool_xor { literals: 0 literals: 4 } })pb"); + + SatParameters params; + params.set_max_time_in_seconds(4.0); + params.set_debug_crash_if_presolve_breaks_hint(true); + + // Enable all fancy heuristics. + params.set_linearization_level(2); + params.set_use_try_edge_reasoning_in_no_overlap_2d(true); + params.set_exploit_all_precedences(true); + params.set_use_hard_precedences_in_cumulative(true); + params.set_max_num_intervals_for_timetable_edge_finding(1000); + params.set_use_overload_checker_in_cumulative(true); + params.set_use_strong_propagation_in_disjunctive(true); + params.set_use_timetable_edge_finding_in_cumulative(true); + params.set_max_pairs_pairwise_reasoning_in_no_overlap_2d(50000); + params.set_use_timetabling_in_no_overlap_2d(true); + params.set_use_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_area_energetic_reasoning_in_no_overlap_2d(true); + params.set_use_conservative_scale_overload_checker(true); + params.set_use_dual_scheduling_heuristics(true); + params.set_maximum_regions_to_split_in_disconnected_no_overlap_2d(100); + + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, ObjectiveOverflow) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + exactly_one { + literals: 0 + literals: 1 + literals: 2 + literals: 3 + literals: 4 + } + } + objective { + vars: 1 + vars: 0 + coeffs: 4611686018427387903 + coeffs: -1771410674732262910 + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_log_search_progress(true); + params.set_linearization_level(2); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + params.set_cp_model_presolve(true); + response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(StopSolveTest, StopBeforeStart) { + CpModelProto model_proto; + AddInterval(0, 2, 4, &model_proto); + AddInterval(1, 2, 4, &model_proto); + ConstraintProto* ct = model_proto.add_constraints(); + ct->mutable_cumulative()->add_intervals(0); + ct->mutable_cumulative()->add_demands()->set_offset(3); + ct->mutable_cumulative()->add_intervals(1); + ct->mutable_cumulative()->add_demands()->set_offset(4); + ct->mutable_cumulative()->mutable_capacity()->set_offset(6); + + Model model; + StopSearch(&model); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::UNKNOWN); +} + +TEST(PresolveCpModelTest, NoOverlap2DCumulativeRelaxationBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 0 domain: 1 } + variables { domain: -1353 domain: 1143 domain: 3041 domain: 3042 } + variables { domain: -2 domain: 5 domain: 1207 domain: 1207 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 1 } + variables { domain: 2 domain: 6 } + variables { domain: 2 domain: 2 } + variables { domain: 1 domain: 4096 } + variables { domain: 1 domain: 4096 } + variables { domain: 2 domain: 6 } + variables { domain: 1 domain: 4096 } + variables { domain: 3 domain: 3 } + variables { domain: 3 domain: 6 } + constraints { + enforcement_literal: 0 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 3 coeffs: 1 } + size { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 5 coeffs: 1 } + end { vars: 7 coeffs: 1 } + size { vars: 6 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 8 coeffs: 1 } + end { vars: 10 coeffs: 1 } + size { vars: 9 coeffs: 1 } + } + } + constraints { no_overlap { intervals: 0 intervals: 1 intervals: 2 } } + constraints { no_overlap_2d {} } + constraints { no_overlap_2d { x_intervals: 0 y_intervals: 0 } } + objective { vars: 0 vars: 1 coeffs: -1 coeffs: -3237 } + search_strategy { + variables: 1 + domain_reduction_strategy: SELECT_UPPER_HALF + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + params.set_use_timetabling_in_no_overlap_2d(true); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(response.inner_objective_lower_bound(), -9846954); +} + +TEST(PresolveCpModelTest, ReservoirBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: 3 domain: 2805 domain: 2923 domain: 2923 } + variables { domain: 0 domain: 0 } + variables { domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + reservoir { + max_level: 2 + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + active_literals: 2 + active_literals: 3 + level_changes { offset: -1 } + level_changes { offset: 1 } + } + } + search_strategy { variables: 0 } + search_strategy { + variable_selection_strategy: CHOOSE_MIN_DOMAIN_SIZE + domain_reduction_strategy: SELECT_MAX_VALUE + exprs { offset: -1 } + } + solution_hint {})pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +TEST(PresolveCpModelTest, IntModBug) { + const CpModelProto cp_model = ParseTestProto( + R"pb( + variables { domain: -1264 domain: -1 domain: 4095 domain: 4096 } + constraints { + int_mod { + target { offset: 1 } + exprs { vars: 0 coeffs: 1 offset: -2607 } + exprs { offset: 2780 } + } + })pb"); + + SatParameters params; + + params.set_cp_model_presolve(false); + CpSolverResponse response = SolveWithParameters(cp_model, params); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_utils_test.cc b/ortools/sat/cp_model_utils_test.cc new file mode 100644 index 0000000000..2d5029272e --- /dev/null +++ b/ortools/sat/cp_model_utils_test.cc @@ -0,0 +1,361 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_utils.h" + +#include + +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/base/stl_util.h" +#include "ortools/port/proto_utils.h" +#include "ortools/sat/cp_model.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::UnorderedElementsAre; + +TEST(LinearExpressionGcdTest, Empty) { + LinearExpressionProto expr; + EXPECT_EQ(LinearExpressionGcd(expr), 0); +} + +TEST(LinearExpressionGcdTest, BasicCases1) { + LinearExpressionProto expr; + expr.set_offset(2); + expr.add_coeffs(4); + EXPECT_EQ(LinearExpressionGcd(expr), 2); +} + +TEST(LinearExpressionGcdTest, BasicCases2) { + LinearExpressionProto expr; + expr.set_offset(5); + expr.add_coeffs(10); + EXPECT_EQ(LinearExpressionGcd(expr), 5); +} + +TEST(LinearExpressionGcdTest, BasicCases3) { + LinearExpressionProto expr; + expr.set_offset(9); + expr.add_coeffs(12); + expr.add_coeffs(24); + EXPECT_EQ(LinearExpressionGcd(expr), 3); +} + +TEST(AddReferencesUsedByConstraintTest, Literals) { + ConstraintProto ct; + ct.add_enforcement_literal(1); + auto* arg = ct.mutable_bool_and(); + arg->add_literals(-10); + arg->add_literals(+10); + arg->add_literals(-20); + const IndexReferences references = GetReferencesUsedByConstraint(ct); + EXPECT_THAT(references.literals, UnorderedElementsAre(-10, +10, -20)); +} + +TEST(AddReferencesUsedByConstraintTest, IntegerConstraint) { + ConstraintProto ct; + auto* arg = ct.mutable_int_prod(); + arg->mutable_target()->add_vars(7); + arg->mutable_target()->add_coeffs(2); + auto* term = arg->add_exprs(); + term->add_vars(8); + term->add_coeffs(2); + term = arg->add_exprs(); + term->add_vars(8); + term->add_coeffs(3); + term = arg->add_exprs(); + term->add_vars(10); + term->add_coeffs(-2); + const IndexReferences references = GetReferencesUsedByConstraint(ct); + EXPECT_THAT(references.variables, UnorderedElementsAre(7, 8, 8, 10)); +} + +TEST(AddReferencesUsedByConstraintTest, LinearMaxConstraint) { + ConstraintProto ct; + auto* arg = ct.mutable_lin_max(); + arg->mutable_target()->add_vars(7); + arg->add_exprs(); + arg->add_exprs(); + arg->mutable_exprs(0)->add_vars(8); + arg->mutable_exprs(0)->add_vars(9); + arg->mutable_exprs(1)->add_vars(9); + arg->mutable_exprs(1)->add_vars(10); + const IndexReferences references = GetReferencesUsedByConstraint(ct); + EXPECT_THAT(references.variables, UnorderedElementsAre(7, 8, 9, 9, 10)); +} + +TEST(AddReferencesUsedByConstraintTest, LinearConstraint) { + ConstraintProto ct; + auto* arg = ct.mutable_linear(); + arg->add_vars(0); + arg->add_vars(1); + arg->add_vars(2); + const IndexReferences references = GetReferencesUsedByConstraint(ct); + EXPECT_THAT(references.variables, UnorderedElementsAre(0, 1, 2)); +} + +TEST(UsedIntervalTest, Intervals) { + ConstraintProto ct; + auto* arg = ct.mutable_no_overlap(); + arg->add_intervals(0); + arg->add_intervals(1); + arg->add_intervals(2); + EXPECT_THAT(UsedIntervals(ct), UnorderedElementsAre(0, 1, 2)); +} + +TEST(UsedVariablesTest, BasicTest) { + ConstraintProto ct; + ct.add_enforcement_literal(NegatedRef(7)); + auto* arg = ct.mutable_linear(); + arg->add_vars(0); + arg->add_vars(1); + arg->add_vars(NegatedRef(2)); + EXPECT_THAT(UsedVariables(ct), testing::ElementsAre(0, 1, 2, 7)); +} + +TEST(UsedVariablesTest, ConstraintWithMultipleEnforcement) { + ConstraintProto ct; + ct.add_enforcement_literal(NegatedRef(7)); + ct.add_enforcement_literal(NegatedRef(18)); + auto* arg = ct.mutable_linear(); + arg->add_vars(0); + arg->add_vars(1); + arg->add_vars(NegatedRef(2)); + EXPECT_THAT(UsedVariables(ct), testing::ElementsAre(0, 1, 2, 7, 18)); +} + +TEST(SetToNegatedLinearExpressionTest, BasicTest) { + LinearExpressionProto expr; + expr.set_offset(5); + expr.add_vars(1); + expr.add_coeffs(2); + expr.add_vars(-1); + expr.add_coeffs(-3); + + LinearExpressionProto negated_expr; + SetToNegatedLinearExpression(expr, &negated_expr); + EXPECT_THAT(negated_expr.vars(), testing::ElementsAre(-2, 0)); + EXPECT_THAT(negated_expr.coeffs(), testing::ElementsAre(2, -3)); + EXPECT_EQ(-5, negated_expr.offset()); + + LinearExpressionProto expr2; + expr2.set_offset(3); + expr2.add_vars(2); + expr2.add_coeffs(3); + expr2.add_vars(-2); + expr2.add_coeffs(-4); + + SetToNegatedLinearExpression(expr2, &negated_expr); + EXPECT_THAT(negated_expr.vars(), testing::ElementsAre(-3, 1)); + EXPECT_THAT(negated_expr.coeffs(), testing::ElementsAre(3, -4)); + EXPECT_EQ(-3, negated_expr.offset()); +} + +void Random(ConstraintProto ct) { + // The behavior of both functions differ on the enforcement_literal, so + // we clear it. TODO(user): make the behavior identical. + ct.clear_enforcement_literal(); + + absl::flat_hash_set expected; + { + const IndexReferences references = GetReferencesUsedByConstraint(ct); + expected.insert(references.variables.begin(), references.variables.end()); + expected.insert(references.literals.begin(), references.literals.end()); + } + + absl::flat_hash_set var_and_literals; + ApplyToAllVariableIndices( + [&var_and_literals](int* ref) { var_and_literals.insert(*ref); }, &ct); + ApplyToAllLiteralIndices( + [&var_and_literals](int* ref) { var_and_literals.insert(*ref); }, &ct); + EXPECT_EQ(var_and_literals, expected) << ProtobufDebugString(ct); + + std::vector intervals; + ApplyToAllIntervalIndices( + [&intervals](int* ref) { intervals.push_back(*ref); }, &ct); + gtl::STLSortAndRemoveDuplicates(&intervals); + EXPECT_EQ(intervals, UsedIntervals(ct)) << ProtobufDebugString(ct); +} + +TEST(ConstraintCaseNameTest, TestFewCases) { + EXPECT_EQ("kBoolOr", + ConstraintCaseName(ConstraintProto::ConstraintCase::kBoolOr)); + EXPECT_EQ("kLinear", + ConstraintCaseName(ConstraintProto::ConstraintCase::kLinear)); + EXPECT_EQ("kEmpty", ConstraintCaseName( + ConstraintProto::ConstraintCase::CONSTRAINT_NOT_SET)); +} + +TEST(ComputeInnerObjectiveTest, SimpleExample) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + objective { + vars: [ 0, 1 ] + coeffs: [ 2, 3 ] + offset: 3 + scaling_factor: 1.5 + } + )pb"); + const std::vector solution = {2, 3}; + EXPECT_EQ(ComputeInnerObjective(model_proto.objective(), solution), 13); + EXPECT_EQ(ScaleObjectiveValue(model_proto.objective(), 13), (13 + 3) * 1.5); + EXPECT_EQ(UnscaleObjectiveValue(model_proto.objective(), (13 + 3) * 1.5), 13); +} + +TEST(GetRefFromExpressionTest, BasicAPI) { + LinearExpressionProto expr; + EXPECT_FALSE(ExpressionContainsSingleRef(expr)); + expr.set_offset(1); + EXPECT_FALSE(ExpressionContainsSingleRef(expr)); + expr.set_offset(0); + expr.add_vars(2); + expr.add_coeffs(1); + EXPECT_TRUE(ExpressionContainsSingleRef(expr)); + EXPECT_EQ(2, GetSingleRefFromExpression(expr)); + expr.set_coeffs(0, -1); + EXPECT_TRUE(ExpressionContainsSingleRef(expr)); + EXPECT_EQ(-3, GetSingleRefFromExpression(expr)); + expr.set_coeffs(0, 2); + EXPECT_FALSE(ExpressionContainsSingleRef(expr)); + expr.set_coeffs(0, 1); + expr.add_vars(3); + expr.add_coeffs(-1); + EXPECT_FALSE(ExpressionContainsSingleRef(expr)); +} + +TEST(AddLinearExpressionToLinearConstraintTest, BasicApi) { + LinearConstraintProto linear; + linear.add_vars(1); + linear.add_coeffs(-1); + linear.add_domain(2); + linear.add_domain(4); + LinearExpressionProto expr; + expr.add_vars(2); + expr.add_coeffs(2); + expr.add_vars(3); + expr.add_coeffs(5); + expr.set_offset(1); + AddLinearExpressionToLinearConstraint(expr, -7, &linear); + const LinearConstraintProto expected_linear_constraint = ParseTestProto(R"pb( + vars: [ 1, 2, 3 ] + coeffs: [ -1, -14, -35 ] + domain: [ 9, 11 ] + )pb"); + EXPECT_THAT(linear, testing::EqualsProto(expected_linear_constraint)); +} + +TEST(AddWeightedLiteralToLinearConstraintTest, BasicApi) { + LinearConstraintProto linear; + linear.add_vars(1); + linear.add_coeffs(-1); + linear.add_domain(2); + linear.add_domain(4); + int64_t offset = 0; + AddWeightedLiteralToLinearConstraint(0, 4, &linear, &offset); + const LinearConstraintProto expected_linear_constraint_1 = ParseTestProto( + R"pb( + vars: [ 1, 0 ] + coeffs: [ -1, 4 ] + domain: [ 2, 4 ] + )pb"); + EXPECT_THAT(linear, testing::EqualsProto(expected_linear_constraint_1)); + EXPECT_EQ(offset, 0); + + AddWeightedLiteralToLinearConstraint(-3, 5, &linear, &offset); + const LinearConstraintProto expected_linear_constraint_2 = + ParseTestProto(R"pb( + vars: [ 1, 0, 2 ] + coeffs: [ -1, 4, -5 ] + domain: [ 2, 4 ] + )pb"); + EXPECT_THAT(linear, testing::EqualsProto(expected_linear_constraint_2)); + EXPECT_EQ(offset, 5); +} + +TEST(SafeAddLinearExpressionToLinearConstraintTest, BasicApi) { + LinearConstraintProto linear; + linear.add_vars(1); + linear.add_coeffs(-1); + linear.add_domain(2); + linear.add_domain(4); + LinearExpressionProto expr; + expr.add_vars(2); + expr.add_coeffs(2); + expr.add_vars(3); + expr.add_coeffs(5); + expr.set_offset(1); + EXPECT_TRUE(SafeAddLinearExpressionToLinearConstraint(expr, -7, &linear)); + LinearExpressionProto large_coeff; + large_coeff.add_vars(0); + large_coeff.add_coeffs(1e10); + EXPECT_FALSE( + SafeAddLinearExpressionToLinearConstraint(large_coeff, 1e10, &linear)); + LinearExpressionProto large_offset; + large_offset.set_offset(1e10); + EXPECT_FALSE( + SafeAddLinearExpressionToLinearConstraint(large_offset, 1e10, &linear)); +} + +// Fingerprint must be stable. So we can use golden values. +TEST(FingerprintTest, BasicApi) { + const LinearExpressionProto lin = ParseTestProto(R"pb( + vars: [ 1, 2, 3 ] + coeffs: [ -1, -14, -35 ] + offset: 11 + )pb"); + EXPECT_EQ(uint64_t{0x871AE5CE74BFBE37}, + FingerprintExpression(lin, kDefaultFingerprintSeed)); + EXPECT_EQ(uint64_t{0x3E7E7DEAEF2AB62C}, + FingerprintRepeatedField(lin.vars(), kDefaultFingerprintSeed)); + EXPECT_EQ(uint64_t{0x85715ADBDFD6F8AD}, + FingerprintSingleField(lin.offset(), kDefaultFingerprintSeed)); +} + +TEST(ConvertCpModelProtoToCnfTest, BasicExample) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_or { literals: [ 0, 1, 2 ] } } + constraints { bool_or { literals: [ -1, -2 ] } } + constraints { bool_or { literals: [ -1, -2 ] } } + constraints { + enforcement_literal: [ 0, 1 ] + bool_and { literals: [ -1, -2 ] } + } + )pb"); + const std::string expected = R"(p cnf 3 5 +1 2 3 0 +-1 -2 0 +-1 -2 0 +-1 -2 -1 0 +-1 -2 -2 0 +)"; + std::string cnf; + EXPECT_TRUE(ConvertCpModelProtoToCnf(model_proto, &cnf)); + EXPECT_EQ(expected, cnf); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/opb_reader.h b/ortools/sat/opb_reader.h index 8be4d750e1..c640d8587b 100644 --- a/ortools/sat/opb_reader.h +++ b/ortools/sat/opb_reader.h @@ -50,6 +50,10 @@ class OpbReader { // Returns the number of variables in the problem. int num_variables() const { return num_variables_; } + // Returns true if the model is supported. A model is not supported if it + // contains an integer that does not fit in int64_t. + bool model_is_supported() const { return model_is_supported_; } + // Loads the given opb filename into the given problem. // Returns true on success. bool LoadAndValidate(const std::string& filename, CpModelProto* model) { @@ -58,6 +62,7 @@ class OpbReader { num_variables_ = 0; int num_lines = 0; + model_is_supported_ = true; // Read constraints line by line (1 constraint per line). // We process into a temporary structure to support non linear constraints @@ -65,6 +70,10 @@ class OpbReader { for (const std::string& line : FileLines(filename)) { ++num_lines; ProcessNewLine(line); + if (!model_is_supported_) { + LOG(ERROR) << "Unsupported model: '" << filename << "'"; + return false; + } } if (num_lines == 0) { LOG(ERROR) << "File '" << filename << "' is empty or can't be read."; @@ -110,7 +119,7 @@ class OpbReader { int64_t soft_cost = std::numeric_limits::max(); }; - // Since the problem name is not stored in the cnf format, we infer it from + // Since the problem name is not stored in the opb format, we infer it from // the file name. static std::string ExtractProblemName(const std::string& filename) { const int found = filename.find_last_of('/'); @@ -132,21 +141,20 @@ class OpbReader { const std::string& word = words[i]; if (word.empty() || word[0] == ';') continue; if (word[0] == 'x') { - int index; - CHECK(absl::SimpleAtoi(word.substr(1), &index)); + const int index = ParseIndex(word.substr(1)); num_variables_ = std::max(num_variables_, index); objective_.back().literals.push_back( PbLiteralToCpModelLiteral(index)); } else if (word[0] == '~' && word[1] == 'x') { - int index; - CHECK(absl::SimpleAtoi(word.substr(2), &index)); + const int index = ParseIndex(word.substr(2)); num_variables_ = std::max(num_variables_, index); objective_.back().literals.push_back( NegatedRef(PbLiteralToCpModelLiteral(index))); } else { - int64_t coefficient; - CHECK(absl::SimpleAtoi(word, &coefficient)); - objective_.push_back({coefficient, {}}); + // Note that coefficient always appear before the variable/variables. + PbTerm term; + if (!ParseInt64Into(word, &term.coeff)) return; + objective_.emplace_back(std::move(term)); } } @@ -165,40 +173,35 @@ class OpbReader { const std::string& word = words[i]; CHECK(!word.empty()); if (word[0] == '[') { // Soft constraint. - int64_t soft_cost; - CHECK(absl::SimpleAtoi(word.substr(1, word.size() - 2), &soft_cost)); - constraint.soft_cost = soft_cost; + if (!ParseInt64Into(word.substr(1, word.size() - 2), + &constraint.soft_cost)) { + return; + } } else if (word == ">=") { CHECK_LT(i + 1, words.size()); - int64_t rhs; - CHECK(absl::SimpleAtoi(words[i + 1], &rhs)); constraint.type = GE_OPERATION; - constraint.rhs = rhs; + if (!ParseInt64Into(words[i + 1], &constraint.rhs)) return; break; } else if (word == "=") { CHECK_LT(i + 1, words.size()); - int64_t rhs; - CHECK(absl::SimpleAtoi(words[i + 1], &rhs)); constraint.type = EQ_OPERATION; - constraint.rhs = rhs; + if (!ParseInt64Into(words[i + 1], &constraint.rhs)) return; break; } else if (word[0] == 'x') { - int index; - CHECK(absl::SimpleAtoi(word.substr(1), &index)); + const int index = ParseIndex(word.substr(1)); num_variables_ = std::max(num_variables_, index); constraint.terms.back().literals.push_back( PbLiteralToCpModelLiteral(index)); } else if (word[0] == '~' && word[1] == 'x') { - int index; - CHECK(absl::SimpleAtoi(word.substr(2), &index)); + const int index = ParseIndex(word.substr(2)); num_variables_ = std::max(num_variables_, index); constraint.terms.back().literals.push_back( NegatedRef(PbLiteralToCpModelLiteral(index))); } else { // Note that coefficient always appear before the variable/variables. - int64_t coefficient; - CHECK(absl::SimpleAtoi(word, &coefficient)); - constraint.terms.push_back({coefficient, {}}); + PbTerm term; + if (!ParseInt64Into(word, &term.coeff)) return; + constraint.terms.emplace_back(std::move(term)); } } @@ -257,6 +260,20 @@ class OpbReader { return pb_literal > 0 ? pb_literal - 1 : -pb_literal; } + bool ParseInt64Into(const std::string& word, int64_t* value) { + if (!absl::SimpleAtoi(word, value)) { + model_is_supported_ = false; + return false; + } + return true; + } + + static int ParseIndex(const std::string& word) { + int index; + CHECK(absl::SimpleAtoi(word, &index)); + return index; + } + int GetVariable(const PbTerm& term, CpModelProto* model) { CHECK(!term.literals.empty()); if (term.literals.size() == 1) { @@ -346,6 +363,7 @@ class OpbReader { std::vector objective_; std::vector constraints_; absl::flat_hash_map, int> product_to_var_; + bool model_is_supported_ = true; }; } // namespace sat diff --git a/ortools/sat/boolean_problem_test.cc b/ortools/sat/opb_reader_test.cc similarity index 93% rename from ortools/sat/boolean_problem_test.cc rename to ortools/sat/opb_reader_test.cc index 9d68a9e999..b5479b086a 100644 --- a/ortools/sat/boolean_problem_test.cc +++ b/ortools/sat/opb_reader_test.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/sat/boolean_problem.h" +#include "ortools/sat/opb_reader.h" #include #include @@ -28,7 +28,6 @@ #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_checker.h" #include "ortools/sat/cp_model_symmetries.h" -#include "ortools/sat/opb_reader.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/util/logging.h" #include "ortools/util/time_limit.h" @@ -98,6 +97,20 @@ TEST(LoadAndValidateBooleanProblemTest, ZeroCoefficients) { EXPECT_FALSE(reader.LoadAndValidate(filename, &problem)); } +TEST(LoadAndValidateBooleanProblemTest, IntegerOverflow) { + std::string file = + "min: 1 x1 1 x2 ;\n" + "1 x1 2 x2 >= 1 ;\n" + "1 x1 123456789123456789123456789 x2 >= 1 ;\n"; + CpModelProto problem; + OpbReader reader; + const std::string filename = + file::JoinPath(::testing::TempDir(), "file2.opb"); + CHECK_OK(file::SetContents(filename, file, file::Defaults())); + EXPECT_FALSE(reader.LoadAndValidate(filename, &problem)); + EXPECT_FALSE(reader.model_is_supported()); +} + void FindSymmetries( absl::string_view file, std::vector>* generators) { diff --git a/ortools/sat/sat_solver.h b/ortools/sat/sat_solver.h index be776ba35e..170502c936 100644 --- a/ortools/sat/sat_solver.h +++ b/ortools/sat/sat_solver.h @@ -147,9 +147,6 @@ class SatSolver { // return true. bool ModelIsUnsat() const { return model_is_unsat_; } - // TODO(user): remove this function. - bool IsModelUnsat() const { return model_is_unsat_; } // DEPRECATED - // Adds and registers the given propagator with the sat solver. Note that // during propagation, they will be called in the order they were added. void AddPropagator(SatPropagator* propagator); @@ -338,7 +335,7 @@ class SatSolver { // Helper functions to get the correct status when one of the functions above // returns false. Status UnsatStatus() const { - return IsModelUnsat() ? INFEASIBLE : ASSUMPTIONS_UNSAT; + return ModelIsUnsat() ? INFEASIBLE : ASSUMPTIONS_UNSAT; } // Extract the current problem clauses. The Output type must support the two @@ -349,7 +346,7 @@ class SatSolver { // TODO(user): also copy the removable clauses? template void ExtractClauses(Output* out) { - CHECK(!IsModelUnsat()); + CHECK(!ModelIsUnsat()); Backtrack(0); if (!FinishPropagation()) return; diff --git a/ortools/sat/simplification.cc b/ortools/sat/simplification.cc index 45e75a002f..12497a1e91 100644 --- a/ortools/sat/simplification.cc +++ b/ortools/sat/simplification.cc @@ -820,7 +820,7 @@ void SatPresolver::RemoveAndRegisterForPostsolve(ClauseIndex ci, Literal x) { } Literal SatPresolver::FindLiteralWithShortestOccurrenceList( - const std::vector& clause) { + absl::Span clause) { DCHECK(!clause.empty()); Literal result = clause.front(); int best_size = literal_to_clause_sizes_[result]; diff --git a/ortools/sat/simplification.h b/ortools/sat/simplification.h index c9ea6ec906..ab7b43685b 100644 --- a/ortools/sat/simplification.h +++ b/ortools/sat/simplification.h @@ -257,7 +257,7 @@ class SatPresolver { // Finds the literal from the clause that occur the less in the clause // database. Literal FindLiteralWithShortestOccurrenceList( - const std::vector& clause); + absl::Span clause); LiteralIndex FindLiteralWithShortestOccurrenceListExcluding( const std::vector& clause, Literal to_exclude); diff --git a/ortools/sat/synchronization_test.cc b/ortools/sat/synchronization_test.cc new file mode 100644 index 0000000000..6276f45cfe --- /dev/null +++ b/ortools/sat/synchronization_test.cc @@ -0,0 +1,1133 @@ +// 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/synchronization.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/integer_base.h" +#include "ortools/sat/model.h" +#include "ortools/sat/util.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::ElementsAre; +using ::testing::EqualsProto; +using ::testing::IsEmpty; + +TEST(SharedSolutionRepository, Api) { + SharedSolutionRepository repository(3); + EXPECT_EQ(repository.NumSolutions(), 0); + repository.Add({8, {1, 2}}); + repository.Add({1, {2, 3}}); + EXPECT_EQ(repository.NumSolutions(), 0); + repository.Synchronize(); + EXPECT_EQ(repository.NumSolutions(), 2); + EXPECT_EQ(repository.GetSolution(0)->rank, 1); + EXPECT_EQ(repository.GetSolution(1)->rank, 8); + EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, + /*solution_index=*/0), + 3); + EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/0, + /*solution_index=*/1), + 1); + + repository.Add({3, {1, 2}}); + repository.Add({5, {1, 2}}); + repository.Add({1, {2, 3}}); + repository.Add({9, {1, 2}}); + EXPECT_EQ(repository.NumSolutions(), 2); + repository.Synchronize(); + EXPECT_EQ(repository.NumSolutions(), 3); + EXPECT_EQ(repository.GetSolution(0)->rank, 1); + EXPECT_EQ(repository.GetSolution(1)->rank, 3); + EXPECT_EQ(repository.GetSolution(2)->rank, 5); + EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, + /*solution_index=*/0), + 3); + EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, + /*solution_index=*/1), + 2); +} + +TEST(SharedSolutionRepository, DuplicateSolutionAreMerged) { + SharedSolutionRepository repository(3); + EXPECT_EQ(repository.NumSolutions(), 0); + repository.Add({1, {1, 50}}); + + // In practice we shouldn't have the same variable values and different + // objective, but the code don't care about this and just test the perfect + // equality. + repository.Add({5, {1, 50}}); + repository.Add({1, {1, 50}}); + EXPECT_EQ(repository.NumSolutions(), 0); + repository.Synchronize(); + EXPECT_EQ(repository.NumSolutions(), 2); + EXPECT_EQ(repository.GetSolution(0)->rank, 1); + EXPECT_EQ(repository.GetSolution(1)->rank, 5); +} + +TEST(SharedSolutionRepository, DuplicateSolutionAreMerged2) { + SharedSolutionRepository repository(3); + EXPECT_EQ(repository.NumSolutions(), 0); + + // All this should count as 1 solution. + repository.Add({3, {1, 50}}); + repository.Add({3, {1, 50}}); + repository.Add({3, {1, 50}}); + repository.Add({3, {1, 50}}); + repository.Add({3, {1, 50}}); + + // So we should be able to Enqueue worse ones. + repository.Add({5, {1, 50}}); + repository.Add({6, {1, 50}}); + + repository.Synchronize(); + EXPECT_EQ(repository.NumSolutions(), 3); + EXPECT_EQ(repository.GetSolution(0)->rank, 3); + EXPECT_EQ(repository.GetSolution(1)->rank, 5); + EXPECT_EQ(repository.GetSolution(2)->rank, 6); +} + +TEST(SharedSolutionRepository, GetRandomBiasedSolution) { + SharedSolutionRepository repository(5); + EXPECT_EQ(repository.NumSolutions(), 0); + + repository.Add({3, {1, 50}}); + repository.Add({3, {1, 51}}); + repository.Add({3, {1, 52}}); + + // Enqueue worse ones. + repository.Add({5, {1, 50}}); + repository.Add({6, {1, 50}}); + + repository.Synchronize(); + EXPECT_EQ(repository.NumSolutions(), 5); + + // We select one of the solution with best objective. + random_engine_t random(0); + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(repository.GetRandomBiasedSolution(random)->rank, 3); + } +} + +TEST(SharedLPSolutionRepository, NewLPSolution) { + SharedLPSolutionRepository lp_solutions(1); + + lp_solutions.NewLPSolution({1.0, 2.0, 1.0}); + + lp_solutions.Synchronize(); + EXPECT_EQ(lp_solutions.NumSolutions(), 1); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[0], 1.0); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[1], 2.0); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[2], 1.0); + + lp_solutions.NewLPSolution({2.0, 3.0, 0.0}); + + lp_solutions.Synchronize(); + EXPECT_EQ(lp_solutions.NumSolutions(), 1); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[0], 2.0); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[1], 3.0); + EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[2], 0.0); +} + +TEST(SharedIncompleteSolutionManager, AddAndRemoveSolutions) { + SharedIncompleteSolutionManager incomplete_solutions; + + EXPECT_FALSE(incomplete_solutions.HasSolution()); + incomplete_solutions.AddSolution({1.0, 2.0, 1.0}); + EXPECT_TRUE(incomplete_solutions.HasSolution()); + std::vector solution = incomplete_solutions.PopLast(); + EXPECT_THAT(solution, ::testing::ElementsAre(1.0, 2.0, 1.0)); + + EXPECT_FALSE(incomplete_solutions.HasSolution()); + incomplete_solutions.AddSolution({2.0, 3.0, 2.0}); + incomplete_solutions.AddSolution({3.0, 4.0, 3.0}); + EXPECT_TRUE(incomplete_solutions.HasSolution()); + solution = incomplete_solutions.PopLast(); + EXPECT_THAT(solution, ::testing::ElementsAre(3.0, 4.0, 3.0)); + + EXPECT_TRUE(incomplete_solutions.HasSolution()); + solution = incomplete_solutions.PopLast(); + EXPECT_THAT(solution, ::testing::ElementsAre(2.0, 3.0, 2.0)); + + EXPECT_FALSE(incomplete_solutions.HasSolution()); +} + +TEST(SharedBoundsManagerTest, Api) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + )pb"); + SharedBoundsManager manager(model); + manager.ReportPotentialNewBounds("auto", {2, 4, 6}, {1, 2, 3}, {11, 12, 13}); + manager.Synchronize(); + + std::vector vars; + std::vector lbs; + std::vector ubs; + + EXPECT_EQ(manager.RegisterNewId(), 0); + manager.GetChangedBounds(0, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(2, 4)); + EXPECT_THAT(lbs, ElementsAre(1, 2)); + EXPECT_THAT(ubs, ElementsAre(11, 12)); + + EXPECT_EQ(manager.RegisterNewId(), 1); + manager.GetChangedBounds(1, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(2, 4)); + EXPECT_THAT(lbs, ElementsAre(1, 2)); + EXPECT_THAT(ubs, ElementsAre(11, 12)); + + EXPECT_EQ(manager.RegisterNewId(), 2); + manager.GetChangedBounds(2, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(2, 4)); + EXPECT_THAT(lbs, ElementsAre(1, 2)); + EXPECT_THAT(ubs, ElementsAre(11, 12)); + + // Test nilpotence. + manager.GetChangedBounds(2, &vars, &lbs, &ubs); + EXPECT_THAT(vars, IsEmpty()); + EXPECT_THAT(lbs, IsEmpty()); + EXPECT_THAT(ubs, IsEmpty()); + + // Non improving bounds, and partially improving bounds. + manager.ReportPotentialNewBounds("fixed", {2, 4}, {0, 5}, {20, 20}); + manager.Synchronize(); + + manager.GetChangedBounds(0, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(4)); + EXPECT_THAT(lbs, ElementsAre(5)); + EXPECT_THAT(ubs, ElementsAre(12)); + + manager.GetChangedBounds(1, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(4)); + EXPECT_THAT(lbs, ElementsAre(5)); + EXPECT_THAT(ubs, ElementsAre(12)); + + manager.GetChangedBounds(2, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(4)); + EXPECT_THAT(lbs, ElementsAre(5)); + EXPECT_THAT(ubs, ElementsAre(12)); + + // Matching bounds. + manager.ReportPotentialNewBounds("fixed", {2}, {1}, {11}); + manager.Synchronize(); + + manager.GetChangedBounds(0, &vars, &lbs, &ubs); + EXPECT_THAT(vars, IsEmpty()); + EXPECT_THAT(lbs, IsEmpty()); + EXPECT_THAT(ubs, IsEmpty()); + + manager.GetChangedBounds(1, &vars, &lbs, &ubs); + EXPECT_THAT(vars, IsEmpty()); + EXPECT_THAT(lbs, IsEmpty()); + EXPECT_THAT(ubs, IsEmpty()); + + manager.GetChangedBounds(2, &vars, &lbs, &ubs); + EXPECT_THAT(vars, IsEmpty()); + EXPECT_THAT(lbs, IsEmpty()); + EXPECT_THAT(ubs, IsEmpty()); +} + +TEST(SharedBoundsManagerTest, WithSymmetry) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + variables { domain: [ 0, 20 ] } + symmetry { + permutations { + support: [ 0, 1, 2 ] + cycle_sizes: [ 3 ] + } + } + )pb"); + SharedBoundsManager manager(model); + manager.ReportPotentialNewBounds("auto", /*variables=*/{2, 1}, {4, 2}, + {7, 9}); + manager.Synchronize(); + + std::vector vars; + std::vector lbs; + std::vector ubs; + + EXPECT_EQ(manager.RegisterNewId(), 0); + manager.GetChangedBounds(0, &vars, &lbs, &ubs); + EXPECT_THAT(vars, ElementsAre(0, 1, 2)); + EXPECT_THAT(lbs, ElementsAre(4, 4, 4)); + EXPECT_THAT(ubs, ElementsAre(7, 7, 7)); +} + +TEST(SharedResponseManagerTest, InitialResponseSAT) { + Model model; + auto* shared_response = model.GetOrCreate(); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + const CpSolverResponse response = ParseTestProto(R"pb( + status: UNKNOWN, + )pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); +} + +TEST(SharedResponseManagerTest, InitialResponseMinimization) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: 1 })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + const CpSolverResponse response = ParseTestProto(R"pb( + status: UNKNOWN, + objective_value: inf, + best_objective_bound: -inf, + inner_objective_lower_bound: -9223372036854775808, + )pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); +} + +TEST(SharedResponseManagerTest, InitialResponseMaximization) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: -1 })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + EXPECT_EQ(0.0, shared_response->GapIntegral()); + const CpSolverResponse response = ParseTestProto(R"pb( + status: UNKNOWN, + objective_value: -inf, + best_objective_bound: inf, + inner_objective_lower_bound: -9223372036854775808, + )pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); +} + +TEST(SharedResponseManagerTest, UnknownResponseMinimization) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: 0 })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), + IntegerValue(7)); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(1.0 * std::log(1 + 11), shared_response->GapIntegral()); + + const std::string response = R"pb( + objective_value: 7, + best_objective_bound: -4, + inner_objective_lower_bound: -4 + )pb"; + auto result = shared_response->GetResponse(); + EXPECT_EQ(result.objective_value(), 7); + EXPECT_EQ(result.best_objective_bound(), -4); + EXPECT_EQ(result.inner_objective_lower_bound(), -4); + EXPECT_EQ(result.status(), UNKNOWN); +} + +TEST(SharedResponseManagerTest, UnknownResponseMaximization) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: -4 })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), + IntegerValue(7)); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(1.0 * log(1 + 4.0 * 11), shared_response->GapIntegral()); + + const std::string response = R"pb( + objective_value: -28, + best_objective_bound: 16, + inner_objective_lower_bound: -4 + )pb"; + auto result = shared_response->GetResponse(); + EXPECT_EQ(result.objective_value(), -28); + EXPECT_EQ(result.best_objective_bound(), 16); + EXPECT_EQ(result.inner_objective_lower_bound(), -4); + EXPECT_EQ(result.status(), UNKNOWN); +} + +TEST(SharedResponseManagerTest, GapIntegralTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: -4 offset: 100 })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + // At the beginning the primal integral is zero, and will start counting + // on the first update, ignoring any earlier time. This leave a change to + // use reasonable bound on the objective. + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + + // Unknown count as max possible difference. + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateGapIntegral(); + const double value1 = + 1.0 * + log(1 + 4 * (static_cast(std::numeric_limits::max()) - + static_cast(std::numeric_limits::min()))); + EXPECT_EQ(value1, shared_response->GapIntegral()); + + // No time, so still same. But the function height will change. + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), + IntegerValue(7)); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(value1, shared_response->GapIntegral()); + + // Add time, increase the integral. + shared_time_limit->AdvanceDeterministicTime(3.0); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(value1 + 3.0 * log(1 + 4.0 * 11), shared_response->GapIntegral()); +} + +TEST(SharedResponseManagerTest, GapIntegralOnEachChangeTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: -4 offset: 100 })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + // Starts with reasonable bound this time. + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), + IntegerValue(10)); + shared_response->SetUpdateGapIntegralOnEachChange(true); + shared_response->UpdateGapIntegral(); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + + // First Update. + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), + IntegerValue(7)); + shared_response->UpdateGapIntegral(); + double expected = 1.0 * log(1 + 4 * 10); + EXPECT_EQ(expected, shared_response->GapIntegral()); + + // Updating bound with no time, do not do anything. + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), + IntegerValue(3)); + EXPECT_EQ(expected, shared_response->GapIntegral()); + + // Add time, and change bound increase the integral. + shared_time_limit->AdvanceDeterministicTime(3.0); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), + IntegerValue(2)); + expected += 3.0 * log(1 + 4.0 * 3); + EXPECT_EQ(expected, shared_response->GapIntegral()); + + // Closing the search still increase it. And we deal correcly with bound + // crossing. + shared_time_limit->AdvanceDeterministicTime(1.0); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(10), + IntegerValue(0)); + expected += 1.0 * log(1 + 4.0 * 2); + EXPECT_EQ(expected, shared_response->GapIntegral()); +} + +TEST(SharedResponseManagerDeathTest, UpdateInnerObjectiveBoundsOnSAT) { + Model model; + auto* shared_response = model.GetOrCreate(); + EXPECT_DEATH(shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), + IntegerValue(0)), + ""); +} + +TEST(SharedResponseManagerTest, Infeasible) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { scaling_factor: 0 })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), + IntegerValue(7)); + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + + const CpSolverResponse response = ParseTestProto(R"pb( + status: INFEASIBLE, + )pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); +} + +TEST(SharedResponseManagerTest, Solution) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(2), + IntegerValue(20)); + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 2, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + + { + const CpSolverResponse response = ParseTestProto(R"pb( + status: FEASIBLE, + solution_info: "test" + solution: [ 1, 2, 1 ] + objective_value: 12 + best_objective_bound: 2 + inner_objective_lower_bound: 2)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_FALSE(shared_response->ProblemIsSolved()); + } + + // Optimal. + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + { + const CpSolverResponse response = ParseTestProto(R"pb( + status: OPTIMAL, + solution_info: "test" + solution: [ 1, 2, 1 ] + objective_value: 12 + best_objective_bound: 12 + inner_objective_lower_bound: 12)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); + } +} + +TEST(SharedResponseManagerTest, BestBound) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + offset: 0.5 + scaling_factor: 3.0 + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + double result = 0.0; + auto observer = [&result](double value) { result = value; }; + shared_response->AddBestBoundCallback(observer); + shared_response->UpdateInnerObjectiveBounds("", 2, 20); + EXPECT_EQ(result, 7.5); +} + +TEST(SharedResponseManagerTest, SolutionToFeasibilityProblem) { + Model model; + auto* shared_response = model.GetOrCreate(); + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 2, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + + { + const CpSolverResponse response = ParseTestProto(R"pb( + status: OPTIMAL, + solution_info: "test" + solution: [ 1, 2, 1 ])pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); + } +} + +TEST(SharedResponseManagerTest, OptimalReachedBecauseOfBounds) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + EXPECT_EQ(0.0, shared_response->GapIntegral()); + shared_time_limit->AdvanceDeterministicTime(10.0); + + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(12), + IntegerValue(12)); + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 2, 1 ] + )pb"); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + + shared_time_limit->AdvanceDeterministicTime(10.0); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + + const CpSolverResponse response = ParseTestProto(R"pb( + status: OPTIMAL, + solution_info: "test" + solution: [ 1, 2, 1 ] + objective_value: 12 + best_objective_bound: 12 + inner_objective_lower_bound: 12)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); +} + +TEST(SharedResponseManagerTest, ProblemCanBeClosedWithJustBoundUpdates) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_time_limit = model.GetOrCreate(); + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + EXPECT_EQ(0.0, shared_response->GapIntegral()); + + shared_time_limit->AdvanceDeterministicTime(10.0); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(13), + IntegerValue(12)); + + EXPECT_EQ(0.0, shared_response->GapIntegral()); + const CpSolverResponse response = ParseTestProto(R"pb( + status: INFEASIBLE)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); +} + +TEST(SharedResponseManagerTest, ProblemCanBeClosedWithJustBoundUpdates2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 2, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(13), + IntegerValue(15)); + + const CpSolverResponse response = ParseTestProto(R"pb( + status: OPTIMAL, + solution_info: "test" + solution: [ 1, 2, 1 ] + objective_value: 12 + best_objective_bound: 12, + inner_objective_lower_bound: 12)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + EXPECT_TRUE(shared_response->ProblemIsSolved()); +} + +#ifndef NDEBUG + +// TODO(user): Having a check sometime fail in multithread. Understand how +// the code can push an invalid lower bound (and still be valid). The likely +// behavior, is that at the end of the search, when the improving problem is +// infeasible, then we might have no guarantee that while incorporating new +// bounds, one thread pushes the lower bound too high ? +TEST(SharedResponseManagerDeathTest, InnerBoundMustBeValid) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + shared_response->UpdateInnerObjectiveBounds("", IntegerValue(20), + IntegerValue(25)); + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 2, 1 ] + )pb"); + + // The lower bound is not globally valid! + EXPECT_DEATH(shared_response->NewSolution(solution.solution(), + solution.solution_info()), + ""); +} + +TEST(SharedResponseManagerDeathTest, OptimalCannotBeImproved) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + shared_response->NewSolution({1, 2, 1}, "test"); + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + { + const CpSolverResponse response = ParseTestProto(R"pb( + status: OPTIMAL, + solution_info: "test" + solution: [ 1, 2, 1 ] + objective_value: 12 + best_objective_bound: 12 + inner_objective_lower_bound: 12)pb"); + EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); + } + + { + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 1, 1 ] + )pb"); + EXPECT_DEATH(shared_response->NewSolution(solution.solution(), + solution.solution_info()), + ""); + } +} + +TEST(SharedResponseManagerDeathTest, + BetterSolutionMustNotArriveAfterInfeasible) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + // First solution. + shared_response->NewSolution({1, 1, 1}, "test"); + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + shared_response->NotifyThatImprovingProblemIsInfeasible(""); + EXPECT_EQ(shared_response->GetResponse().status(), CpSolverStatus::OPTIMAL); + + { + // Better solution are not possible! otherwise there is a bug. + EXPECT_DEATH(shared_response->NewSolution({1, 0, 1}, "test2"), ""); + } +} + +#endif // NDEBUG + +TEST(SharedResponseManagerTest, Callback) { + const CpModelProto model_proto = ParseTestProto(R"pb( + objective: { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 3, 4 ] + })pb"); + + Model model; + auto* shared_response = model.GetOrCreate(); + shared_response->InitializeObjective(model_proto); + + int num_solutions = 0; + const int callback_id = shared_response->AddSolutionCallback( + [&num_solutions](const CpSolverResponse& /*response*/) { + ++num_solutions; + }); + + { + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 1, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + EXPECT_EQ(num_solutions, 1); + } + + // Not improving, so not called. + { + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 1, 1, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + EXPECT_EQ(num_solutions, 1); + } + + // Improving, so called. + { + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 0, 1, 1 ] + )pb"); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + EXPECT_EQ(num_solutions, 2); + } + + // Improving, but unregistered, so not called. + { + const CpSolverResponse solution = ParseTestProto(R"pb( + solution_info: "test" + solution: [ 0, 1, 0 ] + )pb"); + + shared_response->UnregisterCallback(callback_id); + shared_response->NewSolution(solution.solution(), solution.solution_info()); + EXPECT_EQ(num_solutions, 2); + } +} + +TEST(SharedClausesManagerTest, SyncApi) { + SharedClausesManager manager(/*always_synchronize=*/true, + /*share_frequency=*/absl::ZeroDuration()); + EXPECT_EQ(0, manager.RegisterNewId()); + EXPECT_EQ(1, manager.RegisterNewId()); + + manager.AddBinaryClause(/*id=*/0, 1, 2); + std::vector> new_clauses; + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_EQ(1, new_clauses.size()); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(1, 2))); + manager.AddBinaryClause(/*id=*/1, 2, 3); + manager.AddBinaryClause(/*id=*/1, 3, 2); + manager.AddBinaryClause(/*id=*/0, 0, 1); + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(2, 3), + std::make_pair(0, 1))); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(0, 1))); + + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); +} + +TEST(UniqueClauseStreamTest, AddIgnoresDuplicates) { + UniqueClauseStream stream; + + EXPECT_TRUE(stream.Add({1, 2, 3})); + EXPECT_FALSE(stream.Add({3, 2, 1})); + EXPECT_EQ(stream.NumBufferedLiterals(), 3); +} + +TEST(UniqueClauseStreamTest, Delete) { + UniqueClauseStream stream; + + EXPECT_TRUE(stream.Add({3, 2, 1})); + EXPECT_TRUE(stream.Delete({1, 2, 3})); + EXPECT_FALSE(stream.Delete({1, 2, 3, 4})); + EXPECT_THAT(stream.NextBatch(), ::testing::IsEmpty()); + EXPECT_TRUE(stream.Add({2, 3, 1})); + EXPECT_EQ(stream.NumBufferedLiterals(), 3); + stream.NextBatch(); + EXPECT_TRUE(stream.Delete({1, 2, 3})); +} + +TEST(UniqueClauseStreamTest, AddIgnoresInvalidSizeClauses) { + UniqueClauseStream stream; + std::vector long_clause; + long_clause.resize(UniqueClauseStream::kMaxClauseSize + 1); + for (int i = 0; i < long_clause.size(); ++i) long_clause[i] = i; + + EXPECT_FALSE(stream.Add({2, 1})); + EXPECT_FALSE(stream.Add(long_clause)); + EXPECT_EQ(stream.NumBufferedLiterals(), 0); +} + +TEST(UniqueClauseStreamTest, ExportsShortestClauses) { + UniqueClauseStream stream; + for (int i = 0; i < 1024 / 4; ++i) { + stream.Add({i + 1, i + 256, i + 512, -4}); + } + for (int i = 0; i < 1024 / 3; ++i) { + stream.Add({i + 1, i + 256, i + 512}); + } + for (int i = 0; i < 1024 / 5; ++i) { + stream.Add({i + 1, i + 256, i + 512, i + 1024, -2048}); + } + + // Batch 1 should be filled with size 3 clauses. + EXPECT_EQ(stream.NextBatch().size(), 1024 / 3); + // Batch 2 should be filled with size 4 clauses. + EXPECT_EQ(stream.NextBatch().size(), 1024 / 4); + // Batch 3 should be filled with size 5 clauses. + EXPECT_EQ(stream.NextBatch().size(), 1024 / 5); +} + +TEST(UniqueClauseStreamTest, RemoveWorstClauses) { + UniqueClauseStream stream; + // Fill the buffer + for (int i = 0; i < UniqueClauseStream::kMaxBufferedLiterals / 6; ++i) { + stream.Add({i + 1, i + 256, i + 512, -4, -3, -2}); + } + for (int i = 0; i < UniqueClauseStream::kMaxLiteralsPerBatch / 2 / 3; ++i) { + stream.Add({i + 1, i + 256, i + 512}); + } + + stream.RemoveWorstClauses(); + + EXPECT_GE(stream.NumBufferedLiterals(), + UniqueClauseStream::kMaxBufferedLiterals); + EXPECT_LT(stream.NumBufferedLiterals(), + UniqueClauseStream::kMaxBufferedLiterals + 6); + EXPECT_TRUE(stream.CanAccept(3, /*lbd=*/2)); + EXPECT_FALSE(stream.CanAccept(6, /*lbd=*/2)); + // Make sure none of the size 3 clauses were removed. + EXPECT_EQ(stream.NextBatch().size(), + UniqueClauseStream::kMaxLiteralsPerBatch / 2 / 3 + + UniqueClauseStream::kMaxBufferedLiterals / 2 / 6); +} + +TEST(UniqueClauseStreamTest, DropsClauses) { + UniqueClauseStream stream; + // We shouldn't drop any clause where Add returns true. + int literals_successfully_added = 0; + for (int i = 0; i < 256 / 4; ++i) { + literals_successfully_added += + 4 * stream.Add({i + 1, i + 256, i + 512, -4}); + } + for (int i = 0; i < 256 / 3; ++i) { + literals_successfully_added += 3 * stream.Add({i + 1, i + 256, i + 512}); + } + for (int i = 0; i < 1024 * 1024 / 5; ++i) { + literals_successfully_added += + 5 * stream.Add({i + 1, i + 256, i + 512, i + 1024, -2048}); + } + + EXPECT_FALSE(stream.CanAccept(3, /*lbd=*/3)); + EXPECT_TRUE(stream.CanAccept(3, /*lbd=*/2)); + EXPECT_TRUE(stream.CanAccept(4, /*lbd=*/2)); + EXPECT_FALSE(stream.CanAccept(5, /*lbd=*/2)); + EXPECT_EQ(stream.NumBufferedLiterals(), literals_successfully_added); + EXPECT_EQ( + literals_successfully_added, + 256 - 256 % 3 + // size 3 clauses + 256 - 256 % 4 + // size 4 clauses + UniqueClauseStream::kMaxBufferedLiterals - + UniqueClauseStream::kMaxBufferedLiterals % 5); // size 5 clauses + // Batch 1 should be filled with size 3 clauses. + EXPECT_EQ(stream.NextBatch().size(), 256 / 3 + 256 / 4 + 512 / 5); +} + +TEST(SharedClausesManagerTest, NonSyncApi) { + SharedClausesManager manager(/*always_synchronize=*/false, + /*share_frequency=*/absl::ZeroDuration()); + EXPECT_EQ(0, manager.RegisterNewId()); + EXPECT_EQ(1, manager.RegisterNewId()); + + manager.AddBinaryClause(/*id=*/0, 1, 2); + std::vector> new_clauses; + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + + manager.Synchronize(); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_EQ(1, new_clauses.size()); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(1, 2))); + + manager.AddBinaryClause(/*id=*/1, 2, 3); + manager.AddBinaryClause(/*id=*/1, 3, 2); + manager.AddBinaryClause(/*id=*/0, 0, 1); + + // Not synced. + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + + // After sync. + manager.Synchronize(); + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(2, 3), + std::make_pair(0, 1))); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(0, 1))); + + // Not synced. + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + + // After sync. + manager.Synchronize(); + manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); + manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); + EXPECT_TRUE(new_clauses.empty()); +} + +TEST(SharedClausesManagerTest, ShareGlueClauses) { + SharedClausesManager manager(/*always_synchronize=*/true, + absl::ZeroDuration()); + ASSERT_EQ(0, manager.RegisterNewId()); + ASSERT_EQ(1, manager.RegisterNewId()); + auto* stream0 = manager.GetClauseStream(0); + auto* stream1 = manager.GetClauseStream(1); + // Add a bunch of clauses that will be skipped in the first batch. + for (int i = 0; i < 1024 / 8; ++i) { + EXPECT_TRUE(stream0->Add({1, 2, 3, 4, 5, 6, 7, i + 8})); + } + EXPECT_EQ(stream0->NumBufferedLiterals(), 1024); + // Fill 1 batch of shorter clauses. + for (int i = 0; i < 1024 / 4; ++i) { + stream1->Add({1, 2, 3, i + 4}); + } + EXPECT_EQ(stream1->NumBufferedLiterals(), 1024); + + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(1024 / 4)); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(1024 / 4)); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(1024 / 8)); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(1024 / 8)); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); +} + +TEST(SharedClausesManagerTest, ShareFrequency) { + SharedClausesManager manager(/*always_synchronize=*/true, + absl::InfiniteDuration()); + ASSERT_EQ(0, manager.RegisterNewId()); + ASSERT_EQ(1, manager.RegisterNewId()); + auto* stream0 = manager.GetClauseStream(0); + auto* stream1 = manager.GetClauseStream(1); + for (int i = 0; i < 1024 / 5; ++i) { + stream0->Add({i + 1, i + 513, 2048, 2049, -10}); + stream1->Add({i + 1, i + 513, 2048, 2049, -10}); + } + + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); +} + +TEST(SharedClausesManagerTest, LbdThresholdIncrease) { + SharedClausesManager manager(/*always_synchronize=*/true, + absl::ZeroDuration()); + ASSERT_EQ(0, manager.RegisterNewId()); + ASSERT_EQ(1, manager.RegisterNewId()); + auto* stream0 = manager.GetClauseStream(0); + auto* stream1 = manager.GetClauseStream(1); + for (int i = 0; i < 1024 / 5; ++i) { + stream0->Add({i + 1, i + 513, 2048, 2049, -10}); + stream1->Add({i + 1, i + 513, 2048, 2049, -10}); + } + + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(1024 / 5)); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(1024 / 5)); + EXPECT_EQ(stream0->lbd_threshold(), 2); + EXPECT_EQ(stream1->lbd_threshold(), 2); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + EXPECT_EQ(stream0->lbd_threshold(), 3); + EXPECT_EQ(stream1->lbd_threshold(), 3); +} + +TEST(SharedClausesManagerTest, LbdThresholdDecrease) { + SharedClausesManager manager(/*always_synchronize=*/true, + absl::ZeroDuration()); + ASSERT_EQ(0, manager.RegisterNewId()); + ASSERT_EQ(1, manager.RegisterNewId()); + ASSERT_EQ(2, manager.RegisterNewId()); + auto* stream0 = manager.GetClauseStream(0); + auto* stream1 = manager.GetClauseStream(1); + + // Should increase LBD Threshold. + manager.Synchronize(); + // Then add 1/2 batch of clauses to each worker. + for (int i = 0; i < 1024 / 4 / 2; ++i) { + stream0->Add({i + 1, i + 512, 2048, 2049}); + stream1->Add({i + 1, i + 513, 2048, 2049}); + } + // Than add loads of longer clauses to just stream0. + for (int i = 1024 / 5 / 2; i < 3 * 1024 / 5; ++i) { + stream0->Add({i + 1, 2, 3, -10}); + } + + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); + EXPECT_EQ(stream0->lbd_threshold(), 3); + EXPECT_EQ(stream1->lbd_threshold(), 3); + manager.Synchronize(); + EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(1024 / 4)); + EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(1024 / 4)); + EXPECT_EQ(stream0->lbd_threshold(), 2); + EXPECT_EQ(stream1->lbd_threshold(), 3); +} +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/set_cover/set_cover_model.cc b/ortools/set_cover/set_cover_model.cc index cd81873fc6..b273b841eb 100644 --- a/ortools/set_cover/set_cover_model.cc +++ b/ortools/set_cover/set_cover_model.cc @@ -428,7 +428,7 @@ SparseRowView SetCoverModel::ComputeSparseRowViewSlice(SubsetIndex begin_subset, } std::vector SetCoverModel::CutSparseRowViewInSlices( - const std::vector& partition_points) { + absl::Span partition_points) { std::vector row_views; row_views.reserve(partition_points.size()); SubsetIndex begin_subset(0); @@ -441,7 +441,7 @@ std::vector SetCoverModel::CutSparseRowViewInSlices( } SparseRowView SetCoverModel::ReduceSparseRowViewSlices( - const std::vector& slices) { + absl::Span slices) { SparseRowView result_rows; // This is not a ReduceTree. This will be done later through parallelism. result_rows.reserve(num_elements_); diff --git a/ortools/set_cover/set_cover_model.h b/ortools/set_cover/set_cover_model.h index 54ae32946c..b792ad08a1 100644 --- a/ortools/set_cover/set_cover_model.h +++ b/ortools/set_cover/set_cover_model.h @@ -20,6 +20,7 @@ #include "absl/log/check.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/base/strong_int.h" #include "ortools/base/strong_vector.h" #include "ortools/set_cover/base_types.h" @@ -250,13 +251,13 @@ class SetCoverModel { // Returns a vector of row views, each corresponding to a partition of the // problem. The partitions are defined by the given partition points. std::vector CutSparseRowViewInSlices( - const std::vector& partition_points); + absl::Span partition_points); // Returns the union of the rows of the given row views. // The returned view is valid only as long as the given row views are valid. // The indices in the rows are sorted. SparseRowView ReduceSparseRowViewSlices( - const std::vector& row_slices); + absl::Span row_slices); // Returns true if the problem is feasible, i.e. if the subsets cover all // the elements. Could be const, but it updates the feasibility_duration_