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

2512 lines
98 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/sat/routing_cuts.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/log/log.h"
#include "absl/random/distributions.h"
#include "absl/random/random.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/parse_test_proto.h"
#include "ortools/base/strong_vector.h"
#include "ortools/graph/max_flow.h"
#include "ortools/sat/clause.h"
#include "ortools/sat/cp_model.h"
#include "ortools/sat/cuts.h"
#include "ortools/sat/implied_bounds.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_base.h"
#include "ortools/sat/linear_constraint.h"
#include "ortools/sat/linear_constraint_manager.h"
#include "ortools/sat/model.h"
#include "ortools/sat/precedences.h"
#include "ortools/sat/sat_base.h"
#include "ortools/util/sorted_interval_list.h"
#include "ortools/util/strong_integers.h"
namespace operations_research {
namespace sat {
namespace {
using ::google::protobuf::contrib::parse_proto::ParseTestProto;
using ::testing::ElementsAre;
using ::testing::EqualsProto;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
using HeadMinusTailBounds = RouteRelationsHelper::HeadMinusTailBounds;
std::pair<IntegerValue, IntegerValue> ExactDifferenceBounds(
const NodeExpression& x_expr, const NodeExpression& y_expr,
const sat::Relation& r,
const std::pair<IntegerValue, IntegerValue>& x_bounds,
const std::pair<IntegerValue, IntegerValue>& y_bounds) {
IntegerValue lb = kMaxIntegerValue;
IntegerValue ub = kMinIntegerValue;
for (IntegerValue x = x_bounds.first; x <= x_bounds.second; ++x) {
for (IntegerValue y = y_bounds.first; y <= y_bounds.second; ++y) {
const IntegerValue r_value = x * r.expr.coeffs[0] + y * r.expr.coeffs[1];
if (r_value < r.lhs || r_value > r.rhs) continue;
const IntegerValue difference = y_expr.ValueAt(y) - x_expr.ValueAt(x);
lb = std::min(lb, difference);
ub = std::max(ub, difference);
}
}
return {lb, ub};
}
void FilterFalseArcsAtLevelZero(RouteRelationsHelper& route_relations_helper,
std::vector<int>& tails,
std::vector<int>& heads,
std::vector<Literal>& literals, Model& model) {
const Trail& trail = *model.GetOrCreate<Trail>();
if (trail.CurrentDecisionLevel() != 0) return;
const VariablesAssignment& assignment = trail.Assignment();
int new_size = 0;
const int size = static_cast<int>(tails.size());
std::vector<int> removed_arcs;
for (int i = 0; i < size; ++i) {
if (assignment.LiteralIsFalse(literals[i])) {
removed_arcs.push_back(i);
continue;
}
tails[new_size] = tails[i];
heads[new_size] = heads[i];
literals[new_size] = literals[i];
++new_size;
}
tails.resize(new_size);
heads.resize(new_size);
literals.resize(new_size);
route_relations_helper.RemoveArcs(removed_arcs);
}
TEST(GetDifferenceBounds, RandomTest) {
absl::BitGen random;
const IntegerVariable x(0);
const IntegerVariable y(2);
const Literal lit(BooleanVariable(0), true);
const auto x_bounds = std::make_pair(IntegerValue(-5), IntegerValue(13));
const auto y_bounds = std::make_pair(IntegerValue(-7), IntegerValue(17));
for (int i = 0; i < 10000; ++i) {
const IntegerValue A(absl::Uniform<int64_t>(random, -10, 10));
const IntegerValue B(absl::Uniform<int64_t>(random, -10, 10));
const IntegerValue a(absl::Uniform<int64_t>(random, -10, 10));
const IntegerValue b(absl::Uniform<int64_t>(random, -10, 10));
if (a == 0 || b == 0 || A == 0 || B == 0) continue;
IntegerValue lhs = a * (a >= 0 ? x_bounds.first : x_bounds.second) +
b * (b >= 0 ? y_bounds.first : y_bounds.second);
IntegerValue rhs = a * (a >= 0 ? x_bounds.second : x_bounds.first) +
b * (b >= 0 ? y_bounds.second : y_bounds.first);
IntegerValue middle = (lhs + rhs) / 2;
lhs = IntegerValue(
absl::Uniform<int64_t>(random, lhs.value(), middle.value()));
rhs = IntegerValue(
absl::Uniform<int64_t>(random, middle.value(), rhs.value()));
const NodeExpression x_expr(x, A, absl::Uniform<int64_t>(random, -5, 5));
const NodeExpression y_expr(y, B, absl::Uniform<int64_t>(random, -5, 5));
const Relation r{
.enforcement = lit,
.expr = LinearExpression2(x, y, a, b),
.lhs = lhs,
.rhs = rhs,
};
const auto [lb, ub] =
GetDifferenceBounds(x_expr, y_expr, r, x_bounds, y_bounds);
const auto [exact_lb, exact_ub] =
ExactDifferenceBounds(x_expr, y_expr, r, x_bounds, y_bounds);
EXPECT_LE(lb, exact_lb);
ASSERT_GE(ub, exact_ub);
}
}
TEST(MinOutgoingFlowHelperTest, TwoNodesWithoutConstraints) {
Model model;
const std::vector<int> tails = {0, 1};
const std::vector<int> heads = {1, 0};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
MinOutgoingFlowHelper helper(2, tails, heads, literals, &model);
const int min_flow = helper.ComputeMinOutgoingFlow({0, 1});
const int tight_min_flow = helper.ComputeTightMinOutgoingFlow({0, 1});
EXPECT_EQ(min_flow, 1);
EXPECT_EQ(tight_min_flow, 1);
}
TEST(MinOutgoingFlowHelperTest, CapacityConstraints) {
Model model;
const int num_nodes = 5;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
absl::flat_hash_map<std::pair<int, int>, Literal> literal_by_arc;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
literal_by_arc[{tail, head}] = literals.back();
}
}
// For each node, the load of the vehicle leaving it.
std::vector<IntegerVariable> loads;
const int max_capacity = 30;
for (int n = 0; n < num_nodes; ++n) {
loads.push_back(model.Add(NewIntegerVariable(0, max_capacity)));
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (const auto& [arc, literal] : literal_by_arc) {
const auto& [tail, head] = arc;
// We consider that, at each node n other than the depot, n+10 items must be
// picked up by the vehicle leaving n.
const int head_load = head == 0 ? 0 : head + 10;
// loads[head] - loads[tail] >= head_load
repository->Add(literal, LinearExpression2(loads[head], loads[tail], 1, -1),
head_load, 1000);
}
repository->Build();
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeMinOutgoingFlow({1, 2, 3, 4});
const int tight_min_flow = helper.ComputeTightMinOutgoingFlow({1, 2, 3, 4});
// Due to the capacity constraints, a feasible path can have at most 3 nodes,
// hence at least two paths are needed. The lower bound of the vehicle load
// at each node n appearing at position i should be computed as follows:
//
// 1 2 3 4 (position)
// -------------
// node 1 | 0 11 23 -
// 2 | 0 12 23 -
// 3 | 0 13 24 -
// 4 | 0 14 24 -
EXPECT_EQ(min_flow, 2);
EXPECT_EQ(tight_min_flow, 2);
}
class DimensionBasedMinOutgoingFlowHelperTest
: public testing::TestWithParam<std::pair<bool, bool>> {};
TEST_P(DimensionBasedMinOutgoingFlowHelperTest, BasicCapacities) {
// If true, the load variables are the load of the vehicle leaving each node,
// otherwise they are the load of the vehicle arriving at each node.
const bool use_outgoing_load = GetParam().first;
// If true, vehicles pickup items at each node, otherwise they deliver items.
const bool pickup = GetParam().second;
Model model;
const int num_nodes = 5;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
absl::flat_hash_map<std::pair<int, int>, Literal> literal_by_arc;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
literal_by_arc[{tail, head}] = literals.back();
}
}
const std::vector<int> demands = {0, 11, 12, 13, 14};
std::vector<IntegerVariable> loads;
const int max_capacity = 49;
for (int n = 0; n < num_nodes; ++n) {
if (pickup == use_outgoing_load) {
loads.push_back(model.Add(NewIntegerVariable(demands[n], max_capacity)));
} else {
loads.push_back(
model.Add(NewIntegerVariable(0, max_capacity - demands[n])));
}
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (const auto& [arc, literal] : literal_by_arc) {
const auto& [tail, head] = arc;
if (tail == 0 || head == 0) continue;
if (pickup) {
// loads[head] - loads[tail] >= demand
repository->Add(literal,
LinearExpression2(loads[head], loads[tail], 1, -1),
demands[use_outgoing_load ? head : tail], 1000);
} else {
// loads[tail] - loads[head] >= demand
repository->Add(literal,
LinearExpression2(loads[tail], loads[head], 1, -1),
demands[use_outgoing_load ? head : tail], 1000);
}
}
repository->Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, *repository);
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
cumuls.flat_node_dim_expressions,
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
BestBoundHelper best_bound;
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3, 4}, *route_relations_helper, &best_bound);
// The total demand is 50, and the maximum capacity is 49.
EXPECT_EQ(min_flow, 2);
}
TEST_P(DimensionBasedMinOutgoingFlowHelperTest,
NodesWithoutIncomingOrOutgoingArc) {
// If true, the load variables are the load of the vehicle leaving each node,
// otherwise they are the load of the vehicle arriving at each node.
const bool use_outgoing_load = GetParam().first;
// If true, vehicles pickup items at each node, otherwise they deliver items.
const bool pickup = GetParam().second;
Model model;
// A graph with 4 nodes and 4 arcs, with 1 node without incoming arc and 1
// node without outgoing arc:
//
// --> 1 --> 2 -->
// ^ |
// | v
// --> 0 --> 3 -->
//
// We use "outside" arcs from/to node 4 otherwise the problem will be
// infeasible.
const int num_nodes = 5;
std::vector<int> tails = {0, 0, 1, 2, 4, 4, 2, 3};
std::vector<int> heads = {1, 3, 2, 3, 0, 1, 4, 4};
std::vector<Literal> literals(tails.size());
for (int i = 0; i < literals.size(); ++i) {
literals[i] = Literal(model.Add(NewBooleanVariable()), true);
}
const std::vector<int> demands = {11, 12, 13, 14};
std::vector<IntegerVariable> loads;
const int max_capacity = 49;
for (int n = 0; n < demands.size(); ++n) {
if (pickup == use_outgoing_load) {
loads.push_back(model.Add(NewIntegerVariable(demands[n], max_capacity)));
} else {
loads.push_back(
model.Add(NewIntegerVariable(0, max_capacity - demands[n])));
}
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (int i = 0; i < 4; ++i) {
const int head = heads[i];
const int tail = tails[i];
if (pickup) {
// loads[head] - loads[tail] >= demand
repository->Add(literals[i],
LinearExpression2::Difference(loads[head], loads[tail]),
demands[use_outgoing_load ? head : tail], 1000);
} else {
// loads[tail] - loads[head] >= demand
repository->Add(literals[i],
LinearExpression2::Difference(loads[tail], loads[head]),
demands[use_outgoing_load ? head : tail], 1000);
}
}
repository->Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, *repository);
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
cumuls.flat_node_dim_expressions,
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
BestBoundHelper best_bound;
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{0, 1, 2, 3}, *route_relations_helper, &best_bound);
// The total demand is 50, and the maximum capacity is 49.
EXPECT_EQ(min_flow, 2);
}
INSTANTIATE_TEST_SUITE_P(
AllCombinations, DimensionBasedMinOutgoingFlowHelperTest,
testing::Values(std::make_pair(true, true), std::make_pair(true, false),
std::make_pair(false, true), std::make_pair(false, false)));
TEST(MinOutgoingFlowHelperTest, NodeExpressionWithConstant) {
// A graph with 3 nodes: 0 <--> 1 -(demand1)-> 2 <-(demand2)-> 0
Model model;
const int num_nodes = 3;
std::vector<int> tails = {1, 0, 0, 1, 2};
std::vector<int> heads = {2, 1, 2, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 70;
const int demand2 = 40;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2, minus `offset`.
const int offset = 30;
const IntegerVariable offset_load2 =
model.Add(NewIntegerVariable(-offset, capacity - demand2 - offset));
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// Capacity constraint: (offset_load2 + offset) - load1 >= demand1
repository->Add(literals[0], LinearExpression2(offset_load2, load1, 1, -1),
demand1 - offset, 1000);
repository->Build();
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1),
AffineExpression(offset_load2, 1, offset)},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest, ConstantNodeExpression) {
// A graph with 3 nodes: 0 <--> 1 -(demand1)-> 2 <-(demand2)-> 0
Model model;
const int num_nodes = 3;
std::vector<int> tails = {1, 0, 0, 1, 2};
std::vector<int> heads = {2, 1, 2, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 70;
const int demand2 = 40;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2, a constant value.
const IntegerValue load2 = capacity - demand2;
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
auto* implied_bounds = model.GetOrCreate<ImpliedBounds>();
// Capacity constraint: load2 - load1 >= demand1
implied_bounds->Add(literals[0], IntegerLiteral::GreaterOrEqual(
NegationOf(load1), demand1 - load2));
implied_bounds->Add(literals[0],
IntegerLiteral::LowerOrEqual(NegationOf(load1), 1000));
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1),
AffineExpression(load2)},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest, NodeExpressionUsingArcLiteralAsVariable) {
// A graph with 4 nodes:
// 0 <--> 1 -(demand1)-> 2 -(demand2)-> 3 <-(demand3)-> 0
// 0 <-----------------> 2
Model model;
const int num_nodes = 4;
std::vector<int> tails = {1, 2, 0, 0, 0, 1, 2, 3};
std::vector<int> heads = {2, 3, 1, 2, 3, 0, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 80;
const int demand2 = 10;
const int demand3 = 20;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2 is a function of the arc 2->3
// literal l, namely (capacity - demand2) - demand3 * l.
const Literal arc_2_3_lit = literals[1];
const IntegerVariable arc_2_3_var =
CreateNewIntegerVariableFromLiteral(arc_2_3_lit, &model);
const AffineExpression load2 =
AffineExpression(arc_2_3_var, -demand3, capacity - demand2);
// The load of the vehicle arriving at node 3, a constant value.
const IntegerValue load3 = capacity - demand3;
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// Capacity constraint: load2 - load1 >= demand1. This expands to
// (capacity - demand2 - demand3 * l) - load1 >= demand1, i.e.,
// -demand3 * l - load1 >= demand1 + demand2 - capacity
repository->Add(literals[0],
LinearExpression2(arc_2_3_var, load1, -demand3, -1),
demand1 + demand2 - capacity, 1000);
// Capacity constraint: load3 - load2 >= demand2. This expands to
// (capacity - demand3) - (capacity - demand2 - demand3 * l) >= demand2 which,
// when l is 1, simplifies to 0 >= 0. Hence this constraint is ignored.
repository->Build();
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1),
load2, AffineExpression(load3)},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest,
NodeExpressionUsingNegationOfArcLiteralAsVariable) {
// A graph with 4 nodes:
// 0 <--> 1 -(demand1)-> 2 -(demand2)-> 3 <-(demand3)-> 0
// 0 <-----------------> 2
Model model;
const int num_nodes = 4;
std::vector<int> tails = {1, 2, 0, 0, 0, 1, 2, 3};
std::vector<int> heads = {2, 3, 1, 2, 3, 0, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 80;
const int demand2 = 10;
const int demand3 = 20;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2 is a function of the negated arc
// 2->3 literal l, namely (capacity - demand2) - demand3 * (1 - l).
const Literal arc_2_3_lit = literals[1];
const IntegerVariable arc_2_3_var =
CreateNewIntegerVariableFromLiteral(arc_2_3_lit.Negated(), &model);
const AffineExpression load2 =
AffineExpression(arc_2_3_var, demand3, capacity - demand2 - demand3);
// The load of the vehicle arriving at node 3, a constant value.
const IntegerValue load3 = capacity - demand3;
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// Capacity constraint: load2 - load1 >= demand1. This expands to
// (capacity - demand2 - demand3 + demand3 * l) - load1 >= demand1, i.e.,
// demand3 * l - load1 >= demand1 + demand2 + demand3 - capacity
repository->Add(literals[0],
LinearExpression2(arc_2_3_var, load1, demand3, -1),
demand1 + demand2 + demand3 - capacity, 1000);
// Capacity constraint: load3 - load2 >= demand2. This expands to
// (capacity - demand3) - (capacity - demand2 - demand3 + demand3 * l) >=
// demand2 which, when l is 0, simplifies to 0 >= 0. Hence this constraint is
// ignored.
repository->Build();
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1),
load2, AffineExpression(load3)},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest, ArcNodeExpressionsWithSharedVariable) {
// A graph with 4 nodes:
// 0 <--> 1 -(demand1)-> 2 -(demand2)-> 3 <-(demand3)-> 0
// 0 <-----------------> 2
Model model;
const int num_nodes = 4;
std::vector<int> tails = {1, 2, 0, 0, 0, 1, 2, 3};
std::vector<int> heads = {2, 3, 1, 2, 3, 0, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 50;
const int demand2 = 20;
const int demand3 = 40;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2 is a function of an x variable,
// namely (capacity - demand2 - demand3) - coeff * x.
const IntegerVariable x = model.Add(NewIntegerVariable(0, 1));
const int coeff = 30;
const AffineExpression load2 =
AffineExpression(x, -coeff, capacity - demand2 - demand3);
// The load of the vehicle arriving at node 3 is another function of x, namely
// (capacity - demand3) - coeff * x.
const AffineExpression load3 =
AffineExpression(x, -coeff, capacity - demand3);
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// Capacity constraint: load2 - load1 >= demand1. This expands to
// (capacity - demand2 - demand3) - coeff * x - load1 >= demand1, i.e.,
// -coeff * x - load1 >= demand1 + demand2 + demand3 - capacity.
repository->Add(literals[0], LinearExpression2(x, load1, -coeff, -1),
demand1 + demand2 + demand3 - capacity, 1000);
// Capacity constraint: load3 - load2 >= demand2. This expands to
// (capacity - demand3) - (capacity - demand2 - demand3) >= demand2, which
// simplifies to 0 >= 0. Hence this constraint is ignored.
repository->Build();
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(
num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1), load2, load3},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest, UnaryRelationForTwoNodeExpressions) {
// A graph with 4 nodes:
// 0 <--> 1 -(demand1)-> 2 -(demand2)-> 3 <-(demand3)-> 0
// 0 <-----------------> 2
Model model;
const int num_nodes = 4;
std::vector<int> tails = {1, 2, 0, 0, 0, 1, 2, 3};
std::vector<int> heads = {2, 3, 1, 2, 3, 0, 0, 0};
std::vector<Literal> literals;
for (int i = 0; i < tails.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
// The vehicle capacity and the demand at each node.
const int capacity = 100;
const int demand1 = 20;
const int demand2 = 10;
const int demand3 = 80;
// The load of the vehicle arriving at node 1.
const IntegerVariable load1 =
model.Add(NewIntegerVariable(0, capacity - demand1));
// The load of the vehicle arriving at node 2 is a function of an x variable,
// namely (capacity - demand2) - demand1 * x.
const Literal x_lit = Literal(model.Add(NewBooleanVariable()), true);
const IntegerVariable x = CreateNewIntegerVariableFromLiteral(x_lit, &model);
const AffineExpression load2 =
AffineExpression(x, -demand1, capacity - demand2);
// The load of the vehicle arriving at node 3.
const IntegerVariable load3 =
model.Add(NewIntegerVariable(0, capacity - demand3));
// Add an indirect implication x_lit => !arc_1_2_lit (= arc_1_2_lit => x = 0).
const Literal b = Literal(model.Add(NewBooleanVariable()), true);
model.GetOrCreate<BinaryImplicationGraph>()->AddImplication(x_lit, b);
model.GetOrCreate<BinaryImplicationGraph>()->AddImplication(
b, literals[0].Negated());
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// Capacity constraint: load2 - load1 >= demand1. This expands to
// (capacity - demand2) - demand1 * x - load1 >= demand1. Since this
// constraint is enforced by arc_1_2_lit we can assume it is true, which
// implies that x = 0. Hence the constraint simplifies to load1 <= capacity -
// demand2 - demand1.
repository->Add(literals[0],
LinearExpression2(load1, kNoIntegerVariable, 1, 0), 0,
capacity - demand1 - demand2);
// Capacity constraint: load3 - load2 >= demand2. This expands to
// load3 - ((capacity - demand2) - demand1 * x) >= demand2, i.e. to load3 +
// demand1 * x >= capacity
repository->Add(literals[1], LinearExpression2(load3, x, 1, demand1),
capacity, 1000);
repository->Build();
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
{AffineExpression(), AffineExpression(load1),
load2, AffineExpression(load3)},
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3}, *route_relations_helper, &best_bound);
// The total demand exceeds the capacity.
EXPECT_EQ(min_flow, 2);
}
TEST(MinOutgoingFlowHelperTest, NodeMustBeInnerNode) {
// when considering subset {1, 2, 3}, knowing that 2 cannot be reached
// from outside can lead to better bound. The non zero-demands are in () on
// the arcs.
//
// 0 --> 1 -(5)-> 2 -(5)-> 3 --> 0
// 1 <-(3)- 2 -----------> 0
// 1 -----(4)------> 3
// 0 --------------------> 3
for (const bool can_enter_at_2 : {true, false}) {
Model model;
const int num_nodes = 4;
std::vector<int> tails = {0, 1, 2, 3, 2, 2, 1, 0};
std::vector<int> heads = {1, 2, 3, 0, 0, 1, 3, 3};
std::vector<int> demands = {0, 5, 5, 0, 0, 4, 4, 0};
if (can_enter_at_2) {
tails.push_back(0);
heads.push_back(2);
demands.push_back(0);
}
std::vector<Literal> literals;
const int num_arcs = demands.size();
for (int i = 0; i < num_arcs; ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
std::vector<IntegerVariable> loads;
for (int i = 0; i < num_nodes; ++i) {
loads.push_back(model.Add(NewIntegerVariable(0, 8)));
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (int i = 0; i < num_arcs; ++i) {
// loads[head] - loads[tail] >= demand[arc]
repository->Add(
literals[i],
LinearExpression2(loads[heads[i]], loads[tails[i]], 1, -1),
demands[i], 1000);
}
repository->Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, *repository);
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
cumuls.flat_node_dim_expressions,
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3}, *route_relations_helper, &best_bound);
// If we cannot enter at 2, the only possibility is 0->1->2->0 and 0->3->0.
// Otherwise 0->2->1->3->0 is just under the capacity of 8.
EXPECT_EQ(min_flow, can_enter_at_2 ? 1 : 2);
}
}
TEST(MinOutgoingFlowHelperTest, BetterUseOfUpperBound) {
// The non-zero demands are in () on the arcs.
// when considering subset {1, 2}:
//
// 0 --> 1 -(8)-> 2 --> 0
// 0 --> 2 -(8)-> 1 --> 0
for (const bool bounds_forces_two_path : {true, false}) {
Model model;
std::vector<int> tails = {0, 1, 2, 0, 2, 1};
std::vector<int> heads = {1, 2, 0, 2, 1, 0};
std::vector<int> demands = {0, 8, 0, 0, 8, 0};
std::vector<Literal> literals;
const int num_arcs = demands.size();
for (int i = 0; i < num_arcs; ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
std::vector<IntegerVariable> loads;
loads.push_back(model.Add(NewIntegerVariable(0, 10))); // depot.
if (bounds_forces_two_path) {
// Here if we exploit the bound properly, we can see that both possible
// paths are invalid.
loads.push_back(model.Add(NewIntegerVariable(0, 10)));
loads.push_back(model.Add(NewIntegerVariable(5, 5)));
} else {
// Here the path 0->1->2->0 is fine.
loads.push_back(model.Add(NewIntegerVariable(0, 10)));
loads.push_back(model.Add(NewIntegerVariable(5, 10)));
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (int i = 0; i < num_arcs; ++i) {
// loads[head] - loads[tail] >= demand[arc]
repository->Add(
literals[i],
LinearExpression2::Difference(loads[heads[i]], loads[tails[i]]),
demands[i], 1000);
}
repository->Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
loads.size(), tails, heads, literals, *repository);
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(loads.size(), tails, heads, literals,
cumuls.flat_node_dim_expressions,
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
BestBoundHelper best_bound;
MinOutgoingFlowHelper helper(loads.size(), tails, heads, literals, &model);
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2}, *route_relations_helper, &best_bound);
EXPECT_EQ(min_flow, bounds_forces_two_path ? 2 : 1);
}
}
TEST(MinOutgoingFlowHelperTest, DimensionBasedMinOutgoingFlow_IsolatedNodes) {
Model model;
const int num_nodes = 5;
// A star graph with num_nodes-1 nodes and a depot.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
std::vector<IntegerVariable> variables;
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
// The depot variable.
variables.push_back(model.Add(NewIntegerVariable(0, 100)));
for (int head = 1; head < num_nodes; ++head) {
tails.push_back(0);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
variables.push_back(model.Add(NewIntegerVariable(0, 100)));
// Dummy relation, used only to associate a variable with each node.
repository->Add(literals.back(),
LinearExpression2(variables[head], variables[0], 1, -1), 1,
100);
}
repository->Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, *repository);
std::unique_ptr<RouteRelationsHelper> route_relations_helper =
RouteRelationsHelper::Create(num_nodes, tails, heads, literals,
cumuls.flat_node_dim_expressions,
*repository, &model);
ASSERT_NE(route_relations_helper, nullptr);
FilterFalseArcsAtLevelZero(*route_relations_helper, tails, heads, literals,
model);
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
BestBoundHelper best_bound;
const int min_flow = helper.ComputeDimensionBasedMinOutgoingFlow(
{1, 2, 3, 4}, *route_relations_helper, &best_bound);
EXPECT_EQ(min_flow, 4);
}
TEST(MinOutgoingFlowHelperTest, TimeWindows) {
Model model;
const int num_nodes = 5;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
absl::flat_hash_map<std::pair<int, int>, Literal> literal_by_arc;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
literal_by_arc[{tail, head}] = literals.back();
}
}
// For each node, the time at which a vehicle leaves this node.
std::vector<IntegerVariable> times;
times.push_back(model.Add(NewIntegerVariable(0, 100))); // Depot.
times.push_back(model.Add(NewIntegerVariable(8, 12))); // Node 1.
times.push_back(model.Add(NewIntegerVariable(18, 22))); // Node 2.
times.push_back(model.Add(NewIntegerVariable(18, 22))); // Node 3.
times.push_back(model.Add(NewIntegerVariable(28, 32))); // Node 4.
// Travel time constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (const auto& [arc, literal] : literal_by_arc) {
const auto& [tail, head] = arc;
const int travel_time = 10 - tail;
// times[head] - times[tail] >= travel_time
repository->Add(literal, LinearExpression2(times[head], times[tail], 1, -1),
travel_time, 1000);
}
repository->Build();
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
const int min_flow = helper.ComputeMinOutgoingFlow({1, 2, 3, 4});
const int tight_min_flow = helper.ComputeTightMinOutgoingFlow({1, 2, 3, 4});
// Due to the time window constraints, a feasible path can have at most 3
// nodes, hence at least two paths are needed. The earliest departure time
// from each node n appearing at position i should be computed as follows:
//
// 1 2 3 4 (position)
// -------------
// node 1 | 8 - - -
// 2 | 18 18 - -
// 3 | 18 18 - -
// 4 | 28 28 28 -
EXPECT_EQ(min_flow, 2);
EXPECT_EQ(tight_min_flow, 2);
}
std::vector<absl::flat_hash_map<int, AffineExpression>>
GetNodeExpressionsByDimension(const RouteRelationsHelper& helper) {
std::vector<absl::flat_hash_map<int, AffineExpression>> result(
helper.num_dimensions());
for (int n = 0; n < helper.num_nodes(); ++n) {
for (int d = 0; d < helper.num_dimensions(); ++d) {
if (!helper.GetNodeExpression(n, d).IsConstant()) {
result[d][n] = helper.GetNodeExpression(n, d);
}
}
}
return result;
}
int SolveTwoDimensionBinPacking(int capacity, absl::Span<const int> load1,
absl::Span<const int> load2) {
// Lets generate a quick cp-sat model.
const int num_items = load1.size();
const int num_bins = num_items;
CpModelBuilder cp_model;
// x[i][b] == item i in bin b.
std::vector<std::vector<BoolVar>> x(num_items);
for (int i = 0; i < num_items; ++i) {
x[i].resize(num_bins);
for (int b = 0; b < num_bins; ++b) {
x[i][b] = cp_model.NewBoolVar();
}
}
// Place all items.
for (int i = 0; i < num_items; ++i) {
cp_model.AddExactlyOne(x[i]);
}
// Respect capacity.
for (int b = 0; b < num_bins; ++b) {
LinearExpr sum1;
LinearExpr sum2;
for (int i = 0; i < num_items; ++i) {
sum1 += load1[i] * x[i][b];
sum2 += load2[i] * x[i][b];
}
cp_model.AddLessOrEqual(sum1, capacity);
cp_model.AddLessOrEqual(sum2, capacity);
}
// Bin used variables.
std::vector<BoolVar> is_used(num_bins);
for (int b = 0; b < num_bins; ++b) {
is_used[b] = cp_model.NewBoolVar();
for (int i = 0; i < num_items; ++i) {
cp_model.AddImplication(x[i][b], is_used[b]);
}
}
// Objective
cp_model.Minimize(LinearExpr::Sum(is_used));
// Solving part.
const CpSolverResponse response = Solve(cp_model.Build());
return static_cast<int>(response.objective_value());
}
// We test a simple example with 2 dimensions and 4 nodes with demands
// (7, 3) (3, 7) and (3, 1), (1, 3).
TEST(MinOutgoingFlowHelperTest, SubsetMightBeServedWithKRoutes) {
Model model;
const int num_nodes = 5;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
absl::flat_hash_map<std::pair<int, int>, Literal> literal_by_arc;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
literal_by_arc[{tail, head}] = literals.back();
}
}
// Load of each node on both dimensions.
std::vector<int> load1 = {0, 7, 3, 3, 1};
std::vector<int> load2 = {0, 3, 7, 1, 3};
// For each node, one cumul variable per dimension.
std::vector<IntegerVariable> cumul_vars_1;
std::vector<IntegerVariable> cumul_vars_2;
const int64_t capacity(10);
for (int n = 0; n < num_nodes; ++n) {
cumul_vars_1.push_back(model.Add(NewIntegerVariable(load1[n], capacity)));
cumul_vars_2.push_back(model.Add(NewIntegerVariable(load2[n], capacity)));
}
// Capacity constraints on two dimensions.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (const auto& [arc, literal] : literal_by_arc) {
const auto& [tail, head] = arc;
// vars[head] >= vars[tail] + load[head];
repository->Add(
literal,
LinearExpression2(cumul_vars_1[head], cumul_vars_1[tail], 1, -1),
load1[head], 10000);
repository->Add(
literal,
LinearExpression2(cumul_vars_2[head], cumul_vars_2[tail], 1, -1),
load2[head], 10000);
}
repository->Build();
const int optimal = SolveTwoDimensionBinPacking(capacity, load1, load2);
EXPECT_EQ(optimal, 2);
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
std::vector<int> subset = {1, 2, 3, 4};
for (int k = 0; k < subset.size(); ++k) {
if (k < optimal) {
EXPECT_FALSE(helper.SubsetMightBeServedWithKRoutes(k, subset));
} else {
EXPECT_TRUE(helper.SubsetMightBeServedWithKRoutes(k, subset));
}
}
}
// Same as above but with randomization.
// I kept the "golden" test just to make sure things looks reasonable.
TEST(MinOutgoingFlowHelperTest, SubsetMightBeServedWithKRoutesRandom) {
Model model;
absl::BitGen random;
const int num_nodes = 8;
const int capacity = 20;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
absl::flat_hash_map<std::pair<int, int>, Literal> literal_by_arc;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
literal_by_arc[{tail, head}] = literals.back();
}
}
// Load of each node on both dimensions.
std::vector<int> load1(num_nodes, 0);
std::vector<int> load2(num_nodes, 0);
for (int n = 0; n < num_nodes; ++n) {
load1[n] = absl::Uniform(random, 0, capacity);
load2[n] = absl::Uniform(random, 0, capacity);
}
// For each node, one cumul variable per dimension.
std::vector<IntegerVariable> cumul_vars_1;
std::vector<IntegerVariable> cumul_vars_2;
for (int n = 0; n < num_nodes; ++n) {
cumul_vars_1.push_back(model.Add(NewIntegerVariable(load1[n], capacity)));
cumul_vars_2.push_back(model.Add(NewIntegerVariable(load2[n], capacity)));
}
// Capacity constraints on two dimensions.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (const auto& [arc, literal] : literal_by_arc) {
const auto& [tail, head] = arc;
// vars[head] >= vars[tail] + load[head];
repository->Add(
literal,
LinearExpression2::Difference(cumul_vars_1[head], cumul_vars_1[tail]),
load1[head], 10000);
repository->Add(
literal,
LinearExpression2::Difference(cumul_vars_2[head], cumul_vars_2[tail]),
load2[head], 10000);
}
repository->Build();
// To check our indices mapping, lets remove a random nodes from the subset
std::vector<int> subset;
for (int i = 0; i < num_nodes; ++i) subset.push_back(i);
const int to_remove = absl::Uniform(random, 0, num_nodes);
std::swap(subset[to_remove], subset.back());
subset.pop_back();
// We set the load to zero to have the proper optimal.
load1[to_remove] = 0;
load2[to_remove] = 0;
const int optimal = SolveTwoDimensionBinPacking(capacity, load1, load2);
LOG(INFO) << "random problem optimal = " << optimal;
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
for (int k = 0; k < subset.size(); ++k) {
if (k < optimal) {
EXPECT_FALSE(helper.SubsetMightBeServedWithKRoutes(k, subset));
} else {
EXPECT_TRUE(helper.SubsetMightBeServedWithKRoutes(k, subset));
}
}
}
// We are looking for a solution with exactly k vehicles.
bool SolveTimeWindowProblemStartingFrom(
int start, int k, absl::Span<const int> tails, absl::Span<const int> heads,
absl::Span<const int> distance,
absl::Span<const std::pair<int, int>> time_windows) {
CpModelBuilder cp_model;
// Cumul variables.
std::vector<IntVar> cumul_vars;
for (int i = 0; i < time_windows.size(); ++i) {
cumul_vars.push_back(cp_model.NewIntVar(
Domain(time_windows[i].first, time_windows[i].second)));
}
LinearExpr sum_leaving_the_depot;
MultipleCircuitConstraint route = cp_model.AddMultipleCircuitConstraint();
for (int arc = 0; arc < tails.size(); ++arc) {
const BoolVar arc_is_present = cp_model.NewBoolVar();
route.AddArc(tails[arc], heads[arc], arc_is_present);
// Cumul constraint.
// We ignore arcs from/to the depot.
if (tails[arc] != 0 && heads[arc] != 0) {
const IntVar tail_var = cumul_vars[tails[arc]];
const IntVar head_var = cumul_vars[heads[arc]];
cp_model.AddGreaterOrEqual(head_var, tail_var + distance[arc])
.OnlyEnforceIf(arc_is_present);
}
// Collect arc leaving the depot.
if (tails[arc] == 0) {
sum_leaving_the_depot += arc_is_present;
if (heads[arc] == start) {
// Forces to start from there.
cp_model.FixVariable(arc_is_present, true);
}
}
}
// Exactly k vehicles.
cp_model.AddEquality(sum_leaving_the_depot, k);
const CpSolverResponse response = Solve(cp_model.Build());
return response.status() == CpSolverStatus::OPTIMAL;
}
// Generate a problem with time windows.
// Contrary to normal capacity, not all nodes can be used as a starting/ending
// point to serve a subset. This exercises this part of the code.
TEST(MinOutgoingFlowHelperTest,
SubsetMightBeServedWithKRoutesTimeWindowRandom) {
Model model;
absl::BitGen random;
const int num_nodes = 8;
const int horizon = 100;
// A complete graph with num_nodes.
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
std::vector<int> travel_times;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
// Since SubsetMightBeServedWithKRoutes() ignore arcs to outside the
// subset, we make sure these have no cost.
travel_times.push_back(
tail == 0 || head == 0 ? 0 : absl::Uniform(random, 2, 10));
}
}
std::vector<IntegerVariable> cumul_vars;
std::vector<std::pair<int, int>> time_windows;
time_windows.push_back({0, 0});
cumul_vars.push_back(model.Add(NewIntegerVariable(Domain(0)))); // Depot
for (int n = 1; n < num_nodes; ++n) {
const int start = absl::Uniform(random, 0, horizon);
const int length = absl::Uniform(random, 2, 10);
LOG(INFO) << n << " " << Domain(start, start + length);
time_windows.push_back({start, start + length});
cumul_vars.push_back(
model.Add(NewIntegerVariable(Domain(start, start + length))));
}
// Travel time constraint.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (int arc = 0; arc < tails.size(); ++arc) {
const int tail = tails[arc];
const int head = heads[arc];
const Literal literal = literals[arc];
// vars[head] >= vars[tail] + travel_times[arc];
repository->Add(
literal,
LinearExpression2::Difference(cumul_vars[head], cumul_vars[tail]),
travel_times[arc], 10000);
}
repository->Build();
// Serve everyone but the depot.
std::vector<int> subset;
for (int i = 1; i < num_nodes; ++i) subset.push_back(i);
// Subject under test.
MinOutgoingFlowHelper helper(num_nodes, tails, heads, literals, &model);
// Lets compute how many routes we need to serve this subset.
int optimal = -1;
for (int k = 0; k <= subset.size(); ++k) {
if (helper.SubsetMightBeServedWithKRoutes(k, subset)) {
optimal = k;
break;
}
}
LOG(INFO) << "k = " << optimal;
if (optimal > 0) {
for (const int i : subset) {
EXPECT_EQ(SolveTimeWindowProblemStartingFrom(i, optimal, tails, heads,
travel_times, time_windows),
helper.SubsetMightBeServedWithKRoutes(optimal, subset, nullptr,
nullptr,
/*special_node=*/i));
}
}
}
int SolveSpecialBinPackingWithCpSat(
absl::Span<const SpecialBinPackingHelper::ItemOrBin> objects) {
CpModelBuilder cp_model;
const int n = objects.size();
std::vector<BoolVar> item_is_bin(n);
for (int i = 0; i < n; ++i) {
if (objects[i].type == SpecialBinPackingHelper::MUST_BE_BIN) {
item_is_bin[i] = cp_model.TrueVar();
} else if (objects[i].type == SpecialBinPackingHelper::MUST_BE_ITEM) {
item_is_bin[i] = cp_model.FalseVar();
} else {
item_is_bin[i] = cp_model.NewBoolVar();
}
}
// x[i][b] == item i in bin b.
std::vector<std::vector<BoolVar>> x(n);
for (int i = 0; i < n; ++i) {
x[i].resize(n);
for (int b = 0; b < n; ++b) {
if (i == b) {
// We always place a bin into itself in this model.
x[i][b] = item_is_bin[b];
} else {
x[i][b] = cp_model.NewBoolVar();
cp_model.AddImplication(x[i][b], item_is_bin[b]);
}
}
}
// Place all items.
for (int i = 0; i < n; ++i) {
cp_model.AddExactlyOne(x[i]);
}
// Respect capacity.
for (int b = 0; b < n; ++b) {
LinearExpr demands;
for (int i = 0; i < n; ++i) {
if (i == b) continue;
demands += objects[i].demand.value() * x[i][b];
}
// We shift by the bin demand since we always have x[b][b] at true if the
// bin is used as such.
cp_model.AddLessOrEqual(demands, objects[b].capacity.value())
.OnlyEnforceIf(item_is_bin[b]);
}
// Objective
cp_model.Minimize(LinearExpr::Sum(item_is_bin));
// Solving part.
SatParameters params;
params.set_log_search_progress(false);
const CpSolverResponse response =
SolveWithParameters(cp_model.Build(), params);
// This is the convention used in our bound computation function.
if (response.status() == INFEASIBLE) return n + 1;
return static_cast<int>(response.objective_value());
}
// Generate a random problem and make sure our bound is always valid.
// These problems are a bit easy, but with --runs_per_test 1000 there are a few
// instances where our lower bound is strictly worse than the true optimal.
TEST(SpecialBinPackingHelperTest, ComputeMinNumberOfBins) {
Model model;
absl::BitGen random;
const int num_objects = 20;
std::vector<SpecialBinPackingHelper::ItemOrBin> objects;
for (int i = 0; i < num_objects; ++i) {
SpecialBinPackingHelper::ItemOrBin o;
o.capacity = absl::Uniform(random, 0, 100);
o.demand = absl::Uniform(random, 0, 50);
const int type = absl::Uniform(random, 0, 3);
if (type == 0) o.type = SpecialBinPackingHelper::MUST_BE_ITEM;
if (type == 1) o.type = SpecialBinPackingHelper::ITEM_OR_BIN;
if (type == 2) o.type = SpecialBinPackingHelper::MUST_BE_BIN;
objects.push_back(o);
}
std::string info;
SpecialBinPackingHelper helper;
std::vector<int> objects_that_cannot_be_bin_and_reach_minimum;
const int obj_lb = helper.ComputeMinNumberOfBins(
absl::MakeSpan(objects), objects_that_cannot_be_bin_and_reach_minimum,
info);
const int optimal = SolveSpecialBinPackingWithCpSat(objects);
EXPECT_LE(obj_lb, optimal);
if (obj_lb != optimal) {
LOG(INFO) << "bound " << obj_lb << " optimal " << optimal;
}
// For each item in the complement, test that the bound increase if we
// force it to be a bin.
if (objects_that_cannot_be_bin_and_reach_minimum.empty()) return;
std::vector<bool> cannot_be_bin(num_objects, false);
for (const int i : objects_that_cannot_be_bin_and_reach_minimum) {
cannot_be_bin[i] = true;
}
for (int i = 0; i < num_objects; ++i) {
if (cannot_be_bin[i]) {
if (objects[i].type == SpecialBinPackingHelper::MUST_BE_ITEM) continue;
EXPECT_EQ(objects[i].type, SpecialBinPackingHelper::ITEM_OR_BIN);
objects[i].type = SpecialBinPackingHelper::MUST_BE_BIN;
std::vector<int> unused;
const int new_lb =
helper.ComputeMinNumberOfBins(absl::MakeSpan(objects), unused, info);
EXPECT_GT(new_lb, obj_lb);
objects[i].type = SpecialBinPackingHelper::ITEM_OR_BIN;
}
}
}
TEST(SpecialBinPackingHelperTest, GreedyPackingWorks) {
std::vector<SpecialBinPackingHelper::ItemOrBin> objects;
objects.push_back({.capacity = 10});
objects.push_back({.capacity = 10});
objects.push_back({.demand = 5});
objects.push_back({.demand = 2}); // objects[3]
objects.push_back({.demand = 3});
objects.push_back({.demand = 2});
objects.push_back({.demand = 4});
objects.push_back({.demand = 4});
SpecialBinPackingHelper helper;
EXPECT_TRUE(helper.GreedyPackingWorks(2, objects));
// Note that this is order dependent.
std::swap(objects[3], objects.back());
EXPECT_FALSE(helper.GreedyPackingWorks(2, objects));
}
TEST(SpecialBinPackingHelperTest, UseDpToTightenCapacities) {
std::vector<SpecialBinPackingHelper::ItemOrBin> objects;
objects.push_back({.demand = 7, .capacity = 13});
objects.push_back({.demand = 5, .capacity = 12});
objects.push_back({.demand = 7, .capacity = 10});
objects.push_back({.demand = 10, .capacity = 9});
// The maximum reachable under 13 should be 7 + 5 = 12.
SpecialBinPackingHelper helper;
EXPECT_TRUE(helper.UseDpToTightenCapacities(absl::MakeSpan(objects)));
EXPECT_EQ(objects[0].capacity, 12);
EXPECT_EQ(objects[1].capacity, 12);
EXPECT_EQ(objects[2].capacity, 10);
EXPECT_EQ(objects[3].capacity, 9);
}
std::vector<absl::flat_hash_map<int, HeadMinusTailBounds>>
GetRelationByDimensionAndArc(const RouteRelationsHelper& helper) {
std::vector<absl::flat_hash_map<int, HeadMinusTailBounds>> result(
helper.num_dimensions());
for (int i = 0; i < helper.num_arcs(); ++i) {
for (int d = 0; d < helper.num_dimensions(); ++d) {
result[d][i] = helper.GetArcRelation(i, d);
}
}
return result;
}
TEST(RouteRelationsHelperTest, Basic) {
Model model;
// A graph with 6 nodes and the following arcs:
//
// l0 --->0<--- l1
// | |
// 1--l2-->2--l3-->3 4--l4-->5
//
const int num_nodes = 6;
const std::vector<int> tails = {1, 2, 1, 2, 4};
const std::vector<int> heads = {0, 0, 2, 3, 5};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "time" variables A, B, C intended to be associated with
// nodes 0, 1, 2 respectively, and "load" variables U, V, W, X, Y, Z intended
// to be associated with nodes 0, 1, 2, 3, 4, 5 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable u = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable v = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable w = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable z = model.Add(NewIntegerVariable(0, 10));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(a, b), 50, 1000);
repository.Add(literals[1], LinearExpression2::Difference(a, c), 70, 1000);
repository.Add(literals[2], LinearExpression2::Difference(c, b), 40, 1000);
repository.Add(literals[0],
LinearExpression2(NegationOf(u), NegationOf(v), -1, 1), 4,
100);
repository.Add(literals[1], LinearExpression2::Difference(u, w), 4, 100);
repository.Add(literals[2], LinearExpression2(w, v, -1, 1), -100, -3);
repository.Add(literals[3], LinearExpression2::Difference(x, w), 5, 100);
repository.Add(literals[4], LinearExpression2::Difference(z, y), 7, 100);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
// Two dimensions (time and load) on the first connected component, and one
// dimension (load) on the second component.
EXPECT_EQ(helper->num_dimensions(), 3);
EXPECT_EQ(helper->num_nodes(), num_nodes);
EXPECT_EQ(helper->num_arcs(), 5);
// Check the node variables.
EXPECT_THAT(
GetNodeExpressionsByDimension(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, a), Pair(1, b), Pair(2, c)),
UnorderedElementsAre(Pair(0, u), Pair(1, v), Pair(2, w), Pair(3, x)),
// Variables y and z cannot be unambiguously associated with nodes.
IsEmpty()));
// Check the arc relations. No relation for the dimension corresponding to y
// and z are recovered since they cannot be unambiguously associated with
// nodes 4 and 5, and since the other nodes don't have any associated variable
// in this dimension.
EXPECT_THAT(GetRelationByDimensionAndArc(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{50, 100}),
Pair(1, HeadMinusTailBounds{70, 100}),
Pair(2, HeadMinusTailBounds{40, 100}),
Pair(3, HeadMinusTailBounds{-100, 0}),
Pair(4, HeadMinusTailBounds{0, 0})),
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{4, 10}),
Pair(1, HeadMinusTailBounds{4, 10}),
Pair(2, HeadMinusTailBounds{3, 10}),
Pair(3, HeadMinusTailBounds{5, 10}),
Pair(4, HeadMinusTailBounds{0, 0})),
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{0, 0}),
Pair(1, HeadMinusTailBounds{0, 0}),
Pair(2, HeadMinusTailBounds{0, 0}),
Pair(3, HeadMinusTailBounds{0, 0}),
Pair(4, HeadMinusTailBounds{0, 0}))));
helper->RemoveArcs({0, 2});
EXPECT_EQ(helper->num_nodes(), num_nodes);
EXPECT_EQ(helper->num_arcs(), 3);
EXPECT_THAT(GetRelationByDimensionAndArc(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{70, 100}),
Pair(1, HeadMinusTailBounds{-100, 0}),
Pair(2, HeadMinusTailBounds{0, 0})),
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{4, 10}),
Pair(1, HeadMinusTailBounds{5, 10}),
Pair(2, HeadMinusTailBounds{0, 0})),
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{0, 0}),
Pair(1, HeadMinusTailBounds{0, 0}),
Pair(2, HeadMinusTailBounds{0, 0}))));
}
TEST(RouteRelationsHelperTest, UnenforcedRelations) {
Model model;
// Graph: 0--l0-->1
// ^\ |
// l3 | \_l4_ | l1
// | \v
// 3<--l2--2
//
const int num_nodes = 4;
const std::vector<int> tails = {0, 1, 2, 3, 0};
const std::vector<int> heads = {1, 2, 3, 0, 2};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "time" variables A, B, C, D intended to be associated
// with nodes 0, 1, 2, 3 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable d = model.Add(NewIntegerVariable(0, 100));
ConditionalLinear2Bounds repository(&model);
RootLevelLinear2Bounds* bounds = model.GetOrCreate<RootLevelLinear2Bounds>();
repository.Add(literals[0], LinearExpression2::Difference(b, a), 1, 1);
repository.Add(literals[1], LinearExpression2::Difference(c, b), 2, 2);
repository.Add(literals[2], LinearExpression2::Difference(d, c), 3, 3);
repository.Add(literals[3], LinearExpression2::Difference(a, d), 4, 4);
// Several unenforced relations on the diagonal arc. The one with the +/-1
// coefficients should be preferred.
bounds->Add(LinearExpression2(c, a, 3, -2), 1, 9);
bounds->Add(LinearExpression2(c, a, 1, -1), 5, 5);
bounds->Add(LinearExpression2(c, a, 2, -3), 3, 8);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
EXPECT_THAT(GetNodeExpressionsByDimension(*helper),
UnorderedElementsAre(UnorderedElementsAre(
Pair(0, a), Pair(1, b), Pair(2, c), Pair(3, d))));
// The unenforced relation is taken into account.
EXPECT_THAT(GetRelationByDimensionAndArc(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{1, 1}),
Pair(1, HeadMinusTailBounds{2, 2}),
Pair(2, HeadMinusTailBounds{3, 3}),
Pair(3, HeadMinusTailBounds{4, 4}),
Pair(4, HeadMinusTailBounds{5, 5}))));
}
TEST(RouteRelationsHelperTest, SeveralVariablesPerNode) {
Model model;
// A graph with 3 nodes and the following arcs: 0--l0-->1--l2-->2
const int num_nodes = 3;
const std::vector<int> tails = {0, 1};
const std::vector<int> heads = {1, 2};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "time" variables A, B, C and "load" variables X, Y, Z,
// intended to be associated with nodes 0, 1, 2 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable x = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable y = model.Add(NewIntegerVariable(0, 10));
const IntegerVariable z = model.Add(NewIntegerVariable(0, 10));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000);
repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000);
repository.Add(literals[0], LinearExpression2::Difference(z, y), 5, 100);
repository.Add(literals[1], LinearExpression2::Difference(y, x), 7, 100);
// Weird relation linking time and load variables, causing all the variables
// to be in a single "dimension".
repository.Add(literals[0], LinearExpression2::Difference(x, a), 0, 100);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
EXPECT_EQ(helper, nullptr);
}
TEST(RouteRelationsHelperTest, ComplexVariableRelations) {
Model model;
// A graph with 2 nodes and the following arcs: 0--l0-->1
const int num_nodes = 2;
const std::vector<int> tails = {0};
const std::vector<int> heads = {1};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "capacity" variables A and B, associated with nodes 0
// and 1, respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 150));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 1));
ConditionalLinear2Bounds repository(&model);
// "complex" relation with non +1/-1 coefficients.
repository.Add(literals[0], LinearExpression2(b, a, 10, 1), 0, 150);
repository.Build();
const RoutingCumulExpressions cumuls = {
.num_dimensions = 0,
.flat_node_dim_expressions = {AffineExpression(a),
AffineExpression(b, -20, 190)}};
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
// 10b + a in [0, 150] should give (190-20b) - a in [30,190], by using the
// fact that b is in [0, 1].
EXPECT_EQ(helper->GetArcRelation(0, 0), (HeadMinusTailBounds{30, 190}));
}
TEST(RouteRelationsHelperTest, TwoUnaryRelationsPerArc) {
Model model;
// A graph with 2 nodes and the following arcs: 0--l0-->1
const int num_nodes = 2;
const std::vector<int> tails = {0};
const std::vector<int> heads = {1};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "capacity" variables A and B, associated with nodes 0
// and 1, respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
// Two unary relations on the same arc, one for the head and one for the tail.
IntegerEncoder& encoder = *model.GetOrCreate<IntegerEncoder>();
encoder.AssociateToIntegerEqualValue(literals[0], a, 20);
encoder.AssociateToIntegerLiteral(literals[0], {b, 50});
ConditionalLinear2Bounds repository(&model);
repository.Build();
const RoutingCumulExpressions cumuls = {
.num_dimensions = 0,
.flat_node_dim_expressions = {AffineExpression(a), AffineExpression(b)}};
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
// The implied unary relations b >= 50 and a = 20 should be used to compute
// the arc relation (50 - 20 = 30, ub(b) - 20 = 80).
EXPECT_EQ(helper->GetArcRelation(0, 0), (HeadMinusTailBounds{30, 80}));
}
TEST(RouteRelationsHelperTest, SeveralRelationsPerArc) {
Model model;
// A graph with 3 nodes and the following arcs: 0--l0-->1--l1-->2
const int num_nodes = 3;
const std::vector<int> tails = {0, 1};
const std::vector<int> heads = {1, 2};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
// Add relations with "time" variables A, B, C intended to be associated with
// nodes 0, 1, 2 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000);
repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000);
// Add a second relation for some arc.
repository.Add(literals[1], LinearExpression2(c, b, 2, -3), 100, 200);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
EXPECT_EQ(helper->num_dimensions(), 1);
EXPECT_EQ(helper->GetNodeExpression(0, 0), a);
EXPECT_EQ(helper->GetNodeExpression(1, 0), b);
EXPECT_EQ(helper->GetNodeExpression(2, 0), c);
EXPECT_EQ(helper->GetArcRelation(0, 0), (HeadMinusTailBounds{50, 100}));
EXPECT_EQ(helper->GetArcRelation(1, 0), (HeadMinusTailBounds{70, 100}));
}
TEST(RouteRelationsHelperTest, SeveralArcsPerLiteral) {
// A graph with 3 nodes and the following arcs: 0--l0-->1--l0-->2, both
// enforced by the same literal l0.
Model model;
const int num_nodes = 3;
const std::vector<int> tails = {0, 1};
const std::vector<int> heads = {1, 2};
const Literal literal(model.Add(NewBooleanVariable()), true);
const std::vector<Literal> literals = {literal, literal};
// Add relations with "time" variables A, B, C intended to be associated with
// nodes 0, 1, 2 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000);
repository.Add(literals[0], LinearExpression2::Difference(c, b), 40, 1000);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
// No variable should be associated with any node, since there is no unique
// way to do this ([A, B, C] or [C, B, A], for nodes [0, 1, 2] respectively).
// As a consequence, no relation should be recovered either.
EXPECT_EQ(helper, nullptr);
}
TEST(RouteRelationsHelperTest, InconsistentRelationIsSkipped) {
// Graph: 0--l0-->1--l1-->2--l3-->3--l4-->4
// | ^
// | |
// l3 ------->5-------- l5
//
Model model;
const int num_nodes = 6;
const std::vector<int> tails = {0, 1, 2, 3, 1, 5};
const std::vector<int> heads = {1, 2, 3, 4, 5, 3};
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true)};
// Variables a, b, c, d, e, f are supposed to be associated with nodes 0, 1,
// 2, 3, 4, 5 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable d = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable e = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable f = model.Add(NewIntegerVariable(0, 100));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0);
repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1);
repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2);
repository.Add(literals[3], LinearExpression2::Difference(e, d), 3, 3);
repository.Add(literals[4], LinearExpression2::Difference(f, b), 4, 4);
// Inconsistent relation for arc 5->3 (should be between f and d).
repository.Add(literals[5], LinearExpression2(f, b, 2, -1), 5, 5);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
EXPECT_THAT(GetNodeExpressionsByDimension(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, a), Pair(1, b), Pair(2, c),
Pair(3, d), Pair(4, e), Pair(5, f))));
// The relation for arc 5->3 is filtered out because it is inconsistent.
// Instead, the default relation bounds between f and d are used.
EXPECT_THAT(GetRelationByDimensionAndArc(*helper),
UnorderedElementsAre(UnorderedElementsAre(
Pair(0, HeadMinusTailBounds{0, 0}),
Pair(1, HeadMinusTailBounds{1, 1}),
Pair(2, HeadMinusTailBounds{2, 2}),
Pair(3, HeadMinusTailBounds{3, 3}),
Pair(4, HeadMinusTailBounds{4, 4}),
Pair(5, HeadMinusTailBounds{-100, 100}))));
}
TEST(RouteRelationsHelperTest, InconsistentRelationWithMultipleArcsPerLiteral) {
// Graph: 0--l0-->1<---
// ^ | |
// l3 l1 |
// | v l4
// 3<--l2--2 |
// | |
// ----l4----->4
Model model;
const int num_nodes = 5;
const std::vector<int> tails = {0, 1, 2, 3, 4, 3};
const std::vector<int> heads = {1, 2, 3, 0, 1, 4};
const Literal l4 = Literal(model.Add(NewBooleanVariable()), true);
const std::vector<Literal> literals = {
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
Literal(model.Add(NewBooleanVariable()), true),
l4,
l4};
// Variables a, b, c, d, e are supposed to be associated with nodes 0, 1, 2,
// 3, 4 respectively.
const IntegerVariable a = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable b = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable c = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable d = model.Add(NewIntegerVariable(0, 100));
const IntegerVariable e = model.Add(NewIntegerVariable(0, 100));
ConditionalLinear2Bounds repository(&model);
repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0);
repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1);
repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2);
repository.Add(literals[3], LinearExpression2::Difference(a, d), 3, 3);
// Inconsistent relation for arc 4->1 (should be between e and b). Note that
// arcs 4->1 and 4->3 are enforced by the same literal, thus both should
// be true at the same time, hence the crossed bounds below.
repository.Add(literals[4], LinearExpression2::Difference(e, d), 4, 4);
repository.Add(literals[5], LinearExpression2::Difference(e, d), 5, 5);
repository.Build();
const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions(
num_nodes, tails, heads, literals, repository);
std::unique_ptr<RouteRelationsHelper> helper = RouteRelationsHelper::Create(
num_nodes, tails, heads, literals, cumuls.flat_node_dim_expressions,
repository, &model);
ASSERT_NE(helper, nullptr);
EXPECT_THAT(GetNodeExpressionsByDimension(*helper),
UnorderedElementsAre(UnorderedElementsAre(
Pair(0, a), Pair(1, b), Pair(2, c), Pair(3, d), Pair(4, e))));
// The relation for arc 4->1 is filtered out because it is inconsistent.
// Instead, the default relation bounds between e and b are used.
EXPECT_THAT(GetRelationByDimensionAndArc(*helper),
UnorderedElementsAre(
UnorderedElementsAre(Pair(0, HeadMinusTailBounds{0, 0}),
Pair(1, HeadMinusTailBounds{1, 1}),
Pair(2, HeadMinusTailBounds{2, 2}),
Pair(3, HeadMinusTailBounds{3, 3}),
Pair(4, HeadMinusTailBounds{-100, 100}),
Pair(5, HeadMinusTailBounds{5, 4}))));
}
TEST(MaybeFillMissingRoutesConstraintNodeExpressions,
FillsNodeVariablesIfNotPresent) {
// A graph with 4 nodes and the following arcs, with relations implying that
// variables 4, 5, 6, 7 should be associated with nodes 0, 1, 2, 3
// respectively.
//
// l0 --->0<--- l1
// | |
// 1--l2-->2--l3-->3
//
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, 10 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 10 ] }
constraints {
routes {
tails: [ 1, 2, 1, 2 ]
heads: [ 0, 0, 2, 3 ]
literals: [ 0, 1, 2, 3 ]
}
}
constraints {
enforcement_literal: 0
linear {
vars: [ 4, 5 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 1
linear {
vars: [ 4, 6 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 2
linear {
vars: [ 5, 6 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 3
linear {
vars: [ 6, 7 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
)pb");
CpModelProto new_cp_model = initial_model;
const auto [num_routes, num_dimensions] =
MaybeFillMissingRoutesConstraintNodeExpressions(initial_model,
new_cp_model);
EXPECT_EQ(num_routes, 1);
EXPECT_EQ(num_dimensions, 1);
const ConstraintProto expected_constraint = ParseTestProto(R"pb(
routes {
tails: [ 1, 2, 1, 2 ]
heads: [ 0, 0, 2, 3 ]
literals: [ 0, 1, 2, 3 ]
dimensions {
exprs {
vars: [ 4 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 5 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 6 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 7 ]
coeffs: [ 1 ]
}
}
}
)pb");
EXPECT_THAT(new_cp_model.constraints(0), EqualsProto(expected_constraint));
}
TEST(MaybeFillMissingRoutesConstraintNodeExpressions,
KeepsNodeVariablesIfPresent) {
// A graph with 4 nodes and the following arcs, with relations implying that
// variables 4, 5, 6, 7 should be associated with nodes 0, 1, 2, 3
// respectively (but the user provided 7, 6, 5, 4 instead, respectively).
//
// l0 --->0<--- l1
// | |
// 1--l2-->2--l3-->3
//
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, 10 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 10 ] }
variables { domain: [ 0, 10 ] }
constraints {
routes {
tails: [ 1, 2, 1, 2 ]
heads: [ 0, 0, 2, 3 ]
literals: [ 0, 1, 2, 3 ]
dimensions {
exprs {
vars: [ 7 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 6 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 5 ]
coeffs: [ 1 ]
}
exprs {
vars: [ 4 ]
coeffs: [ 1 ]
}
}
}
}
constraints {
enforcement_literal: 0
linear {
vars: [ 4, 5 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 1
linear {
vars: [ 4, 6 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 2
linear {
vars: [ 5, 6 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
constraints {
enforcement_literal: 3
linear {
vars: [ 6, 7 ]
coeffs: [ 1, -1 ]
domain: [ 0, 10 ]
}
}
)pb");
CpModelProto new_cp_model = initial_model;
const auto [num_routes, num_dimensions] =
MaybeFillMissingRoutesConstraintNodeExpressions(initial_model,
new_cp_model);
EXPECT_EQ(num_routes, 0);
EXPECT_EQ(num_dimensions, 0);
EXPECT_THAT(new_cp_model, EqualsProto(initial_model));
}
TEST(ExtractAllSubsetsFromForestTest, Basic) {
std::vector<int> parents = {3, 3, 1, 3, 1, 3};
std::vector<int> buffer;
std::vector<absl::Span<const int>> subsets;
ExtractAllSubsetsFromForest(parents, &buffer, &subsets);
// Post order but we explore high number first.
// Alternatively, we could use unordered here, but the order is stable.
EXPECT_THAT(buffer, ElementsAre(5, 4, 2, 1, 0, 3));
EXPECT_THAT(subsets,
ElementsAre(ElementsAre(5), ElementsAre(4), ElementsAre(2),
ElementsAre(4, 2, 1), ElementsAre(0),
ElementsAre(5, 4, 2, 1, 0, 3)));
}
//
// 0 3 4
// / \ |
// 1 2 5
TEST(ExtractAllSubsetsFromForestTest, BasicForest) {
std::vector<int> parents = {0, 0, 0, 3, 4, 4};
std::vector<int> buffer;
std::vector<absl::Span<const int>> subsets;
ExtractAllSubsetsFromForest(parents, &buffer, &subsets);
// Post order but we explore high number first.
// Alternatively, we could use unordered here, but the order is stable.
EXPECT_THAT(buffer, ElementsAre(2, 1, 0, 3, 5, 4));
EXPECT_THAT(subsets,
ElementsAre(ElementsAre(2), ElementsAre(1), ElementsAre(2, 1, 0),
ElementsAre(3), ElementsAre(5), ElementsAre(5, 4)));
}
TEST(ExtractAllSubsetsFromForestTest, Random) {
const int num_nodes = 20;
absl::BitGen random;
// Create a random tree rooted at zero.
std::vector<int> parents(num_nodes, 0);
for (int i = 2; i < num_nodes; ++i) {
parents[i] = absl::Uniform<int>(random, 0, i); // in [0, i - 1].
}
std::vector<int> buffer;
std::vector<absl::Span<const int>> subsets;
ExtractAllSubsetsFromForest(parents, &buffer, &subsets);
// We don't test that we are exhaustive, but we check basic property.
std::vector<int> in_subset(num_nodes, false);
for (const auto subset : subsets) {
for (const int n : subset) in_subset[n] = true;
// There should be at most one out edge.
int root = -1;
for (const int n : subset) {
if (in_subset[parents[n]]) continue;
if (root != -1) EXPECT_EQ(parents[n], root);
root = parents[n];
}
// No node outside should point inside.
for (int n = 0; n < num_nodes; ++n) {
if (in_subset[n]) continue;
EXPECT_TRUE(!in_subset[parents[n]]);
}
for (const int n : subset) in_subset[n] = false;
}
}
TEST(SymmetrizeArcsTest, BasicTest) {
std::vector<ArcWithLpValue> arcs{{.tail = 0, .head = 1, .lp_value = 0.5},
{.tail = 2, .head = 0, .lp_value = 0.5},
{.tail = 1, .head = 0, .lp_value = 0.5}};
SymmetrizeArcs(&arcs);
EXPECT_THAT(
arcs, ElementsAre(ArcWithLpValue{.tail = 0, .head = 1, .lp_value = 1.0},
ArcWithLpValue{.tail = 0, .head = 2, .lp_value = 0.5}));
}
TEST(ComputeGomoryHuTreeTest, Random) {
absl::BitGen random;
// Lets generate a random graph on a small number of nodes.
const int num_nodes = 10;
const int num_arcs = 100;
std::vector<ArcWithLpValue> arcs;
for (int i = 0; i < num_arcs; ++i) {
const int tail = absl::Uniform<int>(random, 0, num_nodes);
const int head = absl::Uniform<int>(random, 0, num_nodes);
if (tail == head) continue;
const double lp_value = absl::Uniform<double>(random, 0, 1);
arcs.push_back({tail, head, lp_value});
}
// Get all cut from Gomory-Hu tree.
const std::vector<int> parents = ComputeGomoryHuTree(num_nodes, arcs);
std::vector<int> buffer;
std::vector<absl::Span<const int>> subsets;
ExtractAllSubsetsFromForest(parents, &buffer, &subsets);
// Compute the cost of entering (resp. leaving) each subset.
// TODO(user): We need the same scaling as in ComputeGomoryHu(), not super
// clean. We might want an integer input to the function, but ok for now.
std::vector<bool> in_subset(num_nodes, false);
std::vector<int64_t> out_costs(subsets.size(), 0);
std::vector<int64_t> in_costs(subsets.size(), 0);
for (int i = 0; i < subsets.size(); ++i) {
for (const int n : subsets[i]) in_subset[n] = true;
for (const auto& arc : arcs) {
if (in_subset[arc.tail] && !in_subset[arc.head]) {
out_costs[i] += std::round(1.0e6 * arc.lp_value);
}
if (!in_subset[arc.tail] && in_subset[arc.head]) {
in_costs[i] += std::round(1.0e6 * arc.lp_value);
}
}
for (const int n : subsets[i]) in_subset[n] = false;
}
// We will test with an exhaustive comparison. We are in n ^ 3 !
// For all (s,t) pair, get the actual max-flow on the scaled graph.
// Check than one of the cuts separate s and t, with this exact weight.
SimpleMaxFlow max_flow;
for (const auto& [tail, head, lp_value] : arcs) {
// TODO(user): the algo only seems to work on an undirected graph, or
// equivalently when we always have a reverse arc with the same weight.
// Note that you can see below that we compute "min" cut for the sum of
// outgoing + incoming arcs this way.
max_flow.AddArcWithCapacity(tail, head, std::round(1.0e6 * lp_value));
max_flow.AddArcWithCapacity(head, tail, std::round(1.0e6 * lp_value));
}
for (int s = 0; s < num_nodes; ++s) {
for (int t = s + 1; t < num_nodes; ++t) {
ASSERT_EQ(max_flow.Solve(s, t), SimpleMaxFlow::OPTIMAL);
const int64_t flow = max_flow.OptimalFlow();
bool found = false;
for (int i = 0; i < subsets.size(); ++i) {
bool s_out = true;
bool t_out = true;
for (const int n : subsets[i]) {
if (n == s) s_out = false;
if (n == t) t_out = false;
}
if (!s_out && t_out && out_costs[i] + in_costs[i] == flow) found = true;
if (s_out && !t_out && in_costs[i] + out_costs[i] == flow) found = true;
if (found) break;
}
// Debug.
if (!found) {
LOG(INFO) << s << " -> " << t << " flow= " << flow;
for (int i = 0; i < subsets.size(); ++i) {
bool s_out = true;
bool t_out = true;
for (const int n : subsets[i]) {
if (n == s) s_out = false;
if (n == t) t_out = false;
}
if (!s_out && t_out) {
LOG(INFO) << i << " out= " << out_costs[i] + in_costs[i];
}
if (s_out && !t_out) {
LOG(INFO) << i << " in= " << in_costs[i] + out_costs[i];
}
}
}
ASSERT_TRUE(found);
}
}
}
TEST(CreateStronglyConnectedGraphCutGeneratorTest, BasicExample) {
Model model;
// Lets create a simple square graph with arcs in both directions:
//
// 0 ---- 1
// | |
// | |
// 2 ---- 3
const int num_nodes = 4;
std::vector<int> tails{0, 1, 1, 3, 3, 2, 2, 0};
std::vector<int> heads{1, 0, 3, 1, 2, 3, 0, 2};
std::vector<Literal> literals;
std::vector<IntegerVariable> vars;
for (int i = 0; i < 2 * num_nodes; ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
vars.push_back(model.Add(NewIntegerVariableFromLiteral(literals.back())));
}
CutGenerator generator = CreateStronglyConnectedGraphCutGenerator(
num_nodes, tails, heads, literals, &model);
// Suppose only 0-1 and 2-3 are in the lp solution (values do not matter).
auto& lp_values = *model.GetOrCreate<ModelLpValues>();
lp_values.resize(16, 0.0);
lp_values[vars[0]] = 0.5;
lp_values[vars[1]] = 0.5;
lp_values[vars[4]] = 1.0;
lp_values[vars[5]] = 1.0;
LinearConstraintManager manager(&model);
generator.generate_cuts(&manager);
// We should get two cuts.
EXPECT_EQ(manager.num_cuts(), 2);
EXPECT_THAT(manager.AllConstraints().front().constraint.VarsAsSpan(),
ElementsAre(vars[3], vars[6]));
EXPECT_THAT(manager.AllConstraints().back().constraint.VarsAsSpan(),
ElementsAre(vars[2], vars[7]));
}
TEST(CreateStronglyConnectedGraphCutGeneratorTest, AnotherExample) {
// This time, the graph is fully connected, but we still detect that {1, 2,
// 3} do not have enough outgoing flow:
//
// 0.5
// 0 <--> 1
// ^ | 0.5
// 0.5 | | 1 and 2 ----> 1
// v v
// 2 <--- 3
// 1
const int num_nodes = 4;
std::vector<int> tails{0, 1, 0, 2, 1, 3, 2};
std::vector<int> heads{1, 0, 2, 0, 3, 2, 1};
std::vector<double> values{0.5, 0.0, 0.5, 0.0, 1.0, 1.0, 0.5};
Model model;
std::vector<Literal> literals;
auto& lp_values = *model.GetOrCreate<ModelLpValues>();
lp_values.resize(16, 0.0);
for (int i = 0; i < values.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
lp_values[model.Add(NewIntegerVariableFromLiteral(literals.back()))] =
values[i];
}
CutGenerator generator = CreateStronglyConnectedGraphCutGenerator(
num_nodes, tails, heads, literals, &model);
LinearConstraintManager manager(&model);
generator.generate_cuts(&manager);
// The sets {2, 3} and {1, 2, 3} will generate cuts.
// However as an heuristic, we will wait another round to generate {1, 2, 3}.
ASSERT_EQ(manager.num_cuts(), 2);
EXPECT_THAT(manager.AllConstraints().front().constraint.DebugString(),
::testing::StartsWith("1 <= 1*I3 1*I6"));
EXPECT_THAT(manager.AllConstraints().back().constraint.DebugString(),
::testing::StartsWith("1 <= 1*I1 1*I3"));
}
TEST(GenerateInterestingSubsetsTest, BasicExample) {
const int num_nodes = 6;
const std::vector<std::pair<int, int>> arcs = {{0, 5}, {2, 3}, {3, 4}};
// Note that the order is not important, but is currently fixed.
// This document the actual order.
std::vector<int> subset_data;
std::vector<absl::Span<const int>> subsets;
GenerateInterestingSubsets(num_nodes, arcs,
/*stop_at_num_components=*/2, &subset_data,
&subsets);
EXPECT_THAT(
subsets,
ElementsAre(ElementsAre(1), ElementsAre(5), ElementsAre(0),
ElementsAre(5, 0), ElementsAre(3), ElementsAre(2),
ElementsAre(3, 2), ElementsAre(4), ElementsAre(3, 2, 4)));
// We can call it more than once.
GenerateInterestingSubsets(num_nodes, arcs,
/*stop_at_num_components=*/2, &subset_data,
&subsets);
EXPECT_THAT(
subsets,
ElementsAre(ElementsAre(1), ElementsAre(5), ElementsAre(0),
ElementsAre(5, 0), ElementsAre(3), ElementsAre(2),
ElementsAre(3, 2), ElementsAre(4), ElementsAre(3, 2, 4)));
}
TEST(CreateFlowCutGeneratorTest, BasicExample) {
//
// /---> 2
// 0 ---> 1 ^
// \---> 3
//
// With a flow of 2 leaving 0 and a flow of 1 requested at 2 and 3.
// On each arc the flow <= max_flow * arc_indicator where max_flow = 2.
const int num_nodes = 4;
std::vector<int> tails{0, 1, 1, 3};
std::vector<int> heads{1, 2, 3, 2};
std::vector<double> values{1.0, 0.5, 0.5, 0.0};
Model model;
std::vector<AffineExpression> capacities;
auto& lp_values = *model.GetOrCreate<ModelLpValues>();
lp_values.resize(16, 0.0);
for (int i = 0; i < values.size(); ++i) {
AffineExpression expr;
expr.var = model.Add(NewIntegerVariable(0, 1));
expr.coeff = 2;
expr.constant = 0;
capacities.emplace_back(expr);
lp_values[capacities.back().var] = values[i];
}
const auto get_flows = [](const std::vector<bool>& in_subset,
IntegerValue* min_incoming_flow,
IntegerValue* min_outgoing_flow) {
IntegerValue demand(0);
if (in_subset[0]) demand -= 2;
if (in_subset[2]) demand += 1;
if (in_subset[3]) demand += 1;
*min_incoming_flow = std::max(IntegerValue(0), demand);
*min_outgoing_flow = std::max(IntegerValue(0), -demand);
};
CutGenerator generator = CreateFlowCutGenerator(
num_nodes, tails, heads, capacities, get_flows, &model);
LinearConstraintManager manager(&model);
generator.generate_cuts(&manager);
// The sets {2} and {3} will generate incoming flow cuts.
EXPECT_EQ(manager.num_cuts(), 2);
EXPECT_THAT(manager.AllConstraints().front().constraint.DebugString(),
::testing::StartsWith("1 <= 1*I2"));
EXPECT_THAT(manager.AllConstraints().back().constraint.DebugString(),
::testing::StartsWith("1 <= 1*I1 1*I3"));
}
TEST(CreateFlowCutGeneratorTest, WithMinusOneArcs) {
// 0 ---> 1 -->
// |
// \ -->
const int num_nodes = 2;
std::vector<int> tails{0, 1, 1};
std::vector<int> heads{1, -1, -1};
std::vector<double> values{1.0, 0.5, 0.0};
Model model;
std::vector<AffineExpression> capacities;
auto& lp_values = *model.GetOrCreate<ModelLpValues>();
lp_values.resize(16, 0.0);
for (int i = 0; i < values.size(); ++i) {
AffineExpression expr;
expr.var = model.Add(NewIntegerVariable(0, 1));
expr.coeff = 2;
expr.constant = 0;
capacities.emplace_back(expr);
lp_values[capacities.back().var] = values[i];
}
const auto get_flows = [](const std::vector<bool>& in_subset,
IntegerValue* min_incoming_flow,
IntegerValue* min_outgoing_flow) {
IntegerValue demand(0);
if (in_subset[0]) demand -= 2;
*min_incoming_flow = std::max(IntegerValue(0), demand);
*min_outgoing_flow = std::max(IntegerValue(0), -demand);
};
CutGenerator generator = CreateFlowCutGenerator(
num_nodes, tails, heads, capacities, get_flows, &model);
LinearConstraintManager manager(&model);
generator.generate_cuts(&manager);
// We artificially put bad LP values so that {1} generate outgoing flow cut.
EXPECT_EQ(manager.num_cuts(), 1);
EXPECT_THAT(manager.AllConstraints().front().constraint.DebugString(),
::testing::StartsWith("1 <= 1*I1 1*I2"));
}
TEST(CreateCVRPCutGeneratorTest, InfeasiblePathCuts) {
// Graph with the following arcs, (demands), and [LP values]:
//
// (3) (4) (4)
// --[1]--> 1 --[.9]--> 2 --[.9]--> 3 --[1]--
// | \__[.1]__ ^\__[.1]__ ^ |
// depot _| \/ \/ v_ depot
// | __[.1]__/\ __[.1]__/\ ^
// | / v/ v |
// --[1]--> 4 --[.9]--> 5 --[.9]--> 6 --[1]--
// (3) (3) (3)
//
// The path 1->2->3 is infeasible due to the capacity limit. The sum of its LP
// values is 1.8, larger than its length minus 1, so we should get a cut for
// this path.
const int num_nodes = 7;
const std::vector<int> demands{0, 3, 4, 4, 3, 3, 3};
const std::vector<int> tails{0, 0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6};
const std::vector<int> heads{1, 4, 2, 5, 3, 6, 0, 5, 2, 6, 3, 0};
const std::vector<double> values{1.0, 1.0, 0.9, 0.1, 0.9, 0.1,
1.0, 0.9, 0.1, 0.9, 0.1, 1.0};
Model model;
model.GetOrCreate<SatParameters>()
->set_routing_cut_subset_size_for_exact_binary_relation_bound(0);
std::vector<Literal> literals;
auto& lp_values = *model.GetOrCreate<ModelLpValues>();
lp_values.resize(32, 0.0);
for (int i = 0; i < values.size(); ++i) {
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
lp_values[model.Add(NewIntegerVariableFromLiteral(literals.back()))] =
values[i];
}
// The capacity of each vehicle.
const int capacity = 10;
// The load of the vehicle arriving at each node.
std::vector<IntegerVariable> loads;
std::vector<AffineExpression> flat_node_dim_expressions;
for (int i = 0; i < num_nodes; ++i) {
const IntegerVariable load =
model.Add(NewIntegerVariable(0, capacity - demands[i]));
loads.push_back(load);
flat_node_dim_expressions.push_back(AffineExpression(load));
}
// Capacity constraints.
auto* repository = model.GetOrCreate<ConditionalLinear2Bounds>();
for (int i = 0; i < tails.size(); ++i) {
const int tail = tails[i];
const int head = heads[i];
if (tail == 0 || head == 0) continue;
// loads[head] >= loads[tail] + demand[tail]
repository->Add(literals[i],
LinearExpression2(loads[head], loads[tail], 1, -1),
demands[tail], 10000);
}
repository->Build();
// Enable the cut generator.
model.GetOrCreate<SatParameters>()
->set_routing_cut_max_infeasible_path_length(10);
CutGenerator generator = CreateCVRPCutGenerator(
num_nodes, tails, heads, literals, flat_node_dim_expressions, &model);
LinearConstraintManager manager(&model);
generator.generate_cuts(&manager);
ASSERT_EQ(manager.num_cuts(), 2);
// Arcs with ID 2 (1->2) and ID 4 (2->3) should be in the cut.
EXPECT_THAT(manager.AllConstraints().back().constraint.DebugString(),
::testing::StartsWith("0 <= 1*I2 1*I4 <= 1"));
}
} // namespace
} // namespace sat
} // namespace operations_research