Call EvaluationCallback before evaluating the fixed cost.
Fixe a subtle bug in Program::RemoveFixedBlocks, where we call
ResidualBlock::Evaluate on residual blocks with all constant parameter
blocks without paying attention to the presence of an
EvaluationCallback.
In the process also run clang-format on some of the files touched by
this change.
https://github.com/ceres-solver/ceres-solver/issues/482
Change-Id: I342b66f6f975fdee2eef139a31f24d4a3e568e84
diff --git a/internal/ceres/evaluation_callback_test.cc b/internal/ceres/evaluation_callback_test.cc
index f814b01..c8f257f 100644
--- a/internal/ceres/evaluation_callback_test.cc
+++ b/internal/ceres/evaluation_callback_test.cc
@@ -28,24 +28,25 @@
//
// Author: mierle@gmail.com (Keir Mierle)
-#include "ceres/solver.h"
+#include "ceres/evaluation_callback.h"
#include <cmath>
#include <limits>
#include <vector>
-#include "gtest/gtest.h"
-#include "ceres/evaluation_callback.h"
-#include "ceres/sized_cost_function.h"
#include "ceres/problem.h"
#include "ceres/problem_impl.h"
+#include "ceres/sized_cost_function.h"
+#include "ceres/autodiff_cost_function.h"
+#include "ceres/solver.h"
+#include "gtest/gtest.h"
namespace ceres {
namespace internal {
// Use an inline hash function to avoid portability wrangling. Algorithm from
// Daniel Bernstein, known as the "djb2" hash.
-template<typename T>
+template <typename T>
uint64_t Djb2Hash(const T* data, const int size) {
uint64_t hash = 5381;
const uint8_t* data_as_bytes = reinterpret_cast<const uint8_t*>(data);
@@ -59,11 +60,9 @@
// Generally multiple inheritance is a terrible idea, but in this (test)
// case it makes for a relatively elegant test implementation.
-struct WigglyBowlCostFunctionAndEvaluationCallback :
- SizedCostFunction<2, 2>,
- EvaluationCallback {
-
- explicit WigglyBowlCostFunctionAndEvaluationCallback(double *parameter)
+struct WigglyBowlCostFunctionAndEvaluationCallback : SizedCostFunction<2, 2>,
+ EvaluationCallback {
+ explicit WigglyBowlCostFunctionAndEvaluationCallback(double* parameter)
: EvaluationCallback(),
user_parameter_block(parameter),
prepare_num_calls(0),
@@ -134,10 +133,10 @@
residuals[0] = y - a * sin(x);
residuals[1] = x;
if (jacobians != NULL) {
- (*jacobians)[2 * 0 + 0] = - a * cos(x); // df1/dx
- (*jacobians)[2 * 0 + 1] = 1.0; // df1/dy
- (*jacobians)[2 * 1 + 0] = 1.0; // df2/dx
- (*jacobians)[2 * 1 + 1] = 0.0; // df2/dy
+ (*jacobians)[2 * 0 + 0] = -a * cos(x); // df1/dx
+ (*jacobians)[2 * 0 + 1] = 1.0; // df1/dy
+ (*jacobians)[2 * 1 + 0] = 1.0; // df2/dx
+ (*jacobians)[2 * 1 + 1] = 0.0; // df2/dy
}
uint64_t incoming_parameter_hash = Djb2Hash(*parameters, 2);
@@ -212,26 +211,107 @@
EXPECT_GT(summary.num_unsuccessful_steps, 10);
// Ensure PrepareForEvaluation() is called the appropriate number of times.
- EXPECT_EQ(cost_function.prepare_num_calls,
- // Unsuccessful steps are evaluated only once (no jacobians).
- summary.num_unsuccessful_steps +
- // Successful steps are evaluated twice: with and without jacobians.
- 2 * summary.num_successful_steps
- // Final iteration doesn't re-evaluate the jacobian.
- // Note: This may be sensitive to tweaks to the TR algorithm; if
- // this becomes too brittle, remove this EXPECT_EQ() entirely.
- - 1);
+ EXPECT_EQ(
+ cost_function.prepare_num_calls,
+ // Unsuccessful steps are evaluated only once (no jacobians).
+ summary.num_unsuccessful_steps +
+ // Successful steps are evaluated twice: with and without jacobians.
+ 2 * summary.num_successful_steps
+ // Final iteration doesn't re-evaluate the jacobian.
+ // Note: This may be sensitive to tweaks to the TR algorithm; if
+ // this becomes too brittle, remove this EXPECT_EQ() entirely.
+ - 1);
// Ensure the callback calls ran a reasonable number of times.
EXPECT_GT(cost_function.prepare_num_calls, 0);
EXPECT_GT(cost_function.evaluate_num_calls, 0);
- EXPECT_EQ(cost_function.prepare_num_calls,
- cost_function.evaluate_num_calls);
+ EXPECT_EQ(cost_function.prepare_num_calls, cost_function.evaluate_num_calls);
// Ensure that the parameters did actually change.
EXPECT_NE(Djb2Hash(parameters, 2), original_parameters_hash);
}
+// r = 1 - x
+struct LinearResidual {
+ template <typename T>
+ bool operator()(const T* x, T* residuals) const {
+ residuals[0] = 1.0 - x[0];
+ return true;
+ }
+
+ static CostFunction* Create() {
+ return new AutoDiffCostFunction<LinearResidual, 1, 1>(new LinearResidual);
+ };
+};
+
+// Increments a counter everytime PrepareForEvaluation is called.
+class IncrementingEvaluationCallback : public EvaluationCallback {
+ public:
+ void PrepareForEvaluation(bool evaluate_jacobians,
+ bool new_evaluation_point) final {
+ (void) evaluate_jacobians;
+ (void) new_evaluation_point;
+ counter_ += 1.0;
+ }
+
+ const double counter() const { return counter_; }
+
+ private:
+ double counter_ = -1;
+};
+
+
+// r = IncrementingEvaluationCallback::counter - x
+struct EvaluationCallbackResidual {
+ EvaluationCallbackResidual(const IncrementingEvaluationCallback& callback)
+ : callback_(callback) {}
+
+ template <typename T>
+ bool operator()(const T* x, T* residuals) const {
+ residuals[0] = callback_.counter() - x[0];
+ return true;
+ }
+
+ const IncrementingEvaluationCallback& callback_;
+
+ static CostFunction* Create(IncrementingEvaluationCallback& callback) {
+ return new AutoDiffCostFunction<EvaluationCallbackResidual, 1, 1>(
+ new EvaluationCallbackResidual(callback));
+ };
+};
+
+// The following test, constructs a problem with residual blocks all
+// of whose parameters are constant, so they are evaluated once
+// outside the Minimizer to compute Solver::Summary::fixed_cost.
+//
+// The cost function for this residual block depends on the
+// IncrementingEvaluationCallback::counter_, by checking the value of
+// the fixed cost, we can check if the IncrementingEvaluationCallback
+// was called.
+TEST(EvaluationCallback, EvaluationCallbackIsCalledBeforeFixedCostIsEvaluated) {
+ double x = 1;
+ double y = 2;
+ std::unique_ptr<IncrementingEvaluationCallback> callback(
+ new IncrementingEvaluationCallback);
+ Problem::Options problem_options;
+ problem_options.evaluation_callback = callback.get();
+ Problem problem(problem_options);
+ problem.AddResidualBlock(LinearResidual::Create(), nullptr, &x);
+ problem.AddResidualBlock(
+ EvaluationCallbackResidual::Create(*callback),
+ nullptr,
+ &y);
+ problem.SetParameterBlockConstant(&y);
+
+ Solver::Options options;
+ options.linear_solver_type = DENSE_QR;
+ Solver::Summary summary;
+ Solve(options, &problem, &summary);
+ EXPECT_EQ(summary.fixed_cost, 2.0);
+ EXPECT_EQ(summary.final_cost, summary.fixed_cost);
+ EXPECT_GT(callback->counter(), 0);
+}
+
static void WithLineSearchMinimizerImpl(
LineSearchType line_search,
LineSearchDirectionType line_search_direction,
@@ -262,8 +342,7 @@
// Ensure the callback calls ran a reasonable number of times.
EXPECT_GT(summary.num_line_search_steps, 10);
EXPECT_GT(cost_function.prepare_num_calls, 30);
- EXPECT_EQ(cost_function.prepare_num_calls,
- cost_function.evaluate_num_calls);
+ EXPECT_EQ(cost_function.prepare_num_calls, cost_function.evaluate_num_calls);
// Ensure that the parameters did actually change.
EXPECT_NE(Djb2Hash(parameters, 2), original_parameters_hash);
diff --git a/internal/ceres/program.cc b/internal/ceres/program.cc
index 3a8f0e3..f1ded2e 100644
--- a/internal/ceres/program.cc
+++ b/internal/ceres/program.cc
@@ -63,8 +63,7 @@
Program::Program(const Program& program)
: parameter_blocks_(program.parameter_blocks_),
residual_blocks_(program.residual_blocks_),
- evaluation_callback_(program.evaluation_callback_){
-}
+ evaluation_callback_(program.evaluation_callback_) {}
const vector<ParameterBlock*>& Program::parameter_blocks() const {
return parameter_blocks_;
@@ -86,7 +85,7 @@
return evaluation_callback_;
}
-bool Program::StateVectorToParameterBlocks(const double *state) {
+bool Program::StateVectorToParameterBlocks(const double* state) {
for (int i = 0; i < parameter_blocks_.size(); ++i) {
if (!parameter_blocks_[i]->IsConstant() &&
!parameter_blocks_[i]->SetState(state)) {
@@ -97,7 +96,7 @@
return true;
}
-void Program::ParameterBlocksToStateVector(double *state) const {
+void Program::ParameterBlocksToStateVector(double* state) const {
for (int i = 0; i < parameter_blocks_.size(); ++i) {
parameter_blocks_[i]->GetState(state);
state += parameter_blocks_[i]->Size();
@@ -197,7 +196,9 @@
"ParameterBlock: %p with size %d has at least one invalid value.\n"
"First invalid value is at index: %d.\n"
"Parameter block values: ",
- array, size, invalid_index);
+ array,
+ size,
+ invalid_index);
AppendArrayToString(size, array, message);
return false;
}
@@ -244,7 +245,12 @@
"\nFirst infeasible value is at index: %d."
"\nLower bound: %e, value: %e, upper bound: %e"
"\nParameter block values: ",
- parameters, size, j, lower_bound, parameters[j], upper_bound);
+ parameters,
+ size,
+ j,
+ lower_bound,
+ parameters[j],
+ upper_bound);
AppendArrayToString(size, parameters, message);
return false;
}
@@ -263,7 +269,11 @@
"\nFirst infeasible bound is at index: %d."
"\nLower bound: %e, upper bound: %e"
"\nParameter block values: ",
- parameters, size, j, lower_bound, upper_bound);
+ parameters,
+ size,
+ j,
+ lower_bound,
+ upper_bound);
AppendArrayToString(size, parameters, message);
return false;
}
@@ -283,10 +293,9 @@
CHECK(error != nullptr);
std::unique_ptr<Program> reduced_program(new Program(*this));
- if (!reduced_program->RemoveFixedBlocks(removed_parameter_blocks,
- fixed_cost,
- error)) {
- return NULL;
+ if (!reduced_program->RemoveFixedBlocks(
+ removed_parameter_blocks, fixed_cost, error)) {
+ return nullptr;
}
reduced_program->SetParameterOffsetsAndIndex();
@@ -305,6 +314,8 @@
new double[MaxScratchDoublesNeededForEvaluate()]);
*fixed_cost = 0.0;
+ bool need_to_call_prepare_for_evaluation = evaluation_callback_ != nullptr;
+
// Mark all the parameters as unused. Abuse the index member of the
// parameter blocks for the marking.
for (int i = 0; i < parameter_blocks_.size(); ++i) {
@@ -334,18 +345,45 @@
continue;
}
+ // This is an exceedingly rare case, where the user has residual
+ // blocks which are effectively constant but they are also
+ // performance sensitive enough to add an EvaluationCallback.
+ //
+ // In this case before we evaluate the cost of the constant
+ // residual blocks, we must call
+ // EvaluationCallback::PrepareForEvaluation(). Because this call
+ // can be costly, we only call this if we actually encounter a
+ // residual block with all constant parameter blocks.
+ //
+ // It is worth nothing that there is a minor inefficiency here,
+ // that the iteration 0 of TrustRegionMinimizer will also cause
+ // PrepareForEvaluation to be called on the same point, but with
+ // evaluate_jacobians = true. We could try and optimize this here,
+ // but given the rarity of this case, the additional complexity
+ // and long range dependency is not worth it.
+ if (need_to_call_prepare_for_evaluation) {
+ constexpr bool kNewPoint = true;
+ constexpr bool kDoNotEvaluateJacobians = false;
+ evaluation_callback_->PrepareForEvaluation(kDoNotEvaluateJacobians,
+ kNewPoint);
+ need_to_call_prepare_for_evaluation = false;
+ }
+
// The residual is constant and will be removed, so its cost is
// added to the variable fixed_cost.
double cost = 0.0;
if (!residual_block->Evaluate(true,
&cost,
- NULL,
- NULL,
+ nullptr,
+ nullptr,
residual_block_evaluate_scratch.get())) {
- *error = StringPrintf("Evaluation of the residual %d failed during "
- "removal of fixed residual blocks.", i);
+ *error = StringPrintf(
+ "Evaluation of the residual %d failed during "
+ "removal of fixed residual blocks.",
+ i);
return false;
}
+
*fixed_cost += cost;
}
residual_blocks_.resize(num_active_residual_blocks);
@@ -364,11 +402,9 @@
}
parameter_blocks_.resize(num_active_parameter_blocks);
- if (!(((NumResidualBlocks() == 0) &&
- (NumParameterBlocks() == 0)) ||
- ((NumResidualBlocks() != 0) &&
- (NumParameterBlocks() != 0)))) {
- *error = "Congratulations, you found a bug in Ceres. Please report it.";
+ if (!(((NumResidualBlocks() == 0) && (NumParameterBlocks() == 0)) ||
+ ((NumResidualBlocks() != 0) && (NumParameterBlocks() != 0)))) {
+ *error = "Congratulations, you found a bug in Ceres. Please report it.";
return false;
}
@@ -387,8 +423,7 @@
const int num_parameter_blocks = residual_block->NumParameterBlocks();
int count = 0;
for (int i = 0; i < num_parameter_blocks; ++i) {
- count += independent_set.count(
- parameter_blocks[i]->mutable_user_state());
+ count += independent_set.count(parameter_blocks[i]->mutable_user_state());
}
if (count > 1) {
return false;
@@ -442,13 +477,9 @@
return tsm;
}
-int Program::NumResidualBlocks() const {
- return residual_blocks_.size();
-}
+int Program::NumResidualBlocks() const { return residual_blocks_.size(); }
-int Program::NumParameterBlocks() const {
- return parameter_blocks_.size();
-}
+int Program::NumParameterBlocks() const { return parameter_blocks_.size(); }
int Program::NumResiduals() const {
int num_residuals = 0;
@@ -474,6 +505,9 @@
return num_parameters;
}
+// TODO(sameeragarwal): The following methods should just be updated
+// incrementally and the values cached, rather than the linear
+// complexity we have right now on every call.
int Program::MaxScratchDoublesNeededForEvaluate() const {
// Compute the scratch space needed for evaluate.
int max_scratch_bytes_for_evaluate = 0;
@@ -503,8 +537,8 @@
int Program::MaxParametersPerResidualBlock() const {
int max_parameters = 0;
for (int i = 0; i < residual_blocks_.size(); ++i) {
- max_parameters = max(max_parameters,
- residual_blocks_[i]->NumParameterBlocks());
+ max_parameters =
+ max(max_parameters, residual_blocks_[i]->NumParameterBlocks());
}
return max_parameters;
}
@@ -523,8 +557,8 @@
ret += StringPrintf("Number of parameters: %d\n", NumParameters());
ret += "Parameters:\n";
for (int i = 0; i < parameter_blocks_.size(); ++i) {
- ret += StringPrintf("%d: %s\n",
- i, parameter_blocks_[i]->ToString().c_str());
+ ret +=
+ StringPrintf("%d: %s\n", i, parameter_blocks_[i]->ToString().c_str());
}
return ret;
}
diff --git a/internal/ceres/program.h b/internal/ceres/program.h
index dd5f6e1..7971299 100644
--- a/internal/ceres/program.h
+++ b/internal/ceres/program.h
@@ -35,12 +35,11 @@
#include <set>
#include <string>
#include <vector>
+
#include "ceres/internal/port.h"
+#include "ceres/evaluation_callback.h"
namespace ceres {
-
-class EvaluationCallback;
-
namespace internal {
class ParameterBlock;
@@ -76,8 +75,8 @@
// computation of the Jacobian of its local parameterization. If
// this computation fails for some reason, then this method returns
// false and the state of the parameter blocks cannot be trusted.
- bool StateVectorToParameterBlocks(const double *state);
- void ParameterBlocksToStateVector(double *state) const;
+ bool StateVectorToParameterBlocks(const double* state);
+ void ParameterBlocksToStateVector(double* state) const;
// Copy internal state to the user's parameters.
void CopyParameterBlockStateToUserState();
diff --git a/internal/ceres/trust_region_preprocessor.cc b/internal/ceres/trust_region_preprocessor.cc
index 65dee8a..b8c6b49 100644
--- a/internal/ceres/trust_region_preprocessor.cc
+++ b/internal/ceres/trust_region_preprocessor.cc
@@ -32,6 +32,7 @@
#include <numeric>
#include <string>
+
#include "ceres/callbacks.h"
#include "ceres/context_impl.h"
#include "ceres/evaluator.h"
@@ -57,8 +58,7 @@
ParameterBlockOrdering* CreateDefaultLinearSolverOrdering(
const Program& program) {
ParameterBlockOrdering* ordering = new ParameterBlockOrdering;
- const vector<ParameterBlock*>& parameter_blocks =
- program.parameter_blocks();
+ const vector<ParameterBlock*>& parameter_blocks = program.parameter_blocks();
for (int i = 0; i < parameter_blocks.size(); ++i) {
ordering->AddElementToGroup(
const_cast<double*>(parameter_blocks[i]->user_state()), 0);
@@ -69,8 +69,7 @@
// Check if all the user supplied values in the parameter blocks are
// sane or not, and if the program is feasible or not.
bool IsProgramValid(const Program& program, std::string* error) {
- return (program.ParameterBlocksAreFinite(error) &&
- program.IsFeasible(error));
+ return (program.ParameterBlocksAreFinite(error) && program.IsFeasible(error));
}
void AlternateLinearSolverAndPreconditionerForSchurTypeLinearSolver(
@@ -82,27 +81,25 @@
const LinearSolverType linear_solver_type_given = options->linear_solver_type;
const PreconditionerType preconditioner_type_given =
options->preconditioner_type;
- options->linear_solver_type = LinearSolver::LinearSolverForZeroEBlocks(
- linear_solver_type_given);
+ options->linear_solver_type =
+ LinearSolver::LinearSolverForZeroEBlocks(linear_solver_type_given);
std::string message;
if (linear_solver_type_given == ITERATIVE_SCHUR) {
- options->preconditioner_type = Preconditioner::PreconditionerForZeroEBlocks(
- preconditioner_type_given);
+ options->preconditioner_type =
+ Preconditioner::PreconditionerForZeroEBlocks(preconditioner_type_given);
message =
- StringPrintf(
- "No E blocks. Switching from %s(%s) to %s(%s).",
- LinearSolverTypeToString(linear_solver_type_given),
- PreconditionerTypeToString(preconditioner_type_given),
- LinearSolverTypeToString(options->linear_solver_type),
- PreconditionerTypeToString(options->preconditioner_type));
+ StringPrintf("No E blocks. Switching from %s(%s) to %s(%s).",
+ LinearSolverTypeToString(linear_solver_type_given),
+ PreconditionerTypeToString(preconditioner_type_given),
+ LinearSolverTypeToString(options->linear_solver_type),
+ PreconditionerTypeToString(options->preconditioner_type));
} else {
message =
- StringPrintf(
- "No E blocks. Switching from %s to %s.",
- LinearSolverTypeToString(linear_solver_type_given),
- LinearSolverTypeToString(options->linear_solver_type));
+ StringPrintf("No E blocks. Switching from %s to %s.",
+ LinearSolverTypeToString(linear_solver_type_given),
+ LinearSolverTypeToString(options->linear_solver_type));
}
VLOG_IF(1, options->logging_type != SILENT) << message;
@@ -181,8 +178,7 @@
ordering->Remove(pp->removed_parameter_blocks);
if (IsSchurType(options.linear_solver_type) &&
min_group_id != ordering->MinNonZeroGroup()) {
- AlternateLinearSolverAndPreconditionerForSchurTypeLinearSolver(
- &options);
+ AlternateLinearSolverAndPreconditionerForSchurTypeLinearSolver(&options);
}
}
@@ -243,7 +239,7 @@
// blocks for CX_SPARSE.
if ((options.sparse_linear_algebra_library_type == SUITE_SPARSE &&
!SuiteSparse::
- IsConstrainedApproximateMinimumDegreeOrderingAvailable()) ||
+ IsConstrainedApproximateMinimumDegreeOrderingAvailable()) ||
(options.sparse_linear_algebra_library_type == CX_SPARSE)) {
pp->linear_solver_options.use_postordering = true;
}
@@ -262,10 +258,9 @@
pp->evaluator_options.num_eliminate_blocks = 0;
if (IsSchurType(options.linear_solver_type)) {
pp->evaluator_options.num_eliminate_blocks =
- options
- .linear_solver_ordering
- ->group_to_elements().begin()
- ->second.size();
+ options.linear_solver_ordering->group_to_elements()
+ .begin()
+ ->second.size();
}
pp->evaluator_options.num_threads = options.num_threads;
@@ -273,9 +268,8 @@
pp->evaluator_options.context = pp->problem->context();
pp->evaluator_options.evaluation_callback =
pp->reduced_program->mutable_evaluation_callback();
- pp->evaluator.reset(Evaluator::Create(pp->evaluator_options,
- pp->reduced_program.get(),
- &pp->error));
+ pp->evaluator.reset(Evaluator::Create(
+ pp->evaluator_options, pp->reduced_program.get(), &pp->error));
return (pp->evaluator != nullptr);
}
@@ -346,8 +340,7 @@
TrustRegionStrategy::Options strategy_options;
strategy_options.linear_solver = pp->linear_solver.get();
- strategy_options.initial_radius =
- options.initial_trust_region_radius;
+ strategy_options.initial_radius = options.initial_trust_region_radius;
strategy_options.max_radius = options.max_trust_region_radius;
strategy_options.min_lm_diagonal = options.min_lm_diagonal;
strategy_options.max_lm_diagonal = options.max_lm_diagonal;
@@ -361,8 +354,7 @@
} // namespace
-TrustRegionPreprocessor::~TrustRegionPreprocessor() {
-}
+TrustRegionPreprocessor::~TrustRegionPreprocessor() {}
bool TrustRegionPreprocessor::Preprocess(const Solver::Options& options,
ProblemImpl* problem,
@@ -377,10 +369,8 @@
return false;
}
- pp->reduced_program.reset(
- program->CreateReducedProgram(&pp->removed_parameter_blocks,
- &pp->fixed_cost,
- &pp->error));
+ pp->reduced_program.reset(program->CreateReducedProgram(
+ &pp->removed_parameter_blocks, &pp->fixed_cost, &pp->error));
if (pp->reduced_program.get() == NULL) {
return false;
@@ -392,8 +382,7 @@
return true;
}
- if (!SetupLinearSolver(pp) ||
- !SetupEvaluator(pp) ||
+ if (!SetupLinearSolver(pp) || !SetupEvaluator(pp) ||
!SetupInnerIterationMinimizer(pp)) {
return false;
}