Fix covariance truncation for wide Jacobians Count null_space_rank against the number of Jacobian columns so that zero modes omitted by the thin SVD are dropped before retained modes. Add a regression for a one-row, two-column Jacobian with nullity one. The existing covariance suite and the reported reproducer pass. Fixes #1214 Change-Id: I281baac50987a0a5780b6726f195c0b959f6747e
diff --git a/internal/ceres/covariance_impl.cc b/internal/ceres/covariance_impl.cc index b2449f9..411a11b 100644 --- a/internal/ceres/covariance_impl.cc +++ b/internal/ceres/covariance_impl.cc
@@ -747,8 +747,16 @@ sqrt(options_.min_reciprocal_condition_number); const bool automatic_truncation = (options_.null_space_rank < 0); + // The covariance is the pseudo-inverse of J'J, which is num_cols x + // num_cols, so null_space_rank counts its zero modes out of num_cols. + // For a wide Jacobian the thin SVD returns only num_rows singular + // values, and the other num_cols - num_rows modes of J'J are exactly + // zero and never inverted. Keeping at most num_cols - null_space_rank + // of the computed modes therefore drops at least null_space_rank modes + // in total. For square and tall Jacobians, num_singular_values == + // num_cols, so this is num_singular_values - null_space_rank. const int max_rank = std::min(num_singular_values, - num_singular_values - options_.null_space_rank); + jacobian.num_cols - options_.null_space_rank); // Compute the squared inverse of the singular values. Truncate the // computation based on min_singular_value_ratio and
diff --git a/internal/ceres/covariance_test.cc b/internal/ceres/covariance_test.cc index 77aca2e..2a9d9b8 100644 --- a/internal/ceres/covariance_test.cc +++ b/internal/ceres/covariance_test.cc
@@ -982,6 +982,27 @@ } } +TEST(Covariance, TruncatedRankWithWideJacobian) { + double x[2] = {0.0, 0.0}; + const double jacobian[] = {1.0, 1.0}; + Problem problem; + problem.AddResidualBlock(new UnaryCostFunction(1, 2, jacobian), nullptr, x); + + Covariance::Options options; + options.algorithm_type = DENSE_SVD; + options.null_space_rank = 1; + Covariance covariance(options); + ASSERT_TRUE(covariance.Compute(std::vector<const double*>{x}, &problem)); + + // J'J has eigenvalues 2 and 0. Dropping the zero mode leaves a covariance + // matrix with 1/4 in each entry. + double covariance_block[4]; + ASSERT_TRUE(covariance.GetCovarianceBlock(x, x, covariance_block)); + for (double value : covariance_block) { + EXPECT_NEAR(value, 0.25, 1e-14); + } +} + TEST_F(CovarianceTest, DenseCovarianceMatrixFromSetOfParameters) { Covariance::Options options; Covariance covariance(options);