| // Ceres Solver - A fast non-linear least squares minimizer | |
| // Copyright 2024 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/sparse_cholesky.h" | |
| #include <limits> | |
| #include <memory> | |
| #include <random> | |
| #include <sstream> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "Eigen/Cholesky" | |
| #include "Eigen/Core" | |
| #include "Eigen/Dense" | |
| #include "absl/log/check.h" | |
| #include "ceres/block_sparse_matrix.h" | |
| #include "ceres/compressed_row_sparse_matrix.h" | |
| #include "ceres/cuda_sparse_cholesky.h" | |
| #include "ceres/inner_product_computer.h" | |
| #include "ceres/internal/config.h" | |
| #include "ceres/internal/eigen.h" | |
| #include "ceres/iterative_refiner.h" | |
| #include "ceres/linear_solver.h" | |
| #include "ceres/types.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| namespace ceres::internal { | |
| namespace { | |
| std::unique_ptr<BlockSparseMatrix> CreateRandomFullRankMatrix( | |
| const int num_col_blocks, | |
| const int min_col_block_size, | |
| const int max_col_block_size, | |
| const double block_density, | |
| std::mt19937& prng) { | |
| // Create a random matrix | |
| BlockSparseMatrix::RandomMatrixOptions options; | |
| options.num_col_blocks = num_col_blocks; | |
| options.min_col_block_size = min_col_block_size; | |
| options.max_col_block_size = max_col_block_size; | |
| options.num_row_blocks = 2 * num_col_blocks; | |
| options.min_row_block_size = 1; | |
| options.max_row_block_size = max_col_block_size; | |
| options.block_density = block_density; | |
| auto random_matrix = BlockSparseMatrix::CreateRandomMatrix(options, prng); | |
| // Add a diagonal block sparse matrix to make it full rank. | |
| Vector diagonal = Vector::Ones(random_matrix->num_cols()); | |
| auto block_diagonal = BlockSparseMatrix::CreateDiagonalMatrix( | |
| diagonal.data(), random_matrix->block_structure()->cols); | |
| random_matrix->AppendRows(*block_diagonal); | |
| return random_matrix; | |
| } | |
| bool ComputeExpectedSolution(const CompressedRowSparseMatrix& lhs, | |
| const Vector& rhs, | |
| Vector* solution) { | |
| Matrix eigen_lhs; | |
| lhs.ToDenseMatrix(&eigen_lhs); | |
| if (lhs.storage_type() == | |
| CompressedRowSparseMatrix::StorageType::UPPER_TRIANGULAR) { | |
| Matrix full_lhs = eigen_lhs.selfadjointView<Eigen::Upper>(); | |
| Eigen::LLT<Matrix, Eigen::Upper> llt = | |
| eigen_lhs.selfadjointView<Eigen::Upper>().llt(); | |
| if (llt.info() != Eigen::Success) { | |
| return false; | |
| } | |
| *solution = llt.solve(rhs); | |
| return (llt.info() == Eigen::Success); | |
| } | |
| Matrix full_lhs = eigen_lhs.selfadjointView<Eigen::Lower>(); | |
| Eigen::LLT<Matrix, Eigen::Lower> llt = | |
| eigen_lhs.selfadjointView<Eigen::Lower>().llt(); | |
| if (llt.info() != Eigen::Success) { | |
| return false; | |
| } | |
| *solution = llt.solve(rhs); | |
| return (llt.info() == Eigen::Success); | |
| } | |
| void SparseCholeskySolverUnitTest( | |
| const SparseLinearAlgebraLibraryType sparse_linear_algebra_library_type, | |
| const bool use_single_precision, | |
| const OrderingType ordering_type, | |
| const bool use_block_structure, | |
| const int num_blocks, | |
| const int min_block_size, | |
| const int max_block_size, | |
| const double block_density, | |
| std::mt19937& prng) { | |
| LinearSolver::Options sparse_cholesky_options; | |
| #ifndef CERES_NO_CUDSS | |
| ContextImpl context; | |
| sparse_cholesky_options.context = &context; | |
| std::string error; | |
| CHECK(context.InitCuda(&error)) << error; | |
| #endif // CERES_NO_CUDSS | |
| sparse_cholesky_options.sparse_linear_algebra_library_type = | |
| sparse_linear_algebra_library_type; | |
| sparse_cholesky_options.ordering_type = ordering_type; | |
| sparse_cholesky_options.max_num_refinement_iterations = 0; | |
| sparse_cholesky_options.use_mixed_precision_solves = use_single_precision; | |
| auto sparse_cholesky = SparseCholesky::Create(sparse_cholesky_options); | |
| const CompressedRowSparseMatrix::StorageType storage_type = | |
| sparse_cholesky->StorageType(); | |
| auto m = CreateRandomFullRankMatrix( | |
| num_blocks, min_block_size, max_block_size, block_density, prng); | |
| auto inner_product_computer = InnerProductComputer::Create(*m, storage_type); | |
| inner_product_computer->Compute(); | |
| CompressedRowSparseMatrix* lhs = inner_product_computer->mutable_result(); | |
| if (!use_block_structure) { | |
| lhs->mutable_row_blocks()->clear(); | |
| lhs->mutable_col_blocks()->clear(); | |
| } | |
| Vector rhs = Vector::Random(lhs->num_rows()); | |
| Vector expected(lhs->num_rows()); | |
| Vector actual(lhs->num_rows()); | |
| EXPECT_TRUE(ComputeExpectedSolution(*lhs, rhs, &expected)); | |
| std::string message; | |
| EXPECT_EQ( | |
| sparse_cholesky->FactorAndSolve(lhs, rhs.data(), actual.data(), &message), | |
| LinearSolverTerminationType::SUCCESS); | |
| Matrix eigen_lhs; | |
| lhs->ToDenseMatrix(&eigen_lhs); | |
| const double kTolerance = | |
| (use_single_precision ? std::numeric_limits<float>::epsilon() | |
| : std::numeric_limits<double>::epsilon()) * | |
| 20; | |
| EXPECT_NEAR((actual - expected).norm() / actual.norm(), 0.0, kTolerance) | |
| << "\n" | |
| << eigen_lhs; | |
| } | |
| // SparseLinearAlgebraLibraryType | |
| // FLOAT/DOUBLE | |
| // OrderingType | |
| // BlockStructure | |
| using Param = | |
| ::testing::tuple<SparseLinearAlgebraLibraryType, bool, OrderingType, bool>; | |
| std::string ParamInfoToString(testing::TestParamInfo<Param> info) { | |
| Param param = info.param; | |
| std::stringstream ss; | |
| ss << SparseLinearAlgebraLibraryTypeToString(::testing::get<0>(param)) << "_" | |
| << (::testing::get<1>(param) ? "FLOAT" : "DOUBLE") << "_" | |
| << ::testing::get<2>(param) << "_" | |
| << (::testing::get<3>(param) ? "UseBlockStructure" : "NoBlockStructure"); | |
| return ss.str(); | |
| } | |
| } // namespace | |
| class SparseCholeskyTest : public ::testing::TestWithParam<Param> {}; | |
| TEST_P(SparseCholeskyTest, FactorAndSolve) { | |
| constexpr int kMinNumBlocks = 1; | |
| constexpr int kMaxNumBlocks = 10; | |
| constexpr int kNumTrials = 10; | |
| constexpr int kMinBlockSize = 1; | |
| constexpr int kMaxBlockSize = 5; | |
| Param param = GetParam(); | |
| std::mt19937 prng; | |
| std::uniform_real_distribution<double> distribution(0.1, 1.0); | |
| for (int num_blocks = kMinNumBlocks; num_blocks < kMaxNumBlocks; | |
| ++num_blocks) { | |
| for (int trial = 0; trial < kNumTrials; ++trial) { | |
| const double block_density = distribution(prng); | |
| SparseCholeskySolverUnitTest(::testing::get<0>(param), | |
| ::testing::get<1>(param), | |
| ::testing::get<2>(param), | |
| ::testing::get<3>(param), | |
| num_blocks, | |
| kMinBlockSize, | |
| kMaxBlockSize, | |
| block_density, | |
| prng); | |
| } | |
| } | |
| } | |
| namespace { | |
| #ifndef CERES_NO_SUITESPARSE | |
| const auto SuiteSparseCholeskyParameters = ::testing::Combine( | |
| ::testing::Values(SUITE_SPARSE), | |
| #if defined(CERES_NO_CHOLMOD_FLOAT) | |
| ::testing::Values(false), | |
| #else | |
| ::testing::Values(false, true), | |
| #endif // defined(CERES_NO_CHOLMOD_FLOAT) | |
| #if defined(CERES_NO_CHOLMOD_PARTITION) | |
| ::testing::Values(OrderingType::AMD, OrderingType::NATURAL), | |
| #else | |
| ::testing::Values( | |
| OrderingType::AMD, OrderingType::NESDIS, OrderingType::NATURAL), | |
| #endif // defined(CERES_NO_CHOLMOD_PARTITION) | |
| ::testing::Values(true, false)); | |
| INSTANTIATE_TEST_SUITE_P(SuiteSparseCholesky, | |
| SparseCholeskyTest, | |
| SuiteSparseCholeskyParameters, | |
| ParamInfoToString); | |
| #endif // !defined(CERES_NO_SUITESPARSE) | |
| #ifndef CERES_NO_ACCELERATE_SPARSE | |
| INSTANTIATE_TEST_SUITE_P( | |
| AccelerateSparseCholesky, | |
| SparseCholeskyTest, | |
| ::testing::Combine(::testing::Values(ACCELERATE_SPARSE), | |
| ::testing::Values(false, true), | |
| ::testing::Values(OrderingType::AMD, | |
| OrderingType::NESDIS, | |
| OrderingType::NATURAL), | |
| ::testing::Values(true, false)), | |
| ParamInfoToString); | |
| #endif | |
| #ifdef CERES_USE_EIGEN_SPARSE | |
| const auto EigenSparseCholeskyParameters = ::testing::Combine( | |
| ::testing::Values(EIGEN_SPARSE), | |
| ::testing::Values(false, true), | |
| #if defined(CERES_NO_EIGEN_METIS) | |
| ::testing::Values(OrderingType::AMD, OrderingType::NATURAL), | |
| #else | |
| ::testing::Values( | |
| OrderingType::AMD, OrderingType::NATURAL, OrderingType::NESDIS), | |
| #endif // defined(CERES_NO_EIGEN_METIS) | |
| ::testing::Values(true, false)); | |
| INSTANTIATE_TEST_SUITE_P(EigenSparseCholesky, | |
| SparseCholeskyTest, | |
| EigenSparseCholeskyParameters, | |
| ParamInfoToString); | |
| #endif // CERES_USE_EIGEN_SPARSE | |
| #ifndef CERES_NO_CUDSS | |
| INSTANTIATE_TEST_SUITE_P( | |
| CudaCholesky, | |
| SparseCholeskyTest, | |
| ::testing::Combine(::testing::Values(CUDA_SPARSE), | |
| ::testing::Values(false, true), | |
| ::testing::Values(OrderingType::AMD), | |
| ::testing::Values(true, false)), | |
| ParamInfoToString); | |
| #endif // CERES_NO_CUDSS | |
| class MockSparseCholesky : public SparseCholesky { | |
| public: | |
| MOCK_METHOD(CompressedRowSparseMatrix::StorageType, | |
| StorageType, | |
| (), | |
| (const, override)); | |
| MOCK_METHOD(LinearSolverTerminationType, | |
| Factorize, | |
| (CompressedRowSparseMatrix * lhs, std::string* message), | |
| (override)); | |
| MOCK_METHOD(LinearSolverTerminationType, | |
| Solve, | |
| (const double* rhs, double* solution, std::string* message), | |
| (override)); | |
| }; | |
| class MockSparseIterativeRefiner : public SparseIterativeRefiner { | |
| public: | |
| MockSparseIterativeRefiner() : SparseIterativeRefiner(1) {} | |
| MOCK_METHOD(void, | |
| Refine, | |
| (const SparseMatrix& lhs, | |
| const double* rhs, | |
| SparseCholesky* sparse_cholesky, | |
| double* solution), | |
| (override)); | |
| }; | |
| using testing::_; | |
| using testing::Return; | |
| TEST(RefinedSparseCholesky, StorageType) { | |
| auto sparse_cholesky = std::make_unique<MockSparseCholesky>(); | |
| auto iterative_refiner = std::make_unique<MockSparseIterativeRefiner>(); | |
| EXPECT_CALL(*sparse_cholesky, StorageType()) | |
| .WillOnce( | |
| Return(CompressedRowSparseMatrix::StorageType::UPPER_TRIANGULAR)); | |
| EXPECT_CALL(*iterative_refiner, Refine(_, _, _, _)).Times(0); | |
| RefinedSparseCholesky refined_sparse_cholesky(std::move(sparse_cholesky), | |
| std::move(iterative_refiner)); | |
| EXPECT_EQ(refined_sparse_cholesky.StorageType(), | |
| CompressedRowSparseMatrix::StorageType::UPPER_TRIANGULAR); | |
| } | |
| TEST(RefinedSparseCholesky, Factorize) { | |
| auto sparse_cholesky = std::make_unique<MockSparseCholesky>(); | |
| auto iterative_refiner = std::make_unique<MockSparseIterativeRefiner>(); | |
| EXPECT_CALL(*sparse_cholesky, Factorize(_, _)) | |
| .WillOnce(Return(LinearSolverTerminationType::SUCCESS)); | |
| EXPECT_CALL(*iterative_refiner, Refine(_, _, _, _)).Times(0); | |
| RefinedSparseCholesky refined_sparse_cholesky(std::move(sparse_cholesky), | |
| std::move(iterative_refiner)); | |
| CompressedRowSparseMatrix m(1, 1, 1); | |
| std::string message; | |
| EXPECT_EQ(refined_sparse_cholesky.Factorize(&m, &message), | |
| LinearSolverTerminationType::SUCCESS); | |
| } | |
| TEST(RefinedSparseCholesky, FactorAndSolveWithUnsuccessfulFactorization) { | |
| auto sparse_cholesky = std::make_unique<MockSparseCholesky>(); | |
| auto iterative_refiner = std::make_unique<MockSparseIterativeRefiner>(); | |
| EXPECT_CALL(*sparse_cholesky, Factorize(_, _)) | |
| .WillOnce(Return(LinearSolverTerminationType::FAILURE)); | |
| EXPECT_CALL(*sparse_cholesky, Solve(_, _, _)).Times(0); | |
| EXPECT_CALL(*iterative_refiner, Refine(_, _, _, _)).Times(0); | |
| RefinedSparseCholesky refined_sparse_cholesky(std::move(sparse_cholesky), | |
| std::move(iterative_refiner)); | |
| CompressedRowSparseMatrix m(1, 1, 1); | |
| std::string message; | |
| double rhs; | |
| double solution; | |
| EXPECT_EQ( | |
| refined_sparse_cholesky.FactorAndSolve(&m, &rhs, &solution, &message), | |
| LinearSolverTerminationType::FAILURE); | |
| } | |
| TEST(RefinedSparseCholesky, FactorAndSolveWithSuccess) { | |
| auto sparse_cholesky = std::make_unique<MockSparseCholesky>(); | |
| auto iterative_refiner = std::make_unique<MockSparseIterativeRefiner>(); | |
| EXPECT_CALL(*sparse_cholesky, Factorize(_, _)) | |
| .WillOnce(Return(LinearSolverTerminationType::SUCCESS)); | |
| EXPECT_CALL(*sparse_cholesky, Solve(_, _, _)) | |
| .WillOnce(Return(LinearSolverTerminationType::SUCCESS)); | |
| EXPECT_CALL(*iterative_refiner, Refine(_, _, _, _)).Times(1); | |
| RefinedSparseCholesky refined_sparse_cholesky(std::move(sparse_cholesky), | |
| std::move(iterative_refiner)); | |
| CompressedRowSparseMatrix m(1, 1, 1); | |
| std::string message; | |
| double rhs; | |
| double solution; | |
| EXPECT_EQ( | |
| refined_sparse_cholesky.FactorAndSolve(&m, &rhs, &solution, &message), | |
| LinearSolverTerminationType::SUCCESS); | |
| } | |
| } // namespace | |
| } // namespace ceres::internal |