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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.
@@ -0,0 +1,3 @@
1
+ docs
2
+ mlfp
3
+ test
test/test_sample.py ADDED
@@ -0,0 +1,2 @@
1
+ def test_placeholder():
2
+ assert True