/src/quantlib/ql/math/optimization/levenbergmarquardt.cpp
Line | Count | Source |
1 | | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | |
3 | | /* |
4 | | Copyright (C) 2006 Klaus Spanderen |
5 | | Copyright (C) 2015 Peter Caspers |
6 | | Copyright (C) 2026 Aaditya Panikath |
7 | | |
8 | | This file is part of QuantLib, a free-software/open-source library |
9 | | for financial quantitative analysts and developers - http://quantlib.org/ |
10 | | |
11 | | QuantLib is free software: you can redistribute it and/or modify it |
12 | | under the terms of the QuantLib license. You should have received a |
13 | | copy of the license along with this program; if not, please email |
14 | | <quantlib-dev@lists.sf.net>. The license is also available online at |
15 | | <https://www.quantlib.org/license.shtml>. |
16 | | |
17 | | This program is distributed in the hope that it will be useful, but WITHOUT |
18 | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
19 | | FOR A PARTICULAR PURPOSE. See the license for more details. |
20 | | */ |
21 | | |
22 | | #include <ql/math/optimization/constraint.hpp> |
23 | | #include <ql/math/optimization/lmdif.hpp> |
24 | | #include <ql/math/optimization/levenbergmarquardt.hpp> |
25 | | #include <cmath> |
26 | | #include <functional> |
27 | | #include <memory> |
28 | | |
29 | | namespace QuantLib { |
30 | | |
31 | | LevenbergMarquardt::LevenbergMarquardt(Real epsfcn, |
32 | | Real xtol, |
33 | | Real gtol, |
34 | | bool useCostFunctionsJacobian) |
35 | 0 | : epsfcn_(epsfcn), xtol_(xtol), gtol_(gtol), |
36 | 0 | useCostFunctionsJacobian_(useCostFunctionsJacobian) {} |
37 | | |
38 | | EndCriteria::Type LevenbergMarquardt::minimize(Problem& P, |
39 | 0 | const EndCriteria& endCriteria) { |
40 | 0 | P.reset(); |
41 | 0 | const Array& initX = P.currentValue(); |
42 | 0 | currentProblem_ = &P; |
43 | 0 | initCostValues_ = P.costFunction().values(initX); |
44 | 0 | int m = initCostValues_.size(); |
45 | 0 | int n = initX.size(); |
46 | 0 | if (useCostFunctionsJacobian_) { |
47 | 0 | initJacobian_ = Matrix(m,n); |
48 | 0 | P.costFunction().jacobian(initJacobian_, initX); |
49 | 0 | } |
50 | 0 | Array xx = initX; |
51 | 0 | std::unique_ptr<Real[]> fvec(new Real[m]); |
52 | 0 | std::unique_ptr<Real[]> diag(new Real[n]); |
53 | 0 | int mode = 1; |
54 | | // magic number recommended by the documentation |
55 | 0 | Real factor = 100; |
56 | | // lmdif() evaluates cost function n+1 times for each iteration (technically, 2n+1 |
57 | | // times if useCostFunctionsJacobian is true, but lmdif() doesn't account for that) |
58 | 0 | int maxfev = endCriteria.maxIterations() * (n + 1); |
59 | 0 | int nprint = 0; |
60 | 0 | int info = 0; |
61 | 0 | int nfev = 0; |
62 | 0 | std::unique_ptr<Real[]> fjac(new Real[m*n]); |
63 | 0 | int ldfjac = m; |
64 | 0 | std::unique_ptr<int[]> ipvt(new int[n]); |
65 | 0 | std::unique_ptr<Real[]> qtf(new Real[n]); |
66 | 0 | std::unique_ptr<Real[]> wa1(new Real[n]); |
67 | 0 | std::unique_ptr<Real[]> wa2(new Real[n]); |
68 | 0 | std::unique_ptr<Real[]> wa3(new Real[n]); |
69 | 0 | std::unique_ptr<Real[]> wa4(new Real[m]); |
70 | | // requirements; check here to get more detailed error messages. |
71 | 0 | QL_REQUIRE(n > 0, "no variables given"); |
72 | 0 | QL_REQUIRE(m >= n, |
73 | 0 | "less functions (" << m << |
74 | 0 | ") than available variables (" << n << ")"); |
75 | 0 | QL_REQUIRE(endCriteria.functionEpsilon() >= 0.0, |
76 | 0 | "negative f tolerance"); |
77 | 0 | QL_REQUIRE(xtol_ >= 0.0, "negative x tolerance"); |
78 | 0 | QL_REQUIRE(gtol_ >= 0.0, "negative g tolerance"); |
79 | 0 | QL_REQUIRE(maxfev > 0, "null number of evaluations"); |
80 | | |
81 | | // call lmdif to minimize the sum of the squares of m functions |
82 | | // in n variables by the Levenberg-Marquardt algorithm. |
83 | 0 | MINPACK::LmdifCostFunction lmdifCostFunction = |
84 | 0 | [this](const auto m, const auto n, const auto x, const auto fvec, [[maybe_unused]] const auto iflag) { |
85 | 0 | this->fcn(m, n, x, fvec); |
86 | 0 | }; |
87 | 0 | MINPACK::LmdifCostFunction lmdifJacFunction = |
88 | 0 | useCostFunctionsJacobian_ |
89 | 0 | ? [this](const auto m, const auto n, const auto x, const auto fjac, [[maybe_unused]] const auto iflag) { |
90 | 0 | this->jacFcn(m, n, x, fjac); |
91 | 0 | } |
92 | 0 | : MINPACK::LmdifCostFunction(); |
93 | 0 | MINPACK::lmdif(m, n, xx.begin(), fvec.get(), |
94 | 0 | endCriteria.functionEpsilon(), |
95 | 0 | xtol_, |
96 | 0 | gtol_, |
97 | 0 | maxfev, |
98 | 0 | epsfcn_, |
99 | 0 | diag.get(), mode, factor, |
100 | 0 | nprint, &info, &nfev, fjac.get(), |
101 | 0 | ldfjac, ipvt.get(), qtf.get(), |
102 | 0 | wa1.get(), wa2.get(), wa3.get(), wa4.get(), |
103 | 0 | lmdifCostFunction, |
104 | 0 | lmdifJacFunction); |
105 | | // for the time being |
106 | 0 | info_ = info; |
107 | | // check requirements & endCriteria evaluation |
108 | 0 | QL_REQUIRE(info != 0, "MINPACK: improper input parameters"); |
109 | 0 | QL_REQUIRE(info != 7, "MINPACK: xtol is too small. no further " |
110 | 0 | "improvement in the approximate " |
111 | 0 | "solution x is possible."); |
112 | 0 | QL_REQUIRE(info != 8, "MINPACK: gtol is too small. fvec is " |
113 | 0 | "orthogonal to the columns of the " |
114 | 0 | "jacobian to machine precision."); |
115 | | |
116 | 0 | EndCriteria::Type ecType = EndCriteria::None; |
117 | 0 | switch (info) { |
118 | 0 | case 1: |
119 | 0 | case 2: |
120 | 0 | case 3: |
121 | 0 | case 4: |
122 | | // 2 and 3 should be StationaryPoint, 4 a new gradient-related value, |
123 | | // but we keep StationaryFunctionValue for backwards compatibility. |
124 | 0 | ecType = EndCriteria::StationaryFunctionValue; |
125 | 0 | break; |
126 | 0 | case 5: |
127 | 0 | ecType = EndCriteria::MaxIterations; |
128 | 0 | break; |
129 | 0 | case 6: |
130 | 0 | ecType = EndCriteria::FunctionEpsilonTooSmall; |
131 | 0 | break; |
132 | 0 | default: |
133 | 0 | QL_FAIL("unknown MINPACK result: " << info); |
134 | 0 | } |
135 | | // set problem |
136 | 0 | P.setCurrentValue(std::move(xx)); |
137 | 0 | P.setFunctionValue(P.costFunction().value(P.currentValue())); |
138 | |
|
139 | 0 | return ecType; |
140 | 0 | } |
141 | | |
142 | 0 | void LevenbergMarquardt::fcn(int, int n, Real* x, Real* fvec) { |
143 | 0 | Array xt(n); |
144 | 0 | std::copy(x, x+n, xt.begin()); |
145 | 0 | if (currentProblem_->constraint().test(xt)) { |
146 | 0 | const Array& tmp = currentProblem_->values(xt); |
147 | 0 | bool valid = true; |
148 | 0 | for (double i : tmp) { |
149 | 0 | if (!std::isfinite(i)) { |
150 | 0 | valid = false; |
151 | 0 | break; |
152 | 0 | } |
153 | 0 | } |
154 | 0 | if (valid) { |
155 | 0 | std::copy(tmp.begin(), tmp.end(), fvec); |
156 | 0 | return; |
157 | 0 | } |
158 | 0 | } |
159 | | // Constraint violated or evaluation produced non-finite values: |
160 | | // return a large, uniform penalty so the optimizer steers away. |
161 | | // A fixed constant is used instead of the initial cost values because |
162 | | // the latter can be very small (even zero) when the starting point is |
163 | | // near-optimal, which would fail to deter the optimizer from exploring |
164 | | // infeasible regions. |
165 | 0 | std::fill(fvec, fvec + initCostValues_.size(), 1.0e10); |
166 | 0 | } |
167 | | |
168 | 0 | void LevenbergMarquardt::jacFcn(int m, int n, Real* x, Real* fjac) { |
169 | 0 | Array xt(n); |
170 | 0 | std::copy(x, x+n, xt.begin()); |
171 | 0 | if (currentProblem_->constraint().test(xt)) { |
172 | 0 | Matrix tmp(m,n); |
173 | 0 | currentProblem_->costFunction().jacobian(tmp, xt); |
174 | 0 | bool valid = true; |
175 | 0 | for (double & it : tmp) { |
176 | 0 | if (!std::isfinite(it)) { |
177 | 0 | valid = false; |
178 | 0 | break; |
179 | 0 | } |
180 | 0 | } |
181 | 0 | if (valid) { |
182 | 0 | Matrix tmpT = transpose(tmp); |
183 | 0 | std::copy(tmpT.begin(), tmpT.end(), fjac); |
184 | 0 | return; |
185 | 0 | } |
186 | 0 | } |
187 | | // Constraint violated or Jacobian produced non-finite values: |
188 | | // return initial Jacobian so the optimizer doesn't diverge |
189 | 0 | Matrix tmpT = transpose(initJacobian_); |
190 | 0 | std::copy(tmpT.begin(), tmpT.end(), fjac); |
191 | 0 | } |
192 | | |
193 | | } |