Apply deterministic parameter block reordering Change-Id: I0dc58ce754225310ff1a170cec8a3c71a402df57
diff --git a/include/ceres/ordered_groups.h b/include/ceres/ordered_groups.h index 9ae8499..6ca82af 100644 --- a/include/ceres/ordered_groups.h +++ b/include/ceres/ordered_groups.h
@@ -182,7 +182,9 @@ return group_to_elements_; } - const std::map<T, int>& element_to_group() const { return element_to_group_; } + const std::unordered_map<T, int>& element_to_group() const { + return element_to_group_; + } private: std::map<int, std::set<T>> group_to_elements_;
diff --git a/internal/ceres/reorder_program.cc b/internal/ceres/reorder_program.cc index fdf8119..e02af43 100644 --- a/internal/ceres/reorder_program.cc +++ b/internal/ceres/reorder_program.cc
@@ -36,6 +36,7 @@ #include <numeric> #include <set> #include <string> +#include <unordered_map> #include <vector> #include "Eigen/SparseCore" @@ -228,28 +229,49 @@ return false; } - std::vector<ParameterBlock*>* parameter_blocks = - program->mutable_parameter_blocks(); - parameter_blocks->clear(); + const std::unordered_map<double*, int>& element_to_group = + ordering.element_to_group(); - // TODO(sameeragarwal): Investigate whether this should be a set or an - // unordered_set. - const std::map<int, std::set<double*>>& groups = ordering.group_to_elements(); - for (const auto& p : groups) { - const std::set<double*>& group = p.second; - for (double* parameter_block_ptr : group) { - auto it = parameter_map.find(parameter_block_ptr); - if (it == parameter_map.end()) { - *error = absl::StrFormat( - "User specified ordering contains a pointer " - "to a double that is not a parameter block in " - "the problem. The invalid double is in group: %d", - p.first); - return false; - } - parameter_blocks->push_back(it->second); + // Check that all the elements in the ordering are in the problem. + for (const auto& p : element_to_group) { + auto it = parameter_map.find(p.first); + if (it == parameter_map.end()) { + *error = absl::StrFormat( + "User specified ordering contains a pointer " + "to a double that is not a parameter block in " + "the problem. The invalid double is in group: %d", + p.second); + return false; } } + + // Add the parameter blocks to the new ordering, while maintaining the + // original ordering of the parameter blocks within each group. + const std::vector<ParameterBlock*> old_parameter_blocks = + program->parameter_blocks(); + std::vector<ParameterBlock*>* parameter_blocks = + program->mutable_parameter_blocks(); + const std::map<int, std::set<double*>>& group_to_elements = + ordering.group_to_elements(); + std::map<int, int> group_to_index; + int group_cursor = 0; + for (const auto& p : group_to_elements) { + group_to_index[p.first] = group_cursor; + group_cursor += p.second.size(); + } + for (ParameterBlock* block : old_parameter_blocks) { + const auto it = element_to_group.find(block->mutable_user_state()); + if (it == element_to_group.end()) { + *error = absl::StrFormat("No ordering specified for parameter block: %d", + block->index()); + return false; + } + const int group = it->second; + int& index = group_to_index[group]; + (*parameter_blocks)[index] = block; + index++; + } + return true; }
diff --git a/internal/ceres/reorder_program_test.cc b/internal/ceres/reorder_program_test.cc index 0e1fa05..a2cbeb3 100644 --- a/internal/ceres/reorder_program_test.cc +++ b/internal/ceres/reorder_program_test.cc
@@ -171,6 +171,73 @@ EXPECT_EQ(parameter_blocks[2]->user_state(), &y); } +// Test that ApplyOrdering preserves the original program order within each +// group. This is essential for deterministic behavior - without preserving +// the original order, the ordering would depend on pointer addresses which +// can vary between runs due to ASLR or different memory allocation patterns. +TEST(_, ApplyOrderingPreservesOrderWithinGroups) { + // Use heap-allocated parameter blocks to ensure pointer addresses are + // not in any predictable order (simulating what happens in real usage). + std::vector<std::unique_ptr<double[]>> params; + std::vector<double*> param_ptrs; + const int kNumParams = 10; + + for (int i = 0; i < kNumParams; ++i) { + params.push_back(std::make_unique<double[]>(3)); + param_ptrs.push_back(params.back().get()); + } + + // Shuffle the parameter pointers to generate non-deterministic addresses + // in case the allocator happens to return addresses in a specific order. + // We add them to the problem in a specific order, which + // should be preserved within each group after ApplyOrdering. + std::mt19937 rng(42); // Fixed seed for reproducibility + std::shuffle(param_ptrs.begin(), param_ptrs.end(), rng); + + ProblemImpl problem; + + // Add parameter blocks in the order of their index (0, 1, 2, ..., 9) + // but use the original param_ptrs which may have any address ordering. + for (int i = 0; i < kNumParams; ++i) { + problem.AddParameterBlock(param_ptrs[i], 3); + } + + // Assign parameters to groups: + // Group 0: params 0, 2, 4, 6, 8 (evens) + // Group 1: params 1, 3, 5, 7, 9 (odds) + ParameterBlockOrdering linear_solver_ordering; + for (int i = 0; i < kNumParams; ++i) { + linear_solver_ordering.AddElementToGroup(param_ptrs[i], i % 2); + } + + Program* program = problem.mutable_program(); + std::string message; + + EXPECT_TRUE(ApplyOrdering( + problem.parameter_map(), linear_solver_ordering, program, &message)); + + const std::vector<ParameterBlock*>& parameter_blocks = + program->parameter_blocks(); + + EXPECT_EQ(parameter_blocks.size(), kNumParams); + + // Check that group 0 elements (evens) come first and + // maintain their original relative order. + EXPECT_EQ(parameter_blocks[0]->user_state(), param_ptrs[0]); + EXPECT_EQ(parameter_blocks[1]->user_state(), param_ptrs[2]); + EXPECT_EQ(parameter_blocks[2]->user_state(), param_ptrs[4]); + EXPECT_EQ(parameter_blocks[3]->user_state(), param_ptrs[6]); + EXPECT_EQ(parameter_blocks[4]->user_state(), param_ptrs[8]); + + // Check that group 1 elements (odds) come second and + // maintain their original relative order. + EXPECT_EQ(parameter_blocks[5]->user_state(), param_ptrs[1]); + EXPECT_EQ(parameter_blocks[6]->user_state(), param_ptrs[3]); + EXPECT_EQ(parameter_blocks[7]->user_state(), param_ptrs[5]); + EXPECT_EQ(parameter_blocks[8]->user_state(), param_ptrs[7]); + EXPECT_EQ(parameter_blocks[9]->user_state(), param_ptrs[9]); +} + #ifndef CERES_NO_SUITESPARSE class ReorderProgramForSparseCholeskyUsingSuiteSparseTest : public ::testing::Test {