99
1010from pycaputo .utils import Array
1111
12- # {{{ Lubich1986 weights
12+ # {{{ FLMM: Starting Weights
1313
1414
15- def lubich_bdf_starting_weights_count (order : int , alpha : float ) -> int :
16- """An estimate for the number of starting weights from [Lubich1986]_.
15+ def lmm_starting_weights (
16+ w : Array , sigma : Array , alpha : float , * , atol : float | None = None
17+ ) -> Iterator [tuple [int , Array ]]:
18+ r"""Constructs starting weights for a given set of weights *w* of a
19+ fractional-order linear multistep method (FLMM).
1720
18- :arg order: order of the BDF method.
19- :arg alpha: order of the fractional derivative.
21+ The starting weights are introduced to handle a lack of smoothness of the
22+ function :math:`f(x)` being integrated. They are constructed in such a way
23+ that they are exact for a series of monomials, which results in an accurate
24+ quadrature for functions of the form :math:`f(x) = f_i x^{\sigma_i} +
25+ x^{\beta} g(x)`, where :math:`g` is smooth and :math:`\beta < p`
26+ (see Remark 3.6 in [Li2020]_). This ensures that the Taylor expansion near
27+ the origin is sufficiently accurate.
28+
29+ Therefore, they are obtained by solving
30+
31+ .. math::
32+
33+ \sum_{k = 0}^s w_{mk} k^{\sigma_q} =
34+ \frac{\Gamma(\sigma_q +1)}{\Gamma(\sigma_q + \alpha + 1)}
35+ m^{\sigma_q + \alpha} -
36+ \sum_{k = 0}^s w_{n - k} k^{\sigma_q}
37+
38+ for a set of :math:`\sigma_q` powers. In the simplest case, we can just set
39+ :math:`\sigma \in \{0, 1, \dots, p - 1\}` and obtain integer powers. Other
40+ values can be chosen depending on the behaviour of :math:`f(x)` near the origin.
41+
42+ Note that the starting weights are only computed for :math:`m \ge s`.
43+ The initial :math:`s` steps are expected to be computed in some other
44+ fashion.
45+
46+ :arg w: convolution weights of an FLMM.
47+ :arg sigma: an array of monomial powers for which the starting weights are exact.
48+ :arg alpha: order of the fractional derivative to approximate.
49+ :arg atol: absolute tolerance used for the GMRES solver. If *None*, an
50+ exact LU-based solver is used instead (see Section 3.2 in [Diethelm2006]_
51+ for a discussion of these methods).
52+
53+ :returns: the index and starting weights for every point :math:`m \ge s`.
54+ """
55+
56+ from scipy .linalg import lu_factor , lu_solve
57+ from scipy .sparse .linalg import gmres
58+ from scipy .special import gamma
59+
60+ s = sigma .size
61+ j = np .arange (1 , s + 1 ).reshape (- 1 , 1 )
62+
63+ A = j ** sigma
64+ assert A .shape == (s , s )
65+ assert np .all (np .isfinite (A ))
66+
67+ if atol is None :
68+ lu_p = lu_factor (A )
69+
70+ for k in range (s , w .size ):
71+ b = (
72+ gamma (sigma + 1 ) / gamma (sigma + alpha + 1 ) * k ** (sigma + alpha )
73+ - A @ w [k - s : k ][::- 1 ]
74+ )
75+
76+ if atol is None :
77+ omega = lu_solve (lu_p , b )
78+ else :
79+ omega , _ = gmres (A , b , atol = atol )
80+
81+ assert np .all (np .isfinite (omega ))
82+ yield k , omega
83+
84+
85+ def diethelm_starting_powers (order : int , alpha : float ) -> Array :
86+ r"""Generate monomial powers for the starting weights from [Diethelm2006]_.
87+
88+ The proposed starting weights are given in Section 3.1 from [Diethelm2006]_ as
89+
90+ .. math::
91+
92+ \sigma \in \{i + \alpha j \mid i, j \ge 0, i + \alpha j \le p - 1 \},
93+
94+ where :math:`p` is the desired order. For certain values of :math:`\alpha`,
95+ these monomials can repeat, e.g. for :math:`\alpha = 0.1` we get the same
96+ value for :math:`(i, j) = (1, 0)` and :math:`(i, j) = (0, 10)`. This function
97+ returns only unique powers.
98+
99+ :arg order: order of the LMM method.
100+ :arg alpha: order of the fractional operator.
101+ """
102+ from itertools import product
103+
104+ result = np .array ([
105+ gamma
106+ for i , j in product (range (order ), repeat = 2 )
107+ if (gamma := i + abs (alpha ) * j ) <= order - 1
108+ ])
109+
110+ return np .array (np .unique (result ))
111+
112+
113+ def lubich_starting_powers (order : int , alpha : float , * , beta : float = 1.0 ) -> Array :
114+ r"""Generate monomial powers for the starting weights from [Lubich1986]_.
115+
116+ The proposed starting weights are given in Section 4.2 of [Lubich1986]_ as
117+
118+ .. math::
119+
120+ \sigma \in \{i + \beta - 1 \mid q \le p - 1\},
121+
122+ where :math:`\beta` is chosen such that :math:`q + \beta - 1 \le p - 1`
123+ and :math:`q + \alpha + \beta - 1 < p`, according to Theorem 2.4 from
124+ [Lubich1986]_. In general multiple such :math:`\beta` exist and choosing
125+ more can prove beneficial.
126+
127+ :arg order: order of the LMM method.
128+ :arg alpha: order of the fractional operator.
20129 """
21130 from math import floor
22131
23- return floor (1 / abs (alpha )) - 1
132+ # NOTE: trying to satisfy
133+ # 0 <= q <= p - \beta and 0 <= q < p + 1 - alpha - beta
134+ qmax = floor (max (0 , min (order - beta , order - alpha - beta + 1 ))) + 1
135+
136+ return np .array ([q + beta - 1 for q in range (qmax )])
137+
138+
139+ def garrappa_starting_powers (order : int , alpha : float ) -> Array :
140+ r"""Generate monomial powers for the starting weights from
141+ `FLMM2 <https://www.mathworks.com/matlabcentral/fileexchange/47081-flmm2>`__.
142+
143+ :arg order: order of the LMM method.
144+ :arg alpha: order of the fractional operator.
145+ """
146+ from math import ceil , floor
147+
148+ if order == 2 and 0 < alpha < 1 :
149+ # NOTE: this is the estimate from the FLMM2 MATLAB code
150+ k = floor (1 / abs (alpha ))
151+ else :
152+ # NOTE: this should be vaguely the cardinality of `diethelm_bdf_starting_powers`
153+ k = ceil (order * (order - 1 + 2 * abs (alpha )) / (2 * abs (alpha )))
154+
155+ return np .arange (k ) * abs (alpha )
156+
157+
158+ # }}}
159+
160+
161+ # {{{ backward differentiation formulas
24162
25163
26164def lubich_bdf_weights (alpha : float , order : int , n : int ) -> Array :
27- r"""This function generates the weights for the p- BDF methods of [Lubich1986]_.
165+ r"""This function generates the weights for the BDF methods of [Lubich1986]_.
28166
29167 Table 1 from [Lubich1986]_ gives the generating functions. The weights
30168 constructed here are the coefficients of the Taylor expansion of the
@@ -37,7 +175,7 @@ def lubich_bdf_weights(alpha: float, order: int, n: int) -> Array:
37175 \left(\sum_{k = 1}^p \frac{1}{k} (1 - \zeta)^k\right)^\alpha.
38176
39177 The corresponding weights also have an explicit formulation based on the
40- recursion formula
178+ recursion formula (see Theorem 4 in [Garrappa2015b]_)
41179
42180 .. math::
43181
@@ -49,9 +187,11 @@ def lubich_bdf_weights(alpha: float, order: int, n: int) -> Array:
49187 we restrict here to a maximum order of 6, as the classical BDF methods of
50188 larger orders are not stable.
51189
52- :arg alpha: power of the generating function.
190+ :arg alpha: power of the generating function, where a positive value is
191+ used to approximate the integral and a negative value is used to
192+ approximate the derivative.
53193 :arg order: order of the method, only :math:`1 \le p \le 6` is supported.
54- :arg n: number of weights .
194+ :arg n: number of truncated terms in the power series of :math:`w^\alpha_k(\zeta)` .
55195 """
56196 if order <= 0 :
57197 raise ValueError (f"Negative orders are not supported: { order } " )
@@ -84,93 +224,123 @@ def lubich_bdf_weights(alpha: float, order: int, n: int) -> Array:
84224 else :
85225 raise ValueError (f"Unsupported order '{ order } '" )
86226
87- w = np .empty (n )
88- indices = np .arange (n )
227+ import scipy .special as ss
228+
229+ w = np .empty (n + 1 )
230+ indices = np .arange (n + 1 )
89231
90- w [0 ] = c [0 ] ** alpha
91- for k in range (1 , n ):
92- min_j = max (0 , k - c .size + 1 )
93- max_j = k
232+ if order == 1 :
233+ return np .array (ss .poch (indices , alpha - 1 ) / ss .gamma (alpha ))
234+ else :
235+ w [0 ] = c [0 ] ** - alpha
236+ for k in range (1 , n + 1 ):
237+ min_j = max (0 , k - c .size + 1 )
238+ max_j = k
94239
95- j = indices [min_j :max_j ]
96- omega = (alpha * (k - j ) - j ) * c [1 : k + 1 ][::- 1 ]
240+ j = indices [min_j :max_j ]
241+ omega = - (alpha * (k - j ) + j ) * c [1 : k + 1 ][::- 1 ]
97242
98- w [k ] = np .sum (omega * w [min_j :max_j ]) / (k * c [0 ])
243+ w [k ] = np .sum (omega * w [min_j :max_j ]) / (k * c [0 ])
99244
100245 return w
101246
102247
103- def lubich_bdf_starting_weights (
104- w : Array , s : int , alpha : float , * , beta : float = 1.0 , atol : float | None = None
105- ) -> Iterator [Array ]:
106- r"""Constructs starting weights for a given set of weights *w* from [Lubich1986]_.
248+ # }}}
107249
108- The starting weights are introduced to handle a lack of smoothness of the
109- functions being integrated. They are constructed in such a way that they
110- are exact for a series of monomials, which results in an accurate
111- quadrature for functions of the form :math:`f(x) = x^{\beta - 1} g(x)`,
112- where :math:`g` is smooth.
113250
114- Therefore, they are obtained by solving
251+ # {{{ trapezoidal formulas
115252
116- .. math::
117253
118- \sum_{k = 0}^s w_{mk} k^{q + \beta - 1} =
119- \frac{\Gamma(q + \beta)}{\Gamma(q + \beta + \alpha)}
120- m^{q + \beta + \alpha - 1} -
121- \sum_{k = 0}^s w_{n - k} j^{q + \beta - 1}
254+ def trapezoidal_weights (alpha : float , n : int ) -> Array :
255+ r"""This function constructions the weights for the trapezoidal method
256+ from Section 4.1 in [Garrappa2015b]_.
122257
123- where :math:`q \in \{0, 1, \dots, s - 1\}` and :math:`\beta \ne \{0, -1, \dots\}`.
124- In the simplest case, we can take :math:`\beta = 1` and obtain integer
125- powers. Other values can be chosen depending on the behaviour of :math:`f(x)`
126- near the origin.
258+ The trapezoidal method has the generating function
127259
128- Note that the starting weights are only computed for :math:`m \ge s`.
129- The initial :math:`s` steps are expected to be computed in some other
130- fashion.
260+ .. math::
131261
132- :arg w: convolution weights defined by [Lubich1986]_.
133- :arg s: number of starting weights.
134- :arg alpha: order of the fractional derivative to approximate.
135- :arg beta: order of the singularity at the origin.
136- :arg atol: absolute tolerance used for the GMRES solver. If *None*, an
137- exact LU-based solver is used instead.
262+ w^\alpha(\zeta) \triangleq \left(
263+ \frac{1}{2} \frac{1 + \zeta}{1 - \zeta}
264+ \right)^\alpha,
138265
139- :returns: the starting weights for every point :math:`x_m` starting with
140- :math:`m \ge s`.
266+ which expands into an infinite series. This function truncates that series
267+ to the first *n* terms, which give the desired weights.
268+
269+ :arg alpha: power of the generating function, where a positive value is
270+ used to approximate the integral and a negative value is used to
271+ approximate the derivative.
272+ :arg n: number of truncated terms in the power series of :math:`w^\alpha_k(\zeta)`.
141273 """
142274
143- if s <= 0 :
144- raise ValueError (f"Negative s is not allowed: { s } " )
275+ try :
276+ from scipy .fft import fft , ifft
277+ except ImportError :
278+ from numpy .fft import fft , ifft
145279
146- if beta . is_integer () and beta <= 0 :
147- raise ValueError ( f"Values of beta in 0, -1, ... are not supported: { beta } " )
280+ omega_1 = np . empty ( n + 1 )
281+ omega_2 = np . empty ( n + 1 )
148282
149- from scipy .linalg import lu_factor , lu_solve
150- from scipy .sparse .linalg import gmres
151- from scipy .special import gamma
283+ omega_1 [0 ] = omega_2 [0 ] = 1.0
284+ for k in range (1 , n + 1 ):
285+ omega_1 [k ] = ((alpha + 1 ) / k - 1 ) * omega_1 [k - 1 ]
286+ omega_2 [k ] = (1 + (alpha - 1 ) / k ) * omega_2 [k - 1 ]
152287
153- q = np .arange (s ) + beta - 1
154- j = np .arange (1 , s + 1 ).reshape (- 1 , 1 )
288+ # FIXME: this looks like it can be done faster by using rfft?
289+ omega_1_hat = fft (omega_1 , 2 * omega_1 .size )
290+ omega_2_hat = fft (omega_2 , 2 * omega_2 .size )
291+ omega = ifft (omega_1_hat * omega_2_hat )[: omega_1 .size ].real
155292
156- A = j ** q
157- assert A .shape == (s , s )
293+ return np .array (omega / 2 ** alpha )
158294
159- if atol is None :
160- lu , p = lu_factor (A )
161295
162- for k in range (s , w .size ):
163- b = (
164- gamma (q + 1 ) / gamma (q + alpha + 1 ) * k ** (q + alpha )
165- - A @ w [k - s : k ][::- 1 ]
166- )
296+ # }}}
167297
168- if atol is None :
169- omega = lu_solve ((lu , p ), b )
170- else :
171- omega , _ = gmres (A , b , atol = atol )
172298
173- yield omega
299+ # {{{ Newton-Gregory formulas
300+
301+
302+ def newton_gregory_weights (alpha : float , k : int , n : int ) -> Array :
303+ r"""This function constructions the weights for the Newton-Gregory method
304+ from Section 4.2 in [Garrappa2015b]_.
305+
306+ The Newton-Gregory family of methods have the generating functions
307+
308+ .. math::
309+
310+ w^\alpha_k(\zeta) \triangleq
311+ \frac{G^\alpha_k(\zeta)}{(1 - \zeta)^\alpha},
312+
313+ where :math:`G^\alpha_k(\zeta)` is the :math:`k`-term truncated power
314+ series expansion of
315+
316+ .. math::
317+
318+ G^\alpha(\zeta) = \left(\frac{1 - \zeta}{\log \zeta}\right)^\alpha.
319+
320+ Note that this is not a classic expansion in the sense of [Lubich1986]_
321+ because the :math:`G^\alpha_k(\zeta)` function is first raised to the
322+ :math:`\alpha` power and then truncated to :math:`k` terms. In a standard
323+ scheme, the generating function :math:`w^\alpha_k(\zeta)` is the one that
324+ is truncated.
325+
326+ :arg alpha: power of the generating function, where a positive value is
327+ used to approximate the integral and a negative value is used to
328+ approximate the derivative.
329+ :arg k: number of truncated terms in the power series of :math:`G^\alpha_k(\zeta)`.
330+ :arg n: number of truncated terms in the power series of :math:`w^\alpha_k(\zeta)`.
331+ """
332+ if k != 1 :
333+ raise ValueError (f"Only 2-term truncations are supported: '{ k + 1 } '" )
334+
335+ omega = np .empty (n + 1 )
336+ omega [0 ] = 1.0
337+ for i in range (1 , n + 1 ):
338+ omega [i ] = (1 + (alpha - 1 ) / i ) * omega [i - 1 ]
339+
340+ omega [1 :] = (1 - alpha / 2 ) * omega [1 :] + alpha / 2 * omega [:- 1 ]
341+ omega [0 ] = 1 - alpha / 2.0
342+
343+ return omega
174344
175345
176346# }}}
0 commit comments