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;