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);