808 lines
36 KiB
C++
808 lines
36 KiB
C++
// Copyright 2010-2022 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/linear_assignment.h"
|
|
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <vector>
|
|
|
|
#include "absl/random/distributions.h"
|
|
#include "benchmark/benchmark.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include "ortools/base/commandlineflags.h"
|
|
#include "ortools/graph/ebert_graph.h"
|
|
#include "ortools/graph/graph.h"
|
|
|
|
ABSL_DECLARE_FLAG(bool, assignment_stack_order);
|
|
|
|
namespace operations_research {
|
|
|
|
using ::testing::Eq;
|
|
|
|
template <typename AssignmentGraphType>
|
|
static ArcIndex CreateArcWithCost(
|
|
NodeIndex tail, NodeIndex head, CostValue cost,
|
|
AnnotatedGraphBuildManager<AssignmentGraphType>* builder,
|
|
LinearSumAssignment<AssignmentGraphType>* assignment) {
|
|
const ArcIndex arc = builder->AddArc(tail, head);
|
|
assignment->SetArcCost(arc, cost);
|
|
return arc;
|
|
}
|
|
|
|
// A little package containing everything the AnnotatedGraphBuildManager-based
|
|
// tests need to know about an assignment instance.
|
|
template <typename GraphType>
|
|
struct AssignmentProblemSetup {
|
|
// The usual constructor, for normal tests where the graph is balanced.
|
|
AssignmentProblemSetup(NodeIndex num_left_nodes, ArcIndex num_arcs,
|
|
bool optimize_layout)
|
|
: builder(new AnnotatedGraphBuildManager<GraphType>(
|
|
2 * num_left_nodes, num_arcs, optimize_layout)),
|
|
assignment_scoped(
|
|
new LinearSumAssignment<GraphType>(num_left_nodes, num_arcs)),
|
|
assignment(assignment_scoped.get()),
|
|
cycle_handler_scoped(assignment_scoped->ArcAnnotationCycleHandler()),
|
|
cycle_handler(cycle_handler_scoped.get()) {}
|
|
|
|
// A constructor with separate specification of the numbers of left and right
|
|
// nodes, so the tests can set up graphs where the assignment solution is sure
|
|
// to fail.
|
|
AssignmentProblemSetup(NodeIndex num_left_nodes, NodeIndex num_right_nodes,
|
|
ArcIndex num_arcs, bool optimize_layout)
|
|
: builder(new AnnotatedGraphBuildManager<GraphType>(
|
|
num_left_nodes + num_right_nodes, num_arcs, optimize_layout)),
|
|
assignment_scoped(
|
|
new LinearSumAssignment<GraphType>(num_left_nodes, num_arcs)),
|
|
assignment(assignment_scoped.get()),
|
|
cycle_handler_scoped(assignment_scoped->ArcAnnotationCycleHandler()),
|
|
cycle_handler(cycle_handler_scoped.get()) {}
|
|
|
|
// This type is neither copyable nor movable.
|
|
AssignmentProblemSetup(const AssignmentProblemSetup&) = delete;
|
|
AssignmentProblemSetup& operator=(const AssignmentProblemSetup&) = delete;
|
|
|
|
virtual ~AssignmentProblemSetup() { delete &assignment->Graph(); }
|
|
|
|
void Finalize() {
|
|
GraphType* graph = builder->Graph(cycle_handler);
|
|
ASSERT_TRUE(graph != nullptr);
|
|
assignment->SetGraph(graph);
|
|
}
|
|
|
|
AnnotatedGraphBuildManager<GraphType>* builder;
|
|
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment_scoped;
|
|
LinearSumAssignment<GraphType>* assignment; // to avoid ".get()" everywhere
|
|
|
|
std::unique_ptr<PermutationCycleHandler<typename GraphType::ArcIndex>>
|
|
cycle_handler_scoped;
|
|
PermutationCycleHandler<typename GraphType::ArcIndex>* cycle_handler;
|
|
};
|
|
|
|
// A fixture template to collect the types of graphs on which we want to base
|
|
// the LinearSumAssignment template instances that we test in ways that do not
|
|
// require dynamic graphs. All such tests use GraphBuildManager objects to get
|
|
// their underlying graphs; they cannot invoke the LinearSumAssignment<>::
|
|
// OptimizeGraphLayout() method, because it cannot be used on static graphs.
|
|
template <typename GraphType>
|
|
class LinearSumAssignmentTestWithGraphBuilder : public ::testing::Test {};
|
|
|
|
typedef ::testing::Types<
|
|
EbertGraph<int16_t, int16_t>, ForwardEbertGraph<int16_t, int16_t>,
|
|
ForwardStaticGraph<int16_t, int16_t>, EbertGraph<int16_t, ArcIndex>,
|
|
ForwardEbertGraph<int16_t, ArcIndex>, ForwardStaticGraph<int16_t, ArcIndex>,
|
|
EbertGraph<NodeIndex, int16_t>, ForwardEbertGraph<NodeIndex, int16_t>,
|
|
ForwardStaticGraph<NodeIndex, int16_t>, StarGraph, ForwardStarGraph,
|
|
ForwardStarStaticGraph, util::ListGraph<>, util::ReverseArcListGraph<>>
|
|
GraphTypesForAssignmentTestingWithGraphBuilder;
|
|
|
|
TYPED_TEST_SUITE(LinearSumAssignmentTestWithGraphBuilder,
|
|
GraphTypesForAssignmentTestingWithGraphBuilder);
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching0) {
|
|
AssignmentProblemSetup<TypeParam> setup(1, 1, false);
|
|
CreateArcWithCost<TypeParam>(0, 1, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching1) {
|
|
// A problem instance containing a node with no incident arcs.
|
|
AssignmentProblemSetup<TypeParam> setup(2, 1, false);
|
|
// We need the graph to include an arc that mentions the largest-indexed node
|
|
// in order to get better test coverage. Without that node used in an arc,
|
|
// infeasibility is detected very early because the number of nodes in the
|
|
// graph isn't twice the stated number of left-side nodes. With that node
|
|
// mentioned, the number of nodes in the graph alone is not enough to
|
|
// establish infeasibility, so more code runs.
|
|
CreateArcWithCost<TypeParam>(1, 3, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_FALSE(setup.assignment->ComputeAssignment());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching2) {
|
|
AssignmentProblemSetup<TypeParam> setup(2, 4, false);
|
|
CreateArcWithCost<TypeParam>(0, 2, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 3, 2, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 2, 3, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 3, 4, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(4, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching3) {
|
|
AssignmentProblemSetup<TypeParam> setup(4, 10, false);
|
|
// Create arcs with tail nodes out of order to ensure that we test a case in
|
|
// which the cost values must be nontrivially permuted if a static graph is
|
|
// the underlying representation.
|
|
CreateArcWithCost<TypeParam>(0, 5, 19, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 6, 47, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 7, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 4, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 5, 15, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 7, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 6, 13, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 7, 41, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0 + 13 + 15 + 0, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching4) {
|
|
AssignmentProblemSetup<TypeParam> setup(4, 10, false);
|
|
CreateArcWithCost<TypeParam>(0, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 5, 15, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 7, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 4, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 6, 13, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 7, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 5, 19, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 6, 47, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 7, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0 + 13 + 15 + 0, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching5) {
|
|
AssignmentProblemSetup<TypeParam> setup(4, 10, false);
|
|
CreateArcWithCost<TypeParam>(0, 4, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 5, 15, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 7, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 4, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 6, 13, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 7, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 5, 19, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 6, 47, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 7, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0 + 13 + 15 + 0, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching6) {
|
|
AssignmentProblemSetup<TypeParam> setup(4, 10, false);
|
|
CreateArcWithCost<TypeParam>(0, 4, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 6, 13, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 7, 41, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 5, 15, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 7, 60, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 5, 19, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 6, 47, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 7, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0 + 13 + 15 + 0, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, ZeroCostArcs) {
|
|
AssignmentProblemSetup<TypeParam> setup(4, 10, false);
|
|
CreateArcWithCost<TypeParam>(0, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 6, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 7, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 5, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 7, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 4, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 5, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 6, 0, setup.builder, setup.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 7, 0, setup.builder, setup.assignment);
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
EXPECT_EQ(0, setup.assignment->GetCost());
|
|
}
|
|
|
|
// A helper function template for checking that we got the optimum assignment we
|
|
// expected.
|
|
template <typename GraphType>
|
|
static void VerifyAssignment(const LinearSumAssignment<GraphType>& a,
|
|
const NodeIndex expected_right_side[]) {
|
|
for (typename LinearSumAssignment<GraphType>::BipartiteLeftNodeIterator
|
|
node_it(a);
|
|
node_it.Ok(); node_it.Next()) {
|
|
const NodeIndex left_node = node_it.Index();
|
|
const NodeIndex right_node = a.GetMate(left_node);
|
|
EXPECT_EQ(expected_right_side[left_node], right_node);
|
|
}
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, OptimumMatching7) {
|
|
const int kMatrixHeight = 4;
|
|
const int kMatrixWidth = 4;
|
|
// 4x4 problem taken from algorithms/hungarian_test.cc; to eliminate test
|
|
// flakiness, kCost[0][1] is modified so the optimum assignment is unique.
|
|
const int64_t kCost[kMatrixHeight][kMatrixWidth] = {{90, 76, 75, 80},
|
|
{35, 85, 55, 65},
|
|
{125, 95, 90, 105},
|
|
{45, 110, 95, 115}};
|
|
AssignmentProblemSetup<TypeParam> setup(kMatrixHeight,
|
|
kMatrixHeight * kMatrixWidth, false);
|
|
for (int i = 0; i < kMatrixHeight; ++i) {
|
|
for (int j = 0; j < kMatrixWidth; ++j) {
|
|
CreateArcWithCost<TypeParam>(i, j + kMatrixHeight, kCost[i][j],
|
|
setup.builder, setup.assignment);
|
|
}
|
|
}
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
const NodeIndex kExpectedAssignment[] = {7, 6, 5, 4};
|
|
VerifyAssignment(*setup.assignment, kExpectedAssignment);
|
|
EXPECT_EQ(80 + 55 + 95 + 45, setup.assignment->GetCost());
|
|
}
|
|
|
|
// Tests additional small assignment problems, and also tests that the
|
|
// assignment object can be reused to solve a modified version of the original
|
|
// problem.
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder,
|
|
OptimumMatching8WithObjectReuse) {
|
|
const int kMatrixHeight = 4;
|
|
const int kMatrixWidth = 4;
|
|
// 4x4 problem taken from algorithms/hungarian_test.cc; costs are negated to
|
|
// find the assignment whose cost is maximum with non-negated costs.
|
|
const int64_t kCost[kMatrixHeight][kMatrixWidth] = {{-90, -75, -75, -80},
|
|
{-35, 100, -55, -65},
|
|
{-125, -95, -90, -105},
|
|
{-45, -110, -95, -115}};
|
|
AssignmentProblemSetup<TypeParam> setup(kMatrixHeight,
|
|
kMatrixHeight * kMatrixWidth, false);
|
|
// Index of the arc we will remember and modify.
|
|
ArcIndex cost_100_arc = TypeParam::kNilArc;
|
|
for (int i = 0; i < kMatrixHeight; ++i) {
|
|
for (int j = 0; j < kMatrixWidth; ++j) {
|
|
ArcIndex new_arc = CreateArcWithCost(i, j + kMatrixHeight, kCost[i][j],
|
|
setup.builder, setup.assignment);
|
|
if (kCost[i][j] == 100) {
|
|
cost_100_arc = new_arc;
|
|
}
|
|
}
|
|
}
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
const NodeIndex kExpectedAssignment1[] = {6, 7, 4, 5};
|
|
VerifyAssignment(*setup.assignment, kExpectedAssignment1);
|
|
EXPECT_EQ(-75 + -65 + -125 + -110, setup.assignment->GetCost());
|
|
|
|
// For static graphs which cannot be built without the possibility of
|
|
// permuting the arcs, it is important that we have supplied the arcs in such
|
|
// an order that cost_100_arc still refers to the arc that has cost 100.
|
|
ASSERT_EQ(100, setup.assignment->ArcCost(cost_100_arc));
|
|
setup.assignment->SetArcCost(cost_100_arc, -85);
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
const NodeIndex kExpectedAssignment2[] = {6, 5, 4, 7};
|
|
VerifyAssignment(*setup.assignment, kExpectedAssignment2);
|
|
EXPECT_EQ(-75 + -85 + -125 + -115, setup.assignment->GetCost());
|
|
}
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithGraphBuilder, InfeasibleProblems) {
|
|
// No arcs in the graph at all.
|
|
AssignmentProblemSetup<TypeParam> setup0(1, 1, false);
|
|
setup0.Finalize();
|
|
EXPECT_FALSE(setup0.assignment->ComputeAssignment());
|
|
|
|
// Unbalanced graph: 4 nodes on the left, 2 on the right.
|
|
AssignmentProblemSetup<TypeParam> setup1(4, 2, 4, false);
|
|
CreateArcWithCost<TypeParam>(0, 4, 0, setup1.builder, setup1.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 4, 2, setup1.builder, setup1.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 5, 3, setup1.builder, setup1.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 5, 4, setup1.builder, setup1.assignment);
|
|
setup1.Finalize();
|
|
EXPECT_FALSE(setup1.assignment->ComputeAssignment());
|
|
|
|
// Unbalanced graph: 2 nodes on the left, 4 on the right.
|
|
AssignmentProblemSetup<TypeParam> setup2(2, 4, 4, false);
|
|
CreateArcWithCost<TypeParam>(0, 2, 0, setup2.builder, setup2.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 3, 2, setup2.builder, setup2.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 4, 3, setup2.builder, setup2.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 5, 4, setup2.builder, setup2.assignment);
|
|
setup2.Finalize();
|
|
EXPECT_FALSE(setup2.assignment->ComputeAssignment());
|
|
|
|
// Balanced graph with no perfect matching.
|
|
AssignmentProblemSetup<TypeParam> setup3(3, 5, false);
|
|
CreateArcWithCost<TypeParam>(0, 3, 0, setup3.builder, setup3.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 3, 2, setup3.builder, setup3.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 3, 3, setup3.builder, setup3.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 4, 4, setup3.builder, setup3.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 5, 5, setup3.builder, setup3.assignment);
|
|
setup3.Finalize();
|
|
EXPECT_FALSE(setup3.assignment->ComputeAssignment());
|
|
|
|
// Another balanced graph with no perfect matching, but with plenty
|
|
// of in/out degree for each node.
|
|
AssignmentProblemSetup<TypeParam> setup4(5, 12, false);
|
|
CreateArcWithCost<TypeParam>(0, 5, 0, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(0, 6, 2, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 5, 3, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(1, 6, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 5, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(2, 6, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 7, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 8, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(3, 9, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(4, 7, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(4, 8, 4, setup4.builder, setup4.assignment);
|
|
CreateArcWithCost<TypeParam>(4, 9, 4, setup4.builder, setup4.assignment);
|
|
setup4.Finalize();
|
|
EXPECT_FALSE(setup4.assignment->ComputeAssignment());
|
|
}
|
|
|
|
// A helper function template for setting up assignment problems based on
|
|
// dynamic graph types without an interposed GraphBuildManager.
|
|
template <typename DynamicGraphType, typename AssignmentGraphType>
|
|
static ArcIndex CreateArcWithCost(
|
|
NodeIndex tail, NodeIndex head, CostValue cost, DynamicGraphType* graph,
|
|
LinearSumAssignment<AssignmentGraphType>* assignment) {
|
|
ArcIndex arc = graph->AddArc(tail, head);
|
|
assignment->SetArcCost(arc, cost);
|
|
return arc;
|
|
}
|
|
|
|
// An empty fixture template to collect the types of graphs on which we want to
|
|
// base the LinearSumAssignment template instances that we test in ways that
|
|
// require dynamic graphs. The only such tests are the ones that call the
|
|
// LinearSumAssignment<>::OptimizeGraphLayout() method.
|
|
template <typename GraphType>
|
|
class LinearSumAssignmentTestWithDynamicGraph : public ::testing::Test {};
|
|
|
|
typedef ::testing::Types<
|
|
EbertGraph<int16_t, int16_t>, ForwardEbertGraph<int16_t, int16_t>,
|
|
EbertGraph<int16_t, ArcIndex>, ForwardEbertGraph<NodeIndex, int16_t>,
|
|
EbertGraph<NodeIndex, ArcIndex>, ForwardEbertGraph<NodeIndex, ArcIndex>>
|
|
DynamicGraphTypesForAssignmentTesting;
|
|
|
|
TYPED_TEST_SUITE(LinearSumAssignmentTestWithDynamicGraph,
|
|
DynamicGraphTypesForAssignmentTesting);
|
|
|
|
TYPED_TEST(LinearSumAssignmentTestWithDynamicGraph, GraphLayoutTest) {
|
|
// A complete bipartite 3x3 graph (9 edges).
|
|
TypeParam g(6, 9);
|
|
LinearSumAssignment<TypeParam> a(g, 3);
|
|
// We add arcs in a higgledy-piggledy order, with costs that indicate the
|
|
// order the arcs should have after the layout is optimized.
|
|
CreateArcWithCost(0, 3, 1, &g, &a); // in cycle [0]
|
|
CreateArcWithCost(2, 5, 9, &g, &a); // in cycle [1 8 3]
|
|
CreateArcWithCost(1, 5, 6, &g, &a); // in cycle [2 5]
|
|
CreateArcWithCost(0, 4, 2, &g, &a); // in cycle [1 8 3]
|
|
CreateArcWithCost(1, 4, 5, &g, &a); // in cycle [4]
|
|
CreateArcWithCost(0, 5, 3, &g, &a); // in cycle [2 5]
|
|
CreateArcWithCost(2, 4, 8, &g, &a); // in cycle [6 7]
|
|
CreateArcWithCost(2, 3, 7, &g, &a); // in cycle [6 7]
|
|
CreateArcWithCost(1, 3, 4, &g, &a); // in cycle [1 8 3]
|
|
|
|
EXPECT_TRUE(a.ComputeAssignment());
|
|
EXPECT_EQ(1 + 5 + 9, a.GetCost());
|
|
|
|
a.OptimizeGraphLayout(&g);
|
|
EXPECT_TRUE(a.ComputeAssignment());
|
|
EXPECT_EQ(1 + 5 + 9, a.GetCost());
|
|
// The optimized graph layout is supposed to group arcs by their tail nodes
|
|
// and sequence them within each group by their head nodes.
|
|
TailArrayManager<TypeParam> tail_array_manager(&g);
|
|
tail_array_manager.BuildTailArrayFromAdjacencyListsIfForwardGraph();
|
|
for (int i = 0; i < 9; ++i) {
|
|
EXPECT_EQ(i + 1, a.ArcCost(i));
|
|
EXPECT_EQ(i / 3, g.Tail(i));
|
|
EXPECT_EQ(3 + i % 3, g.Head(i));
|
|
}
|
|
tail_array_manager.ReleaseTailArrayIfForwardGraph();
|
|
}
|
|
|
|
// The EpsilonOptimal test and the PrecisionWarning test cannot be parameterized
|
|
// by the type of the underlying graph because doing so is not supported by the
|
|
// FRIEND_TEST() macro used in the LinearSumAssignment class template to grant
|
|
// these tests access to private methods of LinearSumAssignment.
|
|
TEST(LinearSumAssignmentFriendTest, EpsilonOptimal) {
|
|
StarGraph g(4, 4);
|
|
LinearSumAssignment<StarGraph> a(g, 2);
|
|
CreateArcWithCost(0, 2, 0, &g, &a);
|
|
CreateArcWithCost(0, 3, 2, &g, &a);
|
|
CreateArcWithCost(1, 2, 3, &g, &a);
|
|
CreateArcWithCost(1, 3, 4, &g, &a);
|
|
a.FinalizeSetup(); // needed to initialize epsilon_
|
|
EXPECT_TRUE(a.EpsilonOptimal());
|
|
}
|
|
|
|
// TODO(user): The following test is too slow to be run by default with
|
|
// non-optimized builds, but it has actually found a bug so I won't delete it
|
|
// entirely. Figure out a way to get it run regularly in optimized build mode
|
|
// without bogging down the normal set of fastbuild tests people need to run.
|
|
#if LARGE
|
|
TEST(LinearSumAssignmentPrecisionTest, PrecisionWarning) {
|
|
const NodeIndex kNumLeftNodes = 10000000;
|
|
ForwardStarGraph g(2 * kNumLeftNodes, 2 * kNumLeftNodes);
|
|
LinearSumAssignment<ForwardStarGraph> a(g, kNumLeftNodes);
|
|
int64_t node_count = 0;
|
|
for (NodeIndex left_node = ForwardStarGraph::kFirstNode;
|
|
node_count < kNumLeftNodes; ++node_count, ++left_node) {
|
|
CreateArcWithCost(left_node, kNumLeftNodes + left_node, kNumLeftNodes, &g,
|
|
&a);
|
|
}
|
|
EXPECT_FALSE(a.FinalizeSetup());
|
|
// This is a simple problem so we should be able to solve it despite the large
|
|
// arc costs.
|
|
EXPECT_TRUE(a.ComputeAssignment());
|
|
}
|
|
#endif // LARGE
|
|
|
|
class MacholWien
|
|
: public ::testing::TestWithParam<::testing::tuple<NodeIndex, bool>> {};
|
|
|
|
// The following test computes assignments on the instances described in:
|
|
// Robert E. Machol and Michael Wien, "Errata: A Hard Assignment Problem",
|
|
// Operations Research, vol. 25, p. 364, 1977.
|
|
// http://www.jstor.org/stable/169842
|
|
//
|
|
// Such instances proved difficult for the Hungarian method.
|
|
//
|
|
// The test parameter specifies the problem size and the stack/queue active node
|
|
// list flag.
|
|
TEST_P(MacholWien, SolveHardProblem) {
|
|
typedef ForwardStarStaticGraph GraphType;
|
|
NodeIndex n = ::testing::get<0>(GetParam());
|
|
absl::SetFlag(&FLAGS_assignment_stack_order, ::testing::get<1>(GetParam()));
|
|
AssignmentProblemSetup<GraphType> setup(n, n * n, false);
|
|
for (NodeIndex i = 0; i < n; ++i) {
|
|
for (NodeIndex j = 0; j < n; ++j) {
|
|
CreateArcWithCost<GraphType>(i, n + j, i * j, setup.builder,
|
|
setup.assignment);
|
|
}
|
|
}
|
|
setup.Finalize();
|
|
EXPECT_TRUE(setup.assignment->ComputeAssignment());
|
|
for (LinearSumAssignment<GraphType>::BipartiteLeftNodeIterator node_it(
|
|
*setup.assignment);
|
|
node_it.Ok(); node_it.Next()) {
|
|
const NodeIndex left_node = node_it.Index();
|
|
const NodeIndex right_node = setup.assignment->GetMate(left_node);
|
|
EXPECT_EQ(2 * n - 1, left_node + right_node);
|
|
}
|
|
}
|
|
|
|
#if LARGE
|
|
// Without optimizing compilation, a 1000x1000 Machol-Wien problem takes too
|
|
// long to solve as a even a large test. A non-optimized run of the following
|
|
// counts as an enormous test. With optimization it is merely medium in size.
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
MacholWienProblems, MacholWien,
|
|
::testing::Combine(::testing::Values(10, /* trivial */
|
|
100, /* less trivial */
|
|
1000), /* moderate */
|
|
::testing::Bool()));
|
|
#else // LARGE
|
|
INSTANTIATE_TEST_CASE_P(MacholWienProblems, MacholWien,
|
|
::testing::Combine(::testing::Values(10, // trivial
|
|
100), // less
|
|
// trivial
|
|
::testing::Bool()));
|
|
#endif // LARGE
|
|
|
|
// Helper function for random-assignment benchmarks.
|
|
template <typename GraphType, bool optimize_layout>
|
|
void ConstructRandomAssignment(
|
|
const int left_nodes, const int average_degree, const int cost_limit,
|
|
std::unique_ptr<GraphType>* graph,
|
|
std::unique_ptr<LinearSumAssignment<GraphType>>* assignment) {
|
|
const int kNodes = 2 * left_nodes;
|
|
const int kArcs = left_nodes * average_degree;
|
|
const int kRandomSeed = 0;
|
|
std::mt19937 randomizer(kRandomSeed);
|
|
AnnotatedGraphBuildManager<GraphType>* builder =
|
|
new AnnotatedGraphBuildManager<GraphType>(kNodes, kArcs, optimize_layout);
|
|
assignment->reset(new LinearSumAssignment<GraphType>(left_nodes, kArcs));
|
|
for (int i = 0; i < kArcs; ++i) {
|
|
const int left = absl::Uniform(randomizer, 0, left_nodes);
|
|
const int right = left_nodes + absl::Uniform(randomizer, 0, left_nodes);
|
|
const CostValue cost = absl::Uniform(randomizer, 0, cost_limit);
|
|
CreateArcWithCost(left, right, cost, builder, assignment->get());
|
|
}
|
|
std::unique_ptr<PermutationCycleHandler<ArcIndex>> cycle_handler(
|
|
assignment->get()->ArcAnnotationCycleHandler());
|
|
graph->reset(builder->Graph(cycle_handler.get()));
|
|
assignment->get()->SetGraph(graph->get());
|
|
}
|
|
|
|
// Same as ConstructRandomAssignment, but for the new API.
|
|
template <typename GraphType>
|
|
void ConstructRandomAssignmentForNewGraphApi(
|
|
const int left_nodes, const int average_degree, const int cost_limit,
|
|
std::unique_ptr<GraphType>* graph,
|
|
std::unique_ptr<LinearSumAssignment<GraphType>>* assignment) {
|
|
const int kNodes = 2 * left_nodes;
|
|
const int kArcs = left_nodes * average_degree;
|
|
const int kRandomSeed = 0;
|
|
std::mt19937 randomizer(kRandomSeed);
|
|
std::vector<CostValue> arc_costs;
|
|
arc_costs.reserve(kArcs);
|
|
graph->reset(new GraphType(kNodes, kArcs));
|
|
for (int i = 0; i < kArcs; ++i) {
|
|
const int left = absl::Uniform(randomizer, 0, left_nodes);
|
|
const int right = left_nodes + absl::Uniform(randomizer, 0, left_nodes);
|
|
const CostValue cost = absl::Uniform(randomizer, 0, cost_limit);
|
|
graph->get()->AddArc(left, right);
|
|
arc_costs.push_back(cost);
|
|
}
|
|
|
|
// Finalize the graph.
|
|
std::vector<typename GraphType::ArcIndex> permutation;
|
|
graph->get()->Build(&permutation);
|
|
util::Permute(permutation, &arc_costs);
|
|
|
|
// Create the assignment.
|
|
assignment->reset(
|
|
new LinearSumAssignment<GraphType>(*(graph->get()), left_nodes));
|
|
for (int arc = 0; arc < kArcs; ++arc) {
|
|
assignment->get()->SetArcCost(arc, arc_costs[arc]);
|
|
}
|
|
}
|
|
|
|
// Benchmark function for assignment-problem construction only, no solution.
|
|
template <typename GraphType, bool optimize_layout>
|
|
void BM_ConstructRandomAssignmentProblem(benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
for (auto _ : state) {
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignment<GraphType, optimize_layout>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, StarGraph, false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, ForwardStarGraph,
|
|
false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, ForwardStarStaticGraph,
|
|
false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, StarGraph, true);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, ForwardStarGraph,
|
|
true);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructRandomAssignmentProblem, ForwardStarStaticGraph,
|
|
true);
|
|
|
|
template <typename GraphType>
|
|
void BM_ConstructRandomAssignmentProblemWithNewGraphApi(
|
|
benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
for (auto _ : state) {
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignmentForNewGraphApi<GraphType>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE(BM_ConstructRandomAssignmentProblemWithNewGraphApi,
|
|
util::ListGraph<>);
|
|
BENCHMARK_TEMPLATE(BM_ConstructRandomAssignmentProblemWithNewGraphApi,
|
|
util::StaticGraph<>);
|
|
|
|
// Benchmark function for assignment-problem solution only, with
|
|
// problem-construction timing excluded.
|
|
template <typename GraphType, bool optimize_layout>
|
|
void BM_SolveRandomAssignmentProblem(benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignment<GraphType, optimize_layout>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
for (auto _ : state) {
|
|
assignment->ComputeAssignment();
|
|
EXPECT_EQ(65849286, assignment->GetCost());
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, StarGraph, false);
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, ForwardStarGraph, false);
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, ForwardStarStaticGraph,
|
|
false);
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, StarGraph, true);
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, ForwardStarGraph, true);
|
|
BENCHMARK_TEMPLATE2(BM_SolveRandomAssignmentProblem, ForwardStarStaticGraph,
|
|
true);
|
|
|
|
template <typename GraphType>
|
|
void BM_SolveRandomAssignmentProblemWithNewGraphApi(benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignmentForNewGraphApi<GraphType>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
for (auto _ : state) {
|
|
assignment->ComputeAssignment();
|
|
EXPECT_EQ(65849286, assignment->GetCost());
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE(BM_SolveRandomAssignmentProblemWithNewGraphApi,
|
|
util::ListGraph<>);
|
|
BENCHMARK_TEMPLATE(BM_SolveRandomAssignmentProblemWithNewGraphApi,
|
|
util::StaticGraph<>);
|
|
|
|
// Benchmark function for assignment-problem construction and solution, with
|
|
// problem-construction timing included.
|
|
template <typename GraphType, bool optimize_layout>
|
|
void BM_ConstructAndSolveRandomAssignmentProblem(benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
for (auto _ : state) {
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignment<GraphType, optimize_layout>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
assignment->ComputeAssignment();
|
|
EXPECT_EQ(65849286, assignment->GetCost());
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem, StarGraph,
|
|
false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem,
|
|
ForwardStarGraph, false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem,
|
|
ForwardStarStaticGraph, false);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem, StarGraph,
|
|
true);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem,
|
|
ForwardStarGraph, true);
|
|
BENCHMARK_TEMPLATE2(BM_ConstructAndSolveRandomAssignmentProblem,
|
|
ForwardStarStaticGraph, true);
|
|
|
|
template <typename GraphType>
|
|
void BM_ConstructAndSolveRandomAssignmentProblemWithNewGraphApi(
|
|
benchmark::State& state) {
|
|
const int kLeftNodes = 10000;
|
|
const int kAverageDegree = 250;
|
|
const CostValue kCostLimit = 1000000;
|
|
for (auto _ : state) {
|
|
std::unique_ptr<GraphType> graph;
|
|
std::unique_ptr<LinearSumAssignment<GraphType>> assignment;
|
|
ConstructRandomAssignmentForNewGraphApi<GraphType>(
|
|
kLeftNodes, kAverageDegree, kCostLimit, &graph, &assignment);
|
|
assignment->ComputeAssignment();
|
|
EXPECT_EQ(65849286, assignment->GetCost());
|
|
}
|
|
state.SetItemsProcessed(static_cast<int64_t>(state.max_iterations) *
|
|
kLeftNodes * kAverageDegree);
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE(BM_ConstructAndSolveRandomAssignmentProblemWithNewGraphApi,
|
|
util::ListGraph<>);
|
|
BENCHMARK_TEMPLATE(BM_ConstructAndSolveRandomAssignmentProblemWithNewGraphApi,
|
|
util::StaticGraph<>);
|
|
|
|
// The order of initializing the edges in the graph made the difference between
|
|
// finding an optimal assignment and erroneously failing to finding one because
|
|
// an unlucky order of edges could cause price reductions greater than the slack
|
|
// relabeling amount.
|
|
class ReorderedGraphTest : public testing::Test {
|
|
public:
|
|
struct Edge {
|
|
size_t from_node;
|
|
size_t to_node;
|
|
int64_t cost;
|
|
};
|
|
|
|
ReorderedGraphTest() {}
|
|
|
|
void TestMe(const size_t left_nodes, const std::vector<Edge>& ordered_edges) {
|
|
std::vector<int64_t> edge_costs;
|
|
typedef util::StaticGraph<size_t, size_t> GraphType;
|
|
GraphType graph(2 * left_nodes, ordered_edges.size());
|
|
|
|
for (const auto& edge : ordered_edges) {
|
|
graph.AddArc(/* tail= */ edge.from_node,
|
|
/* head= */ edge.to_node);
|
|
edge_costs.push_back(edge.cost);
|
|
}
|
|
ASSERT_THAT(edge_costs.size(), Eq(graph.num_arcs()));
|
|
|
|
{
|
|
std::vector<typename GraphType::ArcIndex> arc_permutation;
|
|
graph.Build(&arc_permutation);
|
|
util::Permute(arc_permutation, &edge_costs);
|
|
}
|
|
|
|
LinearSumAssignment<GraphType> assignment(graph, left_nodes);
|
|
for (size_t arc = 0; arc != edge_costs.size(); ++arc) {
|
|
assignment.SetArcCost(arc, edge_costs[arc]);
|
|
}
|
|
EXPECT_TRUE(assignment.FinalizeSetup());
|
|
EXPECT_TRUE(assignment.ComputeAssignment());
|
|
}
|
|
};
|
|
|
|
// The edge orderings in this test were solved correctly.
|
|
TEST_F(ReorderedGraphTest, Bug64485671PassingOrder) {
|
|
TestMe(2 /* left_nodes */, {
|
|
{0, 3, 393217},
|
|
{1, 3, 393217},
|
|
{1, 2, 393216},
|
|
{0, 2, 163840},
|
|
});
|
|
TestMe(2 /* left_nodes */, {
|
|
{0, 3, 20},
|
|
{1, 3, 20},
|
|
{1, 2, 19},
|
|
{0, 2, 0},
|
|
});
|
|
}
|
|
|
|
// The edge orderings in this test incorrectly triggered infeasibility
|
|
// detection. These are the same edge costs as in the above "PassingOrder" test,
|
|
// just given in a different order.
|
|
TEST_F(ReorderedGraphTest, Bug64485671FailingOrder) {
|
|
TestMe(/*left_nodes=*/2, {
|
|
{0, 3, 393217},
|
|
{1, 2, 393216},
|
|
{0, 2, 163840},
|
|
{1, 3, 393217},
|
|
});
|
|
TestMe(/*left_nodes=*/2, {
|
|
{0, 3, 20},
|
|
{1, 2, 19},
|
|
{0, 2, 0},
|
|
{1, 3, 20},
|
|
});
|
|
}
|
|
|
|
} // namespace operations_research
|