Coverage Report

Created: 2026-10-31 06:47

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