Coverage Report

Created: 2026-03-11 06:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}