blob: 5036aa20acdb55766a149fac8bffeb94ed02a04b [file] [log] [blame]
// Ceres Solver - A fast non-linear least squares minimizer
// Copyright 2015 Google Inc. All rights reserved.
// http://ceres-solver.org/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: sameeragarwal@google.com (Sameer Agarwal)
#include "ceres/block_sparse_matrix.h"
#include <algorithm>
#include <memory>
#include <random>
#include <string>
#include <vector>
#include "ceres/casts.h"
#include "ceres/crs_matrix.h"
#include "ceres/internal/eigen.h"
#include "ceres/linear_least_squares_problems.h"
#include "ceres/triplet_sparse_matrix.h"
#include "glog/logging.h"
#include "gtest/gtest.h"
namespace ceres {
namespace internal {
namespace {
std::unique_ptr<BlockSparseMatrix> CreateTestMatrixFromId(int id) {
if (id == 0) {
// Create the following block sparse matrix:
// [ 1 2 0 0 0 0 ]
// [ 3 4 0 0 0 0 ]
// [ 0 0 5 6 7 0 ]
// [ 0 0 8 9 10 0 ]
CompressedRowBlockStructure* bs = new CompressedRowBlockStructure;
bs->cols = {
// Block size 2, position 0.
Block(2, 0),
// Block size 3, position 2.
Block(3, 2),
// Block size 1, position 5.
Block(1, 5),
};
bs->rows = {CompressedRow(1), CompressedRow(1)};
bs->rows[0].block = Block(2, 0);
bs->rows[0].cells = {Cell(0, 0)};
bs->rows[1].block = Block(2, 2);
bs->rows[1].cells = {Cell(1, 4)};
auto m = std::make_unique<BlockSparseMatrix>(bs);
EXPECT_NE(m, nullptr);
EXPECT_EQ(m->num_rows(), 4);
EXPECT_EQ(m->num_cols(), 6);
EXPECT_EQ(m->num_nonzeros(), 10);
double* values = m->mutable_values();
for (int i = 0; i < 10; ++i) {
values[i] = i + 1;
}
return m;
} else if (id == 1) {
// Create the following block sparse matrix:
// [ 1 2 0 5 6 0 ]
// [ 3 4 0 7 8 0 ]
// [ 0 0 9 0 0 0 ]
CompressedRowBlockStructure* bs = new CompressedRowBlockStructure;
bs->cols = {
// Block size 2, position 0.
Block(2, 0),
// Block size 1, position 2.
Block(1, 2),
// Block size 2, position 3.
Block(2, 3),
// Block size 1, position 5.
Block(1, 5),
};
bs->rows = {CompressedRow(2), CompressedRow(1)};
bs->rows[0].block = Block(2, 0);
bs->rows[0].cells = {Cell(0, 0), Cell(2, 4)};
bs->rows[1].block = Block(1, 2);
bs->rows[1].cells = {Cell(1, 8)};
auto m = std::make_unique<BlockSparseMatrix>(bs);
EXPECT_NE(m, nullptr);
EXPECT_EQ(m->num_rows(), 3);
EXPECT_EQ(m->num_cols(), 6);
EXPECT_EQ(m->num_nonzeros(), 9);
double* values = m->mutable_values();
for (int i = 0; i < 9; ++i) {
values[i] = i + 1;
}
return m;
}
return nullptr;
}
} // namespace
const int kNumThreads = 4;
class BlockSparseMatrixTest : public ::testing::Test {
protected:
void SetUp() final {
std::unique_ptr<LinearLeastSquaresProblem> problem =
CreateLinearLeastSquaresProblemFromId(2);
CHECK(problem != nullptr);
a_.reset(down_cast<BlockSparseMatrix*>(problem->A.release()));
problem = CreateLinearLeastSquaresProblemFromId(1);
CHECK(problem != nullptr);
b_.reset(down_cast<TripletSparseMatrix*>(problem->A.release()));
CHECK_EQ(a_->num_rows(), b_->num_rows());
CHECK_EQ(a_->num_cols(), b_->num_cols());
CHECK_EQ(a_->num_nonzeros(), b_->num_nonzeros());
context_.EnsureMinimumThreads(kNumThreads);
BlockSparseMatrix::RandomMatrixOptions options;
options.num_row_blocks = 1000;
options.min_row_block_size = 1;
options.max_row_block_size = 8;
options.num_col_blocks = 100;
options.min_col_block_size = 1;
options.max_col_block_size = 8;
options.block_density = 0.05;
std::mt19937 rng;
c_ = BlockSparseMatrix::CreateRandomMatrix(options, rng);
}
std::unique_ptr<BlockSparseMatrix> a_;
std::unique_ptr<TripletSparseMatrix> b_;
std::unique_ptr<BlockSparseMatrix> c_;
ContextImpl context_;
};
TEST_F(BlockSparseMatrixTest, SetZeroTest) {
a_->SetZero();
EXPECT_EQ(13, a_->num_nonzeros());
}
TEST_F(BlockSparseMatrixTest, RightMultiplyAndAccumulateTest) {
Vector y_a = Vector::Zero(a_->num_rows());
Vector y_b = Vector::Zero(a_->num_rows());
for (int i = 0; i < a_->num_cols(); ++i) {
Vector x = Vector::Zero(a_->num_cols());
x[i] = 1.0;
a_->RightMultiplyAndAccumulate(x.data(), y_a.data());
b_->RightMultiplyAndAccumulate(x.data(), y_b.data());
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
}
TEST_F(BlockSparseMatrixTest, RightMultiplyAndAccumulateParallelTest) {
Vector y_0 = Vector::Random(a_->num_rows());
Vector y_s = y_0;
Vector y_p = y_0;
Vector x = Vector::Random(a_->num_cols());
a_->RightMultiplyAndAccumulate(x.data(), y_s.data());
a_->RightMultiplyAndAccumulate(x.data(), y_p.data(), &context_, kNumThreads);
// Current parallel implementation is expected to be bit-exact
EXPECT_EQ((y_s - y_p).norm(), 0.);
}
TEST_F(BlockSparseMatrixTest, LeftMultiplyAndAccumulateTest) {
Vector y_a = Vector::Zero(a_->num_cols());
Vector y_b = Vector::Zero(a_->num_cols());
for (int i = 0; i < a_->num_rows(); ++i) {
Vector x = Vector::Zero(a_->num_rows());
x[i] = 1.0;
a_->LeftMultiplyAndAccumulate(x.data(), y_a.data());
b_->LeftMultiplyAndAccumulate(x.data(), y_b.data());
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
}
TEST_F(BlockSparseMatrixTest, LeftMultiplyAndAccumulateParallelTest) {
Vector y_0 = Vector::Random(a_->num_cols());
Vector y_s = y_0;
Vector y_p = y_0;
Vector x = Vector::Random(a_->num_rows());
a_->LeftMultiplyAndAccumulate(x.data(), y_s.data());
a_->LeftMultiplyAndAccumulate(x.data(), y_p.data(), &context_, kNumThreads);
// Parallel implementation for left products uses a different order of
// traversal, thus results might be different
EXPECT_LT((y_s - y_p).norm(), 1e-12);
}
TEST_F(BlockSparseMatrixTest, SquaredColumnNormTest) {
Vector y_a = Vector::Zero(a_->num_cols());
Vector y_b = Vector::Zero(a_->num_cols());
a_->SquaredColumnNorm(y_a.data());
b_->SquaredColumnNorm(y_b.data());
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
TEST_F(BlockSparseMatrixTest, SquaredColumnNormParallelTest) {
Vector y_a = Vector::Zero(c_->num_cols());
Vector y_b = Vector::Zero(c_->num_cols());
c_->SquaredColumnNorm(y_a.data());
c_->SquaredColumnNorm(y_b.data(), &context_, kNumThreads);
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
TEST_F(BlockSparseMatrixTest, ScaleColumnsTest) {
const Vector scale = Vector::Random(c_->num_cols()).cwiseAbs();
const Vector x = Vector::Random(c_->num_rows());
Vector y_expected = Vector::Zero(c_->num_cols());
c_->LeftMultiplyAndAccumulate(x.data(), y_expected.data());
y_expected.array() *= scale.array();
c_->ScaleColumns(scale.data());
Vector y_observed = Vector::Zero(c_->num_cols());
c_->LeftMultiplyAndAccumulate(x.data(), y_observed.data());
EXPECT_GT(y_expected.norm(), 1.);
EXPECT_LT((y_observed - y_expected).norm(), 1e-12 * y_expected.norm());
}
TEST_F(BlockSparseMatrixTest, ScaleColumnsParallelTest) {
const Vector scale = Vector::Random(c_->num_cols()).cwiseAbs();
const Vector x = Vector::Random(c_->num_rows());
Vector y_expected = Vector::Zero(c_->num_cols());
c_->LeftMultiplyAndAccumulate(x.data(), y_expected.data());
y_expected.array() *= scale.array();
c_->ScaleColumns(scale.data(), &context_, kNumThreads);
Vector y_observed = Vector::Zero(c_->num_cols());
c_->LeftMultiplyAndAccumulate(x.data(), y_observed.data());
EXPECT_GT(y_expected.norm(), 1.);
EXPECT_LT((y_observed - y_expected).norm(), 1e-12 * y_expected.norm());
}
TEST_F(BlockSparseMatrixTest, ToDenseMatrixTest) {
Matrix m_a;
Matrix m_b;
a_->ToDenseMatrix(&m_a);
b_->ToDenseMatrix(&m_b);
EXPECT_LT((m_a - m_b).norm(), 1e-12);
}
TEST_F(BlockSparseMatrixTest, AppendRows) {
std::unique_ptr<LinearLeastSquaresProblem> problem =
CreateLinearLeastSquaresProblemFromId(2);
std::unique_ptr<BlockSparseMatrix> m(
down_cast<BlockSparseMatrix*>(problem->A.release()));
a_->AppendRows(*m);
EXPECT_EQ(a_->num_rows(), 2 * m->num_rows());
EXPECT_EQ(a_->num_cols(), m->num_cols());
problem = CreateLinearLeastSquaresProblemFromId(1);
std::unique_ptr<TripletSparseMatrix> m2(
down_cast<TripletSparseMatrix*>(problem->A.release()));
b_->AppendRows(*m2);
Vector y_a = Vector::Zero(a_->num_rows());
Vector y_b = Vector::Zero(a_->num_rows());
for (int i = 0; i < a_->num_cols(); ++i) {
Vector x = Vector::Zero(a_->num_cols());
x[i] = 1.0;
y_a.setZero();
y_b.setZero();
a_->RightMultiplyAndAccumulate(x.data(), y_a.data());
b_->RightMultiplyAndAccumulate(x.data(), y_b.data());
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
}
TEST_F(BlockSparseMatrixTest, AppendDeleteRowsTransposedStructure) {
auto problem = CreateLinearLeastSquaresProblemFromId(2);
std::unique_ptr<BlockSparseMatrix> m(
down_cast<BlockSparseMatrix*>(problem->A.release()));
auto block_structure = a_->block_structure();
// Several AppendRows and DeleteRowBlocks operations are applied to matrix,
// with regular and transpose block structures being compared after each
// operation.
//
// Non-negative values encode number of row blocks to remove
// -1 encodes appending matrix m
const int num_row_blocks_to_delete[] = {0, -1, 1, -1, 8, -1, 10};
for (auto& t : num_row_blocks_to_delete) {
if (t == -1) {
a_->AppendRows(*m);
} else if (t > 0) {
CHECK_GE(block_structure->rows.size(), t);
a_->DeleteRowBlocks(t);
}
auto block_structure = a_->block_structure();
auto transpose_block_structure = a_->transpose_block_structure();
ASSERT_NE(block_structure, nullptr);
ASSERT_NE(transpose_block_structure, nullptr);
EXPECT_EQ(block_structure->rows.size(),
transpose_block_structure->cols.size());
EXPECT_EQ(block_structure->cols.size(),
transpose_block_structure->rows.size());
std::vector<int> nnz_col(transpose_block_structure->rows.size());
for (int i = 0; i < block_structure->cols.size(); ++i) {
EXPECT_EQ(block_structure->cols[i].position,
transpose_block_structure->rows[i].block.position);
const int col_size = transpose_block_structure->rows[i].block.size;
EXPECT_EQ(block_structure->cols[i].size, col_size);
for (auto& col_cell : transpose_block_structure->rows[i].cells) {
int matches = 0;
const int row_block_id = col_cell.block_id;
nnz_col[i] +=
col_size * transpose_block_structure->cols[row_block_id].size;
for (auto& row_cell : block_structure->rows[row_block_id].cells) {
if (row_cell.block_id != i) continue;
EXPECT_EQ(row_cell.position, col_cell.position);
++matches;
}
EXPECT_EQ(matches, 1);
}
EXPECT_EQ(nnz_col[i], transpose_block_structure->rows[i].nnz);
if (i > 0) {
nnz_col[i] += nnz_col[i - 1];
}
EXPECT_EQ(nnz_col[i], transpose_block_structure->rows[i].cumulative_nnz);
}
for (int i = 0; i < block_structure->rows.size(); ++i) {
EXPECT_EQ(block_structure->rows[i].block.position,
transpose_block_structure->cols[i].position);
EXPECT_EQ(block_structure->rows[i].block.size,
transpose_block_structure->cols[i].size);
for (auto& row_cell : block_structure->rows[i].cells) {
int matches = 0;
const int col_block_id = row_cell.block_id;
for (auto& col_cell :
transpose_block_structure->rows[col_block_id].cells) {
if (col_cell.block_id != i) continue;
EXPECT_EQ(col_cell.position, row_cell.position);
++matches;
}
EXPECT_EQ(matches, 1);
}
}
}
}
TEST_F(BlockSparseMatrixTest, AppendAndDeleteBlockDiagonalMatrix) {
const std::vector<Block>& column_blocks = a_->block_structure()->cols;
const int num_cols =
column_blocks.back().size + column_blocks.back().position;
Vector diagonal(num_cols);
for (int i = 0; i < num_cols; ++i) {
diagonal(i) = 2 * i * i + 1;
}
std::unique_ptr<BlockSparseMatrix> appendage(
BlockSparseMatrix::CreateDiagonalMatrix(diagonal.data(), column_blocks));
a_->AppendRows(*appendage);
Vector y_a, y_b;
y_a.resize(a_->num_rows());
y_b.resize(a_->num_rows());
for (int i = 0; i < a_->num_cols(); ++i) {
Vector x = Vector::Zero(a_->num_cols());
x[i] = 1.0;
y_a.setZero();
y_b.setZero();
a_->RightMultiplyAndAccumulate(x.data(), y_a.data());
b_->RightMultiplyAndAccumulate(x.data(), y_b.data());
EXPECT_LT((y_a.head(b_->num_rows()) - y_b.head(b_->num_rows())).norm(),
1e-12);
Vector expected_tail = Vector::Zero(a_->num_cols());
expected_tail(i) = diagonal(i);
EXPECT_LT((y_a.tail(a_->num_cols()) - expected_tail).norm(), 1e-12);
}
a_->DeleteRowBlocks(column_blocks.size());
EXPECT_EQ(a_->num_rows(), b_->num_rows());
EXPECT_EQ(a_->num_cols(), b_->num_cols());
y_a.resize(a_->num_rows());
y_b.resize(a_->num_rows());
for (int i = 0; i < a_->num_cols(); ++i) {
Vector x = Vector::Zero(a_->num_cols());
x[i] = 1.0;
y_a.setZero();
y_b.setZero();
a_->RightMultiplyAndAccumulate(x.data(), y_a.data());
b_->RightMultiplyAndAccumulate(x.data(), y_b.data());
EXPECT_LT((y_a - y_b).norm(), 1e-12);
}
}
TEST(BlockSparseMatrix, CreateDiagonalMatrix) {
std::vector<Block> column_blocks;
column_blocks.emplace_back(2, 0);
column_blocks.emplace_back(1, 2);
column_blocks.emplace_back(3, 3);
const int num_cols =
column_blocks.back().size + column_blocks.back().position;
Vector diagonal(num_cols);
for (int i = 0; i < num_cols; ++i) {
diagonal(i) = 2 * i * i + 1;
}
std::unique_ptr<BlockSparseMatrix> m(
BlockSparseMatrix::CreateDiagonalMatrix(diagonal.data(), column_blocks));
const CompressedRowBlockStructure* bs = m->block_structure();
EXPECT_EQ(bs->cols.size(), column_blocks.size());
for (int i = 0; i < column_blocks.size(); ++i) {
EXPECT_EQ(bs->cols[i].size, column_blocks[i].size);
EXPECT_EQ(bs->cols[i].position, column_blocks[i].position);
}
EXPECT_EQ(m->num_rows(), m->num_cols());
Vector x = Vector::Ones(num_cols);
Vector y = Vector::Zero(num_cols);
m->RightMultiplyAndAccumulate(x.data(), y.data());
for (int i = 0; i < num_cols; ++i) {
EXPECT_NEAR(y[i], diagonal[i], std::numeric_limits<double>::epsilon());
}
}
TEST(BlockSparseMatrix, ToDenseMatrix) {
{
std::unique_ptr<BlockSparseMatrix> m = CreateTestMatrixFromId(0);
Matrix m_dense;
m->ToDenseMatrix(&m_dense);
EXPECT_EQ(m_dense.rows(), 4);
EXPECT_EQ(m_dense.cols(), 6);
Matrix m_expected(4, 6);
m_expected << 1, 2, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 5, 6, 7, 0, 0, 0, 8,
9, 10, 0;
EXPECT_EQ(m_dense, m_expected);
}
{
std::unique_ptr<BlockSparseMatrix> m = CreateTestMatrixFromId(1);
Matrix m_dense;
m->ToDenseMatrix(&m_dense);
EXPECT_EQ(m_dense.rows(), 3);
EXPECT_EQ(m_dense.cols(), 6);
Matrix m_expected(3, 6);
m_expected << 1, 2, 0, 5, 6, 0, 3, 4, 0, 7, 8, 0, 0, 0, 9, 0, 0, 0;
EXPECT_EQ(m_dense, m_expected);
}
}
TEST(BlockSparseMatrix, ToCRSMatrix) {
{
std::unique_ptr<BlockSparseMatrix> m = CreateTestMatrixFromId(0);
CompressedRowSparseMatrix m_crs(
m->num_rows(), m->num_cols(), m->num_nonzeros());
m->ToCompressedRowSparseMatrix(&m_crs);
std::vector<int> rows_expected = {0, 2, 4, 7, 10};
std::vector<int> cols_expected = {0, 1, 0, 1, 2, 3, 4, 2, 3, 4};
std::vector<double> values_expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i = 0; i < rows_expected.size(); ++i) {
EXPECT_EQ(m_crs.rows()[i], rows_expected[i]);
}
for (int i = 0; i < cols_expected.size(); ++i) {
EXPECT_EQ(m_crs.cols()[i], cols_expected[i]);
}
for (int i = 0; i < values_expected.size(); ++i) {
EXPECT_EQ(m_crs.values()[i], values_expected[i]);
}
}
{
std::unique_ptr<BlockSparseMatrix> m = CreateTestMatrixFromId(1);
CompressedRowSparseMatrix m_crs(
m->num_rows(), m->num_cols(), m->num_nonzeros());
m->ToCompressedRowSparseMatrix(&m_crs);
std::vector<int> rows_expected = {0, 4, 8, 9};
std::vector<int> cols_expected = {0, 1, 3, 4, 0, 1, 3, 4, 2};
std::vector<double> values_expected = {1, 2, 5, 6, 3, 4, 7, 8, 9};
for (int i = 0; i < rows_expected.size(); ++i) {
EXPECT_EQ(m_crs.rows()[i], rows_expected[i]);
}
for (int i = 0; i < cols_expected.size(); ++i) {
EXPECT_EQ(m_crs.cols()[i], cols_expected[i]);
}
for (int i = 0; i < values_expected.size(); ++i) {
EXPECT_EQ(m_crs.values()[i], values_expected[i]);
}
}
}
TEST(BlockSparseMatrix, CreateTranspose) {
constexpr int kNumtrials = 10;
BlockSparseMatrix::RandomMatrixOptions options;
options.num_col_blocks = 10;
options.min_col_block_size = 1;
options.max_col_block_size = 3;
options.num_row_blocks = 20;
options.min_row_block_size = 1;
options.max_row_block_size = 4;
options.block_density = 0.25;
std::mt19937 prng;
for (int trial = 0; trial < kNumtrials; ++trial) {
auto a = BlockSparseMatrix::CreateRandomMatrix(options, prng);
auto ap_bs = std::make_unique<CompressedRowBlockStructure>();
*ap_bs = *a->block_structure();
BlockSparseMatrix ap(ap_bs.release());
std::copy_n(a->values(), a->num_nonzeros(), ap.mutable_values());
Vector x = Vector::Random(a->num_cols());
Vector y = Vector::Random(a->num_rows());
Vector a_x = Vector::Zero(a->num_rows());
Vector a_t_y = Vector::Zero(a->num_cols());
Vector ap_x = Vector::Zero(a->num_rows());
Vector ap_t_y = Vector::Zero(a->num_cols());
a->RightMultiplyAndAccumulate(x.data(), a_x.data());
ap.RightMultiplyAndAccumulate(x.data(), ap_x.data());
EXPECT_NEAR((a_x - ap_x).norm() / a_x.norm(),
0.0,
std::numeric_limits<double>::epsilon());
a->LeftMultiplyAndAccumulate(y.data(), a_t_y.data());
ap.LeftMultiplyAndAccumulate(y.data(), ap_t_y.data());
EXPECT_NEAR((a_t_y - ap_t_y).norm() / a_t_y.norm(),
0.0,
std::numeric_limits<double>::epsilon());
}
}
} // namespace internal
} // namespace ceres