blob: 518719bbe9c645708d00871b4d609a3d894e93b0 [file]
// Ceres Solver - A fast non-linear least squares minimizer
// Copyright 2026 Google Inc. All rights reserved.
// http://ceres-solver.org/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
#include "ceres/coordinate_descent_minimizer.h"
#include <string>
#include <vector>
#include "ceres/minimizer.h"
#include "ceres/ordered_groups.h"
#include "ceres/parameter_block.h"
#include "ceres/problem_impl.h"
#include "ceres/program.h"
#include "ceres/sized_cost_function.h"
#include "ceres/solver.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace ceres::internal {
namespace {
using ::testing::ElementsAre;
constexpr double kStateTolerance = 1e-6;
// The default inner LM stop can leave endpoint error below 3e-4.
constexpr double kCoupledOrderingTolerance = 5e-4;
class UnaryResidual final : public SizedCostFunction<1, 1> {
public:
explicit UnaryResidual(double target) : target_(target) {}
bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const override {
residuals[0] = parameters[0][0] - target_;
if (jacobians != nullptr && jacobians[0] != nullptr) {
jacobians[0][0] = 1.0;
}
return true;
}
private:
const double target_;
};
class BinaryResidual final : public SizedCostFunction<1, 1, 1> {
public:
explicit BinaryResidual(double target) : target_(target) {}
bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const override {
residuals[0] = parameters[0][0] + parameters[1][0] - target_;
if (jacobians != nullptr) {
if (jacobians[0] != nullptr) {
jacobians[0][0] = 1.0;
}
if (jacobians[1] != nullptr) {
jacobians[1][0] = 1.0;
}
}
return true;
}
private:
const double target_;
};
struct BlockMetadata {
int index;
int state_offset;
int delta_offset;
bool is_constant;
};
MATCHER_P2(RestoresBlockMetadata,
expected,
state_data,
"restores parameter block metadata") {
if (arg->index() != expected.index) {
*result_listener << "index is " << arg->index() << ", expected "
<< expected.index;
return false;
}
if (arg->state_offset() != expected.state_offset) {
*result_listener << "state offset is " << arg->state_offset()
<< ", expected " << expected.state_offset;
return false;
}
if (arg->delta_offset() != expected.delta_offset) {
*result_listener << "delta offset is " << arg->delta_offset()
<< ", expected " << expected.delta_offset;
return false;
}
if (arg->IsConstant() != expected.is_constant) {
*result_listener << "constant state is " << arg->IsConstant()
<< ", expected " << expected.is_constant;
return false;
}
if (arg->state() != state_data + expected.state_offset) {
*result_listener << "state pointer does not match offset "
<< expected.state_offset;
return false;
}
return true;
}
class CoordinateDescentMinimizerTest : public ::testing::Test {
protected:
bool InitMinimizer(const ParameterBlockOrdering& ordering) {
problem_.mutable_program()->SetParameterOffsetsAndIndex();
state_.resize(problem_.program().NumParameters());
problem_.program().ParameterBlocksToStateVector(state_.data());
initial_state_ = state_;
metadata_.clear();
for (ParameterBlock* block : problem_.program().parameter_blocks()) {
metadata_.push_back({block->index(),
block->state_offset(),
block->delta_offset(),
block->IsConstant()});
}
return minimizer_.Init(
problem_.program(), problem_.parameter_map(), ordering, &error_);
}
void ResetState() { state_ = initial_state_; }
void Minimize(int num_threads = 1) {
Minimizer::Options options;
options.num_threads = num_threads;
problem_.context()->EnsureMinimumThreads(num_threads);
Solver::Summary summary;
minimizer_.Minimize(options, state_.data(), &summary);
}
double x_ = 0.0;
double y_ = 0.0;
double z_ = 0.0;
std::vector<double> state_;
std::vector<double> initial_state_;
std::vector<BlockMetadata> metadata_;
std::string error_;
ProblemImpl problem_;
CoordinateDescentMinimizer minimizer_{problem_.context()};
};
TEST(CoordinateDescentMinimizerOrdering, RejectsCoupledParametersInSameGroup) {
double x = 0.0;
double y = 0.0;
ProblemImpl problem;
problem.AddResidualBlock(new BinaryResidual(3.0), nullptr, &x, &y);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x, 0);
ordering.AddElementToGroup(&y, 0);
std::string error;
EXPECT_FALSE(CoordinateDescentMinimizer::IsOrderingValid(
problem.program(), ordering, &error));
EXPECT_FALSE(error.empty());
}
TEST(CoordinateDescentMinimizerOrdering,
AcceptsCoupledParametersInSeparateGroups) {
double x = 0.0;
double y = 0.0;
ProblemImpl problem;
problem.AddResidualBlock(new BinaryResidual(3.0), nullptr, &x, &y);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x, 0);
ordering.AddElementToGroup(&y, 1);
std::string error;
EXPECT_TRUE(CoordinateDescentMinimizer::IsOrderingValid(
problem.program(), ordering, &error))
<< error;
}
TEST(CoordinateDescentMinimizerOrdering,
AutomaticOrderingSeparatesCoupledParameters) {
double x = 0.0;
double y = 0.0;
double z = 0.0;
ProblemImpl problem;
problem.AddResidualBlock(new BinaryResidual(3.0), nullptr, &x, &y);
problem.AddResidualBlock(new UnaryResidual(1.0), nullptr, &z);
const auto ordering =
CoordinateDescentMinimizer::CreateOrdering(problem.program());
std::string error;
ASSERT_NE(nullptr, ordering);
EXPECT_TRUE(ordering->IsMember(&x));
EXPECT_TRUE(ordering->IsMember(&y));
EXPECT_TRUE(ordering->IsMember(&z));
EXPECT_EQ(3, ordering->NumElements());
EXPECT_EQ(2, ordering->NumGroups());
const int first_group = ordering->MinNonZeroGroup();
EXPECT_EQ(1, ordering->GroupSize(first_group));
EXPECT_EQ(2, ordering->GroupSize(first_group + 1));
EXPECT_NE(ordering->GroupId(&x), ordering->GroupId(&y));
EXPECT_TRUE(CoordinateDescentMinimizer::IsOrderingValid(
problem.program(), *ordering, &error))
<< error;
}
TEST_F(CoordinateDescentMinimizerTest, IndependentBlocksAreOptimized) {
problem_.AddResidualBlock(new UnaryResidual(2.0), nullptr, &x_);
problem_.AddResidualBlock(new UnaryResidual(-3.0), nullptr, &y_);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x_, 0);
ordering.AddElementToGroup(&y_, 0);
ASSERT_TRUE(InitMinimizer(ordering)) << error_;
ASSERT_EQ(problem_.program().NumParameters(), state_.size());
// Minimize requests up to one thread per problem in an independent group.
for (int num_threads : {1, 2}) {
ResetState();
Minimize(num_threads);
EXPECT_THAT(
problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
EXPECT_NEAR(2.0, state_[0], kStateTolerance);
EXPECT_NEAR(-3.0, state_[1], kStateTolerance);
}
}
TEST_F(CoordinateDescentMinimizerTest, UsesUpdatedXWhenMinimizingY) {
problem_.AddResidualBlock(new BinaryResidual(3.0), nullptr, &x_, &y_);
problem_.AddResidualBlock(new UnaryResidual(1.0), nullptr, &y_);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x_, 0);
ordering.AddElementToGroup(&y_, 1);
ASSERT_TRUE(InitMinimizer(ordering)) << error_;
ASSERT_EQ(problem_.program().NumParameters(), state_.size());
Minimize();
EXPECT_THAT(problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
// X reaches 3 first. The single Y sweep then minimizes both residuals.
EXPECT_NEAR(3.0, state_[0], kCoupledOrderingTolerance);
EXPECT_NEAR(0.5, state_[1], kCoupledOrderingTolerance);
}
TEST_F(CoordinateDescentMinimizerTest, UsesUpdatedYWhenMinimizingX) {
problem_.AddResidualBlock(new BinaryResidual(3.0), nullptr, &x_, &y_);
problem_.AddResidualBlock(new UnaryResidual(1.0), nullptr, &y_);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x_, 1);
ordering.AddElementToGroup(&y_, 0);
ASSERT_TRUE(InitMinimizer(ordering)) << error_;
ASSERT_EQ(problem_.program().NumParameters(), state_.size());
Minimize();
EXPECT_THAT(problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
// Y reaches 2 first. The single X sweep then minimizes the binary residual.
EXPECT_NEAR(1.0, state_[0], kCoupledOrderingTolerance);
EXPECT_NEAR(2.0, state_[1], kCoupledOrderingTolerance);
}
TEST_F(CoordinateDescentMinimizerTest,
RepeatedMinimizationRestoresMetadataAfterStateReset) {
problem_.AddResidualBlock(new UnaryResidual(2.0), nullptr, &x_);
problem_.AddResidualBlock(new UnaryResidual(-3.0), nullptr, &y_);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&x_, 0);
ordering.AddElementToGroup(&y_, 1);
ASSERT_TRUE(InitMinimizer(ordering)) << error_;
ASSERT_EQ(1, metadata_[1].index);
ASSERT_EQ(1, metadata_[1].state_offset);
ASSERT_EQ(1, metadata_[1].delta_offset);
ASSERT_EQ(problem_.program().NumParameters(), state_.size());
Minimize();
EXPECT_THAT(problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
EXPECT_NEAR(2.0, state_[0], kStateTolerance);
EXPECT_NEAR(-3.0, state_[1], kStateTolerance);
ResetState();
Minimize();
EXPECT_THAT(problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
EXPECT_NEAR(2.0, state_[0], kStateTolerance);
EXPECT_NEAR(-3.0, state_[1], kStateTolerance);
}
TEST_F(CoordinateDescentMinimizerTest, KeepsExcludedParameterBlockFixed) {
x_ = 1.0;
problem_.AddResidualBlock(new BinaryResidual(5.0), nullptr, &x_, &z_);
ParameterBlockOrdering ordering;
ordering.AddElementToGroup(&z_, 0);
ASSERT_TRUE(InitMinimizer(ordering)) << error_;
ASSERT_EQ(1, metadata_[1].index);
ASSERT_EQ(1, metadata_[1].state_offset);
ASSERT_EQ(1, metadata_[1].delta_offset);
ASSERT_EQ(problem_.program().NumParameters(), state_.size());
Minimize();
EXPECT_THAT(problem_.program().parameter_blocks(),
ElementsAre(RestoresBlockMetadata(metadata_[0], state_.data()),
RestoresBlockMetadata(metadata_[1], state_.data())));
EXPECT_DOUBLE_EQ(1.0, state_[0]);
EXPECT_NEAR(4.0, state_[1], kStateTolerance);
}
} // namespace
} // namespace ceres::internal