Add power series expansion preconditioner
Implementation of "Power Bundle Adjustment for Large-Scale 3D
Reconstruction" by Weber et. al. added in the form of preconditioner.
Change-Id: Ie85526a5fc46f74256f6dfe9173c3571f7160f3a
diff --git a/include/ceres/types.h b/include/ceres/types.h
index c1e973a..52e1e7b 100644
--- a/include/ceres/types.h
+++ b/include/ceres/types.h
@@ -97,7 +97,7 @@
// Block diagonal of the Gauss-Newton Hessian.
JACOBI,
- // Note: The following three preconditioners can only be used with
+ // Note: The following four preconditioners can only be used with
// the ITERATIVE_SCHUR solver. They are well suited for Structure
// from Motion problems.
@@ -105,6 +105,13 @@
// only be used with the ITERATIVE_SCHUR solver.
SCHUR_JACOBI,
+ // Use power series expansion to approximate the inversion of Schur complement
+ // as a preconditioner.
+ // WARNING! Application of this preconditioner currently is not integrated
+ // into linear solvers, so failure to use it via public API is expected
+ // behaviour.
+ SCHUR_POWER_SERIES_EXPANSION,
+
// Visibility clustering based preconditioners.
//
// The following two preconditioners use the visibility structure of
diff --git a/internal/ceres/CMakeLists.txt b/internal/ceres/CMakeLists.txt
index e4ef4c0..afe91fb 100644
--- a/internal/ceres/CMakeLists.txt
+++ b/internal/ceres/CMakeLists.txt
@@ -237,6 +237,7 @@
parameter_block_ordering.cc
partitioned_matrix_view.cc
polynomial.cc
+ power_series_expansion_preconditioner.cc
preconditioner.cc
preprocessor.cc
problem_impl.cc
@@ -529,6 +530,7 @@
ceres_test(parameter_dims)
ceres_test(partitioned_matrix_view)
ceres_test(polynomial)
+ ceres_test(power_series_expansion_preconditioner)
ceres_test(problem)
ceres_test(program)
ceres_test(reorder_program)
diff --git a/internal/ceres/implicit_schur_complement.cc b/internal/ceres/implicit_schur_complement.cc
index 751612f..4e837f3 100644
--- a/internal/ceres/implicit_schur_complement.cc
+++ b/internal/ceres/implicit_schur_complement.cc
@@ -60,7 +60,8 @@
// E'E and F'E.
if (block_diagonal_EtE_inverse_ == nullptr) {
block_diagonal_EtE_inverse_ = A_->CreateBlockDiagonalEtE();
- if (options_.preconditioner_type == JACOBI) {
+ if (options_.preconditioner_type == JACOBI ||
+ options_.preconditioner_type == SCHUR_POWER_SERIES_EXPANSION) {
block_diagonal_FtF_inverse_ = A_->CreateBlockDiagonalFtF();
}
rhs_.resize(A_->num_cols_f());
@@ -71,7 +72,8 @@
tmp_f_cols_.resize(A_->num_cols_f());
} else {
A_->UpdateBlockDiagonalEtE(block_diagonal_EtE_inverse_.get());
- if (options_.preconditioner_type == JACOBI) {
+ if (options_.preconditioner_type == JACOBI ||
+ options_.preconditioner_type == SCHUR_POWER_SERIES_EXPANSION) {
A_->UpdateBlockDiagonalFtF(block_diagonal_FtF_inverse_.get());
}
}
@@ -80,7 +82,8 @@
// contributions from the diagonal D if it is non-null. Add that to
// the block diagonals and invert them.
AddDiagonalAndInvert(D_, block_diagonal_EtE_inverse_.get());
- if (options_.preconditioner_type == JACOBI) {
+ if (options_.preconditioner_type == JACOBI ||
+ options_.preconditioner_type == SCHUR_POWER_SERIES_EXPANSION) {
AddDiagonalAndInvert((D_ == nullptr) ? nullptr : D_ + A_->num_cols_e(),
block_diagonal_FtF_inverse_.get());
}
@@ -129,6 +132,33 @@
A_->LeftMultiplyAndAccumulateF(tmp_rows_.data(), y);
}
+void ImplicitSchurComplement::InversePowerSeriesOperatorRightMultiplyAccumulate(
+ const double* x, double* y) const {
+ // y1 = F x
+ tmp_rows_.setZero();
+ A_->RightMultiplyAndAccumulateF(x, tmp_rows_.data());
+
+ // y2 = E' y1
+ tmp_e_cols_.setZero();
+ A_->LeftMultiplyAndAccumulateE(tmp_rows_.data(), tmp_e_cols_.data());
+
+ // y3 = (E'E)^-1 y2
+ tmp_e_cols_2_.setZero();
+ block_diagonal_EtE_inverse_->RightMultiplyAndAccumulate(tmp_e_cols_.data(),
+ tmp_e_cols_2_.data());
+ // y1 = E y3
+ tmp_rows_.setZero();
+ A_->RightMultiplyAndAccumulateE(tmp_e_cols_2_.data(), tmp_rows_.data());
+
+ // y4 = F' y1
+ tmp_f_cols_.setZero();
+ A_->LeftMultiplyAndAccumulateF(tmp_rows_.data(), tmp_f_cols_.data());
+
+ // y += (F'F)^-1 y4
+ block_diagonal_FtF_inverse_->RightMultiplyAndAccumulate(tmp_f_cols_.data(),
+ y);
+}
+
// Given a block diagonal matrix and an optional array of diagonal
// entries D, add them to the diagonal of the matrix and compute the
// inverse of each diagonal block.
diff --git a/internal/ceres/implicit_schur_complement.h b/internal/ceres/implicit_schur_complement.h
index 8fcc309..435ddda 100644
--- a/internal/ceres/implicit_schur_complement.h
+++ b/internal/ceres/implicit_schur_complement.h
@@ -122,6 +122,12 @@
RightMultiplyAndAccumulate(x, y);
}
+ // Following is useful for approximation of S^-1 via power series expansion.
+ // Z = (F'F)^-1 F'E (E'E)^-1 E'F
+ // y += Zx
+ void InversePowerSeriesOperatorRightMultiplyAccumulate(const double* x,
+ double* y) const;
+
// y = (E'E)^-1 (E'b - E'F x). Given an estimate of the solution to
// the Schur complement system, this method computes the value of
// the e_block variables that were eliminated to form the Schur
diff --git a/internal/ceres/implicit_schur_complement_test.cc b/internal/ceres/implicit_schur_complement_test.cc
index 0ebde31..66c6707 100644
--- a/internal/ceres/implicit_schur_complement_test.cc
+++ b/internal/ceres/implicit_schur_complement_test.cc
@@ -136,17 +136,38 @@
ImplicitSchurComplement isc(options);
isc.Init(*A_, D, b_.get());
- int num_sc_cols = lhs.cols();
+ const int num_f_cols = lhs.cols();
+ const int num_e_cols = num_cols_ - num_f_cols;
- for (int i = 0; i < num_sc_cols; ++i) {
- Vector x(num_sc_cols);
+ Matrix A_dense, E, F, DE, DF;
+ A_->ToDenseMatrix(&A_dense);
+ E = A_dense.leftCols(A_->num_cols() - num_f_cols);
+ F = A_dense.rightCols(num_f_cols);
+ if (D) {
+ DE = VectorRef(D, num_e_cols).asDiagonal();
+ DF = VectorRef(D + num_e_cols, num_f_cols).asDiagonal();
+ } else {
+ DE = Matrix::Zero(num_e_cols, num_e_cols);
+ DF = Matrix::Zero(num_f_cols, num_f_cols);
+ }
+
+ // Z = (block_diagonal(F'F))^-1 F'E (E'E)^-1 E'F
+ // Here, assuming that block_diagonal(F'F) == diagonal(F'F)
+ Matrix Z_reference =
+ (F.transpose() * F + DF).diagonal().asDiagonal().inverse() *
+ F.transpose() * E * (E.transpose() * E + DE).inverse() *
+ E.transpose() * F;
+
+ for (int i = 0; i < num_f_cols; ++i) {
+ Vector x(num_f_cols);
x.setZero();
x(i) = 1.0;
- Vector y(num_sc_cols);
+ Vector y(num_f_cols);
y = lhs * x;
- Vector z(num_sc_cols);
+
+ Vector z(num_f_cols);
isc.RightMultiplyAndAccumulate(x.data(), z.data());
// The i^th column of the implicit schur complement is the same as
@@ -157,6 +178,22 @@
<< "column " << i << ". explicit: " << y.transpose()
<< " implicit: " << z.transpose();
}
+
+ y.setZero();
+ y = Z_reference * x;
+ z.setZero();
+ isc.InversePowerSeriesOperatorRightMultiplyAccumulate(x.data(), z.data());
+
+ // The i^th column of operator Z stored implicitly is the same as its
+ // explicit version.
+ if ((y - z).norm() > kEpsilon) {
+ return testing::AssertionFailure()
+ << "Explicit and Implicit operators used to approximate the "
+ "inversion of schur complement via power series expansion "
+ "differ in column "
+ << i << ". explicit: " << y.transpose()
+ << " implicit: " << z.transpose();
+ }
}
// Compare the rhs of the reduced linear system
diff --git a/internal/ceres/linear_least_squares_problems.cc b/internal/ceres/linear_least_squares_problems.cc
index d0262e3..b8633aa 100644
--- a/internal/ceres/linear_least_squares_problems.cc
+++ b/internal/ceres/linear_least_squares_problems.cc
@@ -61,6 +61,8 @@
return LinearLeastSquaresProblem3();
case 4:
return LinearLeastSquaresProblem4();
+ case 5:
+ return LinearLeastSquaresProblem5();
default:
LOG(FATAL) << "Unknown problem id requested " << id;
}
@@ -629,6 +631,183 @@
return problem;
}
+/*
+A problem with block-diagonal F'F.
+
+ A = [1 0 | 0 0 2
+ 3 0 | 0 0 4
+ 0 -1 | 0 1 0
+ 0 -3 | 0 1 0
+ 0 -1 | 3 0 0
+ 0 -2 | 1 0 0]
+
+ b = [0
+ 1
+ 2
+ 3
+ 4
+ 5]
+
+ c = A'* b = [ 22
+ -25
+ 17
+ 7
+ 4]
+
+ A'A = [10 0 0 0 10
+ 0 15 -5 -4 0
+ 0 -5 10 0 0
+ 0 -4 0 2 0
+ 10 0 0 0 20]
+
+ cond(A'A) = 41.402
+
+ S = [ 8.3333 -1.3333 0
+ -1.3333 0.9333 0
+ 0 0 10.0000]
+
+ r = [ 8.6667
+ -1.6667
+ 1.0000]
+
+ S\r = [ 0.9778
+ -0.3889
+ 0.1000]
+
+ A\b = [ 0.2
+ -1.4444
+ 0.9777
+ -0.3888
+ 0.1]
+*/
+
+std::unique_ptr<LinearLeastSquaresProblem> LinearLeastSquaresProblem5() {
+ int num_rows = 6;
+ int num_cols = 5;
+
+ std::unique_ptr<LinearLeastSquaresProblem> problem =
+ std::make_unique<LinearLeastSquaresProblem>();
+
+ problem->b = std::make_unique<double[]>(num_rows);
+ problem->D = std::make_unique<double[]>(num_cols);
+ problem->num_eliminate_blocks = 2;
+
+ // TODO: add x
+ problem->x = std::make_unique<double[]>(num_cols);
+ problem->x[0] = 0.2;
+ problem->x[1] = -1.4444;
+ problem->x[2] = 0.9777;
+ problem->x[3] = -0.3888;
+ problem->x[4] = 0.1;
+
+ auto* bs = new CompressedRowBlockStructure;
+ std::unique_ptr<double[]> values =
+ std::make_unique<double[]>(num_rows * num_cols);
+
+ for (int c = 0; c < num_cols; ++c) {
+ bs->cols.emplace_back();
+ bs->cols.back().size = 1;
+ bs->cols.back().position = c;
+ }
+
+ int nnz = 0;
+
+ // Row 1
+ {
+ values[nnz++] = -1;
+ values[nnz++] = 2;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 0;
+ row.cells.emplace_back(0, 0);
+ row.cells.emplace_back(4, 1);
+ }
+
+ // Row 2
+ {
+ values[nnz++] = 3;
+ values[nnz++] = 4;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 1;
+ row.cells.emplace_back(0, 2);
+ row.cells.emplace_back(4, 3);
+ }
+
+ // Row 3
+ {
+ values[nnz++] = -1;
+ values[nnz++] = 1;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 2;
+ row.cells.emplace_back(1, 4);
+ row.cells.emplace_back(3, 5);
+ }
+
+ // Row 4
+ {
+ values[nnz++] = -3;
+ values[nnz++] = 1;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 3;
+ row.cells.emplace_back(1, 6);
+ row.cells.emplace_back(3, 7);
+ }
+
+ // Row 5
+ {
+ values[nnz++] = -1;
+ values[nnz++] = 3;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 4;
+ row.cells.emplace_back(1, 8);
+ row.cells.emplace_back(2, 9);
+ }
+
+ // Row 6
+ {
+ // values[nnz++] = 2;
+ values[nnz++] = -2;
+ values[nnz++] = 1;
+
+ bs->rows.emplace_back();
+ CompressedRow& row = bs->rows.back();
+ row.block.size = 1;
+ row.block.position = 5;
+ // row.cells.emplace_back(0, 10);
+ row.cells.emplace_back(1, 10);
+ row.cells.emplace_back(2, 11);
+ }
+
+ auto A = std::make_unique<BlockSparseMatrix>(bs);
+ memcpy(A->mutable_values(), values.get(), nnz * sizeof(*A->values()));
+
+ for (int i = 0; i < num_cols; ++i) {
+ problem->D.get()[i] = 1;
+ }
+
+ for (int i = 0; i < num_rows; ++i) {
+ problem->b.get()[i] = i;
+ }
+
+ problem->A = std::move(A);
+
+ return problem;
+}
+
namespace {
bool DumpLinearLeastSquaresProblemToConsole(const SparseMatrix* A,
const double* D,
diff --git a/internal/ceres/linear_least_squares_problems.h b/internal/ceres/linear_least_squares_problems.h
index 7968c46..0e14517 100644
--- a/internal/ceres/linear_least_squares_problems.h
+++ b/internal/ceres/linear_least_squares_problems.h
@@ -73,6 +73,8 @@
std::unique_ptr<LinearLeastSquaresProblem> LinearLeastSquaresProblem3();
CERES_NO_EXPORT
std::unique_ptr<LinearLeastSquaresProblem> LinearLeastSquaresProblem4();
+CERES_NO_EXPORT
+std::unique_ptr<LinearLeastSquaresProblem> LinearLeastSquaresProblem5();
// Write the linear least squares problem to disk. The exact format
// depends on dump_format_type.
diff --git a/internal/ceres/power_series_expansion_preconditioner.cc b/internal/ceres/power_series_expansion_preconditioner.cc
new file mode 100644
index 0000000..1b2dbb9
--- /dev/null
+++ b/internal/ceres/power_series_expansion_preconditioner.cc
@@ -0,0 +1,81 @@
+// Ceres Solver - A fast non-linear least squares minimizer
+// Copyright 2022 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: markshachkov@gmail.com (Mark Shachkov)
+
+#include "ceres/power_series_expansion_preconditioner.h"
+
+namespace ceres::internal {
+
+PowerSeriesExpansionPreconditioner::PowerSeriesExpansionPreconditioner(
+ const ImplicitSchurComplement* isc,
+ const double spse_tolerance,
+ const int min_num_iterations,
+ const int max_num_iterations)
+ : isc_(isc),
+ spse_tolerance_(spse_tolerance),
+ min_num_iterations_(min_num_iterations),
+ max_num_iterations_(max_num_iterations) {}
+
+PowerSeriesExpansionPreconditioner::~PowerSeriesExpansionPreconditioner() =
+ default;
+
+bool PowerSeriesExpansionPreconditioner::Update(const LinearOperator& A,
+ const double* D) {
+ return true;
+}
+
+void PowerSeriesExpansionPreconditioner::RightMultiplyAndAccumulate(
+ const double* x, double* y) const {
+ VectorRef yref(y, num_rows());
+ Vector series_term(num_rows());
+ Vector previous_series_term(num_rows());
+ yref.setZero();
+ isc_->block_diagonal_FtF_inverse()->RightMultiplyAndAccumulate(x, y);
+ previous_series_term = yref;
+
+ const double norm_threshold = spse_tolerance_ * yref.norm();
+
+ for (int i = 1;; i++) {
+ series_term.setZero();
+ isc_->InversePowerSeriesOperatorRightMultiplyAccumulate(
+ previous_series_term.data(), series_term.data());
+ yref += series_term;
+ if (i >= min_num_iterations_ &&
+ (i >= max_num_iterations_ || series_term.norm() < norm_threshold)) {
+ break;
+ }
+ std::swap(previous_series_term, series_term);
+ }
+}
+
+int PowerSeriesExpansionPreconditioner::num_rows() const {
+ return isc_->num_rows();
+}
+
+} // namespace ceres::internal
diff --git a/internal/ceres/power_series_expansion_preconditioner.h b/internal/ceres/power_series_expansion_preconditioner.h
new file mode 100644
index 0000000..c5cf32d
--- /dev/null
+++ b/internal/ceres/power_series_expansion_preconditioner.h
@@ -0,0 +1,70 @@
+// Ceres Solver - A fast non-linear least squares minimizer
+// Copyright 2022 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: markshachkov@gmail.com (Mark Shachkov)
+
+#ifndef CERES_INTERNAL_POWER_SERIES_EXPANSION_PRECONDITIONER_H_
+#define CERES_INTERNAL_POWER_SERIES_EXPANSION_PRECONDITIONER_H_
+
+#include "ceres/implicit_schur_complement.h"
+#include "ceres/internal/eigen.h"
+#include "ceres/internal/export.h"
+#include "ceres/preconditioner.h"
+
+namespace ceres::internal {
+
+// This is a preconditioner via power series expansion of Schur
+// complement inverse based on "Weber et al, Power Bundle Adjustment for
+// Large-Scale 3D Reconstruction".
+
+class CERES_NO_EXPORT PowerSeriesExpansionPreconditioner
+ : public Preconditioner {
+ public:
+ PowerSeriesExpansionPreconditioner(const ImplicitSchurComplement* isc,
+ const double spse_tolerance_,
+ const int min_num_iterations_,
+ const int max_num_iterations_);
+ PowerSeriesExpansionPreconditioner(
+ const PowerSeriesExpansionPreconditioner&) = delete;
+ void operator=(const PowerSeriesExpansionPreconditioner&) = delete;
+ ~PowerSeriesExpansionPreconditioner() override;
+
+ void RightMultiplyAndAccumulate(const double* x, double* y) const final;
+ bool Update(const LinearOperator& A, const double* D) final;
+ int num_rows() const final;
+
+ private:
+ const ImplicitSchurComplement* isc_;
+ const double spse_tolerance_;
+ const int min_num_iterations_;
+ const int max_num_iterations_;
+};
+
+} // namespace ceres::internal
+
+#endif // CERES_INTERNAL_POWER_SERIES_EXPANSION_PRECONDITIONER_H_
diff --git a/internal/ceres/power_series_expansion_preconditioner_test.cc b/internal/ceres/power_series_expansion_preconditioner_test.cc
new file mode 100644
index 0000000..a101831
--- /dev/null
+++ b/internal/ceres/power_series_expansion_preconditioner_test.cc
@@ -0,0 +1,173 @@
+// Ceres Solver - A fast non-linear least squares minimizer
+// Copyright 2022 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: markshachkov@gmail.com (Mark Shachkov)
+
+#include "ceres/power_series_expansion_preconditioner.h"
+
+#include "Eigen/Dense"
+#include "ceres/linear_least_squares_problems.h"
+#include "gtest/gtest.h"
+
+namespace ceres::internal {
+
+const double kEpsilon = 1e-14;
+
+class PowerSeriesExpansionPreconditionerTest : public ::testing::Test {
+ protected:
+ void SetUp() final {
+ problem_ = CreateLinearLeastSquaresProblemFromId(5);
+ const auto A = down_cast<BlockSparseMatrix*>(problem_->A.get());
+ const auto D = problem_->D.get();
+
+ LinearSolver::Options options;
+ options.elimination_groups.push_back(problem_->num_eliminate_blocks);
+ options.preconditioner_type = SCHUR_POWER_SERIES_EXPANSION;
+ isc_ = std::make_unique<ImplicitSchurComplement>(options);
+ isc_->Init(*A, D, problem_->b.get());
+ num_f_cols_ = isc_->rhs().rows();
+ const int num_rows = A->num_rows(), num_cols = A->num_cols(),
+ num_e_cols = num_cols - num_f_cols_;
+
+ // Using predefined linear operator with schur structure and block-diagonal
+ // F'F to explicitly construct schur complement and to calculate its inverse
+ // to be used as a reference.
+ Matrix A_dense, E, F, DE, DF;
+ problem_->A->ToDenseMatrix(&A_dense);
+ E = A_dense.leftCols(num_e_cols);
+ F = A_dense.rightCols(num_f_cols_);
+ DE = VectorRef(D, num_e_cols).asDiagonal();
+ DF = VectorRef(D + num_e_cols, num_f_cols_).asDiagonal();
+
+ sc_inverse_expected_ =
+ (F.transpose() *
+ (Matrix::Identity(num_rows, num_rows) -
+ E * (E.transpose() * E + DE).inverse() * E.transpose()) *
+ F +
+ DF)
+ .inverse();
+ }
+ std::unique_ptr<LinearLeastSquaresProblem> problem_;
+ std::unique_ptr<ImplicitSchurComplement> isc_;
+ int num_f_cols_;
+ Matrix sc_inverse_expected_;
+};
+
+TEST_F(PowerSeriesExpansionPreconditionerTest,
+ InverseValidPreconditionerToleranceReached) {
+ const double spse_tolerance = kEpsilon;
+ const int min_iterations = 1;
+ const int max_iterations = 50;
+ PowerSeriesExpansionPreconditioner preconditioner(
+ isc_.get(), spse_tolerance, min_iterations, max_iterations);
+
+ Vector x(num_f_cols_), y(num_f_cols_);
+ for (int i = 0; i < num_f_cols_; i++) {
+ x.setZero();
+ x(i) = 1.0;
+
+ y.setZero();
+ preconditioner.RightMultiplyAndAccumulate(x.data(), y.data());
+ EXPECT_LT((y - sc_inverse_expected_.col(i)).norm(), kEpsilon)
+ << "Reference Schur complement inverse and its estimate via "
+ "PowerSeriesExpansionPreconditioner differs in "
+ << i
+ << " column.\nreference : " << sc_inverse_expected_.col(i).transpose()
+ << "\nestimated: " << y.transpose();
+ }
+}
+
+TEST_F(PowerSeriesExpansionPreconditionerTest,
+ InverseValidPreconditionerMaxIterations) {
+ const double spse_tolerance = 1 / kEpsilon;
+ const int num_iterations = 50;
+ PowerSeriesExpansionPreconditioner preconditioner_fixed_n_iterations(
+ isc_.get(), spse_tolerance, num_iterations, num_iterations);
+
+ Vector x(num_f_cols_), y(num_f_cols_);
+ for (int i = 0; i < num_f_cols_; i++) {
+ x.setZero();
+ x(i) = 1.0;
+
+ y.setZero();
+ preconditioner_fixed_n_iterations.RightMultiplyAndAccumulate(x.data(),
+ y.data());
+ EXPECT_LT((y - sc_inverse_expected_.col(i)).norm(), kEpsilon)
+ << "Reference Schur complement inverse and its estimate via "
+ "PowerSeriesExpansionPreconditioner differs in "
+ << i
+ << " column.\nreference : " << sc_inverse_expected_.col(i).transpose()
+ << "\nestimated: " << y.transpose();
+ }
+}
+
+TEST_F(PowerSeriesExpansionPreconditionerTest,
+ InverseInvalidBadPreconditionerTolerance) {
+ const double spse_tolerance = 1 / kEpsilon;
+ const int min_iterations = 1;
+ const int max_iterations = 50;
+ PowerSeriesExpansionPreconditioner preconditioner_bad_tolerance(
+ isc_.get(), spse_tolerance, min_iterations, max_iterations);
+
+ Vector x(num_f_cols_), y(num_f_cols_);
+ for (int i = 0; i < num_f_cols_; i++) {
+ x.setZero();
+ x(i) = 1.0;
+
+ y.setZero();
+ preconditioner_bad_tolerance.RightMultiplyAndAccumulate(x.data(), y.data());
+ EXPECT_GT((y - sc_inverse_expected_.col(i)).norm(), kEpsilon)
+ << "Reference Schur complement inverse and its estimate via "
+ "PowerSeriesExpansionPreconditioner are too similar, tolerance "
+ "stopping criteria failed.";
+ }
+}
+
+TEST_F(PowerSeriesExpansionPreconditionerTest,
+ InverseInvalidBadPreconditionerMaxIterations) {
+ const double spse_tolerance = kEpsilon;
+ const int num_iterations = 1;
+ PowerSeriesExpansionPreconditioner preconditioner_bad_iterations_limit(
+ isc_.get(), spse_tolerance, num_iterations, num_iterations);
+
+ Vector x(num_f_cols_), y(num_f_cols_);
+ for (int i = 0; i < num_f_cols_; i++) {
+ x.setZero();
+ x(i) = 1.0;
+
+ y.setZero();
+ preconditioner_bad_iterations_limit.RightMultiplyAndAccumulate(x.data(),
+ y.data());
+ EXPECT_GT((y - sc_inverse_expected_.col(i)).norm(), kEpsilon)
+ << "Reference Schur complement inverse and its estimate via "
+ "PowerSeriesExpansionPreconditioner are too similar, maximum "
+ "iterations stopping criteria failed.";
+ }
+}
+
+} // namespace ceres::internal
\ No newline at end of file