version 3.10-dev
leastsquares.hh
Go to the documentation of this file.
1// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2// vi: set et ts=4 sw=4 sts=4:
3//
4// SPDX-FileCopyrightInfo: Copyright © DuMux Project contributors, see AUTHORS.md in root folder
5// SPDX-License-Identifier: GPL-3.0-or-later
6//
12#ifndef DUMUX_NONLINEAR_LEASTSQUARES_HH
13#define DUMUX_NONLINEAR_LEASTSQUARES_HH
14
15#include <iostream>
16#include <cmath>
17#include <memory>
18
19#include <dune/common/exceptions.hh>
20#include <dune/istl/bvector.hh>
21#include <dune/istl/matrix.hh>
22#if HAVE_SUITESPARSE_CHOLMOD
23#include <dune/istl/cholmod.hh>
24#endif // HAVE_SUITESPARSE_CHOLMOD
25#include <dune/istl/io.hh>
26
29#include <dumux/io/format.hh>
31
33
35template<class Variables>
36class Solver
37{
38public:
39 virtual ~Solver() = default;
40 virtual bool apply(Variables& vars) = 0;
41};
42
43} // end namespace Dumux::Optimization
44
45#ifndef DOXYGEN // exclude from doxygen
46// helper code for curve fitting
48
49template<class T, class F>
50class Assembler
51{
52public:
53 using Scalar = T;
54 using JacobianMatrix = Dune::Matrix<T>;
55 using ResidualType = Dune::BlockVector<T>;
56 using SolutionVector = Dune::BlockVector<T>;
57 using Variables = Dune::BlockVector<T>;
58
65 Assembler(const F& f, const SolutionVector& x0, std::size_t residualSize)
66 : prevSol_(x0), f_(f), solSize_(x0.size()), residualSize_(residualSize)
67 , JT_(solSize_, residualSize_), regularizedJTJ_(solSize_, solSize_)
68 , residual_(residualSize_), projectedResidual_(solSize_)
69 {
70 printMatrix_ = getParamFromGroup<bool>("LevenbergMarquardt", "PrintMatrix", false);
71 baseEpsilon_ = getParamFromGroup<Scalar>("LevenbergMarquardt", "BaseEpsilon", 1e-1);
72 }
73
75 void setLinearSystem()
76 {
77 std::cout << "Setting up linear system with " << solSize_ << " variables and "
78 << residualSize_ << " residuals." << std::endl;
79
80 JT_ = 0.0;
81 regularizedJTJ_ = 0.0;
82 residual_ = 0.0;
83 projectedResidual_ = 0.0;
84 }
85
87 JacobianMatrix& jacobian() { return regularizedJTJ_; }
88
90 ResidualType& residual() { return projectedResidual_; }
91
93 void setLambda(const T lambda) { lambda_ = lambda; }
94 T lambda() { return lambda_; }
95
97 void assembleResidual(const SolutionVector& x)
98 {
99 projectedResidual_ = 0.0;
100 JT_ = 0.0;
101
102 // assemble residual, J^T, and projected residual, J^T r
103 for (auto rowIt = JT_.begin(); rowIt != JT_.end(); ++rowIt)
104 {
105 const auto paramIdx = rowIt.index();
106 const auto residual = f_(x);
107 auto p = x;
108 const auto eps = baseEpsilon_;
109 p[paramIdx] = x[paramIdx] + eps;
110 auto deflectedResidual = f_(p);
111 deflectedResidual -= residual;
112 deflectedResidual /= eps;
113 for (auto colIt = rowIt->begin(); colIt != rowIt->end(); ++colIt)
114 *colIt += deflectedResidual[colIt.index()];
115
116 projectedResidual_[paramIdx] = residual * deflectedResidual;
117 }
118
119 if (printMatrix_)
120 {
121 std::cout << std::endl << "J^T = " << std::endl;
122 Dune::printmatrix(std::cout, JT_, "", "");
123 }
124 }
125
127 void assembleJacobianAndResidual(const SolutionVector& x)
128 {
129 assembleResidual(x);
130
131 regularizedJTJ_ = 0.0;
132 for (auto rowIt = regularizedJTJ_.begin(); rowIt != regularizedJTJ_.end(); ++rowIt)
133 {
134 for (auto colIt = rowIt->begin(); colIt != rowIt->end(); ++colIt)
135 {
136 for (int j = 0; j < residualSize_; ++j)
137 *colIt += JT_[rowIt.index()][j]*JT_[colIt.index()][j];
138
139 if (rowIt.index() == colIt.index())
140 {
141 *colIt += lambda_*(*colIt);
142
143 if (*colIt == 0.0)
144 *colIt = 1e-6*lambda_;
145 }
146 }
147 }
148
149 if (printMatrix_)
150 {
151 std::cout << std::endl << "J^T J + λ diag(J^T J) = " << std::endl;
152 Dune::printmatrix(std::cout, regularizedJTJ_, "", "");
153 }
154 }
155
157 T computeMeanSquaredError(const SolutionVector& x) const
158 { return f_(x).two_norm2(); }
159
161 const SolutionVector& prevSol() const
162 { return prevSol_; }
163
164private:
165 SolutionVector prevSol_; // initial guess
166 const F& f_;
167 std::size_t solSize_, residualSize_;
168
169 JacobianMatrix JT_; // J^T
170 JacobianMatrix regularizedJTJ_; // J^T J + λ diag(J^T J)
171 ResidualType residual_; // r = f_(x)
172 ResidualType projectedResidual_; // J^T r
173
174 Scalar lambda_ = 0.0; // regularization parameter
175 Scalar baseEpsilon_; // for numerical differentiation
176
177 bool printMatrix_ = false;
178};
179
180#if HAVE_SUITESPARSE_CHOLMOD
181template<class Matrix, class Vector>
182class CholmodLinearSolver
183{
184public:
185 void setResidualReduction(double residualReduction) {}
186
187 bool solve(const Matrix& A, Vector& x, const Vector& b) const
188 {
189 Dune::Cholmod<Vector> solver; // matrix is symmetric
190 solver.setMatrix(A);
191 Dune::InverseOperatorResult r;
192 auto bCopy = b;
193 auto xCopy = x;
194 solver.apply(xCopy, bCopy, r);
195 checkResult_(xCopy, r);
196 if (!r.converged )
197 DUNE_THROW(NumericalProblem, "Linear solver did not converge.");
198 x = xCopy;
199 return r.converged;
200 }
201
202 auto norm(const Vector& residual) const
203 { return residual.two_norm(); }
204
205private:
206 void checkResult_(Vector& x, Dune::InverseOperatorResult& result) const
207 {
208 flatVectorForEach(x, [&](auto&& entry, std::size_t){
209 using std::isnan, std::isinf;
210 if (isnan(entry) || isinf(entry))
211 result.converged = false;
212 });
213 }
214};
215#endif // HAVE_SUITESPARSE_CHOLMOD
216
217} // end namespace Dumux::Optimization::Detail
218#endif // DOXYGEN
219
221
229template<class Assembler, class LinearSolver>
230class LevenbergMarquardt : private Dumux::NewtonSolver<Assembler, LinearSolver, DefaultPartialReassembler, Dune::Communication<Dune::No_Comm>>
231{
233 using Scalar = typename Assembler::Scalar;
234 using Variables = typename Assembler::Variables;
235 using SolutionVector = typename Assembler::SolutionVector;
236 using ResidualVector = typename Assembler::ResidualType;
237 using Backend = typename ParentType::Backend;
238public:
240
241 LevenbergMarquardt(std::shared_ptr<Assembler> assembler,
242 std::shared_ptr<LinearSolver> linearSolver,
243 int verbosity = 0)
244 : ParentType(assembler, linearSolver, {}, "", "LevenbergMarquardt", verbosity)
245 {
246 assembler->setLambda(getParamFromGroup<Scalar>(ParentType::paramGroup(), "LevenbergMarquardt.LambdaInitial", 0.001));
247 maxLambdaDivisions_ = getParamFromGroup<std::size_t>(ParentType::paramGroup(), "LevenbergMarquardt.MaxLambdaDivisions", 10);
248
249 this->setUseLineSearch();
250 this->setMaxRelativeShift(1e-3);
251 }
252
262 bool apply(Variables& vars) override
263 {
264 // try solving the non-linear system
265 for (std::size_t i = 0; i <= maxLambdaDivisions_; ++i)
266 {
267 // linearize & solve
268 const bool converged = ParentType::apply(vars);
269
270 if (converged)
271 return true;
272
273 else if (!converged && i < maxLambdaDivisions_)
274 {
275 if (this->verbosity() >= 1)
276 std::cout << Fmt::format("LevenbergMarquardt solver did not converge with λ = {:.2e}. ", ParentType::assembler().lambda())
277 << Fmt::format("Retrying with λ = {:.2e}.\n", ParentType::assembler().lambda() * 9);
278
279 ParentType::assembler().setLambda(ParentType::assembler().lambda() * 9);
280 }
281
282 // convergence criterium not fulfilled
283 // return best solution so far
284 else
285 {
286 this->solutionChanged_(vars, bestSol_);
287 if (this->verbosity() >= 1)
288 std::cout << Fmt::format("Choose best solution so far with a MSE of {:.4e}", minResidual_) << std::endl;
289
290 return false;
291 }
292 }
293
294 return false;
295 }
296
297private:
298 void newtonEndStep(Variables &vars, const SolutionVector &uLastIter) override
299 {
300 ParentType::newtonEndStep(vars, uLastIter);
301
303 {
304 if (ParentType::assembler().lambda() < 1e5)
305 {
306 ParentType::assembler().setLambda(ParentType::assembler().lambda() * 9);
307 if (this->verbosity() > 0)
308 std::cout << "-- Increasing λ to " << ParentType::assembler().lambda();
309 }
310 else
311 {
312 if (this->verbosity() > 0)
313 std::cout << "-- Keeping λ at " << ParentType::assembler().lambda();
314 }
315 }
316 else
317 {
318 if (ParentType::reduction_ < 0.1 && ParentType::assembler().lambda() > 1e-5)
319 {
320 ParentType::assembler().setLambda(ParentType::assembler().lambda() / 11.0);
321 if (this->verbosity() > 0)
322 std::cout << "-- Decreasing λ to " << ParentType::assembler().lambda();
323 }
324 else
325 {
326 if (this->verbosity() > 0)
327 std::cout << "-- Keeping λ at " << ParentType::assembler().lambda();
328 }
329 }
330
331 if (this->verbosity() > 0)
332 std::cout << ", error reduction: " << ParentType::reduction_
333 << " and mean squared error: " << ParentType::residualNorm_ << std::endl;
334
335 // store best solution
336 if (ParentType::residualNorm_ < minResidual_)
337 {
338 minResidual_ = ParentType::residualNorm_;
339 bestSol_ = uLastIter;
340 }
341 }
342
343 void lineSearchUpdate_(Variables& vars,
344 const SolutionVector& uLastIter,
345 const ResidualVector& deltaU) override
346 {
347 alpha_ = 1.0;
348 auto uCurrentIter = uLastIter;
349
350 while (true)
351 {
352 Backend::axpy(-alpha_, deltaU, uCurrentIter);
353 this->solutionChanged_(vars, uCurrentIter);
354
355 ParentType::residualNorm_ = ParentType::assembler().computeMeanSquaredError(Backend::dofs(vars));
356 if (ParentType::numSteps_ == 0)
357 ParentType::initialResidual_ = ParentType::assembler().computeMeanSquaredError(ParentType::assembler().prevSol());
359
360 if (ParentType::reduction_ < ParentType::lastReduction_ || alpha_ <= 0.001)
361 {
362 ParentType::endIterMsgStream_ << Fmt::format(", residual reduction {:.4e}->{:.4e}@α={:.4f}", ParentType::lastReduction_, ParentType::reduction_, alpha_);
363 return;
364 }
365
366 // try with a smaller update and reset solution
367 alpha_ *= 0.5;
368 uCurrentIter = uLastIter;
369 }
370 }
371
372 Scalar minResidual_ = std::numeric_limits<Scalar>::max();
373 SolutionVector bestSol_;
374 std::size_t maxLambdaDivisions_ = 10;
375 Scalar alpha_ = 1.0;
376};
377
378#if HAVE_SUITESPARSE_CHOLMOD
379template<class T, class F>
380class NonlinearLeastSquaresSolver : public Solver<typename Optimization::Detail::Assembler<T, F>::Variables>
381{
382 using Assembler = Optimization::Detail::Assembler<T, F>;
383 using LinearSolver = Optimization::Detail::CholmodLinearSolver<typename Assembler::JacobianMatrix, typename Assembler::ResidualType>;
384 using Optimizer = Optimization::Detail::LevenbergMarquardt<Assembler, LinearSolver>;
385public:
386 using Variables = typename Assembler::Variables;
387
388 NonlinearLeastSquaresSolver(const F& f, const Dune::BlockVector<T>& x0, std::size_t size)
389 : solver_(std::make_unique<Optimizer>(std::make_shared<Assembler>(f, x0, size), std::make_shared<LinearSolver>(), 2))
390 {}
391
392 bool apply(Variables& vars) override
393 { return solver_->apply(vars); }
394
395private:
396 std::unique_ptr<Optimizer> solver_;
397};
398#endif // HAVE_SUITESPARSE_CHOLMOD
399
400} // end namespace Dumux::Optimization::Detail
401
402namespace Dumux::Optimization {
403
404#if HAVE_SUITESPARSE_CHOLMOD
405
416template<class T, class F>
417auto makeNonlinearLeastSquaresSolver(const F& f, const Dune::BlockVector<T>& x0, std::size_t size)
418-> std::unique_ptr<Solver<Dune::BlockVector<T>>>
419{ return std::make_unique<Optimization::Detail::NonlinearLeastSquaresSolver<T, F>>(f, x0, size); }
420
421#endif // HAVE_SUITESPARSE_CHOLMOD
422
423} // end namespace Dumux::Optimization
424
425#endif
An implementation of a Newton solver.
Definition: nonlinear/newtonsolver.hh:181
const std::string & paramGroup() const
Returns the parameter group.
Definition: nonlinear/newtonsolver.hh:821
int numSteps_
actual number of steps done so far
Definition: nonlinear/newtonsolver.hh:887
int verbosity() const
Return the verbosity level.
Definition: nonlinear/newtonsolver.hh:803
virtual void newtonEndStep(Variables &vars, const SolutionVector &uLastIter)
Indicates that one Newton iteration was finished.
Definition: nonlinear/newtonsolver.hh:608
virtual void solutionChanged_(Variables &vars, const SolutionVector &uCurrentIter)
Update solution-dependent quantities like grid variables after the solution has changed.
Definition: nonlinear/newtonsolver.hh:855
bool apply(Variables &vars) override
Run the Newton method to solve a non-linear system. The solver is responsible for all the strategic d...
Definition: nonlinear/newtonsolver.hh:377
void setUseLineSearch(bool val=true)
Specify whether line search is enabled or not.
Definition: nonlinear/newtonsolver.hh:809
std::ostringstream endIterMsgStream_
message stream to be displayed at the end of iterations
Definition: nonlinear/newtonsolver.hh:900
VariablesBackend< typename ParentType::Variables > Backend
Definition: nonlinear/newtonsolver.hh:185
void setMaxRelativeShift(Scalar tolerance)
Set the maximum acceptable difference of any primary variable between two iterations for declaring co...
Definition: nonlinear/newtonsolver.hh:248
A nonlinear least squares solver with model parameters and observations.
Definition: leastsquares.hh:231
bool apply(Variables &vars) override
Solve the nonlinear least squares problem.
Definition: leastsquares.hh:262
LevenbergMarquardt(std::shared_ptr< Assembler > assembler, std::shared_ptr< LinearSolver > linearSolver, int verbosity=0)
Definition: leastsquares.hh:241
a solver base class
Definition: leastsquares.hh:37
virtual ~Solver()=default
virtual bool apply(Variables &vars)=0
const LinearSolver & linearSolver() const
Access the linear solver.
Definition: common/pdesolver.hh:133
const Assembler & assembler() const
Access the assembler.
Definition: common/pdesolver.hh:121
Some exceptions thrown in DuMux
Formatting based on the fmt-library which implements std::format of C++20.
Definition: leastsquares.hh:220
Definition: leastsquares.hh:32
Reference implementation of a Newton solver.
The infrastructure to retrieve run-time parameters from Dune::ParameterTrees.