blob: f1ded2e5d5a0bcd461572af4a1f8f5d3dbdf41e4 [file] [log] [blame]
Austin Schuh70cc9552019-01-21 19:46:48 -08001// Ceres Solver - A fast non-linear least squares minimizer
2// Copyright 2015 Google Inc. All rights reserved.
3// http://ceres-solver.org/
4//
5// Redistribution and use in source and binary forms, with or without
6// modification, are permitted provided that the following conditions are met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13// * Neither the name of Google Inc. nor the names of its contributors may be
14// used to endorse or promote products derived from this software without
15// specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27// POSSIBILITY OF SUCH DAMAGE.
28//
29// Author: keir@google.com (Keir Mierle)
30
31#include "ceres/program.h"
32
33#include <algorithm>
34#include <map>
35#include <memory>
36#include <vector>
37
38#include "ceres/array_utils.h"
39#include "ceres/casts.h"
40#include "ceres/compressed_row_sparse_matrix.h"
41#include "ceres/cost_function.h"
42#include "ceres/evaluator.h"
43#include "ceres/internal/port.h"
44#include "ceres/local_parameterization.h"
45#include "ceres/loss_function.h"
46#include "ceres/map_util.h"
47#include "ceres/parameter_block.h"
48#include "ceres/problem.h"
49#include "ceres/residual_block.h"
50#include "ceres/stl_util.h"
51#include "ceres/triplet_sparse_matrix.h"
52
53namespace ceres {
54namespace internal {
55
56using std::max;
57using std::set;
58using std::string;
59using std::vector;
60
61Program::Program() {}
62
63Program::Program(const Program& program)
64 : parameter_blocks_(program.parameter_blocks_),
Austin Schuh1d1e6ea2020-12-23 21:56:30 -080065 residual_blocks_(program.residual_blocks_),
66 evaluation_callback_(program.evaluation_callback_) {}
Austin Schuh70cc9552019-01-21 19:46:48 -080067
68const vector<ParameterBlock*>& Program::parameter_blocks() const {
69 return parameter_blocks_;
70}
71
72const vector<ResidualBlock*>& Program::residual_blocks() const {
73 return residual_blocks_;
74}
75
76vector<ParameterBlock*>* Program::mutable_parameter_blocks() {
77 return &parameter_blocks_;
78}
79
80vector<ResidualBlock*>* Program::mutable_residual_blocks() {
81 return &residual_blocks_;
82}
83
Austin Schuh1d1e6ea2020-12-23 21:56:30 -080084EvaluationCallback* Program::mutable_evaluation_callback() {
85 return evaluation_callback_;
86}
87
88bool Program::StateVectorToParameterBlocks(const double* state) {
Austin Schuh70cc9552019-01-21 19:46:48 -080089 for (int i = 0; i < parameter_blocks_.size(); ++i) {
90 if (!parameter_blocks_[i]->IsConstant() &&
91 !parameter_blocks_[i]->SetState(state)) {
92 return false;
93 }
94 state += parameter_blocks_[i]->Size();
95 }
96 return true;
97}
98
Austin Schuh1d1e6ea2020-12-23 21:56:30 -080099void Program::ParameterBlocksToStateVector(double* state) const {
Austin Schuh70cc9552019-01-21 19:46:48 -0800100 for (int i = 0; i < parameter_blocks_.size(); ++i) {
101 parameter_blocks_[i]->GetState(state);
102 state += parameter_blocks_[i]->Size();
103 }
104}
105
106void Program::CopyParameterBlockStateToUserState() {
107 for (int i = 0; i < parameter_blocks_.size(); ++i) {
108 parameter_blocks_[i]->GetState(parameter_blocks_[i]->mutable_user_state());
109 }
110}
111
112bool Program::SetParameterBlockStatePtrsToUserStatePtrs() {
113 for (int i = 0; i < parameter_blocks_.size(); ++i) {
114 if (!parameter_blocks_[i]->IsConstant() &&
115 !parameter_blocks_[i]->SetState(parameter_blocks_[i]->user_state())) {
116 return false;
117 }
118 }
119 return true;
120}
121
122bool Program::Plus(const double* state,
123 const double* delta,
124 double* state_plus_delta) const {
125 for (int i = 0; i < parameter_blocks_.size(); ++i) {
126 if (!parameter_blocks_[i]->Plus(state, delta, state_plus_delta)) {
127 return false;
128 }
129 state += parameter_blocks_[i]->Size();
130 delta += parameter_blocks_[i]->LocalSize();
131 state_plus_delta += parameter_blocks_[i]->Size();
132 }
133 return true;
134}
135
136void Program::SetParameterOffsetsAndIndex() {
137 // Set positions for all parameters appearing as arguments to residuals to one
138 // past the end of the parameter block array.
139 for (int i = 0; i < residual_blocks_.size(); ++i) {
140 ResidualBlock* residual_block = residual_blocks_[i];
141 for (int j = 0; j < residual_block->NumParameterBlocks(); ++j) {
142 residual_block->parameter_blocks()[j]->set_index(-1);
143 }
144 }
145 // For parameters that appear in the program, set their position and offset.
146 int state_offset = 0;
147 int delta_offset = 0;
148 for (int i = 0; i < parameter_blocks_.size(); ++i) {
149 parameter_blocks_[i]->set_index(i);
150 parameter_blocks_[i]->set_state_offset(state_offset);
151 parameter_blocks_[i]->set_delta_offset(delta_offset);
152 state_offset += parameter_blocks_[i]->Size();
153 delta_offset += parameter_blocks_[i]->LocalSize();
154 }
155}
156
157bool Program::IsValid() const {
158 for (int i = 0; i < residual_blocks_.size(); ++i) {
159 const ResidualBlock* residual_block = residual_blocks_[i];
160 if (residual_block->index() != i) {
161 LOG(WARNING) << "Residual block: " << i
162 << " has incorrect index: " << residual_block->index();
163 return false;
164 }
165 }
166
167 int state_offset = 0;
168 int delta_offset = 0;
169 for (int i = 0; i < parameter_blocks_.size(); ++i) {
170 const ParameterBlock* parameter_block = parameter_blocks_[i];
171 if (parameter_block->index() != i ||
172 parameter_block->state_offset() != state_offset ||
173 parameter_block->delta_offset() != delta_offset) {
174 LOG(WARNING) << "Parameter block: " << i
175 << "has incorrect indexing information: "
176 << parameter_block->ToString();
177 return false;
178 }
179
180 state_offset += parameter_blocks_[i]->Size();
181 delta_offset += parameter_blocks_[i]->LocalSize();
182 }
183
184 return true;
185}
186
187bool Program::ParameterBlocksAreFinite(string* message) const {
188 CHECK(message != nullptr);
189 for (int i = 0; i < parameter_blocks_.size(); ++i) {
190 const ParameterBlock* parameter_block = parameter_blocks_[i];
191 const double* array = parameter_block->user_state();
192 const int size = parameter_block->Size();
193 const int invalid_index = FindInvalidValue(size, array);
194 if (invalid_index != size) {
195 *message = StringPrintf(
196 "ParameterBlock: %p with size %d has at least one invalid value.\n"
197 "First invalid value is at index: %d.\n"
198 "Parameter block values: ",
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800199 array,
200 size,
201 invalid_index);
Austin Schuh70cc9552019-01-21 19:46:48 -0800202 AppendArrayToString(size, array, message);
203 return false;
204 }
205 }
206 return true;
207}
208
209bool Program::IsBoundsConstrained() const {
210 for (int i = 0; i < parameter_blocks_.size(); ++i) {
211 const ParameterBlock* parameter_block = parameter_blocks_[i];
212 if (parameter_block->IsConstant()) {
213 continue;
214 }
215 const int size = parameter_block->Size();
216 for (int j = 0; j < size; ++j) {
217 const double lower_bound = parameter_block->LowerBoundForParameter(j);
218 const double upper_bound = parameter_block->UpperBoundForParameter(j);
219 if (lower_bound > -std::numeric_limits<double>::max() ||
220 upper_bound < std::numeric_limits<double>::max()) {
221 return true;
222 }
223 }
224 }
225 return false;
226}
227
228bool Program::IsFeasible(string* message) const {
229 CHECK(message != nullptr);
230 for (int i = 0; i < parameter_blocks_.size(); ++i) {
231 const ParameterBlock* parameter_block = parameter_blocks_[i];
232 const double* parameters = parameter_block->user_state();
233 const int size = parameter_block->Size();
234 if (parameter_block->IsConstant()) {
235 // Constant parameter blocks must start in the feasible region
236 // to ultimately produce a feasible solution, since Ceres cannot
237 // change them.
238 for (int j = 0; j < size; ++j) {
239 const double lower_bound = parameter_block->LowerBoundForParameter(j);
240 const double upper_bound = parameter_block->UpperBoundForParameter(j);
241 if (parameters[j] < lower_bound || parameters[j] > upper_bound) {
242 *message = StringPrintf(
243 "ParameterBlock: %p with size %d has at least one infeasible "
244 "value."
245 "\nFirst infeasible value is at index: %d."
246 "\nLower bound: %e, value: %e, upper bound: %e"
247 "\nParameter block values: ",
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800248 parameters,
249 size,
250 j,
251 lower_bound,
252 parameters[j],
253 upper_bound);
Austin Schuh70cc9552019-01-21 19:46:48 -0800254 AppendArrayToString(size, parameters, message);
255 return false;
256 }
257 }
258 } else {
259 // Variable parameter blocks must have non-empty feasible
260 // regions, otherwise there is no way to produce a feasible
261 // solution.
262 for (int j = 0; j < size; ++j) {
263 const double lower_bound = parameter_block->LowerBoundForParameter(j);
264 const double upper_bound = parameter_block->UpperBoundForParameter(j);
265 if (lower_bound >= upper_bound) {
266 *message = StringPrintf(
267 "ParameterBlock: %p with size %d has at least one infeasible "
268 "bound."
269 "\nFirst infeasible bound is at index: %d."
270 "\nLower bound: %e, upper bound: %e"
271 "\nParameter block values: ",
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800272 parameters,
273 size,
274 j,
275 lower_bound,
276 upper_bound);
Austin Schuh70cc9552019-01-21 19:46:48 -0800277 AppendArrayToString(size, parameters, message);
278 return false;
279 }
280 }
281 }
282 }
283
284 return true;
285}
286
287Program* Program::CreateReducedProgram(
288 vector<double*>* removed_parameter_blocks,
289 double* fixed_cost,
290 string* error) const {
291 CHECK(removed_parameter_blocks != nullptr);
292 CHECK(fixed_cost != nullptr);
293 CHECK(error != nullptr);
294
295 std::unique_ptr<Program> reduced_program(new Program(*this));
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800296 if (!reduced_program->RemoveFixedBlocks(
297 removed_parameter_blocks, fixed_cost, error)) {
298 return nullptr;
Austin Schuh70cc9552019-01-21 19:46:48 -0800299 }
300
301 reduced_program->SetParameterOffsetsAndIndex();
302 return reduced_program.release();
303}
304
305bool Program::RemoveFixedBlocks(vector<double*>* removed_parameter_blocks,
306 double* fixed_cost,
307 string* error) {
308 CHECK(removed_parameter_blocks != nullptr);
309 CHECK(fixed_cost != nullptr);
310 CHECK(error != nullptr);
311
312 std::unique_ptr<double[]> residual_block_evaluate_scratch;
313 residual_block_evaluate_scratch.reset(
314 new double[MaxScratchDoublesNeededForEvaluate()]);
315 *fixed_cost = 0.0;
316
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800317 bool need_to_call_prepare_for_evaluation = evaluation_callback_ != nullptr;
318
Austin Schuh70cc9552019-01-21 19:46:48 -0800319 // Mark all the parameters as unused. Abuse the index member of the
320 // parameter blocks for the marking.
321 for (int i = 0; i < parameter_blocks_.size(); ++i) {
322 parameter_blocks_[i]->set_index(-1);
323 }
324
325 // Filter out residual that have all-constant parameters, and mark
326 // all the parameter blocks that appear in residuals.
327 int num_active_residual_blocks = 0;
328 for (int i = 0; i < residual_blocks_.size(); ++i) {
329 ResidualBlock* residual_block = residual_blocks_[i];
330 int num_parameter_blocks = residual_block->NumParameterBlocks();
331
332 // Determine if the residual block is fixed, and also mark varying
333 // parameters that appear in the residual block.
334 bool all_constant = true;
335 for (int k = 0; k < num_parameter_blocks; k++) {
336 ParameterBlock* parameter_block = residual_block->parameter_blocks()[k];
337 if (!parameter_block->IsConstant()) {
338 all_constant = false;
339 parameter_block->set_index(1);
340 }
341 }
342
343 if (!all_constant) {
344 residual_blocks_[num_active_residual_blocks++] = residual_block;
345 continue;
346 }
347
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800348 // This is an exceedingly rare case, where the user has residual
349 // blocks which are effectively constant but they are also
350 // performance sensitive enough to add an EvaluationCallback.
351 //
352 // In this case before we evaluate the cost of the constant
353 // residual blocks, we must call
354 // EvaluationCallback::PrepareForEvaluation(). Because this call
355 // can be costly, we only call this if we actually encounter a
356 // residual block with all constant parameter blocks.
357 //
358 // It is worth nothing that there is a minor inefficiency here,
359 // that the iteration 0 of TrustRegionMinimizer will also cause
360 // PrepareForEvaluation to be called on the same point, but with
361 // evaluate_jacobians = true. We could try and optimize this here,
362 // but given the rarity of this case, the additional complexity
363 // and long range dependency is not worth it.
364 if (need_to_call_prepare_for_evaluation) {
365 constexpr bool kNewPoint = true;
366 constexpr bool kDoNotEvaluateJacobians = false;
367 evaluation_callback_->PrepareForEvaluation(kDoNotEvaluateJacobians,
368 kNewPoint);
369 need_to_call_prepare_for_evaluation = false;
370 }
371
Austin Schuh70cc9552019-01-21 19:46:48 -0800372 // The residual is constant and will be removed, so its cost is
373 // added to the variable fixed_cost.
374 double cost = 0.0;
375 if (!residual_block->Evaluate(true,
376 &cost,
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800377 nullptr,
378 nullptr,
Austin Schuh70cc9552019-01-21 19:46:48 -0800379 residual_block_evaluate_scratch.get())) {
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800380 *error = StringPrintf(
381 "Evaluation of the residual %d failed during "
382 "removal of fixed residual blocks.",
383 i);
Austin Schuh70cc9552019-01-21 19:46:48 -0800384 return false;
385 }
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800386
Austin Schuh70cc9552019-01-21 19:46:48 -0800387 *fixed_cost += cost;
388 }
389 residual_blocks_.resize(num_active_residual_blocks);
390
391 // Filter out unused or fixed parameter blocks.
392 int num_active_parameter_blocks = 0;
393 removed_parameter_blocks->clear();
394 for (int i = 0; i < parameter_blocks_.size(); ++i) {
395 ParameterBlock* parameter_block = parameter_blocks_[i];
396 if (parameter_block->index() == -1) {
397 removed_parameter_blocks->push_back(
398 parameter_block->mutable_user_state());
399 } else {
400 parameter_blocks_[num_active_parameter_blocks++] = parameter_block;
401 }
402 }
403 parameter_blocks_.resize(num_active_parameter_blocks);
404
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800405 if (!(((NumResidualBlocks() == 0) && (NumParameterBlocks() == 0)) ||
406 ((NumResidualBlocks() != 0) && (NumParameterBlocks() != 0)))) {
407 *error = "Congratulations, you found a bug in Ceres. Please report it.";
Austin Schuh70cc9552019-01-21 19:46:48 -0800408 return false;
409 }
410
411 return true;
412}
413
414bool Program::IsParameterBlockSetIndependent(
415 const set<double*>& independent_set) const {
416 // Loop over each residual block and ensure that no two parameter
417 // blocks in the same residual block are part of
418 // parameter_block_ptrs as that would violate the assumption that it
419 // is an independent set in the Hessian matrix.
420 for (const ResidualBlock* residual_block : residual_blocks_) {
421 ParameterBlock* const* parameter_blocks =
422 residual_block->parameter_blocks();
423 const int num_parameter_blocks = residual_block->NumParameterBlocks();
424 int count = 0;
425 for (int i = 0; i < num_parameter_blocks; ++i) {
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800426 count += independent_set.count(parameter_blocks[i]->mutable_user_state());
Austin Schuh70cc9552019-01-21 19:46:48 -0800427 }
428 if (count > 1) {
429 return false;
430 }
431 }
432 return true;
433}
434
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800435std::unique_ptr<TripletSparseMatrix>
436Program::CreateJacobianBlockSparsityTranspose(int start_residual_block) const {
Austin Schuh70cc9552019-01-21 19:46:48 -0800437 // Matrix to store the block sparsity structure of the Jacobian.
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800438 const int num_rows = NumParameterBlocks();
439 const int num_cols = NumResidualBlocks() - start_residual_block;
440
441 std::unique_ptr<TripletSparseMatrix> tsm(
442 new TripletSparseMatrix(num_rows, num_cols, 10 * num_cols));
Austin Schuh70cc9552019-01-21 19:46:48 -0800443 int num_nonzeros = 0;
444 int* rows = tsm->mutable_rows();
445 int* cols = tsm->mutable_cols();
446 double* values = tsm->mutable_values();
447
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800448 for (int c = start_residual_block; c < residual_blocks_.size(); ++c) {
Austin Schuh70cc9552019-01-21 19:46:48 -0800449 const ResidualBlock* residual_block = residual_blocks_[c];
450 const int num_parameter_blocks = residual_block->NumParameterBlocks();
451 ParameterBlock* const* parameter_blocks =
452 residual_block->parameter_blocks();
453
454 for (int j = 0; j < num_parameter_blocks; ++j) {
455 if (parameter_blocks[j]->IsConstant()) {
456 continue;
457 }
458
459 // Re-size the matrix if needed.
460 if (num_nonzeros >= tsm->max_num_nonzeros()) {
461 tsm->set_num_nonzeros(num_nonzeros);
462 tsm->Reserve(2 * num_nonzeros);
463 rows = tsm->mutable_rows();
464 cols = tsm->mutable_cols();
465 values = tsm->mutable_values();
466 }
467
468 const int r = parameter_blocks[j]->index();
469 rows[num_nonzeros] = r;
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800470 cols[num_nonzeros] = c - start_residual_block;
Austin Schuh70cc9552019-01-21 19:46:48 -0800471 values[num_nonzeros] = 1.0;
472 ++num_nonzeros;
473 }
474 }
475
476 tsm->set_num_nonzeros(num_nonzeros);
477 return tsm;
478}
479
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800480int Program::NumResidualBlocks() const { return residual_blocks_.size(); }
Austin Schuh70cc9552019-01-21 19:46:48 -0800481
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800482int Program::NumParameterBlocks() const { return parameter_blocks_.size(); }
Austin Schuh70cc9552019-01-21 19:46:48 -0800483
484int Program::NumResiduals() const {
485 int num_residuals = 0;
486 for (int i = 0; i < residual_blocks_.size(); ++i) {
487 num_residuals += residual_blocks_[i]->NumResiduals();
488 }
489 return num_residuals;
490}
491
492int Program::NumParameters() const {
493 int num_parameters = 0;
494 for (int i = 0; i < parameter_blocks_.size(); ++i) {
495 num_parameters += parameter_blocks_[i]->Size();
496 }
497 return num_parameters;
498}
499
500int Program::NumEffectiveParameters() const {
501 int num_parameters = 0;
502 for (int i = 0; i < parameter_blocks_.size(); ++i) {
503 num_parameters += parameter_blocks_[i]->LocalSize();
504 }
505 return num_parameters;
506}
507
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800508// TODO(sameeragarwal): The following methods should just be updated
509// incrementally and the values cached, rather than the linear
510// complexity we have right now on every call.
Austin Schuh70cc9552019-01-21 19:46:48 -0800511int Program::MaxScratchDoublesNeededForEvaluate() const {
512 // Compute the scratch space needed for evaluate.
513 int max_scratch_bytes_for_evaluate = 0;
514 for (int i = 0; i < residual_blocks_.size(); ++i) {
515 max_scratch_bytes_for_evaluate =
516 max(max_scratch_bytes_for_evaluate,
517 residual_blocks_[i]->NumScratchDoublesForEvaluate());
518 }
519 return max_scratch_bytes_for_evaluate;
520}
521
522int Program::MaxDerivativesPerResidualBlock() const {
523 int max_derivatives = 0;
524 for (int i = 0; i < residual_blocks_.size(); ++i) {
525 int derivatives = 0;
526 ResidualBlock* residual_block = residual_blocks_[i];
527 int num_parameters = residual_block->NumParameterBlocks();
528 for (int j = 0; j < num_parameters; ++j) {
529 derivatives += residual_block->NumResiduals() *
530 residual_block->parameter_blocks()[j]->LocalSize();
531 }
532 max_derivatives = max(max_derivatives, derivatives);
533 }
534 return max_derivatives;
535}
536
537int Program::MaxParametersPerResidualBlock() const {
538 int max_parameters = 0;
539 for (int i = 0; i < residual_blocks_.size(); ++i) {
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800540 max_parameters =
541 max(max_parameters, residual_blocks_[i]->NumParameterBlocks());
Austin Schuh70cc9552019-01-21 19:46:48 -0800542 }
543 return max_parameters;
544}
545
546int Program::MaxResidualsPerResidualBlock() const {
547 int max_residuals = 0;
548 for (int i = 0; i < residual_blocks_.size(); ++i) {
549 max_residuals = max(max_residuals, residual_blocks_[i]->NumResiduals());
550 }
551 return max_residuals;
552}
553
554string Program::ToString() const {
555 string ret = "Program dump\n";
556 ret += StringPrintf("Number of parameter blocks: %d\n", NumParameterBlocks());
557 ret += StringPrintf("Number of parameters: %d\n", NumParameters());
558 ret += "Parameters:\n";
559 for (int i = 0; i < parameter_blocks_.size(); ++i) {
Austin Schuh1d1e6ea2020-12-23 21:56:30 -0800560 ret +=
561 StringPrintf("%d: %s\n", i, parameter_blocks_[i]->ToString().c_str());
Austin Schuh70cc9552019-01-21 19:46:48 -0800562 }
563 return ret;
564}
565
566} // namespace internal
567} // namespace ceres