Preserve caller orderings during preprocessing Trust-region preprocessing removes constant blocks from parameter orderings. Solver::Options copies share these objects. Removal therefore changes the caller's ordering, and a later Solve can reject it. Copy supplied linear solver and inner iteration orderings before preprocessing mutates them. Keep automatic ordering creation unchanged. Add public Solve tests for both supplied orderings. The linear solver test makes a constant block variable between calls. The inner iteration test checks caller group membership after both calls. Tests: focused regressions 2/2 and affected CTest targets 3/3. Full build passed. Full CTest passed 139/191; 52 bundle tests could not find source data from the out-of-tree build. Each of those 52 tests passed with TEST_SRCDIR set to internal/ceres within the checkout. Tested on arm64 macOS with Apple Clang 21.0.0. Issue: https://github.com/ceres-solver/ceres-solver/issues/836 Codex used. Change-Id: Ia0df3722c0905715cf37677621933040d18c1fa6
diff --git a/internal/ceres/solver_test.cc b/internal/ceres/solver_test.cc index f6fd9c7..0623903 100644 --- a/internal/ceres/solver_test.cc +++ b/internal/ceres/solver_test.cc
@@ -375,6 +375,97 @@ EXPECT_EQ(y, 1.0); } +TEST(Solver, RepeatedSolvePreservesCallerLinearSolverOrdering) { + double x = 0.0; + double y = 0.0; + Problem problem; + problem.AddResidualBlock(QuadraticCostFunctor::Create(), nullptr, &x); + problem.AddResidualBlock(QuadraticCostFunctor::Create(), nullptr, &y); + + Solver::Options options; + options.linear_solver_type = DENSE_SCHUR; + options.dense_linear_algebra_library_type = EIGEN; + options.function_tolerance = 0.0; + options.parameter_tolerance = 1e-12; + options.linear_solver_ordering = std::make_shared<ParameterBlockOrdering>(); + options.linear_solver_ordering->AddElementToGroup(&x, 0); + options.linear_solver_ordering->AddElementToGroup(&y, 1); + + // Fix x first; preprocessing must preserve its caller group. + problem.SetParameterBlockConstant(&x); + Solver::Summary summary; + Solve(options, &problem, &summary); + EXPECT_EQ(summary.termination_type, CONVERGENCE) << summary.message; + EXPECT_EQ(x, 0.0); + EXPECT_NEAR(y, 5.0, 1e-10); + EXPECT_EQ(options.linear_solver_ordering->GroupId(&x), 0); + EXPECT_EQ(options.linear_solver_ordering->GroupId(&y), 1); + + // Re-enable x and solve with the same caller ordering. + problem.SetParameterBlockVariable(&x); + y = 0.0; + Solve(options, &problem, &summary); + EXPECT_EQ(summary.termination_type, CONVERGENCE) << summary.message; + EXPECT_NEAR(x, 5.0, 1e-10); + EXPECT_NEAR(y, 5.0, 1e-10); + EXPECT_EQ(options.linear_solver_ordering->GroupId(&x), 0); + EXPECT_EQ(options.linear_solver_ordering->GroupId(&y), 1); + + // Check that a later solve also works without a supplied ordering. + options.linear_solver_ordering = nullptr; + x = 0.0; + y = 0.0; + Solve(options, &problem, &summary); + EXPECT_EQ(summary.termination_type, CONVERGENCE) << summary.message; + EXPECT_NEAR(x, 5.0, 1e-10); + EXPECT_NEAR(y, 5.0, 1e-10); +} + +TEST(Solver, RepeatedSolvePreservesCallerInnerIterationOrdering) { + double x = 0.0; + double y = 0.0; + double z = 0.0; + Problem problem; + problem.AddResidualBlock(QuadraticCostFunctor::Create(), nullptr, &x); + problem.AddResidualBlock(QuadraticCostFunctor::Create(), nullptr, &y); + problem.AddResidualBlock(QuadraticCostFunctor::Create(), nullptr, &z); + + Solver::Options options; + options.linear_solver_type = DENSE_QR; + options.use_inner_iterations = true; + options.function_tolerance = 0.0; + options.parameter_tolerance = 1e-12; + options.inner_iteration_ordering = std::make_shared<ParameterBlockOrdering>(); + options.inner_iteration_ordering->AddElementToGroup(&x, 0); + options.inner_iteration_ordering->AddElementToGroup(&y, 1); + options.inner_iteration_ordering->AddElementToGroup(&z, 2); + + // Fix x first; preprocessing must preserve its caller group. + problem.SetParameterBlockConstant(&x); + Solver::Summary summary; + Solve(options, &problem, &summary); + EXPECT_EQ(summary.termination_type, CONVERGENCE) << summary.message; + EXPECT_EQ(x, 0.0); + EXPECT_NEAR(y, 5.0, 1e-10); + EXPECT_NEAR(z, 5.0, 1e-10); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&x), 0); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&y), 1); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&z), 2); + + // Re-enable x and solve with the same caller ordering. + problem.SetParameterBlockVariable(&x); + y = 0.0; + z = 0.0; + Solve(options, &problem, &summary); + EXPECT_EQ(summary.termination_type, CONVERGENCE) << summary.message; + EXPECT_NEAR(x, 5.0, 1e-10); + EXPECT_NEAR(y, 5.0, 1e-10); + EXPECT_NEAR(z, 5.0, 1e-10); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&x), 0); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&y), 1); + EXPECT_EQ(options.inner_iteration_ordering->GroupId(&z), 2); +} + TEST(Solver, DenseNormalCholeskyOptions) { std::string message; Solver::Options options;
diff --git a/internal/ceres/trust_region_preprocessor.cc b/internal/ceres/trust_region_preprocessor.cc index 9d7a37d..a430c60 100644 --- a/internal/ceres/trust_region_preprocessor.cc +++ b/internal/ceres/trust_region_preprocessor.cc
@@ -381,6 +381,18 @@ EventLogger event_logger("TrustRegionPreprocessor::Preprocess"); CHECK(pp != nullptr); pp->options = options; + // The options copy shares caller-owned orderings. Clone them before removing + // fixed parameter blocks during preprocessing. + if (pp->options.linear_solver_ordering != nullptr) { + pp->options.linear_solver_ordering = + std::make_shared<ParameterBlockOrdering>( + *pp->options.linear_solver_ordering); + } + if (pp->options.inner_iteration_ordering != nullptr) { + pp->options.inner_iteration_ordering = + std::make_shared<ParameterBlockOrdering>( + *pp->options.inner_iteration_ordering); + } ChangeNumThreadsIfNeeded(&pp->options); pp->problem = problem;