/src/quantlib/ql/math/ode/fractionaladams.hpp
Line | Count | Source |
1 | | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | |
3 | | /* |
4 | | Copyright (C) 2026 Colin Alberts |
5 | | |
6 | | This file is part of QuantLib, a free-software/open-source library |
7 | | for financial quantitative analysts and developers - http://quantlib.org/ |
8 | | |
9 | | QuantLib is free software: you can redistribute it and/or modify it |
10 | | under the terms of the QuantLib license. You should have received a |
11 | | copy of the license along with this program; if not, please email |
12 | | <quantlib-dev@lists.sf.net>. The license is also available online at |
13 | | <https://www.quantlib.org/license.shtml>. |
14 | | |
15 | | This program is distributed in the hope that it will be useful, but WITHOUT |
16 | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
17 | | FOR A PARTICULAR PURPOSE. See the license for more details. |
18 | | */ |
19 | | |
20 | | /*! \file fractionaladams.hpp |
21 | | \brief fractional Adams-Bashforth-Moulton predictor-corrector scheme |
22 | | */ |
23 | | |
24 | | #ifndef quantlib_fractional_adams_hpp |
25 | | #define quantlib_fractional_adams_hpp |
26 | | |
27 | | #include <ql/errors.hpp> |
28 | | #include <ql/math/distributions/gammadistribution.hpp> |
29 | | #include <ql/types.hpp> |
30 | | #include <cmath> |
31 | | #include <functional> |
32 | | #include <vector> |
33 | | |
34 | | namespace QuantLib { |
35 | | |
36 | | //! fractional Adams-Bashforth-Moulton predictor-corrector scheme |
37 | | /*! Solves the fractional-order initial value problem |
38 | | \f[ |
39 | | D^{\alpha} y(t) = f(t, y(t)),\qquad y(0) = y_0, |
40 | | \qquad \alpha \in (0, 1], |
41 | | \f] |
42 | | where \f$ D^{\alpha} \f$ denotes the Caputo fractional derivative. |
43 | | For \f$ \alpha = 1 \f$ the scheme reduces to the classical one-step |
44 | | Adams-Bashforth-Moulton method. The memory of the fractional |
45 | | kernel makes the cost \f$ O(N^2) \f$ in the number of time steps. |
46 | | |
47 | | References: |
48 | | K. Diethelm, N.J. Ford, A.D. Freed, A Predictor-Corrector Approach |
49 | | for the Numerical Solution of Fractional Differential Equations, |
50 | | Nonlinear Dynamics 29, 3-22 (2002). |
51 | | */ |
52 | | |
53 | | template <class T = Real> |
54 | | class FractionalAdams { |
55 | | public: |
56 | | typedef std::function<T(Real, const T&)> OdeFct; |
57 | | |
58 | 0 | explicit FractionalAdams(Real alpha) : alpha_(alpha) { |
59 | 0 | QL_REQUIRE(alpha > 0.0 && alpha <= 1.0, |
60 | 0 | "fractional order alpha (" << alpha << ") must be in (0, 1]"); |
61 | 0 | } |
62 | | |
63 | | //! solution values on the uniform grid \f$ \{0, \Delta t, \ldots, t_{max}\} \f$ |
64 | 0 | std::vector<T> solve(const OdeFct& ode, const T& y0, Real tMax, Size steps) const { |
65 | 0 | QL_REQUIRE(tMax > 0.0, "tMax (" << tMax << ") must be positive"); |
66 | 0 | QL_REQUIRE(steps > 0, "at least one step required"); |
67 | | |
68 | 0 | const Real dt(tMax / steps); |
69 | 0 | const Real dtAlpha{std::pow(dt, alpha_)}; |
70 | 0 | const Real gamma1{GammaFunction().value(alpha_ + 1.0)}; |
71 | 0 | const Real gamma2{GammaFunction().value(alpha_ + 2.0)}; |
72 | | |
73 | | // powers k ^ alpha and k ^ (alpha + 1) for the quadrature weights |
74 | 0 | std::vector<Real> alphaPowers(steps + 2), alphaPlusOnePowers(steps + 2); |
75 | 0 | for (Size k{0}; k <= steps + 1; ++k) { |
76 | 0 | alphaPowers[k] = std::pow(static_cast<Real>(k), alpha_); |
77 | 0 | alphaPlusOnePowers[k] = alphaPowers[k] * k; |
78 | 0 | } |
79 | | |
80 | | // predictor weights b_k = (k + 1) ^ alpha - k ^ alpha and corrector |
81 | | // weights c_k = (k + 2) ^ (alpha + 1) + k ^ (alpha + 1) - 2(k + 1)^(alpha + 1), |
82 | | // both depending only on the distance k = n - j to the current step |
83 | 0 | std::vector<Real> predictorWeights(steps), correctorWeights(steps); |
84 | 0 | for (Size k{0}; k < steps; ++k) { |
85 | 0 | predictorWeights[k] = alphaPowers[k + 1] - alphaPowers[k]; |
86 | 0 | correctorWeights[k] = alphaPlusOnePowers[k + 2] + alphaPlusOnePowers[k] - |
87 | 0 | 2.0 * alphaPlusOnePowers[k + 1]; |
88 | 0 | } |
89 | |
|
90 | 0 | std::vector<T> y(steps + 1), f(steps + 1); |
91 | 0 | y[0] = y0; |
92 | 0 | f[0] = ode(0.0, y0); |
93 | |
|
94 | 0 | for (Size n{0}; n < steps; ++n) { |
95 | 0 | const Real tNext{(n + 1) * dt}; |
96 | | |
97 | | // corrector weight of f_0: n ^ (alpha + 1) - (n - alpha)(n + 1) ^ alpha |
98 | 0 | const Real a0{alphaPlusOnePowers[n] - (n - alpha_) * alphaPowers[n + 1]}; |
99 | |
|
100 | 0 | T predictorSum{predictorWeights[n] * f[0]}; |
101 | 0 | T correctorSum{a0 * f[0]}; |
102 | |
|
103 | 0 | for (Size j{1}; j <= n; ++j) { |
104 | 0 | const T& fj{f[j]}; |
105 | 0 | predictorSum += predictorWeights[n - j] * fj; |
106 | 0 | correctorSum += correctorWeights[n - j] * fj; |
107 | 0 | } |
108 | |
|
109 | 0 | const T yP{y0 + dtAlpha / gamma1 * predictorSum}; |
110 | 0 | correctorSum += ode(tNext, yP); |
111 | |
|
112 | 0 | y[n + 1] = y0 + dtAlpha / gamma2 * correctorSum; |
113 | 0 | f[n + 1] = ode(tNext, y[n + 1]); |
114 | 0 | } |
115 | |
|
116 | 0 | return y; |
117 | 0 | } |
118 | | |
119 | | private: |
120 | | const Real alpha_; |
121 | | }; |
122 | | |
123 | | |
124 | | //! quadrature weights of the product-trapezoidal Riemann-Liouville integral |
125 | | /*! |
126 | | The weights depend only on the integration order \f$ \alpha \f$ and on |
127 | | the number of grid steps. Callers that integrate repeatedly on the same grid, |
128 | | as the rough Heston characteristic function does once per quadrature node, can |
129 | | build once and reuse. |
130 | | */ |
131 | | class RiemannLiouvilleWeights { |
132 | | public: |
133 | | RiemannLiouvilleWeights(Real alpha, Size steps); |
134 | | |
135 | 0 | Real alpha() const { return alpha_; } |
136 | | |
137 | | //! \f$ I^{\alpha} y(t_N) \f$ for grid values y of spacing dt |
138 | | template <class T> |
139 | 0 | T integrate(const std::vector<T>& y, Real dt) const { |
140 | 0 | QL_REQUIRE(y.size() == steps_ + 1, "grid size (" << y.size() |
141 | 0 | << ") does not match the weights (" |
142 | 0 | << steps_ + 1 << " values expected)"); |
143 | 0 | QL_REQUIRE(dt > 0.0, "grid spacing dt (" << dt << ") must be positive"); |
144 | | |
145 | | // y[steps_] carries weight exactly 1 and is not stored, so w_ is |
146 | | // one shorter than y |
147 | 0 | T sum{0}; |
148 | |
|
149 | 0 | for (Size j{0}; j < steps_; ++j) { |
150 | 0 | sum += w_[j] * y[j]; |
151 | 0 | } |
152 | 0 | sum += y[steps_]; |
153 | |
|
154 | 0 | return std::pow(dt, alpha_) / gamma_ * sum; |
155 | 0 | } |
156 | | |
157 | | private: |
158 | | Real alpha_{0.0}; |
159 | | Size steps_{0}; |
160 | | Real gamma_{0.0}; |
161 | | std::vector<Real> w_; |
162 | | }; |
163 | | |
164 | 0 | inline RiemannLiouvilleWeights::RiemannLiouvilleWeights(Real alpha, Size steps) { |
165 | 0 | QL_REQUIRE(alpha >= 0.0, "integration order alpha (" << alpha << ") must be non-negative"); |
166 | 0 | QL_REQUIRE(steps > 0, "at least one grid step required"); |
167 | | |
168 | 0 | alpha_ = alpha; |
169 | 0 | steps_ = steps; |
170 | 0 | gamma_ = GammaFunction().value(alpha + 2.0); |
171 | 0 | w_ = std::vector<Real>(steps); |
172 | | |
173 | | // powers m ^ (alpha + 1) for m = 0, ..., steps + 1, reused across the |
174 | | // shifted (k - 1, k, k + 1) terms in the weights below |
175 | 0 | std::vector<Real> powAlphaPlus1(steps + 2); |
176 | 0 | for (Size m{0}; m <= steps + 1; ++m) { |
177 | 0 | powAlphaPlus1[m] = std::pow(static_cast<Real>(m), alpha + 1.0); |
178 | 0 | } |
179 | | |
180 | | // weight of y_0: (n - 1) ^ (alpha + 1) - (n - 1 - alpha) n ^ alpha |
181 | 0 | w_[0] = powAlphaPlus1[steps - 1] - (steps - 1 - alpha) * std::pow(Real(steps), alpha); |
182 | |
|
183 | 0 | for (Size j{1}; j < steps; ++j) { |
184 | 0 | const Size k{steps - j}; |
185 | 0 | w_[j] = powAlphaPlus1[k + 1] + powAlphaPlus1[k - 1] - 2.0 * powAlphaPlus1[k]; |
186 | 0 | } |
187 | 0 | } |
188 | | |
189 | | |
190 | | //! product-trapezoidal Riemann-Liouville fractional integral |
191 | | /*! Approximates |
192 | | \f[ |
193 | | I^{\alpha} y(t_N) = \frac{1}{\Gamma(\alpha)} |
194 | | \int_0^{t_N} (t_N - s)^{\alpha - 1} y(s)\,ds, |
195 | | \qquad \alpha \ge 0, |
196 | | \f] |
197 | | given values of \f$ y \f$ on a uniform grid, by integrating the |
198 | | piecewise linear interpolant of \f$ y \f$ against the kernel. |
199 | | For \f$ \alpha = 1 \f$ this is the trapezoidal rule; |
200 | | \f$ \alpha \to 0 \f$ recovers the identity. |
201 | | |
202 | | \note The weights are rebuilt on every call. Callers integrating |
203 | | repeatedly on the same \f$ (\alpha, N) \f$ grid should hold a |
204 | | RiemannLiouvilleWeights instead. |
205 | | */ |
206 | | template <class T = Real> |
207 | | T riemannLiouvilleIntegral(const std::vector<T>& y, Real alpha, Real dt) { |
208 | | QL_REQUIRE(alpha >= 0.0, "integration order alpha (" << alpha << ") must be non-negative"); |
209 | | QL_REQUIRE(y.size() >= 2, "at least two grid values required"); |
210 | | QL_REQUIRE(dt > 0.0, "grid spacing dt (" << dt << ") must be positive"); |
211 | | |
212 | | return RiemannLiouvilleWeights(alpha, y.size() - 1).integrate(y, dt); |
213 | | } |
214 | | } |
215 | | |
216 | | #endif |