mlfp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/conf.py +1 -0
- mlfp/__init__.py +50 -0
- mlfp/_version.py +10 -0
- mlfp/mlfp.py +614 -0
- mlfp-0.1.0.dist-info/METADATA +297 -0
- mlfp-0.1.0.dist-info/RECORD +10 -0
- mlfp-0.1.0.dist-info/WHEEL +5 -0
- mlfp-0.1.0.dist-info/licenses/LICENSE +21 -0
- mlfp-0.1.0.dist-info/top_level.txt +3 -0
- test/test_sample.py +2 -0
docs/conf.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
mlfp/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mlfp: Mittag-Leffler and Prabhakar function, MLF,
|
|
3
|
+
Mittag-Leffler and Prabhakar function evaluation:
|
|
4
|
+
Mittag-Leffler ve Prabhakar fonksiyonlarının hesaplanması.
|
|
5
|
+
|
|
6
|
+
- Two-parameter Mittag-Leffler function, gamma = 1:
|
|
7
|
+
|
|
8
|
+
E_{alpha,beta}(z)
|
|
9
|
+
|
|
10
|
+
is evaluated with Garrappa's optimal parabolic contour (OPC)
|
|
11
|
+
Laplace-transform inversion, with a Taylor series branch for
|
|
12
|
+
small |z|.
|
|
13
|
+
|
|
14
|
+
- Three-parameter Mittag-Leffler / Prabhakar function, gamma != 1:
|
|
15
|
+
|
|
16
|
+
E_{alpha,beta}^gamma(z)
|
|
17
|
+
|
|
18
|
+
is evaluated from its defining power series. The OPC residue
|
|
19
|
+
treatment is not mathematically valid for gamma != 1 because the
|
|
20
|
+
singularities are generally branch points rather than simple poles.
|
|
21
|
+
|
|
22
|
+
Compatible with Python 3.15+.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__author__ = "Mehmet Keçeci"
|
|
27
|
+
__main__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
from .mlfp import (
|
|
30
|
+
# Temel eğri sınıfları
|
|
31
|
+
MittagLefflerError,
|
|
32
|
+
_safe_z_limit,
|
|
33
|
+
_find_optimal_bounded_param,
|
|
34
|
+
_find_optimal_unbounded_param,
|
|
35
|
+
_invert_laplace_transform_two_parameter,
|
|
36
|
+
MittagLeffler,
|
|
37
|
+
mittag_leffler,
|
|
38
|
+
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"MittagLefflerError",
|
|
43
|
+
"_safe_z_limit",
|
|
44
|
+
"_find_optimal_bounded_param",
|
|
45
|
+
"_find_optimal_unbounded_param",
|
|
46
|
+
"_invert_laplace_transform_two_parameter",
|
|
47
|
+
"MittagLeffler",
|
|
48
|
+
"mittag_leffler",
|
|
49
|
+
|
|
50
|
+
]
|
mlfp/_version.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# _version.py
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
__description__ = "mlfp: Mittag-Leffler and Prabhakar function, MLF"
|
|
7
|
+
__author__ = "Mehmet Keçeci"
|
|
8
|
+
__url__ = "https://github.com/WhiteSymmetry/mlfp"
|
|
9
|
+
__docs__ = "https://github.com/WhiteSymmetry/mlfp" # Opsiyonel: Dokümantasyon linki
|
|
10
|
+
__dependencies__ = ["python>=3.11"] # Diğer bağımlılıkları da ekleyebilirsiniz
|
mlfp/mlfp.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
mlfp: Mittag-Leffler and Prabhakar function, MLF,
|
|
4
|
+
Mittag-Leffler and Prabhakar function evaluation:
|
|
5
|
+
Mittag-Leffler ve Prabhakar fonksiyonlarının hesaplanması.
|
|
6
|
+
|
|
7
|
+
- Two-parameter Mittag-Leffler function, gamma = 1:
|
|
8
|
+
|
|
9
|
+
E_{alpha,beta}(z)
|
|
10
|
+
|
|
11
|
+
is evaluated with Garrappa's optimal parabolic contour (OPC)
|
|
12
|
+
Laplace-transform inversion, with a Taylor series branch for
|
|
13
|
+
small |z|.
|
|
14
|
+
|
|
15
|
+
- Three-parameter Mittag-Leffler / Prabhakar function, gamma != 1:
|
|
16
|
+
|
|
17
|
+
E_{alpha,beta}^gamma(z)
|
|
18
|
+
|
|
19
|
+
is evaluated from its defining power series. The OPC residue
|
|
20
|
+
treatment is not mathematically valid for gamma != 1 because the
|
|
21
|
+
singularities are generally branch points rather than simple poles.
|
|
22
|
+
|
|
23
|
+
Compatible with Python 3.15+.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import cmath
|
|
29
|
+
import math
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import ClassVar, Final
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
from numpy.typing import ArrayLike, NDArray
|
|
35
|
+
from scipy.special import gamma as gamma_func
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MittagLefflerError(ValueError):
|
|
39
|
+
"""Raised for invalid Mittag-Leffler parameters or arguments."""
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Float64 stability limits
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
_LOG_MAX_FLOAT64: Final[float] = math.log(np.finfo(np.float64).max) # ≈ 709.78
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_z_limit(alpha: float, safety: float = 0.9) -> float:
|
|
48
|
+
"""
|
|
49
|
+
Upper bound on |z| for which the OPC residue sum fits in float64.
|
|
50
|
+
|
|
51
|
+
The residue of the branch point s satisfies |Res| ≲ exp(t·Re(s))·|s|^(1-β),
|
|
52
|
+
with Re(s) ≈ |z|^(1/α) along the positive real axis. Requiring
|
|
53
|
+
t·Re(s) < log(float64 max) yields |z| < log_max^α.
|
|
54
|
+
"""
|
|
55
|
+
if alpha <= 0.0:
|
|
56
|
+
return 0.0
|
|
57
|
+
return safety * _LOG_MAX_FLOAT64 ** alpha
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Low-level optimal parabolic contour routines (Garrappa OPC)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
def _find_optimal_bounded_param(
|
|
64
|
+
t: float,
|
|
65
|
+
phi_star0: float,
|
|
66
|
+
phi_star1: float,
|
|
67
|
+
p: float,
|
|
68
|
+
q: float,
|
|
69
|
+
*,
|
|
70
|
+
log_eps: float,
|
|
71
|
+
log_machine_eps: float,
|
|
72
|
+
fac: float = 1.01,
|
|
73
|
+
p_eps: float = 1.0e-14,
|
|
74
|
+
q_eps: float = 1.0e-14,
|
|
75
|
+
) -> tuple[float, float, float]:
|
|
76
|
+
"""Find optimal contour parameters for a right-bounded region."""
|
|
77
|
+
f_max = math.exp(log_eps - log_machine_eps)
|
|
78
|
+
threshold = 2.0 * math.sqrt((log_eps - log_machine_eps) / t)
|
|
79
|
+
|
|
80
|
+
sqrt_phi0 = math.sqrt(phi_star0)
|
|
81
|
+
sqrt_phi1 = min(math.sqrt(phi_star1), threshold - sqrt_phi0)
|
|
82
|
+
|
|
83
|
+
if p < p_eps and q < q_eps:
|
|
84
|
+
sqrt_phi_bar0 = sqrt_phi0
|
|
85
|
+
sqrt_phi_bar1 = sqrt_phi1
|
|
86
|
+
admissible = True
|
|
87
|
+
|
|
88
|
+
elif p < p_eps:
|
|
89
|
+
sqrt_phi_bar0 = sqrt_phi0
|
|
90
|
+
if sqrt_phi0 > 0.0:
|
|
91
|
+
f_min = fac * (sqrt_phi0 / (sqrt_phi1 - sqrt_phi0)) ** q
|
|
92
|
+
else:
|
|
93
|
+
f_min = fac
|
|
94
|
+
if f_min < f_max:
|
|
95
|
+
f_bar = f_min + f_min / f_max * (f_max - f_min)
|
|
96
|
+
f_q = f_bar ** (-1.0 / q)
|
|
97
|
+
sqrt_phi_bar1 = (2.0 * sqrt_phi1 - f_q * sqrt_phi0) / (2.0 + f_q)
|
|
98
|
+
admissible = True
|
|
99
|
+
else:
|
|
100
|
+
admissible = False
|
|
101
|
+
|
|
102
|
+
elif q < q_eps:
|
|
103
|
+
sqrt_phi_bar1 = sqrt_phi1
|
|
104
|
+
f_min = fac * (sqrt_phi1 / (sqrt_phi1 - sqrt_phi0)) ** p
|
|
105
|
+
if f_min < f_max:
|
|
106
|
+
f_bar = f_min + f_min / f_max * (f_max - f_min)
|
|
107
|
+
f_p = f_bar ** (-1.0 / p)
|
|
108
|
+
sqrt_phi_bar0 = (2.0 * sqrt_phi0 - f_p * sqrt_phi1) / (2.0 - f_p)
|
|
109
|
+
admissible = True
|
|
110
|
+
else:
|
|
111
|
+
admissible = False
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
f_min = (
|
|
115
|
+
fac
|
|
116
|
+
* (sqrt_phi1 + sqrt_phi0)
|
|
117
|
+
/ (sqrt_phi1 - sqrt_phi0) ** max(p, q)
|
|
118
|
+
)
|
|
119
|
+
if f_min < f_max:
|
|
120
|
+
f_min = max(f_min, 1.5)
|
|
121
|
+
f_bar = f_min + f_min / f_max * (f_max - f_min)
|
|
122
|
+
f_p = f_bar ** (-1.0 / p)
|
|
123
|
+
f_q = f_bar ** (-1.0 / q)
|
|
124
|
+
weight = -phi_star1 * t / log_eps
|
|
125
|
+
denominator = 2.0 + weight - (1.0 + weight) * f_p + f_q
|
|
126
|
+
sqrt_phi_bar0 = (
|
|
127
|
+
(2.0 + weight + f_q) * sqrt_phi0 + f_p * sqrt_phi1
|
|
128
|
+
) / denominator
|
|
129
|
+
sqrt_phi_bar1 = (
|
|
130
|
+
-(1.0 + weight) * f_q * sqrt_phi0
|
|
131
|
+
+ (2.0 + weight - (1.0 + weight) * f_p) * sqrt_phi1
|
|
132
|
+
) / denominator
|
|
133
|
+
admissible = True
|
|
134
|
+
else:
|
|
135
|
+
admissible = False
|
|
136
|
+
|
|
137
|
+
if not admissible:
|
|
138
|
+
return 0.0, math.inf, 0.0
|
|
139
|
+
|
|
140
|
+
log_eps -= math.log(f_bar)
|
|
141
|
+
weight = -sqrt_phi_bar1**2 * t / log_eps
|
|
142
|
+
|
|
143
|
+
mu = (
|
|
144
|
+
((1.0 + weight) * sqrt_phi_bar0 + sqrt_phi_bar1) / (2.0 + weight)
|
|
145
|
+
) ** 2
|
|
146
|
+
|
|
147
|
+
step = (
|
|
148
|
+
-2.0
|
|
149
|
+
* math.pi
|
|
150
|
+
/ log_eps
|
|
151
|
+
* (sqrt_phi_bar1 - sqrt_phi_bar0)
|
|
152
|
+
/ ((1.0 + weight) * sqrt_phi_bar0 + sqrt_phi_bar1)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
nodes = math.ceil(math.sqrt(1.0 - log_eps / t / mu) / step)
|
|
156
|
+
return mu, nodes, step
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _find_optimal_unbounded_param(
|
|
160
|
+
t: float,
|
|
161
|
+
phi_star: float,
|
|
162
|
+
p: float,
|
|
163
|
+
*,
|
|
164
|
+
log_eps: float,
|
|
165
|
+
log_machine_eps: float,
|
|
166
|
+
fac: float = 1.01,
|
|
167
|
+
p_eps: float = 1.0e-14,
|
|
168
|
+
) -> tuple[float, float, float]:
|
|
169
|
+
"""Find optimal contour parameters for a right-unbounded region."""
|
|
170
|
+
sqrt_phi_star = math.sqrt(phi_star)
|
|
171
|
+
phi_bar = fac * phi_star if phi_star > 0.0 else 0.01
|
|
172
|
+
sqrt_phi_bar = math.sqrt(phi_bar)
|
|
173
|
+
|
|
174
|
+
f_min = 1.0
|
|
175
|
+
f_max = 10.0
|
|
176
|
+
f_target = 5.0
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
phi_t = phi_bar * t
|
|
180
|
+
log_eps_phi_t = log_eps / phi_t
|
|
181
|
+
nodes = math.ceil(
|
|
182
|
+
phi_t
|
|
183
|
+
/ math.pi
|
|
184
|
+
* (
|
|
185
|
+
1.0
|
|
186
|
+
- 3.0 * log_eps_phi_t / 2.0
|
|
187
|
+
+ math.sqrt(1.0 - 2.0 * log_eps_phi_t)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
a = math.pi * nodes / phi_t
|
|
191
|
+
sqrt_mu = (
|
|
192
|
+
sqrt_phi_bar * abs(4.0 - a)
|
|
193
|
+
/ abs(7.0 - math.sqrt(1.0 + 12.0 * a))
|
|
194
|
+
)
|
|
195
|
+
f_bar = ((sqrt_phi_bar - sqrt_phi_star) / sqrt_mu) ** (-p)
|
|
196
|
+
if p < p_eps or f_min < f_bar < f_max:
|
|
197
|
+
break
|
|
198
|
+
sqrt_phi_bar = f_target ** (-1.0 / p) * sqrt_mu + sqrt_phi_star
|
|
199
|
+
phi_bar = sqrt_phi_bar**2
|
|
200
|
+
|
|
201
|
+
mu = sqrt_mu**2
|
|
202
|
+
step = (
|
|
203
|
+
(-3.0 * a - 2.0 + 2.0 * math.sqrt(1.0 + 12.0 * a))
|
|
204
|
+
/ (4.0 - a)
|
|
205
|
+
/ nodes
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
roundoff_threshold = (log_eps - log_machine_eps) / t
|
|
209
|
+
if mu > roundoff_threshold:
|
|
210
|
+
q = (
|
|
211
|
+
0.0
|
|
212
|
+
if abs(p) < p_eps
|
|
213
|
+
else f_target ** (-1.0 / p) * math.sqrt(mu)
|
|
214
|
+
)
|
|
215
|
+
phi_bar = (q + sqrt_phi_star) ** 2
|
|
216
|
+
if phi_bar < roundoff_threshold:
|
|
217
|
+
weight = math.sqrt(
|
|
218
|
+
log_machine_eps / (log_machine_eps - log_eps)
|
|
219
|
+
)
|
|
220
|
+
u = math.sqrt(-phi_bar * t / log_machine_eps)
|
|
221
|
+
mu = roundoff_threshold
|
|
222
|
+
nodes = math.ceil(
|
|
223
|
+
weight * log_eps / (2.0 * math.pi * (u * weight - 1.0))
|
|
224
|
+
)
|
|
225
|
+
step = weight / nodes
|
|
226
|
+
else:
|
|
227
|
+
return mu, math.inf, 0.0
|
|
228
|
+
|
|
229
|
+
return mu, nodes, step
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _invert_laplace_transform_two_parameter(
|
|
233
|
+
t: float,
|
|
234
|
+
z: complex,
|
|
235
|
+
*,
|
|
236
|
+
alpha: float,
|
|
237
|
+
beta: float,
|
|
238
|
+
tolerance: float,
|
|
239
|
+
) -> complex:
|
|
240
|
+
"""Evaluate E_{alpha,beta}(z t^alpha) via OPC Laplace inversion."""
|
|
241
|
+
if abs(z) == 0.0:
|
|
242
|
+
return complex(1.0 / gamma_func(beta))
|
|
243
|
+
|
|
244
|
+
machine_eps = float(np.finfo(np.float64).eps)
|
|
245
|
+
log_machine_eps = math.log(machine_eps)
|
|
246
|
+
log_tolerance = math.log(tolerance)
|
|
247
|
+
log_10 = math.log(10.0)
|
|
248
|
+
log_tolerance_difference = log_tolerance - log_machine_eps
|
|
249
|
+
|
|
250
|
+
theta = cmath.phase(z)
|
|
251
|
+
k_min = math.ceil(-alpha / 2.0 - theta / (2.0 * math.pi))
|
|
252
|
+
k_max = math.floor(alpha / 2.0 - theta / (2.0 * math.pi))
|
|
253
|
+
|
|
254
|
+
k_values = np.arange(k_min, k_max + 1, dtype=np.float64)
|
|
255
|
+
poles = abs(z) ** (1.0 / alpha) * np.exp(
|
|
256
|
+
1j * (theta + 2.0 * math.pi * k_values) / alpha
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
phi_values = (poles.real + np.abs(poles)) / 2.0
|
|
260
|
+
order = np.argsort(phi_values)
|
|
261
|
+
poles = poles[order]
|
|
262
|
+
phi_values = phi_values[order]
|
|
263
|
+
|
|
264
|
+
nonzero = phi_values > machine_eps
|
|
265
|
+
poles = poles[nonzero]
|
|
266
|
+
phi_values = phi_values[nonzero]
|
|
267
|
+
|
|
268
|
+
poles = np.insert(poles, 0, 0.0 + 0.0j)
|
|
269
|
+
phi_values = np.insert(phi_values, 0, 0.0)
|
|
270
|
+
|
|
271
|
+
strengths = np.ones(poles.size, dtype=np.float64)
|
|
272
|
+
strengths[0] = max(0.0, -2.0 * (alpha - beta + 1.0))
|
|
273
|
+
right_strengths = np.ones(poles.size, dtype=np.float64)
|
|
274
|
+
right_strengths[-1] = math.inf
|
|
275
|
+
|
|
276
|
+
phi_values = np.insert(phi_values, phi_values.size, math.inf)
|
|
277
|
+
|
|
278
|
+
region_indices = np.flatnonzero(
|
|
279
|
+
(phi_values[:-1] < log_tolerance_difference / t)
|
|
280
|
+
& (phi_values[:-1] < phi_values[1:])
|
|
281
|
+
)
|
|
282
|
+
if region_indices.size == 0:
|
|
283
|
+
raise MittagLefflerError(
|
|
284
|
+
"No admissible integration region was found."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
region_count = region_indices[-1] + 1
|
|
288
|
+
mu_values = np.full(region_count, math.inf, dtype=np.float64)
|
|
289
|
+
node_values = np.full(region_count, math.inf, dtype=np.float64)
|
|
290
|
+
step_values = np.full(region_count, math.inf, dtype=np.float64)
|
|
291
|
+
|
|
292
|
+
current_log_tolerance = log_tolerance
|
|
293
|
+
while True:
|
|
294
|
+
for region in region_indices:
|
|
295
|
+
if region < poles.size - 1:
|
|
296
|
+
(mu_values[region],
|
|
297
|
+
node_values[region],
|
|
298
|
+
step_values[region]) = _find_optimal_bounded_param(
|
|
299
|
+
t,
|
|
300
|
+
phi_values[region],
|
|
301
|
+
phi_values[region + 1],
|
|
302
|
+
strengths[region],
|
|
303
|
+
right_strengths[region],
|
|
304
|
+
log_eps=current_log_tolerance,
|
|
305
|
+
log_machine_eps=log_machine_eps,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
(mu_values[region],
|
|
309
|
+
node_values[region],
|
|
310
|
+
step_values[region]) = _find_optimal_unbounded_param(
|
|
311
|
+
t,
|
|
312
|
+
phi_values[region],
|
|
313
|
+
strengths[region],
|
|
314
|
+
log_eps=current_log_tolerance,
|
|
315
|
+
log_machine_eps=log_machine_eps,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if np.min(node_values) <= 200.0:
|
|
319
|
+
break
|
|
320
|
+
current_log_tolerance += log_10
|
|
321
|
+
if current_log_tolerance >= 0.0:
|
|
322
|
+
raise MittagLefflerError(
|
|
323
|
+
"Failed to find an admissible parabolic contour region."
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
selected = int(np.argmin(node_values))
|
|
327
|
+
mu = float(mu_values[selected])
|
|
328
|
+
nodes = int(node_values[selected])
|
|
329
|
+
step = float(step_values[selected])
|
|
330
|
+
|
|
331
|
+
indices = np.arange(-nodes, nodes + 1, dtype=np.float64)
|
|
332
|
+
u_values = step * indices
|
|
333
|
+
|
|
334
|
+
contour = mu * (1j * u_values + 1.0) ** 2
|
|
335
|
+
derivative = -2.0 * mu * u_values + 2j * mu
|
|
336
|
+
integrand = (
|
|
337
|
+
np.exp(contour * t)
|
|
338
|
+
* contour ** (alpha - beta)
|
|
339
|
+
/ (contour**alpha - z)
|
|
340
|
+
* derivative
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
integral = step * integrand.sum() / (2j * math.pi)
|
|
344
|
+
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
# Residue contribution from branch points to the right of the contour
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
residue_poles = poles[selected + 1 :]
|
|
349
|
+
|
|
350
|
+
if residue_poles.size:
|
|
351
|
+
# |Res_k| ≈ exp(t·Re(s_k)) · |s_k|^(1-β)
|
|
352
|
+
# We check the total log-magnitude BEFORE calling exp() so that
|
|
353
|
+
# overflow is turned into a clear error instead of a silent nan/inf.
|
|
354
|
+
magnitudes = np.maximum(np.abs(residue_poles), 1e-300)
|
|
355
|
+
log_mags = (
|
|
356
|
+
t * residue_poles.real
|
|
357
|
+
+ (1.0 - beta) * np.log(magnitudes)
|
|
358
|
+
)
|
|
359
|
+
max_log_mag = float(np.max(log_mags))
|
|
360
|
+
|
|
361
|
+
if max_log_mag > _LOG_MAX_FLOAT64 - 1.0:
|
|
362
|
+
raise MittagLefflerError(
|
|
363
|
+
"The Mittag-Leffler value is too large to represent in "
|
|
364
|
+
f"float64: max log|residue| ≈ {max_log_mag:.1f} exceeds "
|
|
365
|
+
f"log(float64_max) ≈ {_LOG_MAX_FLOAT64:.1f}. "
|
|
366
|
+
f"|z| = {abs(z):.3g} exceeds the safe range for "
|
|
367
|
+
f"α = {alpha} (safe limit ≈ {_safe_z_limit(alpha):.3g}). "
|
|
368
|
+
"Reduce |z|, or use mpmath for higher-precision arithmetic."
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
residues = (
|
|
372
|
+
residue_poles ** (1.0 - beta) * np.exp(t * residue_poles)
|
|
373
|
+
).sum() / alpha
|
|
374
|
+
else:
|
|
375
|
+
residues = 0.0
|
|
376
|
+
|
|
377
|
+
return complex(integral + residues)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# Public evaluator
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
@dataclass(slots=True)
|
|
384
|
+
class MittagLeffler:
|
|
385
|
+
"""
|
|
386
|
+
Evaluator for the Mittag-Leffler and Prabhakar functions.
|
|
387
|
+
|
|
388
|
+
For gamma = 1: ``E_{alpha,beta}(z)`` is evaluated via Garrappa's
|
|
389
|
+
optimal parabolic contour method (with a Taylor-series branch for
|
|
390
|
+
small ``|z|``).
|
|
391
|
+
|
|
392
|
+
For gamma != 1: ``E_{alpha,beta}^gamma(z)`` is evaluated from the
|
|
393
|
+
defining power series.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
alpha : float
|
|
398
|
+
Positive real order parameter.
|
|
399
|
+
beta : float
|
|
400
|
+
Real second parameter.
|
|
401
|
+
gamma : float
|
|
402
|
+
Positive real Prabhakar parameter.
|
|
403
|
+
tolerance : float
|
|
404
|
+
Target relative accuracy (must satisfy ``0 < tolerance < 1``).
|
|
405
|
+
series_threshold : float
|
|
406
|
+
For ``gamma == 1``, arguments with ``|z| <= series_threshold``
|
|
407
|
+
use the Taylor series instead of OPC.
|
|
408
|
+
max_series_terms : int
|
|
409
|
+
Maximum number of terms in the series evaluator.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
alpha: float
|
|
413
|
+
beta: float = 1.0
|
|
414
|
+
gamma: float = 1.0
|
|
415
|
+
tolerance: float = 1.0e-15
|
|
416
|
+
series_threshold: float = 0.1
|
|
417
|
+
max_series_terms: int = 100_000
|
|
418
|
+
|
|
419
|
+
# NOTE: ClassVar so that `slots=True` does not treat it as a field.
|
|
420
|
+
_MACHINE_EPS: ClassVar[float] = float(np.finfo(np.float64).eps)
|
|
421
|
+
|
|
422
|
+
def __post_init__(self) -> None:
|
|
423
|
+
if not math.isfinite(self.alpha) or self.alpha <= 0.0:
|
|
424
|
+
raise MittagLefflerError(
|
|
425
|
+
"alpha must be a positive real number."
|
|
426
|
+
)
|
|
427
|
+
if not math.isfinite(self.beta):
|
|
428
|
+
raise MittagLefflerError("beta must be a finite real number.")
|
|
429
|
+
if not math.isfinite(self.gamma) or self.gamma <= 0.0:
|
|
430
|
+
raise MittagLefflerError(
|
|
431
|
+
"gamma must be a positive real number."
|
|
432
|
+
)
|
|
433
|
+
if not 0.0 < self.tolerance < 1.0:
|
|
434
|
+
raise MittagLefflerError(
|
|
435
|
+
"tolerance must satisfy 0 < tolerance < 1."
|
|
436
|
+
)
|
|
437
|
+
if self.series_threshold < 0.0:
|
|
438
|
+
raise MittagLefflerError(
|
|
439
|
+
"series_threshold must be non-negative."
|
|
440
|
+
)
|
|
441
|
+
if self.max_series_terms < 1:
|
|
442
|
+
raise MittagLefflerError(
|
|
443
|
+
"max_series_terms must be a positive integer."
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# ------------------------------------------------------------------
|
|
447
|
+
# Real-valued public interface
|
|
448
|
+
# ------------------------------------------------------------------
|
|
449
|
+
def __call__(
|
|
450
|
+
self, z: ArrayLike
|
|
451
|
+
) -> NDArray[np.float64] | np.float64:
|
|
452
|
+
"""Evaluate for real arguments (raises on complex input)."""
|
|
453
|
+
z_array = np.asarray(z, dtype=np.complex128)
|
|
454
|
+
|
|
455
|
+
if z_array.ndim == 0:
|
|
456
|
+
return np.float64(self._evaluate_scalar(complex(z_array)))
|
|
457
|
+
|
|
458
|
+
evaluate = np.vectorize(
|
|
459
|
+
lambda value: self._evaluate_scalar(complex(value)),
|
|
460
|
+
otypes=[np.float64],
|
|
461
|
+
)
|
|
462
|
+
return evaluate(z_array)
|
|
463
|
+
|
|
464
|
+
def evaluate(
|
|
465
|
+
self, z: ArrayLike
|
|
466
|
+
) -> NDArray[np.float64] | np.float64:
|
|
467
|
+
"""Explicit alias for :meth:`__call__`."""
|
|
468
|
+
return self(z)
|
|
469
|
+
|
|
470
|
+
# ------------------------------------------------------------------
|
|
471
|
+
# Complex-valued public interface
|
|
472
|
+
# ------------------------------------------------------------------
|
|
473
|
+
def evaluate_complex(
|
|
474
|
+
self, z: ArrayLike
|
|
475
|
+
) -> NDArray[np.complex128] | np.complex128:
|
|
476
|
+
"""
|
|
477
|
+
Evaluate the Mittag-Leffler / Prabhakar function and return the
|
|
478
|
+
full complex-valued result.
|
|
479
|
+
|
|
480
|
+
For ``gamma == 1`` the OPC method (or the Taylor series, if
|
|
481
|
+
``|z| <= series_threshold``) is used; for ``gamma != 1`` the
|
|
482
|
+
defining power series is used.
|
|
483
|
+
"""
|
|
484
|
+
z_array = np.asarray(z, dtype=np.complex128)
|
|
485
|
+
|
|
486
|
+
if z_array.ndim == 0:
|
|
487
|
+
return np.complex128(
|
|
488
|
+
self._evaluate_scalar_complex(complex(z_array))
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
evaluate = np.vectorize(
|
|
492
|
+
lambda value: self._evaluate_scalar_complex(complex(value)),
|
|
493
|
+
otypes=[np.complex128],
|
|
494
|
+
)
|
|
495
|
+
return evaluate(z_array)
|
|
496
|
+
|
|
497
|
+
# ------------------------------------------------------------------
|
|
498
|
+
# Internal evaluators
|
|
499
|
+
# ------------------------------------------------------------------
|
|
500
|
+
def _evaluate_scalar(self, z: complex) -> float:
|
|
501
|
+
"""Return the real part for real z; raise otherwise."""
|
|
502
|
+
value = self._evaluate_scalar_complex(z)
|
|
503
|
+
if z.imag == 0.0:
|
|
504
|
+
return float(value.real)
|
|
505
|
+
raise MittagLefflerError(
|
|
506
|
+
"For complex z, use evaluate_complex() to obtain the full "
|
|
507
|
+
"complex-valued result."
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def _evaluate_scalar_complex(self, z: complex) -> complex:
|
|
511
|
+
"""Return the full complex-valued Mittag-Leffler result."""
|
|
512
|
+
if abs(z) < self._MACHINE_EPS:
|
|
513
|
+
return complex(1.0 / gamma_func(self.beta))
|
|
514
|
+
|
|
515
|
+
if self.gamma == 1.0:
|
|
516
|
+
if abs(z) <= self.series_threshold:
|
|
517
|
+
return self._evaluate_series(z)
|
|
518
|
+
return _invert_laplace_transform_two_parameter(
|
|
519
|
+
t=1.0,
|
|
520
|
+
z=z,
|
|
521
|
+
alpha=self.alpha,
|
|
522
|
+
beta=self.beta,
|
|
523
|
+
tolerance=self.tolerance,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return self._evaluate_series(z)
|
|
527
|
+
|
|
528
|
+
def _evaluate_series(self, z: complex) -> complex:
|
|
529
|
+
r"""
|
|
530
|
+
Evaluate the Mittag-Leffler / Prabhakar function from
|
|
531
|
+
|
|
532
|
+
E_{alpha,beta}^gamma(z)
|
|
533
|
+
= sum_{k=0}^infty (gamma)_k z^k
|
|
534
|
+
/ (k! Gamma(alpha k + beta)).
|
|
535
|
+
|
|
536
|
+
.. note::
|
|
537
|
+
The k=0 term equals ``1/Gamma(beta)`` (NOT 1 when beta != 1).
|
|
538
|
+
"""
|
|
539
|
+
# ---- CRITICAL FIX ----
|
|
540
|
+
# k=0 term is 1/Gamma(beta), not 1.
|
|
541
|
+
total = complex(1.0 / gamma_func(self.beta))
|
|
542
|
+
|
|
543
|
+
pochhammer = 1.0
|
|
544
|
+
z_power = 1.0 + 0.0j
|
|
545
|
+
factorial = 1
|
|
546
|
+
|
|
547
|
+
for k in range(1, self.max_series_terms + 1):
|
|
548
|
+
pochhammer *= self.gamma + k - 1.0
|
|
549
|
+
z_power *= z
|
|
550
|
+
factorial *= k
|
|
551
|
+
|
|
552
|
+
term = (
|
|
553
|
+
pochhammer
|
|
554
|
+
* z_power
|
|
555
|
+
/ (factorial * gamma_func(self.alpha * k + self.beta))
|
|
556
|
+
)
|
|
557
|
+
total += term
|
|
558
|
+
|
|
559
|
+
if abs(term) <= self.tolerance * (1.0 + abs(total)):
|
|
560
|
+
return total
|
|
561
|
+
|
|
562
|
+
raise MittagLefflerError(
|
|
563
|
+
"The Mittag-Leffler series did not converge to the requested "
|
|
564
|
+
"tolerance within max_series_terms terms."
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def __repr__(self) -> str:
|
|
568
|
+
return (
|
|
569
|
+
f"{type(self).__name__}("
|
|
570
|
+
f"alpha={self.alpha!r}, beta={self.beta!r}, "
|
|
571
|
+
f"gamma={self.gamma!r}, tolerance={self.tolerance!r}, "
|
|
572
|
+
f"series_threshold={self.series_threshold!r}, "
|
|
573
|
+
f"max_series_terms={self.max_series_terms!r})"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
# Convenience function
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
def mittag_leffler(
|
|
581
|
+
z: ArrayLike,
|
|
582
|
+
alpha: float,
|
|
583
|
+
beta: float = 1.0,
|
|
584
|
+
gamma: float = 1.0,
|
|
585
|
+
tolerance: float = 1.0e-15,
|
|
586
|
+
series_threshold: float = 0.1,
|
|
587
|
+
) -> (
|
|
588
|
+
NDArray[np.float64]
|
|
589
|
+
| np.float64
|
|
590
|
+
| NDArray[np.complex128]
|
|
591
|
+
| np.complex128
|
|
592
|
+
):
|
|
593
|
+
"""
|
|
594
|
+
Convenience wrapper.
|
|
595
|
+
|
|
596
|
+
If ``z`` has a complex dtype the full complex result is returned;
|
|
597
|
+
otherwise the real part is returned.
|
|
598
|
+
|
|
599
|
+
Examples
|
|
600
|
+
--------
|
|
601
|
+
>>> mittag_leffler(1.0, alpha=0.5)
|
|
602
|
+
5.008980080762283
|
|
603
|
+
"""
|
|
604
|
+
evaluator = MittagLeffler(
|
|
605
|
+
alpha=alpha,
|
|
606
|
+
beta=beta,
|
|
607
|
+
gamma=gamma,
|
|
608
|
+
tolerance=tolerance,
|
|
609
|
+
series_threshold=series_threshold,
|
|
610
|
+
)
|
|
611
|
+
z_array = np.asarray(z)
|
|
612
|
+
if np.iscomplexobj(z_array):
|
|
613
|
+
return evaluator.evaluate_complex(z_array)
|
|
614
|
+
return evaluator(z_array)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlfp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: mlfp: Mittag-Leffler and Prabhakar function, MLF
|
|
5
|
+
Home-page: https://github.com/WhiteSymmetry/mlfp
|
|
6
|
+
Author: Mehmet Keçeci
|
|
7
|
+
Author-email: Mehmet Keçeci <enfo@tuta.io>
|
|
8
|
+
Maintainer: Mehmet Keçeci
|
|
9
|
+
Maintainer-email: enfo@tuta.io
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Mehmet Keçeci
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
|
|
32
|
+
Project-URL: Homepage, https://github.com/WhiteSymmetry/mlfp
|
|
33
|
+
Project-URL: Bug Tracker, https://github.com/WhiteSymmetry/mlfp/issues
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Intended Audience :: Science/Research
|
|
37
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
38
|
+
Requires-Python: >=3.11
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: scipy
|
|
42
|
+
Requires-Dist: matplotlib
|
|
43
|
+
Requires-Dist: numpy
|
|
44
|
+
Provides-Extra: test
|
|
45
|
+
Requires-Dist: pytest; extra == "test"
|
|
46
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
47
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
48
|
+
Requires-Dist: ruff; extra == "test"
|
|
49
|
+
Requires-Dist: scipy; extra == "test"
|
|
50
|
+
Requires-Dist: matplotlib; extra == "test"
|
|
51
|
+
Requires-Dist: numpy; extra == "test"
|
|
52
|
+
Dynamic: author
|
|
53
|
+
Dynamic: home-page
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
Dynamic: maintainer
|
|
56
|
+
Dynamic: maintainer-email
|
|
57
|
+
Dynamic: requires-python
|
|
58
|
+
|
|
59
|
+
# mlfp
|
|
60
|
+
mlfp: Mittag-Leffler and Prabhakar function, MLF, Mittag-Leffler and Prabhakar function evaluation: Mittag-Leffler ve Prabhakar fonksiyonlarının hesaplanması.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# mlfp <img src="https://github.com/WhiteSymmetry/mlfp/blob/main/docs/logo.png" alt="mlfp" align="right" height="140"/>
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
Parameter validation tests
|
|
69
|
+
|
|
70
|
+
=======================================================
|
|
71
|
+
|
|
72
|
+
[OK] negative alpha → MittagLefflerError
|
|
73
|
+
|
|
74
|
+
[OK] zero alpha → MittagLefflerError
|
|
75
|
+
|
|
76
|
+
[OK] nan alpha → MittagLefflerError
|
|
77
|
+
|
|
78
|
+
[OK] infinite beta → MittagLefflerError
|
|
79
|
+
|
|
80
|
+
[OK] zero gamma → MittagLefflerError
|
|
81
|
+
|
|
82
|
+
[OK] tolerance = 1 → MittagLefflerError
|
|
83
|
+
|
|
84
|
+
[OK] tolerance = 0 → MittagLefflerError
|
|
85
|
+
|
|
86
|
+
[OK] negative series_threshold → MittagLefflerError
|
|
87
|
+
|
|
88
|
+
[OK] zero max_series_terms → MittagLefflerError
|
|
89
|
+
|
|
90
|
+
[OK] complex z to __call__ → MittagLefflerError
|
|
91
|
+
|
|
92
|
+
========================================================
|
|
93
|
+
|
|
94
|
+
Safe-limit overflow detection
|
|
95
|
+
|
|
96
|
+
====================================================================================================
|
|
97
|
+
|
|
98
|
+
[OK ] α=0.25, |z|=10.3 → MittagLefflerError (The Mittag-Leffler value is too large to represent in f…)
|
|
99
|
+
|
|
100
|
+
[OK ] α=0.5 , |z|=49 → MittagLefflerError (The Mittag-Leffler value is too large to represent in f…)
|
|
101
|
+
|
|
102
|
+
[OK ] α=1.0 , |z|=1.28e+03 → MittagLefflerError (The Mittag-Leffler value is too large to represent in f…)
|
|
103
|
+
|
|
104
|
+
[OK ] α=2.0 , |z|=9.07e+05 → MittagLefflerError (The Mittag-Leffler value is too large to represent in f…)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
============================================================================
|
|
108
|
+
|
|
109
|
+
Numerical comparison: mlfp vs mpmath (ground truth) vs pymittagleffler
|
|
110
|
+
|
|
111
|
+
mlfp tolerance : 1.0e-13
|
|
112
|
+
|
|
113
|
+
mpmath digits : dynamic (>=50, up to 600)
|
|
114
|
+
|
|
115
|
+
pass criterion : relative error < 1e-09
|
|
116
|
+
|
|
117
|
+
=============================================================================
|
|
118
|
+
|
|
119
|
+
angle = 0.000000 rad (0.0°)
|
|
120
|
+
|
|
121
|
+
----------------------------------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=1.11e+190, max_rel=7.23e-14 | vs pymtl: max_rel=7.19e-14
|
|
124
|
+
|
|
125
|
+
angle = 0.785398 rad (45.0°)
|
|
126
|
+
|
|
127
|
+
----------------------------------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=3.40e-14, max_rel=3.40e-14 | vs pymtl: max_rel=3.41e-14
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
angle = 1.570796 rad (90.0°)
|
|
133
|
+
|
|
134
|
+
----------------------------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=2.56e-14, max_rel=2.56e-14 | vs pymtl: max_rel=2.57e-14
|
|
137
|
+
|
|
138
|
+
angle = 2.356194 rad (135.0°)
|
|
139
|
+
|
|
140
|
+
----------------------------------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=2.23e-14, max_rel=2.23e-14 | vs pymtl: max_rel=2.25e-14
|
|
143
|
+
|
|
144
|
+
angle = 3.141593 rad (180.0°)
|
|
145
|
+
|
|
146
|
+
----------------------------------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=2.08e-14, max_rel=2.08e-14 | vs pymtl: max_rel=2.09e-14
|
|
149
|
+
|
|
150
|
+
angle = 3.926991 rad (225.0°)
|
|
151
|
+
|
|
152
|
+
----------------------------------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
[OK ] α=0.25, β=0.5 | n= 13 | vs mpmath: max_abs=2.11e-14, max_rel=2.11e-14 | vs pymtl: max_rel=2.13e-14
|
|
155
|
+
|
|
156
|
+
=====================================================================
|
|
157
|
+
|
|
158
|
+
Series vs OPC consistency (gamma = 1) — small |z|, both methods valid
|
|
159
|
+
|
|
160
|
+
=====================================================================
|
|
161
|
+
|
|
162
|
+
[OK ] α=0.5 , β=1.0 | max_abs=2.01e-13, max_rel=5.13e-14
|
|
163
|
+
[OK ] α=1.0 , β=1.5 | max_abs=3.65e-14, max_rel=1.18e-14
|
|
164
|
+
[OK ] α=1.5 , β=0.5 | max_abs=8.20e-14, max_rel=5.60e-14
|
|
165
|
+
[OK ] α=2.0 , β=2.0 | max_abs=3.11e-15, max_rel=2.66e-15
|
|
166
|
+
|
|
167
|
+
=====================================================================
|
|
168
|
+
|
|
169
|
+
Sample values (angle = π/2, mpmath reference uses adaptive precision)
|
|
170
|
+
|
|
171
|
+
=====================================================================
|
|
172
|
+
|
|
173
|
+
alpha beta r mlfp Re ref Re mlfp Im ref Im
|
|
174
|
+
|
|
175
|
+
---------------------------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
0.500 1.000 0.0000 1.00000000e+00 1.00000000e+00 0.00000000e+00 0.00000000e+00
|
|
178
|
+
|
|
179
|
+
================
|
|
180
|
+
|
|
181
|
+
Overall: PASS ✅
|
|
182
|
+
|
|
183
|
+
================
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
# Examples
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import numpy as np
|
|
190
|
+
from mlfp import mittag_leffler
|
|
191
|
+
|
|
192
|
+
alpha, beta = 1.0, 2.0
|
|
193
|
+
z = np.linspace(0.0, 2.0, 64)
|
|
194
|
+
result = mittag_leffler(z, alpha, beta)
|
|
195
|
+
print(result)
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
import numpy as np
|
|
201
|
+
from mlfp import MittagLeffler, mittag_leffler
|
|
202
|
+
|
|
203
|
+
# 1. Two-parameter Mittag-Leffler function
|
|
204
|
+
ml = MittagLeffler(alpha=0.5, beta=1.0)
|
|
205
|
+
|
|
206
|
+
print(ml(0.0)) # 1.0
|
|
207
|
+
print(ml(0.5)) # 1.9523604891825568
|
|
208
|
+
print(ml(1.0)) # 5.008980080762283
|
|
209
|
+
print(ml(2.0)) # 108.94090438997797
|
|
210
|
+
|
|
211
|
+
# 2. Array evaluation
|
|
212
|
+
z = np.array([0.0, 0.25, 0.5, 1.0, 2.0, 5.0])
|
|
213
|
+
print(ml(z))
|
|
214
|
+
|
|
215
|
+
# 3. Small arguments now use the reliable Taylor-series branch
|
|
216
|
+
ml07 = MittagLeffler(alpha=0.7, beta=1.0)
|
|
217
|
+
print(ml07(0.001)) # 1.0011013529...
|
|
218
|
+
print(ml07(0.01)) # 1.0110864355...
|
|
219
|
+
print(ml07(1.0)) # 3.7041461454...
|
|
220
|
+
|
|
221
|
+
# 4. Prabhakar / three-parameter function
|
|
222
|
+
prabhakar = MittagLeffler(alpha=0.5, beta=1.0, gamma=0.7)
|
|
223
|
+
print(prabhakar(0.1)) # 1.0853656185...
|
|
224
|
+
print(prabhakar(0.5)) # 1.6154153961...
|
|
225
|
+
print(prabhakar(1.0)) # 3.3153696560...
|
|
226
|
+
print(prabhakar(2.0)) # 47.137926541...
|
|
227
|
+
|
|
228
|
+
# 5. Functional interface
|
|
229
|
+
print(mittag_leffler(1.0, alpha=0.5, beta=1.0))
|
|
230
|
+
print(mittag_leffler(0.5, alpha=0.5, beta=1.0, gamma=0.7))
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import numpy as np
|
|
236
|
+
import mpmath as mp
|
|
237
|
+
from pymittagleffler import mittag_leffler as pymittag_leffler
|
|
238
|
+
from mlfp import MittagLeffler
|
|
239
|
+
|
|
240
|
+
mp.mp.dps = 40
|
|
241
|
+
|
|
242
|
+
def mpmath_reference(z, alpha, beta, terms=2000):
|
|
243
|
+
z = mp.mpc(z)
|
|
244
|
+
total = mp.mpc(0)
|
|
245
|
+
for k in range(terms):
|
|
246
|
+
term = z**k / mp.gamma(alpha * k + beta)
|
|
247
|
+
total += term
|
|
248
|
+
if abs(term) < mp.mpf(10) ** -30:
|
|
249
|
+
break
|
|
250
|
+
return complex(total)
|
|
251
|
+
|
|
252
|
+
alpha, beta = 1.0, 1.5
|
|
253
|
+
r = np.linspace(0.0, 12.0, 256)
|
|
254
|
+
z = r * np.exp(1j * np.pi / 2)
|
|
255
|
+
|
|
256
|
+
mlfp = MittagLeffler(
|
|
257
|
+
alpha=alpha,
|
|
258
|
+
beta=beta,
|
|
259
|
+
gamma=1.0,
|
|
260
|
+
tolerance=1.0e-13,
|
|
261
|
+
series_threshold=0.1,
|
|
262
|
+
).evaluate_complex(z)
|
|
263
|
+
|
|
264
|
+
reference = pymittag_leffler(z, alpha, beta)
|
|
265
|
+
mp_reference = np.array(
|
|
266
|
+
[mpmath_reference(value, alpha, beta) for value in z],
|
|
267
|
+
dtype=np.complex128,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
difference = np.abs(mlfp - reference)
|
|
271
|
+
jmax = int(np.argmax(difference))
|
|
272
|
+
|
|
273
|
+
print(f"Maximum difference at r = {r[jmax]:.6f}")
|
|
274
|
+
print(f"z = {z[jmax]}")
|
|
275
|
+
print(f"mlfp = {mlfp[jmax]}")
|
|
276
|
+
print(f"pymittagleffler = {reference[jmax]}")
|
|
277
|
+
print(f"mpmath reference = {mp_reference[jmax]}")
|
|
278
|
+
print(f"|mlfp - pymtl| = {difference[jmax]:.6e}")
|
|
279
|
+
print(f"|mlfp - mpmath| = {abs(mlfp[jmax] - mp_reference[jmax]):.6e}")
|
|
280
|
+
print(f"|pymtl - mpmath| = {abs(reference[jmax] - mp_reference[jmax]):.6e}")
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
import numpy as np
|
|
286
|
+
from mlfp import mittag_leffler
|
|
287
|
+
z = np.linspace(0.4, 2.0) + 3j * np.linspace(0.5, 1.5)
|
|
288
|
+
ml = mittag_leffler(z, alpha=0.5, beta=2.5)
|
|
289
|
+
print(z)
|
|
290
|
+
print(ml)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
docs/conf.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
mlfp/__init__.py,sha256=RiRCt0ZE-Ny0plE9aUOrq48eW4ElaZ-aPPdBlAmYMOQ,1268
|
|
3
|
+
mlfp/_version.py,sha256=LI-tbl5E-8iQCZ4b6ruHwgLDBE5Zt7eMR_IJrjiArAI,402
|
|
4
|
+
mlfp/mlfp.py,sha256=Zjh02kdmCHf2tZvCVysg63npbuAkoV5mK3O7RCfWhkc,20050
|
|
5
|
+
mlfp-0.1.0.dist-info/licenses/LICENSE,sha256=3_wkcrcaqnKREB2UQtLWCQYk1qeo7e3uObJIsmc7sKg,1071
|
|
6
|
+
test/test_sample.py,sha256=9OBU4nWtu0rrJ2VC6adSFER2405hhpVVMHTtes78QOc,40
|
|
7
|
+
mlfp-0.1.0.dist-info/METADATA,sha256=PWM-Z7zJmANFJvfF2tlGOCeSsgaQ2Iqa3ZmhwRxdOeo,9622
|
|
8
|
+
mlfp-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
mlfp-0.1.0.dist-info/top_level.txt,sha256=zUN4vUTOkMu5puFk9hfAj4af1B57TaLfnCvFyPLSyso,15
|
|
10
|
+
mlfp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mehmet Keçeci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
test/test_sample.py
ADDED