• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

markrogoyski / math-php / 21585310639

02 Feb 2026 09:52AM UTC coverage: 99.457% (-0.2%) from 99.66%
21585310639

Pull #488

github

web-flow
Merge 1a4d42436 into c064e80bf
Pull Request #488: Add multilinear regression

72 of 84 new or added lines in 6 files covered. (85.71%)

7 existing lines in 3 files now uncovered.

8615 of 8662 relevant lines covered (99.46%)

223.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.09
/src/Functions/Special.php
1
<?php
2

3
namespace MathPHP\Functions;
4

5
use MathPHP\Probability\Combinatorics;
6
use MathPHP\Functions\Map\Single;
7
use MathPHP\Exception;
8

9
class Special
10
{
11
    /**
12
     * Maximum iterations for iterative algorithms to prevent infinite loops
13
     */
14
    private const MAX_ITERATIONS = 10000;
15

16
    /**
17
     * Sign function (signum function) - sgn
18
     * Extracts the sign of a real number.
19
     * https://en.wikipedia.org/wiki/Sign_function
20
     *
21
     *          { -1 if x < 0
22
     * sgn(x) = {  0 if x = 0
23
     *          {  1 if x > 0
24
     *
25
     * @param float $x
26
     *
27
     * @return int
28
     */
29
    public static function signum(float $x): int
30
    {
31
        return $x <=> 0;
298✔
32
    }
33

34
    /**
35
     * Sign function (signum function) - sgn
36
     * Convenience wrapper for signum function.
37
     *
38
     * @param float $x
39
     *
40
     * @return int
41
     */
42
    public static function sgn(float $x): int
43
    {
44
        return self::signum($x);
298✔
45
    }
46

47
    /**
48
     * Gamma function convenience method
49
     *
50
     * @param float $n
51
     *
52
     * @return float
53
     *
54
     * @throws Exception\OutOfBoundsException
55
     */
56
    public static function Γ(float $n): float
57
    {
58
        return self::gamma($n);
1,834✔
59
    }
60

61
    /**
62
     * Gamma function - Lanczos' approximation
63
     * https://en.wikipedia.org/wiki/Gamma_function
64
     * https://en.wikipedia.org/wiki/Lanczos_approximation
65
     *
66
     * For positive integers:
67
     *  Γ(n) = (n - 1)!
68
     *
69
     * If z is < 0.5, use reflection formula:
70
     *
71
     *                   π
72
     *  Γ(1 - z)Γ(z) = ------
73
     *                 sin πz
74
     *
75
     *  therefore:
76
     *
77
     *                π
78
     *  Γ(z) = -----------------
79
     *         sin πz · Γ(1 - z)
80
     *
81
     * otherwise:
82
     *              __  /        1 \ z+½
83
     *  Γ(z + 1) = √2π | z + g + -  |    e^-(z+g+½) A(z)
84
     *                  \        2 /
85
     *
86
     *  use pre-computed p coefficients: g = 7, n = 9
87
     *
88
     * @param float $z
89
     *
90
     * @return float
91
     *
92
     * @throws Exception\OutOfBoundsException
93
     */
94
    public static function gammaLanczos(float $z): float
95
    {
96
        // Basic integer/factorial cases
97
        if ($z == 0) {
46✔
98
            return \INF;
2✔
99
        }
100
        // Negative integer, or negative int as a float
101
        if (($z === \floor($z)) && $z < 0) {
44✔
102
            return -\INF;
3✔
103
        }
104
        // Positive integer, or positive int as a float (Ex: from beta(0.1, 0.9) since it will call Γ(x + y))
105
        if (($z === \floor($z)) && $z > 0) {
41✔
106
            return Combinatorics::factorial((int) $z - 1);
12✔
107
        }
108

109
        // p coefficients: g = 7, n = 9
110
        static $p = [
29✔
111
            0.99999999999980993227684700473478,
29✔
112
            676.520368121885098567009190444019,
29✔
113
            -1259.13921672240287047156078755283,
29✔
114
            771.3234287776530788486528258894,
29✔
115
            -176.61502916214059906584551354,
29✔
116
            12.507343278686904814458936853,
29✔
117
            -0.13857109526572011689554707,
29✔
118
            9.984369578019570859563e-6,
29✔
119
            1.50563273514931155834e-7,
29✔
120
        ];
29✔
121
        static $g = 7;
29✔
122
        static $π = \M_PI;
29✔
123

124
        /**
125
         * Use reflection formula when z < 0.5
126
         *                π
127
         *  Γ(z) = -----------------
128
         *         sin πz * Γ(1 - z)
129
         */
130
        if ($z < 0.5) {
29✔
131
            $Γ⟮1 − z⟯ = self::gammaLanczos(1 - $z);
8✔
132
            return $π / ( \sin($π * $z) * $Γ⟮1 − z⟯);
8✔
133
        }
134

135
        // Standard Lanczos formula when z ≥ 0.5
136

137
        // Compute A(z)
138
        $z--;
29✔
139
        $A⟮z⟯ = $p[0];
29✔
140
        for ($i = 1; $i < \count($p); $i++) {
29✔
141
            $A⟮z⟯ += $p[$i] / ($z + $i);
29✔
142
        }
143

144
        // Compute parts of equation
145
        $√2π = \sqrt(2 * $π);
29✔
146
        $⟮z + g +½⟯ᶻ⁺½ = \pow($z + $g + 0.5, $z + 0.5);
29✔
147
        $ℯ^−⟮z + g +½⟯ = \exp(-($z + $g + 0.5));
29✔
148

149
        /**
150
         * Put it all together:
151
         *   __  /        1 \ z+½
152
         *  √2π | z + g + -  |    e^-(z+g+½) A(z)
153
         *       \        2 /
154
         */
155
        return $√2π * $⟮z + g +½⟯ᶻ⁺½ * $ℯ^−⟮z + g +½⟯ * $A⟮z⟯;
29✔
156
    }
157

158
    /**
159
     * Gamma function - Stirling approximation
160
     * https://en.wikipedia.org/wiki/Gamma_function
161
     * https://en.wikipedia.org/wiki/Stirling%27s_approximation
162
     * https://www.wolframalpha.com/input/?i=Gamma(n)&lk=3
163
     *
164
     * For positive integers:
165
     *  Γ(n) = (n - 1)!
166
     *
167
     * For positive real numbers -- approximation:
168
     *                   ___
169
     *         __       / 1  /         1      \ n
170
     *  Γ(n)≈ √2π ℯ⁻ⁿ  /  - | n + ----------- |
171
     *                √   n  \    12n - 1/10n /
172
     *
173
     * @param float $n
174
     *
175
     * @return float
176
     *
177
     * @throws Exception\OutOfBoundsException
178
     */
179
    public static function gammaStirling(float $n): float
180
    {
181
        // Basic integer/factorial cases
182
        if ($n == 0) {
21✔
183
            return \INF;
1✔
184
        }
185
        // Negative integer, or negative int as a float
186
        if (($n === \floor($n)) && $n < 0) {
20✔
187
            return -\INF;
3✔
188
        }
189
        // Positive integer, or postive int as a float
190
        if (($n === \floor($n)) && $n > 0) {
17✔
191
            return Combinatorics::factorial((int) $n - 1);
7✔
192
        }
193

194
        // Compute parts of equation
195
        $√2π                    = \sqrt(2 * \M_PI);
10✔
196
        $ℯ⁻ⁿ                    = \exp(-$n);
10✔
197
        $√1/n                  = \sqrt(1 / $n);
10✔
198
        $⟮n + 1/⟮12n − 1/10n⟯⟯ⁿ = \pow($n + 1 / (12 * $n - 1 / (10 * $n)), $n);
10✔
199

200
        /**
201
         * Put it all together:
202
         *                   ___
203
         *         __       / 1  /         1      \ n
204
         *  Γ(n)≈ √2π ℯ⁻ⁿ  /  - | n + ----------- |
205
         *                √   n  \    12n - 1/10n /
206
         */
207
        return $√2π * $ℯ⁻ⁿ * $√1/n * $⟮n + 1/⟮12n − 1/10n⟯⟯ⁿ;
10✔
208
    }
209

210
    /**
211
     * The log of the error term in the Stirling-De Moivre factorial series
212
     *
213
     * log(n!) = .5*log(2πn) + n*log(n) - n + δ(n)
214
     * Where δ(n) is the log of the error.
215
     *
216
     * For n ≤ 15, integers or half-integers, uses stored values.
217
     *
218
     * For n = 0: infinity
219
     * For n < 0: NAN
220
     *
221
     * The implementation is heavily inspired by the R language's C implementation of stirlerr.
222
     * It can be considered a reimplementation in PHP.
223
     * R Project for Statistical Computing: https://www.r-project.org/
224
     * R Source: https://svn.r-project.org/R/
225
     *
226
     * @param float $n
227
     *
228
     * @return float log of the error
229
     *
230
     * @throws Exception\NanException
231
     */
232
    public static function stirlingError(float $n): float
233
    {
234
        if ($n < 0) {
424✔
235
            throw new Exception\NanException("stirlingError NAN for n < 0: given $n");
4✔
236
        }
237

238
        static $S0 = 0.083333333333333333333;        // 1/12
420✔
239
        static $S1 = 0.00277777777777777777778;      // 1/360
420✔
240
        static $S2 = 0.00079365079365079365079365;   // 1/1260
420✔
241
        static $S3 = 0.000595238095238095238095238;  // 1/1680
420✔
242
        static $S4 = 0.0008417508417508417508417508; // 1/1188
420✔
243

244
        static $sferr_halves = [
420✔
245
            \INF,                          // 0
420✔
246
            0.1534264097200273452913848,   // 0.5
420✔
247
            0.0810614667953272582196702,   // 1.0
420✔
248
            0.0548141210519176538961390,   // 1.5
420✔
249
            0.0413406959554092940938221,   // 2.0
420✔
250
            0.03316287351993628748511048,  // 2.5
420✔
251
            0.02767792568499833914878929,  // 3.0
420✔
252
            0.02374616365629749597132920,  // 3.5
420✔
253
            0.02079067210376509311152277,  // 4.0
420✔
254
            0.01848845053267318523077934,  // 4.5
420✔
255
            0.01664469118982119216319487,  // 5.0
420✔
256
            0.01513497322191737887351255,  // 5.5
420✔
257
            0.01387612882307074799874573,  // 6.0
420✔
258
            0.01281046524292022692424986,  // 6.5
420✔
259
            0.01189670994589177009505572,  // 7.0
420✔
260
            0.01110455975820691732662991,  // 7.5
420✔
261
            0.010411265261972096497478567, // 8.0
420✔
262
            0.009799416126158803298389475, // 8.5
420✔
263
            0.009255462182712732917728637, // 9.0
420✔
264
            0.008768700134139385462952823, // 9.5
420✔
265
            0.008330563433362871256469318, // 10.0
420✔
266
            0.007934114564314020547248100, // 10.5
420✔
267
            0.007573675487951840794972024, // 11.0
420✔
268
            0.007244554301320383179543912, // 11.5
420✔
269
            0.006942840107209529865664152, // 12.0
420✔
270
            0.006665247032707682442354394, // 12.5
420✔
271
            0.006408994188004207068439631, // 13.0
420✔
272
            0.006171712263039457647532867, // 13.5
420✔
273
            0.005951370112758847735624416, // 14.0
420✔
274
            0.005746216513010115682023589, // 14.5
420✔
275
            0.005554733551962801371038690, // 15.0
420✔
276
        ];
420✔
277

278
        if ($n <= 15.0) {
420✔
279
            $nn = $n + $n;
263✔
280
            if ($nn == (int) $nn) {
263✔
281
                return $sferr_halves[$nn];
250✔
282
            }
283
            $M_LN_SQRT_2PI = \log(\sqrt(2 * \M_PI));
13✔
284
            return self::logGamma($n + 1) - ($n + 0.5) * \log($n) + $n - $M_LN_SQRT_2PI;
13✔
285
        }
286

287
        $n² = $n * $n;
200✔
288
        if ($n > 500) {
200✔
289
            return ($S0 - $S1 / $n²) / $n;
16✔
290
        }
291
        if ($n > 80) {
190✔
292
            return ($S0 - ($S1 - $S2 / $n²) / $n²) / $n;
91✔
293
        }
294
        if ($n > 35) {
108✔
295
            return ($S0 - ($S1 - ($S2 - $S3/$n²) / $n²) / $n²) / $n;
60✔
296
        }
297
        // 15 < n ≤ 35
298
        return ($S0 - ($S1 - ($S2 - ($S3 - $S4/$n²) / $n²) / $n²) / $n²) / $n;
72✔
299
    }
300

301
    /**
302
     * Gamma function
303
     * https://en.wikipedia.org/wiki/Gamma_function
304
     * https://en.wikipedia.org/wiki/Particular_values_of_the_Gamma_function
305
     *
306
     * For positive integers:
307
     *  Γ(n) = (n - 1)!
308
     *
309
     * For half integers:
310
     *
311
     *             _   (2n)!
312
     * Γ(½ + n) = √π  -------
313
     *                 4ⁿ n!
314
     *
315
     * For real numbers: use Lanczos approximation
316
     *
317
     * Implementation notes:
318
     * The original MathPHP implementation was based on the textbook mathematical formula, but this led to numerical
319
     * issues with very large numbers. Last version with this implementation was v2.4.0.
320
     *
321
     * The current implementation is heavily inspired by the R language's C implementation of gammafn, which itself is
322
     * a translation of a Fortran subroutine by W. Fullerton of Los Alamos Scientific Laboratory.
323
     * It can be considered a reimplementation in PHP.
324
     * R Project for Statistical Computing: https://www.r-project.org/
325
     * R Source: https://svn.r-project.org/R/
326
     *
327
     * @param float $x
328
     *
329
     * @return float
330
     *
331
     * @throws Exception\OutOfBoundsException
332
     * @throws Exception\NanException
333
     */
334
    public static function gamma(float $x)
335
    {
336
        static $gamcs = [
2,133✔
337
            +.8571195590989331421920062399942e-2,
2,133✔
338
            +.4415381324841006757191315771652e-2,
2,133✔
339
            +.5685043681599363378632664588789e-1,
2,133✔
340
            -.4219835396418560501012500186624e-2,
2,133✔
341
            +.1326808181212460220584006796352e-2,
2,133✔
342
            -.1893024529798880432523947023886e-3,
2,133✔
343
            +.3606925327441245256578082217225e-4,
2,133✔
344
            -.6056761904460864218485548290365e-5,
2,133✔
345
            +.1055829546302283344731823509093e-5,
2,133✔
346
            -.1811967365542384048291855891166e-6,
2,133✔
347
            +.3117724964715322277790254593169e-7,
2,133✔
348
            -.5354219639019687140874081024347e-8,
2,133✔
349
            +.9193275519859588946887786825940e-9,
2,133✔
350
            -.1577941280288339761767423273953e-9,
2,133✔
351
            +.2707980622934954543266540433089e-10,
2,133✔
352
            -.4646818653825730144081661058933e-11,
2,133✔
353
            +.7973350192007419656460767175359e-12,
2,133✔
354
            -.1368078209830916025799499172309e-12,
2,133✔
355
            +.2347319486563800657233471771688e-13,
2,133✔
356
            -.4027432614949066932766570534699e-14,
2,133✔
357
            +.6910051747372100912138336975257e-15,
2,133✔
358
            -.1185584500221992907052387126192e-15,
2,133✔
359
            +.2034148542496373955201026051932e-16,
2,133✔
360
            -.3490054341717405849274012949108e-17,
2,133✔
361
            +.5987993856485305567135051066026e-18,
2,133✔
362
            -.1027378057872228074490069778431e-18,
2,133✔
363
            +.1762702816060529824942759660748e-19,
2,133✔
364
            -.3024320653735306260958772112042e-20,
2,133✔
365
            +.5188914660218397839717833550506e-21,
2,133✔
366
            -.8902770842456576692449251601066e-22,
2,133✔
367
            +.1527474068493342602274596891306e-22,
2,133✔
368
            -.2620731256187362900257328332799e-23,
2,133✔
369
            +.4496464047830538670331046570666e-24,
2,133✔
370
            -.7714712731336877911703901525333e-25,
2,133✔
371
            +.1323635453126044036486572714666e-25,
2,133✔
372
            -.2270999412942928816702313813333e-26,
2,133✔
373
            +.3896418998003991449320816639999e-27,
2,133✔
374
            -.6685198115125953327792127999999e-28,
2,133✔
375
            +.1146998663140024384347613866666e-28,
2,133✔
376
            -.1967938586345134677295103999999e-29,
2,133✔
377
            +.3376448816585338090334890666666e-30,
2,133✔
378
            -.5793070335782135784625493333333e-31
2,133✔
379
        ];
2,133✔
380

381
        static $ngam  = 22;
2,133✔
382
        static $xmin  = -170.5674972726612;
2,133✔
383
        static $xmax  = 171.61447887182298;
2,133✔
384
        static $xsml  = 2.2474362225598545e-308;
2,133✔
385
        static $dxrel = 1.490116119384765696e-8;
2,133✔
386

387
        if (\is_nan($x)) {
2,133✔
388
            throw new Exception\NanException("gamma cannot compute x when NAN");
1✔
389
        }
390

391
        // Undefined (NAN) if x ≤ 0
392
        if ($x == 0 || ($x < 0 && $x == \round($x))) {
2,132✔
393
            throw new Exception\NanException("gamma undefined for x of $x");
9✔
394
        }
395

396
        $y = \abs($x);
2,123✔
397

398
        // Compute gamma for -10 ≤ x ≤ 10
399
        if ($y <= 10) {
2,123✔
400
            // First reduce the interval and find gamma(1 + y) for 0 ≤ y < 1
401
            $n = (int) $x;
2,058✔
402
            if ($x < 0) {
2,058✔
403
                --$n;
81✔
404
            }
405
            $y = $x - $n; // n = floor(x) ==> y in [0, 1)
2,058✔
406
            --$n;
2,058✔
407
            $value = self::chebyshevEval($y * 2 - 1, $gamcs, $ngam) + .9375;
2,058✔
408
            if ($n == 0) {
2,058✔
409
                return $value; // x = 1.dddd = 1+y
509✔
410
            }
411

412
            // Compute gamma(x) for -10 ≤ x < 1
413
            // Exactly 0 or -n was checked above already
414
            if ($n < 0) {
1,925✔
415
                // The argument is so close to 0 that the result would overflow.
416
                if ($y < $xsml) {
795✔
417
                    return \INF;
1✔
418
                }
419

420
                $n = -$n;
794✔
421
                for ($i = 0; $i < $n; $i++) {
794✔
422
                    $value /= ($x + $i);
794✔
423
                }
424
                return $value;
794✔
425
            }
426

427
            // gamma(x) for 2 ≤ x ≤ 10
428
            for ($i = 1; $i <= $n; $i++) {
1,423✔
429
                $value *= ($y + $i);
1,423✔
430
            }
431
            return $value;
1,423✔
432
        }
433
        // gamma(x) for y = |x| > 10.
434

435
        // Overflow (INF is the best answer)
436
        if ($x > $xmax) {
179✔
437
            return \INF;
1✔
438
        }
439

440
        // Underflow (0 is the best answer)
441
        if ($x < $xmin) {
178✔
442
            return 0;
1✔
443
        }
444

445
        if ($y <= 50 && $y == (int) $y) {
177✔
446
            $value = Combinatorics::factorial((int) $y - 1);
95✔
447
        } else { // Normal case
448
            $M_LN_SQRT_2PI = (\M_LNPI + \M_LN2) / 2;
98✔
449
            $value = \exp(($y - 0.5) * \log($y) - $y + $M_LN_SQRT_2PI + ((2*$y == (int)2*$y)? self::stirlingError($y) : self::logGammaCorr($y)));
98✔
450
        }
451

452
        if ($x > 0) {
177✔
453
            return $value;
175✔
454
        }
455

456
        // The answer is less than half precision because the argument is too near a negative integer.
457
        if (\abs(($x - (int)($x - 0.5)) / $x) < $dxrel) {
2✔
458
            // Just move on.
459
        }
460

461
        $sin⟮πy⟯ = \sin(\M_PI * $y);
2✔
462
        return -\M_PI / ($y * $sin⟮πy⟯ * $value);
2✔
463
    }
464

465
    /**
466
     * Log Gamma
467
     * Natural logarithm of gamma function - ln Γ(x)
468
     * https://en.wikipedia.org/wiki/Gamma_function
469
     *
470
     * The implementation is heavily inspired by the R language's C implementation of lgammafn, which itself is
471
     * a translation of a Fortran subroutine by W. Fullerton of Los Alamos Scientific Laboratory.
472
     * It can be considered a reimplementation in PHP.
473
     * R Project for Statistical Computing: https://www.r-project.org/
474
     * R Source: https://svn.r-project.org/R/
475
     *
476
     *           ⌠∞
477
     * ln Γ(x) = ⎮  t^(x-1) e^(-t) dt
478
     *           ⌡₀
479
     *
480
     * Properties:
481
     *   ln Γ(x+1) = ln(x) + ln Γ(x)
482
     *   ln Γ(1) = 0
483
     *   ln Γ(1/2) = ln(√π)
484
     *
485
     * More numerically stable than computing Γ(x) then taking logarithm.
486
     * Used for large arguments where Γ(x) would overflow.
487
     *
488
     * @param float $x
489
     *
490
     * @return float|int
491
     *
492
     * @throws Exception\NanException
493
     * @throws Exception\OutOfBoundsException
494
     */
495
    public static function logGamma(float $x)
496
    {
497
        static $xmax = 2.5327372760800758e+305;
889✔
498

499
        if (\is_nan($x)) {
889✔
500
            throw new Exception\NanException("Cannot compute logGamma when x is NAN");
1✔
501
        }
502

503
        // Negative integer argument
504
        if ($x <= 0 && $x == (int) $x) {
888✔
505
            return \INF; // INF is the best answer
3✔
506
        }
507

508
        $y = \abs($x);
885✔
509

510
        if ($y < 1e-306) {
885✔
511
            return -\log($y); // Denormalized range
2✔
512
        }
513
        if ($y <= 10) {
884✔
514
            return \log(abs(self::Γ($x)));
867✔
515
        }
516

517
        // From this point, y = |x| > 10
518

519
        if ($y > $xmax) {
340✔
520
            return \INF; // INF is the best answer
1✔
521
        }
522

523
        // y = x > 10
524
        if ($x > 0) {
339✔
525
            if ($x > 1e17) {
338✔
526
                return $x * (\log($x) - 1);
1✔
527
            }
528
            $M_LN_SQRT_2PI = (\M_LNPI + \M_LN2) / 2;
337✔
529
            if ($x > 4934720) {
337✔
530
                return($M_LN_SQRT_2PI + ($x - 0.5) * \log($x) - $x);
1✔
531
            }
532
            return $M_LN_SQRT_2PI + ($x - 0.5) * \log($x) - $x + self::logGammaCorr($x);
336✔
533
        }
534

535
        $M_LN_SQRT_PId2 = 0.225791352644727432363097614947; // log(sqrt(pi/2))
1✔
536
        $sin⟮πy⟯ = \abs(sin(\M_PI * $y));
1✔
537
        return $M_LN_SQRT_PId2 + ($x - 0.5) * \log($y) - $x - \log($sin⟮πy⟯) - self::logGammaCorr($y);
1✔
538
    }
539

540
    /**
541
     * Beta function convenience method
542
     *
543
     * @param  float $x
544
     * @param  float $y
545
     *
546
     * @return float
547
     *
548
     * @throws Exception\OutOfBoundsException
549
     */
550
    public static function β(float $x, float $y): float
551
    {
552
        return self::beta($x, $y);
51✔
553
    }
554

555
    /**
556
     * Beta function
557
     *
558
     * https://en.wikipedia.org/wiki/Beta_function
559
     *
560
     * Selects the best beta algorithm for the provided values
561
     *
562
     * @param  float $a
563
     * @param  float $b
564
     *
565
     * @return float
566
     *
567
     * @throws Exception\OutOfBoundsException
568
     * @throws Exception\NanException
569
     */
570
    public static function beta(float $a, float $b): float
571
    {
572
        static $xmax = 171.61447887182298;
358✔
573

574
        if (\is_nan($a) || \is_nan($b)) {
358✔
575
            throw new Exception\NanException("Cannot compute beta when a or b is NAN: got a:$a, b:$b");
1✔
576
        }
577
        if ($a < 0 || $b < 0) {
357✔
578
            throw new Exception\OutOfBoundsException("a and b must be non-negative for beta: got a:$a, b:$b");
1✔
579
        }
580
        if ($a == 0 || $b == 0) {
356✔
581
            return \INF;
2✔
582
        }
583
        if (\is_infinite($a) || \is_infinite($b)) {
354✔
584
            return 0;
2✔
585
        }
586

587
        if ($a + $b < $xmax) {
352✔
588
            return self::betaBasic($a, $b);
351✔
589
        }
590

591
        $val = self::logBeta($a, $b);
1✔
592
        return \exp($val);
1✔
593
    }
594

595
    /**
596
     * Beta function
597
     *
598
     * https://en.wikipedia.org/wiki/Beta_function
599
     *
600
     *           Γ(x)Γ(y)
601
     * β(x, y) = --------
602
     *           Γ(x + y)
603
     *
604
     * @param  float $x
605
     * @param  float $y
606
     *
607
     * @return float
608
     *
609
     * @throws Exception\OutOfBoundsException
610
     */
611
    private static function betaBasic(float $x, float $y): float
612
    {
613
        $Γ⟮x⟯  = self::Γ($x);
351✔
614
        $Γ⟮y⟯  = self::Γ($y);
351✔
615
        $Γ⟮x + y⟯ = self::Γ($x + $y);
351✔
616

617
        return 1 / $Γ⟮x + y⟯ * $Γ⟮x⟯ * $Γ⟮y⟯;
351✔
618
    }
619

620
    /**
621
     * Natural logarithm of beta function - ln β(a,b)
622
     * https://en.wikipedia.org/wiki/Beta_function
623
     *
624
     * ln β(a,b) = ln Γ(a) + ln Γ(b) - ln Γ(a+b)
625
     *
626
     * More numerically stable than computing β(a,b) then taking logarithm.
627
     * Avoids overflow for large arguments.
628
     *
629
     * The implementation is heavily inspired by the R language's C implementation of lbeta, which itself is
630
     * a translation of a Fortran subroutine by W. Fullerton of Los Alamos Scientific Laboratory.
631
     * It can be considered a reimplementation in PHP.
632
     * R Project for Statistical Computing: https://www.r-project.org/
633
     * R Source: https://svn.r-project.org/R/
634
     *
635
     * @param  float $a (a > 0)
636
     * @param  float $b (b > 0)
637
     *
638
     * @return float
639
     *
640
     * @throws Exception\OutOfBoundsException
641
     * @throws Exception\NanException
642
     */
643
    public static function logBeta(float $a, float $b): float
644
    {
645
        if (\is_nan($a) || \is_nan($b)) {
18✔
646
            throw new Exception\NanException("Cannot compute logBeta if a or b is NAN: got a:$a, b:$b");
1✔
647
        }
648

649
        $p = \min($a, $b);
17✔
650
        $q = \max($a, $b);
17✔
651

652
        // Both arguments must be >= 0
653
        if ($p < 0) {
17✔
654
            throw new Exception\OutOfBoundsException("p must be non-negative at this point of logBeta calculation: got $p");
1✔
655
        }
656
        if ($p == 0) {
16✔
657
            return \INF;
3✔
658
        }
659
        if (\is_infinite($q)) {
13✔
660
            return -\INF;
1✔
661
        }
662

663
        if ($p >= 10) {
12✔
664
            // p and q are big.
665
            $corr = self::logGammaCorr($p) + self::logGammaCorr($q) - self::logGammaCorr($p + $q);
2✔
666
            $M_LN_SQRT_2PI = (\M_LNPI + \M_LN2) / 2;
2✔
667
            return \log($q) * -0.5 + $M_LN_SQRT_2PI + $corr + ($p - 0.5) * \log($p / ($p + $q)) + $q * \log1p(-$p / ($p + $q));
2✔
668
        }
669
        if ($q >= 10) {
10✔
670
            // p is small, but q is big.
671
            $corr = self::logGammaCorr($q) - self::logGammaCorr($p + $q);
2✔
672
            return self::logGamma($p) + $corr + $p - $p * \log($p + $q) + ($q - 0.5) * \log1p(-$p / ($p + $q));
2✔
673
        }
674
        // p and q are small: p <= q < 10. */
675
        if ($p < 1e-306) {
8✔
676
            return self::logGamma($p) + (self::logGamma($q) - self::logGamma($p+$q));
1✔
677
        }
678
        return \log(self::beta($p, $q));
7✔
679
    }
680

681
    /**
682
     * Log gamma correction
683
     *
684
     * Compute the log gamma correction factor for x >= 10 so that
685
     * log(gamma(x)) = .5*log(2*pi) + (x-.5)*log(x) -x + lgammacor(x)
686
     *
687
     * The implementation is heavily inspired by the R language's C implementation of lgammacor, which itself is
688
     * a translation of a Fortran subroutine by W. Fullerton of Los Alamos Scientific Laboratory.
689
     * It can be considered a reimplementation in PHP.
690
     * R Project for Statistical Computing: https://www.r-project.org/
691
     * R Source: https://svn.r-project.org/R/
692
     *
693
     * @param float $x
694
     *
695
     * @return float
696
     */
697
    public static function logGammaCorr(float $x): float
698
    {
699
        static $algmcs = [
342✔
700
            +.1666389480451863247205729650822e+0,
342✔
701
            -.1384948176067563840732986059135e-4,
342✔
702
            +.9810825646924729426157171547487e-8,
342✔
703
            -.1809129475572494194263306266719e-10,
342✔
704
            +.6221098041892605227126015543416e-13,
342✔
705
            -.3399615005417721944303330599666e-15,
342✔
706
            +.2683181998482698748957538846666e-17,
342✔
707
            -.2868042435334643284144622399999e-19,
342✔
708
            +.3962837061046434803679306666666e-21,
342✔
709
            -.6831888753985766870111999999999e-23,
342✔
710
            +.1429227355942498147573333333333e-24,
342✔
711
            -.3547598158101070547199999999999e-26,
342✔
712
            +.1025680058010470912000000000000e-27,
342✔
713
            -.3401102254316748799999999999999e-29,
342✔
714
            +.1276642195630062933333333333333e-30,
342✔
715
        ];
342✔
716

717
        /**
718
         * For IEEE double precision DBL_EPSILON = 2^-52 = 2.220446049250313e-16 :
719
         * xbig = 2 ^ 26.5
720
         * xmax = DBL_MAX / 48 =  2^1020 / 3
721
         */
722
        static $nalgm = 5;
342✔
723
        static $xbig  = 94906265.62425156;
342✔
724
        static $xmax  = 3.745194030963158e306;
342✔
725

726
        if ($x < 10) {
342✔
727
            throw new Exception\OutOfBoundsException("x cannot be < 10: got $x");
1✔
728
        }
729
        if ($x >= $xmax) {
341✔
730
            // allow to underflow below
731
        } elseif ($x < $xbig) {
341✔
732
            $tmp = 10 / $x;
341✔
733
            return self::chebyshevEval($tmp * $tmp * 2 - 1, $algmcs, $nalgm) / $x;
341✔
734
        }
735
        return 1 / ($x * 12);
1✔
736
    }
737

738
    /**
739
     * Evaluate a Chebyshev Series with the Clenshaw Algorithm
740
     * https://en.wikipedia.org/wiki/Clenshaw_algorithm#Special_case_for_Chebyshev_series
741
     *
742
     * The implementation is inspired by the R language's C implementation of chebyshev_eval, which itself is
743
     * a translation of a Fortran subroutine by W. Fullerton of Los Alamos Scientific Laboratory.
744
     * It can be considered a reimplementation in PHP.
745
     *
746
     * @param float   $x
747
     * @param float[] $a
748
     * @param int     $n
749
     *
750
     * @return float
751
     *
752
     * @throws Exception\OutOfBoundsException
753
     */
754
    private static function chebyshevEval(float $x, array $a, int $n): float
755
    {
756
        if ($n < 1 || $n > 1000) {
2,075✔
757
            throw new Exception\OutOfBoundsException("n cannot be < 1 or > 1000: got $n");
2✔
758
        }
759
        if ($x < -1.1 || $x > 1.1) {
2,073✔
760
            throw new Exception\OutOfBoundsException("x cannot be < -1.1 or greater than 1.1: got $x");
2✔
761
        }
762

763
        $2x = $x * 2;
2,071✔
764
        [$b0, $b1, $b2] = [0, 0, 0];
2,071✔
765

766
        for ($i = 1; $i <= $n; $i++) {
2,071✔
767
            $b2 = $b1;
2,071✔
768
            $b1 = $b0;
2,071✔
769
            $b0 = $2x * $b1 - $b2 + $a[$n - $i];
2,071✔
770
        }
771
        return ($b0 - $b2) * 0.5;
2,071✔
772
    }
773

774
    /**
775
     * Multivariate Beta function
776
     * https://en.wikipedia.org/wiki/Beta_function#Multivariate_beta_function
777
     *
778
     *                     Γ(α₁)Γ(α₂) ⋯ Γ(αₙ)
779
     * B(α₁, α₂, ... αn) = ──────────────────
780
     *                     Γ(α₁ + α₂ +  ⋯ αₙ)
781
     *
782
     * @param float[] $αs
783
     *
784
     * @return float
785
     *
786
     * @throws Exception\OutOfBoundsException
787
     */
788
    public static function multivariateBeta(array $αs): float
789
    {
790
        foreach ($αs as $α) {
65✔
791
            if ($α == 0) {
65✔
792
                return \INF;
2✔
793
            }
794
        }
795

796
        static $xmax = 171.61447887182298;
63✔
797

798
        $∑α = \array_sum($αs);
63✔
799
        if ($∑α == \INF) {
63✔
800
            return 0;
2✔
801
        }
802
        if ($∑α < $xmax) {  // ~= 171.61 for IEEE
61✔
803
            $Γ⟮∑α⟯ = self::Γ($∑α);
60✔
804
            $∏= 1 / $Γ⟮∑α⟯;
60✔
805
            foreach ($αs as $α) {
60✔
806
                $∏ *= self::Γ($α);
60✔
807
            }
808

809
            return $∏;
60✔
810
        }
811

812
        $∑ = -self::logGamma($∑α);
1✔
813
        foreach ($αs as $α) {
1✔
814
            $∑ += self::logGamma($α);
1✔
815
        }
816
        return \exp($∑);
1✔
817
    }
818

819
    /**
820
     * Logistic function (logistic sigmoid function)
821
     * A logistic function or logistic curve is a common "S" shape (sigmoid curve).
822
     * https://en.wikipedia.org/wiki/Logistic_function
823
     *
824
     *             L
825
     * f(x) = -----------
826
     *        1 + ℯ⁻ᵏ⁽ˣ⁻ˣ⁰⁾
827
     *
828
     *
829
     * @param float $x₀ x-value of the sigmoid's midpoint
830
     * @param float $L  the curve's maximum value
831
     * @param float $k  the steepness of the curve
832
     * @param float $x
833
     *
834
     * @return float
835
     */
836
    public static function logistic(float $x₀, float $L, float $k, float $x): float
837
    {
838
        $ℯ⁻ᵏ⁽ˣ⁻ˣ⁰⁾ = \exp(-$k * ($x - $x₀));
12✔
839

840
        return $L / (1 + $ℯ⁻ᵏ⁽ˣ⁻ˣ⁰⁾);
12✔
841
    }
842

843
    /**
844
     * Sigmoid function
845
     * A sigmoid function is a mathematical function having an "S" shaped curve (sigmoid curve).
846
     * Often, sigmoid function refers to the special case of the logistic function
847
     * https://en.wikipedia.org/wiki/Sigmoid_function
848
     *
849
     *           1
850
     * S(t) = -------
851
     *        1 + ℯ⁻ᵗ
852
     *
853
     * Rather than implement the formula definition exactly, we take a slightly more complicated conditional approach
854
     * for numerical stability reasons.
855
     *
856
     * For example, consider a large positive t such as t = 750: ℯ⁻⁷⁵⁰
857
     * php > var_dump(M_E ** -750);
858
     * float(0)
859
     *
860
     *            1          1
861
     * S(t) = --------- = ------- = 1.0
862
     *        1 + ℯ⁻⁷⁵⁰    1 + 0
863
     *
864
     * There are no numerical issues with this, as we approach 0.
865
     *
866
     * However, consider a large negative t such as t = -750: ℯ⁻⁽⁻⁷⁵⁰⁾
867
     * php > var_dump(M_E ** -(-750));
868
     * float(INF)
869
     *
870
     *
871
     *             1            1
872
     * S(t) = ------------ = ------- = 0.0
873
     *        1 + ℯ⁻⁽⁻⁷⁵⁰⁾   1 + INF
874
     *
875
     * We observe an overflow to PHP infinity.
876
     *
877
     * Therefore, we do an alternative, slightly more complex formula and implementation to avoid overflowing to
878
     * infinity in an attempt to maintain numerical stability.
879
     *
880
     *          ℯᵗ
881
     * S(t) = -------
882
     *        1 + ℯᵗ
883
     *
884
     * Consider the same large negative t again t = -750: ℯ⁻⁷⁵⁰
885
     *
886
     *          ℯ⁻⁷⁵⁰        0
887
     * S(t) = --------- = -------- = 0.0
888
     *        1 + ℯ⁻⁷⁵⁰    1 + 0
889
     *
890
     * We arrive at the same value of 0 without any overflows to infinity, maintaining numerical stability.
891
     *
892
     * For t >= 0: Use standard formula
893
     * For t < 0: Use alternate formula to avoid overflow in ℯ⁻ᵗ
894
     *
895
     * @param  float $t
896
     *
897
     * @return float
898
     */
899
    public static function sigmoid(float $t): float
900
    {
901
        if ($t >= 0) {
36✔
902
            $ℯ⁻ᵗ = \exp(-$t);
21✔
903
            return 1 / (1 + $ℯ⁻ᵗ);
21✔
904
        } else {
905
            $ℯᵗ = \exp($t);
15✔
906
            return $ℯᵗ / (1 + $ℯᵗ);
15✔
907
        }
908
    }
909

910
    /**
911
     * Error function (Gauss error function)
912
     * https://en.wikipedia.org/wiki/Error_function
913
     *
914
     *           2  ⌠ˣ
915
     * erf(x) = ──  ⎮  e^(-t²) dt
916
     *          √π  ⌡₀
917
     *
918
     * Improved implementation with domain-specific algorithms:
919
     * - Small arguments (|x| ≤ 0.01): Taylor series (8 terms, optimized)
920
     * - Medium arguments (0.01 < |x| ≤ 4): Taylor series with convergence (up to 50 terms)
921
     * - Large arguments (|x| > 4): Asymptotic expansion (4 terms)
922
     *
923
     * This implementation prioritizes accuracy (< 1e-12 error) over the classical
924
     * Abramowitz & Stegun 7.1.26 approximation (max error: 1.5e-7).
925
     *
926
     * Taylor series: erf(x) = (2/√π) * Σ((-1)^n * x^(2n+1) / (n! * (2n+1)))
927
     * Asymptotic: erf(x) = 1 - erfc(x) where erfc(x) ≈ (e^(-x²) / (x√π)) * (1 - 1/(2x²) + ...)
928
     *
929
     * Precision: Better than 1e-12 for all x
930
     *
931
     * @param  float $x
932
     *
933
     * @return float
934
     */
935
    public static function errorFunction(float $x): float
936
    {
937
        if ($x == 0) {
1,044✔
938
            return 0;
231✔
939
        }
940

941
        $ax = \abs($x);
1,007✔
942

943
        $two/√π = 1.1283791670955125738961589031215451716881;
1,007✔
944

945
        // Small arguments: Use Taylor series
946
        // erf(x) = (2/√π) * (x - x³/3 + x⁵/10 - x⁷/42 + x⁹/216 - ...)
947
        if ($ax <= 0.01) {
1,007✔
948
            $x² = $x * $x;
40✔
949
            $x³ = $x² * $x;
40✔
950
            $x⁵ = $x³ * $x²;
40✔
951
            $x⁷ = $x⁵ * $x²;
40✔
952
            $x⁹ = $x⁷ * $x²;
40✔
953
            $x¹¹ = $x⁹ * $x²;
40✔
954
            $x¹³ = $x¹¹ * $x²;
40✔
955
            $x¹⁵ = $x¹³ * $x²;
40✔
956

957
            // Coefficients: 2/√π * 1/(n! * (2n+1))
958
            $sum = $x - $x³ / 3.0 + $x⁵ / 10.0 - $x⁷ / 42.0 + $x⁹ / 216.0 - $x¹¹ / 1320.0 + $x¹³ / 9360.0 - $x¹⁵ / 75600.0;
40✔
959
            return $sum * $two/√π;
40✔
960
        }
961

962
        // Large arguments: For x > 4, compute using erfc for consistency
963
        // erf(x) = 1 - erfc(x), but computed via asymptotic expansion to avoid cancellation
964
        if ($ax > 4.0) {
967✔
965
            // For positive x: erf(x) = 1 - erfc(x)
966
            if ($x > 0) {
62✔
967
                return 1.0 - self::erfcAsymptoticSeries($x);
27✔
968
            } else {
969
                // For negative x: erf(-x) = -erf(x)
970
                return -(1.0 - self::erfcAsymptoticSeries($ax));
39✔
971
            }
972
        }
973

974
        // Medium arguments: Use Taylor series
975
        // erf(x) = (2/√π) * Σ((-1)^n * x^(2n+1) / (n! * (2n+1)))
976
        $x² = $ax * $ax;
905✔
977
        $sum = $ax;
905✔
978
        $term = $ax;
905✔
979

980
        for ($n = 1; $n <= 50; $n++) {
905✔
981
            $term *= -$x² / $n;
905✔
982
            $sum += $term / (2 * $n + 1);
905✔
983

984
            // Early exit if converged
985
            if (\abs($term / (2 * $n + 1)) < 1e-15) {
905✔
986
                break;
846✔
987
            }
988
        }
989

990
        $result = $sum * $two/√π;
905✔
991

992
        return ($x > 0) ? $result : -$result;
905✔
993
    }
994

995
    /**
996
     * Error function (Gauss error function)
997
     * Convenience method for errorFunction
998
     *
999
     * @param  float $x
1000
     *
1001
     * @return float
1002
     */
1003
    public static function erf(float $x): float
1004
    {
1005
        return self::errorFunction($x);
633✔
1006
    }
1007

1008
    /**
1009
     * Asymptotic series for erfc
1010
     * erfc(x) ≈ (e^(-x²) / (x√π)) * (1 - 1/(2x²) + 3/(4x⁴) - ...)
1011
     *
1012
     * @param  float $x Must be positive and >= 4.0
1013
     *
1014
     * @return float
1015
     */
1016
    private static function erfcAsymptoticSeries(float $x): float
1017
    {
1018
        $x² = $x * $x;
89✔
1019
        $x⁴ = $x² * $x²;
89✔
1020
        $x⁶ = $x⁴ * $x²;
89✔
1021

1022
        $series = 1.0;
89✔
1023
        $series -= 1.0 / (2.0 * $x²);
89✔
1024
        $series += 3.0 / (4.0 * $x⁴);
89✔
1025
        $series -= 15.0 / (8.0 * $x⁶);
89✔
1026

1027
        $factor = \exp(-$x²) / ($x * 1.7724538509055160272981674833411451828);  // √π
89✔
1028

1029
        return $factor * $series;
89✔
1030
    }
1031

1032
    /**
1033
     * Complementary error function (erfc)
1034
     * erfc(x) ≡ 1 - erf(x)
1035
     * https://en.wikipedia.org/wiki/Error_function
1036
     *
1037
     *                         2  ⌠∞
1038
     * erfc(x) ≡ 1 - erf(x) = ──  ⎮  e^(-t²) dt
1039
     *                        √π  ⌡ₓ
1040
     *
1041
     * Properties:
1042
     *   erfc(-x) = 2 - erfc(x)
1043
     *   erfc(0) = 1
1044
     *   erfc(∞) = 0
1045
     *
1046
     * More numerically stable than computing 1 - erf(x) for large x.
1047
     * For large x, computes directly to avoid catastrophic cancellation
1048
     *
1049
     * @param  int|float $x
1050
     *
1051
     * @return float
1052
     */
1053
    public static function complementaryErrorFunction($x): float
1054
    {
1055
        // For large positive x, use asymptotic expansion to avoid 1 - erf(x) cancellation
1056
        if ($x >= 4.0) {
446✔
1057
            return self::erfcAsymptoticSeries($x);
28✔
1058
        }
1059

1060
        // For large negative x, erfc(-x) = 2 - erfc(x)
1061
        if ($x <= -6.0) {
428✔
1062
            return 2 - self::erfc(-$x);
10✔
1063
        }
1064

1065
        // Otherwise use erf
1066
        return 1 - self::errorFunction($x);
418✔
1067
    }
1068

1069
    /**
1070
     * Complementary error function (erfc)
1071
     * Convenience method for complementaryErrorFunction
1072
     *
1073
     * @param  float $x
1074
     *
1075
     * @return float
1076
     */
1077
    public static function erfc(float $x): float
1078
    {
1079
        return self::complementaryErrorFunction($x);
446✔
1080
    }
1081

1082
    /**
1083
     * Upper Incomplete Gamma Function - Γ(s,x)
1084
     * https://en.wikipedia.org/wiki/Incomplete_gamma_function
1085
     * NIST DLMF: https://dlmf.nist.gov/8.2
1086
     *
1087
     *          ⌠∞
1088
     * Γ(s,x) = ⎮  t^(s-1) e^(-t) dt
1089
     *          ⌡ₓ
1090
     *
1091
     * Relationship to complete gamma:
1092
     *   Γ(s,x) + γ(s,x) = Γ(s)
1093
     *
1094
     * Special cases:
1095
     *   Γ(s,0) = Γ(s)
1096
     *   Γ(s,∞) = 0
1097
     *
1098
     * Algorithm:
1099
     * - For x < s+1: Use series for γ(s,x), then Γ(s,x) = Γ(s) - γ(s,x)
1100
     * - For x >= s+1: Use continued fraction (direct computation)
1101
     *
1102
     * @param float $s shape parameter (s > 0)
1103
     * @param float $x lower limit of integration (x ≥ 0)
1104
     *
1105
     * @return float
1106
     *
1107
     * @throws Exception\OutOfBoundsException if s is ≤ 0
1108
     */
1109
    public static function upperIncompleteGamma(float $s, float $x): float
1110
    {
1111
        if ($s <= 0) {
23✔
1112
            throw new Exception\OutOfBoundsException("S must be > 0. S = $s");
1✔
1113
        }
1114

1115
        // Choose algorithm based on x relative to s
1116
        if ($x < $s + 1) {
22✔
1117
            // Use lower incomplete gamma, then subtract from Γ(s)
1118
            return self::Γ($s) - self::lowerIncompleteGammaSeries($s, $x);
11✔
1119
        } else {
1120
            // Use continued fraction directly
1121
            return self::upperIncompleteGammaCF($s, $x);
11✔
1122
        }
1123
    }
1124

1125
    /**
1126
     * Lower incomplete gamma function - γ(s,t)
1127
     * https://en.wikipedia.org/wiki/Incomplete_gamma_function#Lower_incomplete_Gamma_function
1128
     * NIST DLMF: https://dlmf.nist.gov/8.2
1129
     *
1130
     *          ⌠ˣ
1131
     * γ(s,x) = ⎮  t^(s-1) e^(-t) dt
1132
     *          ⌡₀
1133
     *
1134
     * Relationship to complete gamma:
1135
     *   γ(s,x) + Γ(s,x) = Γ(s)
1136
     *
1137
     * Special cases:
1138
     *   γ(s,0) = 0
1139
     *   γ(s,∞) = Γ(s)
1140
     *
1141
     * Algorithm:
1142
     * - For x < s+1: Use series expansion (fast convergence)
1143
     * - For x >= s+1: Use continued fraction for Γ(s,x), then γ(s,x) = Γ(s) - Γ(s,x)
1144
     * - Use log-space computation to avoid overflow for large s or x
1145
     * - Use relative convergence criterion for numerical stability
1146
     *
1147
     * @param float $s shape parameter (s > 0)
1148
     * @param float $x lower limit of integration (x ≥ 0)
1149
     *
1150
     * @return float
1151
     */
1152
    public static function lowerIncompleteGamma(float $s, float $x): float
1153
    {
1154
        // Special cases
1155
        if ($x == 0) {
242✔
1156
            return 0;
17✔
1157
        }
1158
        if ($s == 0) {
225✔
1159
            return \NAN;
4✔
1160
        }
1161

1162
        // Fast paths for special values
1163
        if ($s == 1) {
221✔
1164
            return 1 - \exp(-1 * $x);
40✔
1165
        }
1166
        if ($s == .5) {
181✔
1167
            $√π = \sqrt(\M_PI);
35✔
1168
            $√x = \sqrt($x);
35✔
1169
            return $√π * self::erf($√x);
35✔
1170
        }
1171

1172
        // Choose algorithm based on x relative to s
1173
        // Series converges faster for x < s+1, continued fraction for x >= s+1
1174
        if ($x < $s + 1) {
146✔
1175
            return self::lowerIncompleteGammaSeries($s, $x);
97✔
1176
        } else {
1177
            // Use upper incomplete gamma via continued fraction, then subtract from Γ(s)
1178
            $Γs = self::Γ($s);
49✔
1179
            $Γsx = self::upperIncompleteGammaCF($s, $x);
49✔
1180
            return $Γs - $Γsx;
49✔
1181
        }
1182
    }
1183

1184
    /**
1185
     * Lower incomplete gamma via series expansion
1186
     * γ(s,x) = x^s * e^(-x) / s * (1 + x/(s+1) + x^2/((s+1)(s+2)) + ...)
1187
     *
1188
     *             xˢe⁻ˣ    /      x          x²             x³             \
1189
     * γ(s,x) =  ────────  | 1 + ───── + ────────── + ─────────────── + ... |
1190
     *               s     \      s+1    (s+1)(s+2)   (s+1)(s+2)(s+3)      /
1191
     *
1192
     *
1193
     * Works well for x < s+1. Uses log-space computation and relative convergence.
1194
     *
1195
     * @param float $s shape parameter > 0
1196
     * @param float $x argument
1197
     *
1198
     * @return float
1199
     */
1200
    private static function lowerIncompleteGammaSeries(float $s, float $x): float
1201
    {
1202
        $tol = 1e-14;  // Relative tolerance
108✔
1203
        $tiny = 1e-100; // Absolute tolerance for very small values
108✔
1204

1205
        // Compute log(xˢ * e⁻ˣ / s) to avoid overflow
1206
        $log_term = $s * \log($x) - $x - \log($s);
108✔
1207

1208
        // Series: 1 + x/(s+1) + x²/((s+1)(s+2)) + ...
1209
        $sum = 1.0;
108✔
1210
        $term = 1.0;
108✔
1211
        $sp = $s; // s prime, will be incremented
108✔
1212

1213
        for ($n = 1; $n < self::MAX_ITERATIONS; $n++) {
108✔
1214
            $sp += 1.0;
108✔
1215
            $term *= $x / $sp;
108✔
1216
            $sum += $term;
108✔
1217

1218
            // Relative convergence check (hybrid: relative OR absolute)
1219
            if (\abs($term) < $tiny) {
108✔
1220
                break;
×
1221
            }
1222
            if (\abs($sum) > 0 && \abs($term / $sum) < $tol) {
108✔
1223
                break;
108✔
1224
            }
1225
        }
1226

1227
        if ($n >= self::MAX_ITERATIONS) {
108✔
1228
            throw new Exception\FunctionFailedToConvergeException(
×
1229
                "lowerIncompleteGamma series failed to converge after $n iterations with s=$s, x=$x"
×
UNCOV
1230
            );
×
1231
        }
1232

1233
        $result = \exp($log_term) * $sum;
108✔
1234
        // Clamp to non-negative to handle floating-point precision errors
1235
        return ($x > 0 && $result < 0) ? 0 : $result;
108✔
1236
    }
1237

1238
    /**
1239
     * Upper incomplete gamma via continued fraction
1240
     * Γ(s,x) = x^s * e^(-x) * (1/(x+) (1-s)/(1+) 1/(x+) (2-s)/(1+) 2/(x+) ...)
1241
     *
1242
     * Works well for x >= s+1. Uses Lentz's method for continued fraction evaluation.
1243
     *
1244
     * @param float $s shape parameter > 0
1245
     * @param float $x argument
1246
     *
1247
     * @return float
1248
     */
1249
    private static function upperIncompleteGammaCF(float $s, float $x): float
1250
    {
1251
        $tol = 1e-14;  // Relative tolerance
60✔
1252
        $tiny = 1e-30; // Prevent division by zero
60✔
1253

1254
        // Compute log(x^s * e^(-x)) to avoid overflow
1255
        $log_term = $s * \log($x) - $x;
60✔
1256

1257
        // Continued fraction using Lentz's method
1258
        // CF: 1/(x + 1-s/(1 + 1/(x + 2-s/(1 + 2/(x + ...)))))
1259
        $b = $x + 1.0 - $s;
60✔
1260
        $c = 1.0 / $tiny;
60✔
1261
        $d = 1.0 / $b;
60✔
1262
        $h = $d;
60✔
1263

1264
        for ($i = 1; $i < self::MAX_ITERATIONS; $i++) {
60✔
1265
            $an = -$i * ($i - $s);
60✔
1266
            $b += 2.0;
60✔
1267

1268
            $d = $an * $d + $b;
60✔
1269
            if (\abs($d) < $tiny) {
60✔
1270
                $d = $tiny;
×
1271
            }
1272

1273
            $c = $b + $an / $c;
60✔
1274
            if (\abs($c) < $tiny) {
60✔
1275
                $c = $tiny;
×
1276
            }
1277

1278
            $d = 1.0 / $d;
60✔
1279
            $delta = $d * $c;
60✔
1280
            $h *= $delta;
60✔
1281

1282
            // Convergence check: delta should approach 1
1283
            if (\abs($delta - 1.0) < $tol) {
60✔
1284
                break;
60✔
1285
            }
1286
        }
1287

1288
        if ($i >= self::MAX_ITERATIONS) {
60✔
1289
            throw new Exception\FunctionFailedToConvergeException(
×
1290
                "upperIncompleteGamma continued fraction failed to converge after $i iterations with s=$s, x=$x"
×
UNCOV
1291
            );
×
1292
        }
1293

1294
        $result = \exp($log_term) * $h;
60✔
1295
        return $result;
60✔
1296
    }
1297

1298
    /**
1299
     * γ - Convenience method for lower incomplete gamma function
1300
     * https://en.wikipedia.org/wiki/Incomplete_gamma_function#Lower_incomplete_Gamma_function
1301
     *
1302
     * @param float $s > 0
1303
     * @param float $x ≥ 0
1304
     *
1305
     * @return float
1306
     */
1307
    public static function γ(float $s, float $x): float
1308
    {
1309
        return self::lowerIncompleteGamma($s, $x);
64✔
1310
    }
1311

1312
    /**
1313
     * Regularized lower incomplete gamma function - P(s,x)
1314
     * https://en.wikipedia.org/wiki/Incomplete_gamma_function#Regularized_Gamma_functions_and_Poisson_random_variables
1315
     *
1316
     *          γ(s,x)
1317
     * P(s,x) = ------
1318
     *           Γ(s)
1319
     *
1320
     * P(s,x) is the cumulative distribution function for Gamma random variables with shape parameter s and scale parameter 1
1321
     *
1322
     *
1323
     * @param float $s > 0
1324
     * @param float $x ≥ 0
1325
     *
1326
     * @return float
1327
     *
1328
     * @throws Exception\OutOfBoundsException
1329
     */
1330
    public static function regularizedLowerIncompleteGamma(float $s, float $x): float
1331
    {
1332
        $γ⟮s、x⟯ = self::lowerIncompleteGamma($s, $x);
110✔
1333
        $Γ⟮s⟯    = self::Γ($s);
110✔
1334

1335
        return $γ⟮s、x⟯ / $Γ⟮s⟯;
110✔
1336
    }
1337

1338
    /**
1339
     * Incomplete Beta Function - B(x;a,b)
1340
     *
1341
     * Generalized form of the beta function
1342
     * https://en.wikipedia.org/wiki/Beta_function#Incomplete_beta_function
1343
     *
1344
     *            ⌠ˣ
1345
     * B(x;a,b) = ⎮ t^(a-1) * (1-t)^(b-1) dt
1346
     *            ⌡₀
1347
     *
1348
     * @param float $x Upper limit of the integration 0 ≦ x ≦ 1
1349
     * @param float $a Shape parameter a > 0
1350
     * @param float $b Shape parameter b > 0
1351
     *
1352
     * @return float
1353
     *
1354
     * @throws Exception\BadDataException
1355
     * @throws Exception\BadParameterException
1356
     * @throws Exception\OutOfBoundsException
1357
     */
1358
    public static function incompleteBeta(float $x, float $a, float $b): float
1359
    {
1360
        return self::regularizedIncompleteBeta($x, $a, $b) * self::beta($a, $b);
23✔
1361
    }
1362

1363
    /**
1364
     * Regularized incomplete beta function - Iₓ(a, b)
1365
     *
1366
     * https://en.wikipedia.org/wiki/Beta_function#Incomplete_beta_function
1367
     *
1368
     *             B(x;a,b)   1      ⌠ˣ
1369
     * Iₓ(a,b) = --------- = ----    ⎮  t^(a-1) (1-t)^(b-1) dt
1370
     *             B(a,b)    B(a,b)  ⌡₀
1371
     *
1372
     * Properties:
1373
     *   I₀(a,b) = 0
1374
     *   I₁(a,b) = 1
1375
     *   Iₓ(a,b) = 1 - I₍₁₋ₓ₎(b,a)  [symmetry relation]
1376
     *
1377
     * Cumulative distribution function of Beta distribution.
1378
     * Implemented using continued fraction expansion for numerical stability.
1379
     *
1380
     * This function looks at the values of x, a, and b, and determines which algorithm is best to calculate
1381
     * the value of Iₓ(a, b)
1382
     *
1383
     * There are several ways to calculate the incomplete beta function (See: https://dlmf.nist.gov/8.17).
1384
     * This follows the continued fraction form, which consists of a term followed by a converging series of fractions.
1385
     * Lentz's Algorithm is used to solve the continued fraction.
1386
     *
1387
     * The implementation of the continued fraction using Lentz's Algorithm is heavily inspired by Lewis Van Winkle's
1388
     * reference implementation in C: https://github.com/codeplea/incbeta
1389
     *
1390
     * Other implementations used as references in the past:
1391
     *  http://www.boost.org/doc/libs/1_35_0/libs/math/doc/sf_and_dist/html/math_toolkit/special/sf_beta/ibeta_function.html
1392
     *  https://github.com/boostorg/math/blob/develop/include/boost/math/special_functions/beta.hpp
1393
     *
1394
     * @param float $x Upper limit of the integration 0 ≦ x ≦ 1
1395
     * @param float $a Shape parameter a > 0
1396
     * @param float $b Shape parameter b > 0
1397
     *
1398
     * @return float
1399
     *
1400
     * @throws Exception\BadDataException
1401
     * @throws Exception\BadParameterException
1402
     * @throws Exception\FunctionFailedToConvergeException
1403
     * @throws Exception\NanException
1404
     * @throws Exception\OutOfBoundsException
1405
     */
1406
    public static function regularizedIncompleteBeta(float $x, float $a, float $b): float
1407
    {
1408
        $limits = [
1,175✔
1409
            'x' => '[0, 1]',
1,175✔
1410
            'a' => '(0,∞)',
1,175✔
1411
            'b' => '(0,∞)',
1,175✔
1412
        ];
1,175✔
1413
        Support::checkLimits($limits, ['x' => $x, 'a' => $a, 'b' => $b]);
1,175✔
1414

1415
        if ($x == 1 || $x == 0) {
1,173✔
1416
            return $x;
300✔
1417
        }
1418

1419
        if ($a == 1) {
1,131✔
1420
            return 1 - (1 - $x) ** $b;
290✔
1421
        }
1422

1423
        if ($b == 1) {
916✔
1424
            return $x ** $a;
69✔
1425
        }
1426

1427
        if ($x > ($a + 1)/($a + $b + 2)) {
848✔
1428
            return 1 - self::regularizedIncompleteBeta((1 - $x), $b, $a);
542✔
1429
        }
1430

1431
        // Continued fraction using Lentz's Algorithm.
1432

1433
        $first_term = \exp(\log($x) * $a + \log(1.0 - $x) * $b - (self::logGamma($a) + self::logGamma($b) - self::logGamma($a + $b))) / $a;
848✔
1434

1435
        // PHP 7.2.0 offers PHP_FLOAT_EPSILON, but 1.0e-30 is used in Lewis Van Winkle's
1436
        // reference implementation to prevent division-by-zero errors, so we use the same here.
1437
        $ε = 1.0e-30;
848✔
1438

1439
        // These starting values are changed from the reference implementation to precalculate $i = 0 and avoid the
1440
        // extra conditional expression inside the loop.
1441
        $d = 1.0;
848✔
1442
        $c = 2.0;
848✔
1443
        $f = $c * $d;
848✔
1444

1445
        $m = 0;
848✔
1446
        for ($i = 1; $i <= 500; $i++) {
848✔
1447
            if ($i % 2 === 0) {
848✔
1448
                // Even term
1449
                $m++;
848✔
1450
                $numerator = ($m * ($b - $m) * $x) / (($a + 2.0 * $m - 1.0) * ($a + 2.0 * $m));
848✔
1451
            } else {
1452
                // Odd term
1453
                $numerator = -(($a + $m) * ($a + $b + $m) * $x) / (($a + 2.0 * $m) * ($a + 2.0 * $m + 1));
848✔
1454
            }
1455

1456
            // Lentz's Algorithm
1457
            $d = 1.0 + $numerator * $d;
848✔
1458
            $d = 1.0 / (\abs($d) < $ε ? $ε : $d);
848✔
1459
            $c = 1.0 + $numerator / (\abs($c) < $ε ? $ε : $c);
848✔
1460
            $f *= $c * $d;
848✔
1461

1462
            if (\abs(1.0 - $c * $d) < 1.0e-14) {
848✔
1463
                return $first_term * ($f - 1.0);
848✔
1464
            }
1465
        }
1466

1467
        // Series did not converge.
1468
        throw new Exception\FunctionFailedToConvergeException(sprintf('Continuous fraction series is not converging for x = %f, a = %f, b = %f', $x, $a, $b));
×
1469
    }
1470

1471

1472
    /**
1473
     * Generalized Hypergeometric Function
1474
     *
1475
     * https://en.wikipedia.org/wiki/Generalized_hypergeometric_function
1476
     *
1477
     *                                       ∞
1478
     *                                      ____
1479
     *                                      \     ∏ap⁽ⁿ⁾ * zⁿ
1480
     * pFq(a₁,a₂,...ap;b₁,b₂,...,bq;z) =     >    ------------
1481
     *                                      /      ∏bq⁽ⁿ⁾ * n!
1482
     *                                      ‾‾‾‾
1483
     *                                       n=0
1484
     *
1485
     * Where a⁽ⁿ⁾ is the Pochhammer Function or Rising Factorial
1486
     *
1487
     * We are evaluating this as a series:
1488
     *
1489
     *               (a + n - 1) * z
1490
     * ∏n = ∏n₋₁  * -----------------
1491
     *               (b + n - 1) * n
1492
     *
1493
     *                  n   (a + n - 1) * z
1494
     *   ₁F₁ = ₁F₁n₋₁ + ∏  -----------------  = ₁F₁n₋₁ + ∏n
1495
     *                  1   (b + n - 1) * n
1496
     *
1497
     * @param int    $p         the number of parameters in the numerator
1498
     * @param int    $q         the number of parameters in the denominator
1499
     * @param float  ...$params a collection of the a, b, and z parameters
1500
     *
1501
     * @return float
1502
     *
1503
     * @throws Exception\BadParameterException if the number of parameters is incorrect
1504
     */
1505
    public static function generalizedHypergeometric(int $p, int $q, float ...$params): float
1506
    {
1507
        $n = \count($params);
460✔
1508
        if ($n !== $p + $q + 1) {
460✔
1509
            $expected_num_params = $p + $q + 1;
1✔
1510
            throw new Exception\BadParameterException("Number of parameters is incorrect. Expected $expected_num_params; got $n");
1✔
1511
        }
1512

1513
        $a         = \array_slice($params, 0, $p);
459✔
1514
        $b         = \array_slice($params, $p, $q);
459✔
1515
        $z         = $params[$n - 1];
459✔
1516
        $tol       = .00000001;
459✔
1517
        $n         = 1;
459✔
1518
        $sum       = 0;
459✔
1519
        $product   = 1;
459✔
1520
        $iteration = 0;
459✔
1521

1522
        do {
1523
            $sum     += $product;
459✔
1524
            $a_sum    = \array_product(Single::add($a, $n - 1));
459✔
1525
            $b_sum    = \array_product(Single::add($b, $n - 1));
459✔
1526
            $product *= $a_sum * $z / $b_sum / $n;
459✔
1527
            $n++;
459✔
1528
            $iteration++;
459✔
1529
        } while (\abs($product / $sum) > $tol && $iteration < self::MAX_ITERATIONS);
459✔
1530

1531
        if ($iteration >= self::MAX_ITERATIONS) {
459✔
1532
            throw new Exception\FunctionFailedToConvergeException(
×
1533
                "generalizedHypergeometric failed to converge after $iteration iterations with p=$p, q=$q, z=$z"
×
UNCOV
1534
            );
×
1535
        }
1536

1537
        return $sum;
459✔
1538
    }
1539

1540
    /**
1541
     * Confluent Hypergeometric Function
1542
     *
1543
     * https://en.wikipedia.org/wiki/Confluent_hypergeometric_function
1544
     *         ∞
1545
     *        ____
1546
     *        \     a⁽ⁿ⁾ * zⁿ
1547
     *  ₁F₁ =  >    ---------
1548
     *        /     b⁽ⁿ⁾ * n!
1549
     *        ‾‾‾‾
1550
     *        n=0
1551
     *
1552
     * @param float $a the numerator value
1553
     * @param float $b the denominator value
1554
     * @param float $z
1555
     *
1556
     * @return float
1557
     *
1558
     * @throws Exception\BadParameterException
1559
     */
1560
    public static function confluentHypergeometric(float $a, float $b, float $z): float
1561
    {
1562
        return self::generalizedHypergeometric(1, 1, $a, $b, $z);
78✔
1563
    }
1564

1565
    /**
1566
     * Hypergeometric Function
1567
     *
1568
     * https://en.wikipedia.org/wiki/Hypergeometric_function
1569
     *         ∞
1570
     *        ____
1571
     *        \     a⁽ⁿ⁾ * b⁽ⁿ⁾ * zⁿ
1572
     *  ₂F₁ =  >    ----------------
1573
     *        /         c⁽ⁿ⁾ * n!
1574
     *        ‾‾‾‾
1575
     *        n=0
1576
     *
1577
     * @param float $a the first numerator value
1578
     * @param float $b the second numerator value
1579
     * @param float $c the denominator value
1580
     * @param float $z |z| < 1
1581
     *
1582
     * @return float
1583
     *
1584
     * @throws Exception\OutOfBoundsException if |z| >= 1
1585
     * @throws Exception\BadParameterException
1586
     */
1587
    public static function hypergeometric(float $a, float $b, float $c, float $z): float
1588
    {
1589
        if (\abs($z) >= 1) {
382✔
1590
             throw new Exception\OutOfBoundsException('|z| must be < 1. |z| = ' . \abs($z));
1✔
1591
        }
1592

1593
        return self::generalizedHypergeometric(2, 1, $a, $b, $c, $z);
381✔
1594
    }
1595

1596
    /**
1597
     * Softmax (normalized exponential)
1598
     * A generalization of the logistic function that "squashes" a K-dimensional
1599
     * vector z of arbitrary real values to a K-dimensional vector σ(z) of real values
1600
     * in the range (0, 1) that add up to 1.
1601
     * https://en.wikipedia.org/wiki/Softmax_function
1602
     *
1603
     *           ℯᶻⱼ
1604
     * σ(𝐳)ⱼ = ------  for j = 1 to K
1605
     *          ᴷ
1606
     *          ∑ ℯᶻᵢ
1607
     *         ⁱ⁼¹
1608
     *
1609
     * To ensure numerical stability and prevent floating-point overflow/underflow,
1610
     * this implementation uses the "Log-sum-exp trick" (or "Max trick").
1611
     * https://en.wikipedia.org/wiki/LogSumExp
1612
     *
1613
     * The standard formula suffers from two main issues:
1614
     *   1. Overflow (Catastrophic Failure): If any input zⱼ is a large positive number (e.g., > 709 for 64-bit float),
1615
     *      exp(zⱼ) overflows to INF. If all exp(zᵢ) overflow, the result is INF/INF = NaN.
1616
     *   2. Underflow (Loss of Precision): If all zⱼ are large negative numbers, all exp(zⱼ) underflow to 0.
1617
     *      The result is 0/0 = NaN.
1618
     *
1619
     * We use an equivalent, transformed formula:
1620
     *
1621
     *           eᶻʲ⁻ᶜ
1622
     * σ(z)ʲ = ---------  where c = max(z)
1623
     *          ᴷ
1624
     *          ∑ eᶻⁱ⁻ᶜ
1625
     *          ⁱ⁼¹
1626
     *
1627
     * This transformation is mathematically valid because we multiply the numerator and denominator by e⁻ᶜ:
1628
     *
1629
     *   exp(zʲ)      exp(zʲ) ⋅ exp(-c)    exp(zʲ - c)
1630
     *  --------- = ------------------- = --------------
1631
     *  ∑ exp(zⁱ)   ∑ exp(zⁱ) ⋅ exp(-c)   ∑ exp(zⁱ - c)
1632
     *
1633
     * For numerical stability, subtract max(z) (c in formula) before exponentiating.
1634
     * This prevents exp(large_number) from overflowing to INF.
1635
     *
1636
     * Concrete Example of Overflow Prevention (Max Trick):
1637
     * Let z = [750, 770]. Maximum c = 770. (Note: exp(709) is the float limit)
1638
     *  php > var_dump(\exp(709));
1639
     *  float(8.218407461554972E+307)
1640
     *  php > var_dump(\exp(710));
1641
     *  float(INF)
1642
     *
1643
     * Standard Formula Issue:
1644
     *  Numerators: exp(750) -> INF, exp(770) -> INF
1645
     *  Denominator: INF + INF -> INF
1646
     *  Result: INF/INF = NaN (Failure)
1647
     *
1648
     * Stable Formula Solution (zᵢ - c):
1649
     *  Numerators (Shifted): exp(750 - 770) = exp(-20) ≈ 2.06e-9
1650
     *  exp(770 - 770) = exp(0) = 1.0
1651
     *  Denominator Sum: exp(-20) + 1.0 ≈ 1.0
1652
     *  Result: [≈ 2.06e-9, 1.0] (Correct, stable calculation)
1653
     *
1654
     * By shifting the inputs, the largest exponent input is 0, guaranteeing that exp(zⱼ-c) will never overflow to INF.
1655
     *
1656
     * @param  float[] $𝐳
1657
     *
1658
     * @return float[]
1659
     */
1660
    public static function softmax(array $𝐳): array
1661
    {
1662
        // Log-sum-exp trick to prevent overflow/underflow
1663
        // For numerical stability, subtract max(z) before exponentiating
1664
        // This prevents exp(large_number) from overflowing to INF
1665
        $c = \max($𝐳);
73✔
1666
        $eᶻʲ⁻ᶜ_array = \array_map(
73✔
1667
            function ($z) use ($c) {
73✔
1668
                return \exp($z - $c);
73✔
1669
            },
73✔
1670
            $𝐳
73✔
1671
        );
73✔
1672

1673
        $∑ᴷeᶻʲ⁻ᶜ = \array_sum($eᶻʲ⁻ᶜ_array);
73✔
1674

1675
        $σ⟮𝐳⟯ⱼ = \array_map(
73✔
1676
            function ($exp_z) use ($∑ᴷeᶻʲ⁻ᶜ) {
73✔
1677
                return $exp_z / $∑ᴷeᶻʲ⁻ᶜ;
73✔
1678
            },
73✔
1679
            $eᶻʲ⁻ᶜ_array
73✔
1680
        );
73✔
1681

1682
        return $σ⟮𝐳⟯ⱼ;
73✔
1683
    }
1684

1685
    /**************************************************************************
1686
     * BESSEL FUNCTIONS
1687
     *
1688
     *  https://en.wikipedia.org/wiki/Bessel_function
1689
     *
1690
     *          ∞
1691
     *         ____      (-1)ᵏ   ⎛x⎞^(ν+2k)
1692
     *  Jᵥ(x) = \     ────────── ⎜─⎟
1693
     *          /     k! Γ(ν+k+1)⎝2⎠
1694
     *         ‾‾‾‾
1695
     *         k=0
1696
     *
1697
     *************************************************************************/
1698

1699
    /**
1700
     * Bessel function of the first kind, order 0 - J₀(x)
1701
     * https://en.wikipedia.org/wiki/Bessel_function
1702
     *
1703
     *          ∞
1704
     *         ____      (-1)ᵏ   ⎛x⎞^(2k)
1705
     *  J₀(x) = \     ────────── ⎜─⎟
1706
     *          /        (k!)²   ⎝2⎠
1707
     *         ‾‾‾‾
1708
     *         k=0
1709
     *
1710
     *
1711
     * Uses polynomial approximations for numerical stability
1712
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
1713
     *
1714
     * @param float $x
1715
     *
1716
     * @return float
1717
     */
1718
    public static function besselJ0(float $x): float
1719
    {
1720
        if ($x == 0) {
259✔
1721
            return 1.0;
2✔
1722
        }
1723

1724
        $|x| = \abs($x);
257✔
1725

1726
        // Small argument approximation
1727
        if ($|x| < 8.0) {
257✔
1728
            $y = $x * $x;
204✔
1729
            $numerator = 57568490574.0 + $y * (-13362590354.0 + $y * (651619640.7 + $y * (-11214424.18 + $y * (77392.33017 + $y * (-184.9052456)))));
204✔
1730
            $denominator = 57568490411.0 + $y * (1029532985.0 + $y * (9494680.718 + $y * (59272.64853 + $y * (267.8532712 + $y * 1.0))));
204✔
1731
            return $numerator / $denominator;
204✔
1732
        }
1733

1734
        // Large argument asymptotic expansion
1735
        $z = 8.0 / $|x|;
53✔
1736
        $y = $z * $z;
53✔
1737
        $θ = $|x| - 0.785398164;
53✔
1738

1739
        $P = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6)));
53✔
1740
        $Q = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 - $y * 0.934935152e-7)));
53✔
1741

1742
        return \sqrt(0.636619772 / $|x|) * (\cos($θ) * $P - $z * \sin($θ) * $Q);
53✔
1743
    }
1744

1745
    /**
1746
     * Bessel function of the first kind, order 1 - J₁(x)
1747
     * https://en.wikipedia.org/wiki/Bessel_function
1748
     *
1749
     *         ∞
1750
     *        ____      (-1)ᵏ     ⎛x⎞^(2k+1)
1751
     * J₁(x) = \     ──────────── ⎜─⎟
1752
     *        /      k! (k+1)!    ⎝2⎠
1753
     *        ‾‾‾‾
1754
     *        k=0
1755
     *
1756
     * Properties:
1757
     *   J₁(0) = 0
1758
     *   J₁(-x) = -J₁(x)  [odd function]
1759
     *   J'₀(x) = -J₁(x)
1760
     *
1761
     * Uses polynomial approximations for numerical stability
1762
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
1763
     *
1764
     * @param float $x
1765
     *
1766
     * @return float
1767
     */
1768
    public static function besselJ1(float $x): float
1769
    {
1770
        $|x| = \abs($x);
279✔
1771

1772
        // Small argument approximation
1773
        if ($|x| < 8.0) {
279✔
1774
            $y = $x * $x;
219✔
1775
            $numerator = $x * (72362614232.0 + $y * (-7895059235.0 + $y * (242396853.1 + $y * (-2972611.439 + $y * (15704.48260 + $y * (-30.16036606))))));
219✔
1776
            $denominator = 144725228442.0 + $y * (2300535178.0 + $y * (18583304.74 + $y * (99447.43394 + $y * (376.9991397 + $y * 1.0))));
219✔
1777
            return $numerator / $denominator;
219✔
1778
        }
1779

1780
        // Large argument asymptotic expansion
1781
        $z = 8.0 / $|x|;
60✔
1782
        $y = $z * $z;
60✔
1783
        $θ = $|x| - 2.356194491;
60✔
1784

1785
        $P = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6))));
60✔
1786
        $Q = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * (-0.88228987e-6 + $y * 0.105787412e-6)));
60✔
1787

1788
        $result = \sqrt(0.636619772 / $|x|) * (\cos($θ) * $P - $z * \sin($θ) * $Q);
60✔
1789
        return $x < 0.0 ? -$result : $result;
60✔
1790
    }
1791

1792
    /**
1793
     * Bessel function of the first kind, order n - Jₙ(x)
1794
     * https://en.wikipedia.org/wiki/Bessel_function
1795
     *
1796
     *         ∞
1797
     *        ____      (-1)ᵏ     ⎛x⎞^(n+2k)
1798
     * Jₙ(x) = \     ──────────── ⎜─⎟
1799
     *        /      k! (n+k)!    ⎝2⎠
1800
     *        ‾‾‾‾
1801
     *        k=0
1802
     *
1803
     * Recurrence relation (used in implementation):
1804
     *   Jₙ₊₁(x) = (2n/x)Jₙ(x) - Jₙ₋₁(x)
1805
     *
1806
     * Properties:
1807
     *   Jₙ(0) = δₙ₀  [Kronecker delta]
1808
     *   J₋ₙ(x) = (-1)ⁿJₙ(x)
1809
     *
1810
     * Implementation uses forward recurrence from J₀ and J₁.
1811
     * Uses Miller's backward recurrence algorithm for integer orders
1812
     * Reference: Numerical Recipes in C
1813
     *
1814
     * @param int $n order (n ≥ 0)
1815
     * @param float $x
1816
     *
1817
     * @return float
1818
     *
1819
     * @throws Exception\OutOfBoundsException if n < 0
1820
     */
1821
    public static function besselJn(int $n, float $x): float
1822
    {
1823
        if ($n < 0) {
335✔
1824
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
1825
        }
1826

1827
        if ($n === 0) {
334✔
1828
            return self::besselJ0($x);
49✔
1829
        }
1830
        if ($n === 1) {
303✔
1831
            return self::besselJ1($x);
77✔
1832
        }
1833

1834
        $|x| = \abs($x);
254✔
1835
        if ($|x| < 1e-10) {
254✔
1836
            return 0.0;
×
1837
        }
1838

1839
        // Always use Miller's backward recurrence algorithm for n > 1
1840
        // Reference: Numerical Recipes in C, Section 6.5
1841
        $iacc = 40; // Accuracy parameter
254✔
1842
        $m1 = $n + (int)\sqrt($iacc * $n);
254✔
1843

1844
        // For large x, need m > x to ensure convergence
1845
        if ($|x| > 100) {
254✔
1846
            $buffer = 80;
11✔
1847
        } elseif ($|x| > 50) {
243✔
1848
            $buffer = 50;
18✔
1849
        } elseif ($|x| > 30) {
225✔
1850
            $buffer = 30;
31✔
1851
        } else {
1852
            $buffer = 20;
194✔
1853
        }
1854
        $m2 = (int)$|x| + $buffer;
254✔
1855
        $m  = \max($m1, $m2);
254✔
1856
        // Make m even
1857
        if ($m % 2 === 1) {
254✔
1858
            $m++;
99✔
1859
        }
1860

1861
        $Jⱼ₊₁   = 0.0;
254✔
1862
        $∑      = 0.0;
254✔
1863
        $Jⱼ     = 1.0;
254✔
1864
        $result = 0.0;
254✔
1865

1866
        for ($j = $m; $j > 0; $j--) {
254✔
1867
            $Jⱼ₋₁ = $j * 2.0 / $|x| * $Jⱼ - $Jⱼ₊₁;
254✔
1868
            $Jⱼ₊₁ = $Jⱼ;
254✔
1869
            $Jⱼ   = $Jⱼ₋₁;
254✔
1870

1871
            // Rescale to prevent overflow
1872
            if (\abs($Jⱼ) > 1.0e10) {
254✔
1873
                $Jⱼ     *= 1.0e-10;
222✔
1874
                $Jⱼ₊₁   *= 1.0e-10;
222✔
1875
                $result *= 1.0e-10;
222✔
1876
                $∑      *= 1.0e-10;
222✔
1877
            }
1878

1879
            // Accumulate sum for all odd j
1880
            if ($j % 2 == 1) {
254✔
1881
                $∑ += $Jⱼ;
254✔
1882
            }
1883

1884
            // Store result for order n
1885
            if ($j === $n) {
254✔
1886
                $result = $Jⱼ₊₁;
254✔
1887
            }
1888
        }
1889

1890
        // The sum should be equal to 1 when normalized properly
1891
        // J₀ + 2*(J₂ + J₄ + J₆ + ...) = 1
1892
        $∑ = 2.0 * $∑ - $Jⱼ;
254✔
1893
        $result = $result / $∑;
254✔
1894

1895
        return $x < 0.0 && ($n % 2 === 1) ? -$result : $result;
254✔
1896
    }
1897

1898
    /**
1899
     * Bessel function of the first kind, order v - Jᵥ(x)
1900
     * https://en.wikipedia.org/wiki/Bessel_function
1901
     *
1902
     *         ∞
1903
     *        ____      (-1)ᵏ     ⎛x⎞^(ν+2k)
1904
     * Jᵥ(x) = \     ──────────── ⎜─⎟
1905
     *        /      k! Γ(ν+k+1)  ⎝2⎠
1906
     *        ‾‾‾‾
1907
     *        k=0
1908
     *
1909
     * Asymptotic expansion for large x:
1910
     *             ___
1911
     *           √ 2     ⎛     π   νπ ⎞
1912
     *   Jᵥ(x) ≈ ──── cos⎜x - ── - ── ⎟
1913
     *            √πx    ⎝     4    2 ⎠
1914
     *
1915
     * Implemented using recurrence relations and series expansions
1916
     * depending on argument size for numerical stability.
1917
     *
1918
     * For integer orders, uses Miller's backward recurrence.
1919
     * For non-integer orders with small x, uses series expansion.
1920
     * For non-integer orders with large x, uses asymptotic expansion (DLMF 10.17).
1921
     *
1922
     * @param float $ν order (real: can be negative for non-integer)
1923
     * @param float $x
1924
     *
1925
     * @return float
1926
     */
1927
    public static function besselJv(float $ν, float $x): float
1928
    {
1929
        $|x| = \abs($x);
230✔
1930

1931
        // For integer orders, use the integer-optimized function
1932
        if ($ν === \floor($ν)) {
230✔
1933
            $n = (int) $ν;
65✔
1934
            if ($n < 0) {
65✔
1935
                // J₋ₙ(x) = (-1)ⁿ Jₙ(x) for integer n
1936
                return (($n % 2 === 0) ? 1 : -1) * self::besselJn(-$n, $x);
35✔
1937
            }
1938
            return self::besselJn($n, $x);
34✔
1939
        }
1940

1941
        if ($|x| < 1e-10) {
165✔
1942
            return 0.0;
×
1943
        }
1944

1945
        // For large x, use asymptotic expansion (DLMF 10.17.3)
1946
        // Jᵥ(x) ~ sqrt(2/(πx)) * [cos(ω) * P - sin(ω) * Q]
1947
        // where ω = x - vπ/2 - π/4
1948
        // More accurate than series expansion for x > 30
1949
        if ($|x| > 30.0) {
165✔
1950
            // For negative v, compute Jᵥ(|v|) and Yᵥ(|v|), then use relationship
1951
            if ($ν < 0) {
52✔
1952
                $|ν|  = \abs($ν);
52✔
1953
                $j_pos = self::besselJv($|ν|, $|x|);
52✔
1954

1955
                // Need Yᵥ for the relationship: J₋ᵥ(x) = cos(νπ) Jᵥ(x) - sin(νπ) Yᵥ(x)
1956
                // For non-integer orders, compute Yᵥ using asymptotic expansion
1957
                $μ = 4.0 * $|ν| * $|ν|;
52✔
1958
                $ω = $|x| - ($|ν| * \M_PI / 2.0) - (\M_PI / 4.0);
52✔
1959

1960
                // Yᵥ asymptotic: Yᵥ(x) ~ sqrt(2/(πx)) * [sin(ω) * P + cos(ω) * Q]
1961
                $P      = 1.0;
52✔
1962
                $P_term = 1.0;
52✔
1963
                for ($k = 1; $k <= 10; $k++) {
52✔
1964
                    $P_term *= -($μ - (2*$k - 1) * (2*$k - 1)) / (2 * $k * (2*$k - 1) * 8.0 * $|x| * $|x|);
52✔
1965
                    $P      += $P_term;
52✔
1966
                    if (\abs($P_term) < \abs($P) * 1e-15) {
52✔
1967
                        break;
52✔
1968
                    }
1969
                }
1970

1971
                $Q      = ($μ - 1.0) / (8.0 * $|x|);
52✔
1972
                $Q_term = $Q;
52✔
1973
                for ($k = 1; $k <= 10; $k++) {
52✔
1974
                    $Q_term *= -($μ - (2*$k + 1) * (2*$k + 1)) / (2 * $k * (2*$k + 1) * 8.0 * $|x| * $|x|);
52✔
1975
                    $Q      += $Q_term;
52✔
1976
                    if (\abs($Q_term) < \abs($Q) * 1e-15) {
52✔
1977
                        break;
52✔
1978
                    }
1979
                }
1980

1981
                $Y_pos = \sqrt(2.0 / (\M_PI * $|x|)) * (\sin($ω) * $P + \cos($ω) * $Q);
52✔
1982

1983
                // J₋ᵥ(x) = cos(νπ) Jᵥ(x) - sin(νπ) Yᵥ(x)
1984
                return \cos($|ν| * \M_PI) * $j_pos - \sin($|ν| * \M_PI) * $Y_pos;
52✔
1985
            }
1986

1987
            // For positive ν
1988
            $μ = 4.0 * $ν * $ν;
52✔
1989

1990
            // ω = x - νπ/2 - π/4
1991
            $ω = $|x| - ($ν * \M_PI / 2.0) - (\M_PI / 4.0);
52✔
1992

1993
            // Compute P series: Σ (-1)ᵏ * a₂ₖ / x²ᵏ
1994
            // a₀ = 1, a₂ₖ involves products (μ - (2j-1)²)
1995
            $P      = 1.0;
52✔
1996
            $P_term = 1.0;
52✔
1997
            for ($k = 1; $k <= 10; $k++) {
52✔
1998
                $P_term *= -($μ - (2*$k - 1) * (2*$k - 1)) / (2 * $k * (2*$k - 1) * 8.0 * $|x| * $|x|);
52✔
1999
                $P      += $P_term;
52✔
2000
                if (\abs($P_term) < \abs($P) * 1e-15) {
52✔
2001
                    break;
52✔
2002
                }
2003
            }
2004

2005
            // Compute Q series: Σ (-1)ᵏ * a₂ₖ₊₁ / x²ᵏ⁺¹
2006
            $Q      = ($μ - 1.0) / (8.0 * $|x|);
52✔
2007
            $Q_term = $Q;
52✔
2008
            for ($k = 1; $k <= 10; $k++) {
52✔
2009
                $Q_term *= -($μ - (2*$k + 1) * (2*$k + 1)) / (2 * $k * (2*$k + 1) * 8.0 * $|x| * $|x|);
52✔
2010
                $Q      += $Q_term;
52✔
2011
                if (\abs($Q_term) < \abs($Q) * 1e-15) {
52✔
2012
                    break;
52✔
2013
                }
2014
            }
2015

2016
            return \sqrt(2.0 / (\M_PI * $|x|)) * (\cos($ω) * $P - \sin($ω) * $Q);
52✔
2017
        }
2018

2019
        // For negative orders, use series expansion
2020
        if ($ν < 0) {
113✔
2021
            // Jᵥ(x) = (x/2)ᵛ * Σ (-1)ᵏ (x²/4)ᵏ / (k! * Γ(ν+k+1))
2022
            // Same formula, just ν happens to be negative
2023
            $∑    = 0.0;
68✔
2024
            $term = \pow($|x| / 2.0, $ν) / self::Γ($ν + 1);
68✔
2025
            $∑   += $term;
68✔
2026

2027
            for ($k = 1; $k < 200; $k++) {
68✔
2028
                $term *= -($|x| * $|x| / 4.0) / ($k * ($ν + $k));
68✔
2029
                $∑    += $term;
68✔
2030
                if (\abs($term) < \abs($∑) * 1e-15) {
68✔
2031
                    break;
68✔
2032
                }
2033
            }
2034

2035
            return $∑;
68✔
2036
        }
2037

2038
        // For positive non-integer orders, use series expansion
2039
        // Jᵥ(x) = (x/2)ᵛ * Σ (-1)ᵏ (x²/4)ᵏ / (k! * Γ(ν+k+1))
2040
        $∑    = 0.0;
113✔
2041
        $term = \pow($|x| / 2.0, $ν) / self::Γ($ν + 1);
113✔
2042
        $∑   += $term;
113✔
2043

2044
        for ($k = 1; $k < 200; $k++) {
113✔
2045
            $term *= -($|x| * $|x| / 4.0) / ($k * ($ν + $k));
113✔
2046
            $∑    += $term;
113✔
2047
            if (\abs($term) < \abs($∑) * 1e-15) {
113✔
2048
                break;
113✔
2049
            }
2050
        }
2051

2052
        return $∑;
113✔
2053
    }
2054

2055
    /**
2056
     * Bessel function of the second kind, order 0 - Y₀(x)
2057
     * https://en.wikipedia.org/wiki/Bessel_function
2058
     *
2059
     *             Jᵥ(x) cos(νπ) - J₋ᵥ(x)
2060
     * Yᵥ(x) = lim ────────────────────────
2061
     *         ν→0        sin(νπ)
2062
     *
2063
     * Alternative representation:
2064
     *         2     ⎛x⎞
2065
     * Y₀(x) = ─ [ln ⎜─⎟ + γ] J₀(x) + [series]
2066
     *         π     ⎝2⎠
2067
     *
2068
     * where γ is Euler-Mascheroni constant.
2069
     *
2070
     * Series:
2071
     *      ∞
2072
     *  2  ___  (-1)ᵐ⁺¹   /x \²ᵐ
2073
     *  ─  \    ───────  | ─ |  Hₘ
2074
     *  π  /__    (m!)²  \ 2 /
2075
     *     m-1
2076
     *
2077
     * Singular at x = 0: Y₀(0⁺) = -∞
2078
     *
2079
     * Uses polynomial approximations for numerical stability
2080
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2081
     *
2082
     * @param float $x (x > 0)
2083
     *
2084
     * @return float
2085
     *
2086
     * @throws Exception\OutOfBoundsException if x ≤ 0
2087
     */
2088
    public static function besselY0(float $x): float
2089
    {
2090
        if ($x <= 0.0) {
288✔
2091
            throw new Exception\OutOfBoundsException("Y₀(x) requires x > 0");
2✔
2092
        }
2093

2094
        if ($x < 8.0) {
286✔
2095
            $J₀ = self::besselJ0($x);
144✔
2096
            $y  = $x * $x;
144✔
2097
            $numerator = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y * (-86327.92757 + $y * 228.4622733))));
144✔
2098
            $denominator = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * (47447.26470 + $y * (226.1030244 + $y * 1.0))));
144✔
2099
            return ($numerator / $denominator) + 0.636619772 * $J₀ * \log($x);
144✔
2100
        }
2101

2102
        $z = 8.0 / $x;
142✔
2103
        $y = $z * $z;
142✔
2104
        $θ = $x - 0.785398164;
142✔
2105

2106
        $P = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6)));
142✔
2107
        $Q = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 - $y * 0.934935152e-7)));
142✔
2108

2109
        return \sqrt(0.636619772 / $x) * (\sin($θ) * $P + $z * \cos($θ) * $Q);
142✔
2110
    }
2111

2112
    /**
2113
     * Bessel function of the second kind, order 1 - Y₁(x)
2114
     * https://en.wikipedia.org/wiki/Bessel_function
2115
     *
2116
     *           2            x           1
2117
     * Y₁(x) = ─── J₁(x) (ln(───) + γ) − ─── + (series terms)
2118
     *          πx²          2           πx
2119
     *
2120
     * Recurrence relation:
2121
     *   Yₙ₊₁(x) = (2n/x)Yₙ(x) - Yₙ₋₁(x)
2122
     *
2123
     * Singular at x = 0: Y₁(0⁺) = -∞
2124
     *
2125
     * Uses polynomial approximations for numerical stability
2126
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2127
     *
2128
     * @param float $x (x > 0)
2129
     *
2130
     * @return float
2131
     *
2132
     * @throws Exception\OutOfBoundsException if x ≤ 0
2133
     */
2134
    public static function besselY1(float $x): float
2135
    {
2136
        if ($x <= 0.0) {
281✔
2137
            throw new Exception\OutOfBoundsException("Y₁(x) requires x > 0");
2✔
2138
        }
2139

2140
        if ($x < 8.0) {
279✔
2141
            $J₁ = self::besselJ1($x);
141✔
2142
            $y  = $x * $x;
141✔
2143
            $numerator = $x * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y * (0.7349264551e9 + $y * (-0.4237922726e7 + $y * 0.8511937935e4)))));
141✔
2144
            $denominator = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * (0.1020426050e6 + $y * (0.3549632885e3 + $y)))));
141✔
2145
            return ($numerator / $denominator) + 0.636619772 * ($J₁ * \log($x) - 1.0 / $x);
141✔
2146
        }
2147

2148
        $z = 8.0 / $x;
138✔
2149
        $y = $z * $z;
138✔
2150
        $θ = $x - 2.356194491;
138✔
2151

2152
        $P = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6))));
138✔
2153
        $Q = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * (-0.88228987e-6 + $y * 0.105787412e-6)));
138✔
2154

2155
        return \sqrt(0.636619772 / $x) * (\sin($θ) * $P + $z * \cos($θ) * $Q);
138✔
2156
    }
2157

2158
    /**
2159
     * Bessel function of the second kind, order n - Yₙ(x)
2160
     * https://en.wikipedia.org/wiki/Bessel_function
2161
     *
2162
     *          Jₙ(x) cos(nπ) - J₋ₙ(x)
2163
     * Yₙ(x) = ────────────────────────
2164
     *                 sin(nπ)
2165
     *
2166
     * Recurrence relation:
2167
     *   Yₙ₊₁(x) = (2n/x)Yₙ(x) - Yₙ₋₁(x)
2168
     *
2169
     * Wronskian:
2170
     *   Jₙ(x)Yₙ₊₁(x) - Jₙ₊₁(x)Yₙ(x) = 2/(πx)
2171
     *
2172
     * Implementation uses forward recurrence from Y₀ and Y₁.
2173
     *
2174
     * @param int   $n order (n ≥ 0)
2175
     * @param float $x (x > 0)
2176
     *
2177
     * @return float
2178
     *
2179
     * @throws Exception\OutOfBoundsException if n < 0 or x ≤ 0
2180
     */
2181
    public static function besselYn(int $n, float $x): float
2182
    {
2183
        if ($n < 0) {
240✔
2184
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2185
        }
2186
        if ($x <= 0.0) {
239✔
2187
            throw new Exception\OutOfBoundsException("Yₙ(x) requires x > 0");
2✔
2188
        }
2189

2190
        if ($n === 0) {
237✔
2191
            return self::besselY0($x);
43✔
2192
        }
2193
        if ($n === 1) {
211✔
2194
            return self::besselY1($x);
52✔
2195
        }
2196

2197
        // Forward recurrence
2198
        $Yₙ₋₁ = self::besselY0($x);
178✔
2199
        $Yₙ   = self::besselY1($x);
178✔
2200
        for ($j = 1; $j < $n; $j++) {
178✔
2201
            $Yₙ₊₁ = $j * 2.0 / $x * $Yₙ - $Yₙ₋₁;
178✔
2202
            $Yₙ₋₁ = $Yₙ;
178✔
2203
            $Yₙ   = $Yₙ₊₁;
178✔
2204
        }
2205

2206
        return $Yₙ;
178✔
2207
    }
2208

2209
    /**
2210
     * Modified Bessel function of the first kind, order 0 - I₀(x)
2211
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2212
     *
2213
     *         ∞
2214
     *        ____      1    ⎛x⎞^(2k)
2215
     * I₀(x) = \     ─────── ⎜─⎟
2216
     *        /       (k!)²  ⎝2⎠
2217
     *        ‾‾‾‾
2218
     *        k=0
2219
     *
2220
     * Relationship to J₀:
2221
     *   I₀(x) = J₀(ix)  where i = √(-1)
2222
     *
2223
     * Properties:
2224
     *   I₀(0) = 1
2225
     *   I₀(x) > 0 for all real x
2226
     *   I₀(x) = I₀(-x)  [even function]
2227
     *
2228
     * Uses polynomial approximations for numerical stability
2229
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2230
     *
2231
     * @param float $x
2232
     *
2233
     * @return float
2234
     */
2235
    public static function besselI0(float $x): float
2236
    {
2237
        $|x| = \abs($x);
159✔
2238

2239
        // For large x, use asymptotic expansion from besselIv (more accurate)
2240
        if ($|x| > 15.0) {
159✔
2241
            return self::besselIv(0.0, $x);
11✔
2242
        }
2243

2244
        if ($|x| < 3.75) {
148✔
2245
            $y = ($x / 3.75) * ($x / 3.75);
86✔
2246
            return 1.0 + $y * (3.5156229 + $y * (3.0899424 + $y * (1.2067492 + $y * (0.2659732 + $y * (0.360768e-1 + $y * 0.45813e-2)))));
86✔
2247
        }
2248

2249
        $y = 3.75 / $|x|;
62✔
2250
        $result = (\exp($|x|) / \sqrt($|x|)) * (0.39894228 + $y * (0.1328592e-1
62✔
2251
            + $y * (0.225319e-2 + $y * (-0.157565e-2 + $y * (0.916281e-2
62✔
2252
            + $y * (-0.2057706e-1 + $y * (0.2635537e-1 + $y * (-0.1647633e-1
62✔
2253
            + $y * 0.392377e-2))))))));
62✔
2254

2255
        return $result;
62✔
2256
    }
2257

2258
    /**
2259
     * Modified Bessel function of the first kind, order 1 - I₁(x)
2260
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2261
     *
2262
     *         ∞
2263
     *        ____      1     ⎛x⎞^(2k+1)
2264
     * I₁(x) = \     ──────── ⎜─⎟
2265
     *        /      k!(k+1)! ⎝2⎠
2266
     *        ‾‾‾‾
2267
     *        k=0
2268
     *
2269
     * Relationship to J₁:
2270
     *   I₁(x) = -i·J₁(ix)  where i = √(-1)
2271
     *
2272
     * Properties:
2273
     *   I₁(0) = 0
2274
     *   I₁(-x) = -I₁(x)  [odd function]
2275
     * Uses polynomial approximations for numerical stability
2276
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2277
     *
2278
     * @param float $x
2279
     *
2280
     * @return float
2281
     */
2282
    public static function besselI1(float $x): float
2283
    {
2284
        $|x| = \abs($x);
166✔
2285

2286
        // For large x, use asymptotic expansion from besselIv (more accurate)
2287
        if ($|x| > 15.0) {
166✔
2288
            return self::besselIv(1.0, $x);
11✔
2289
        }
2290

2291
        if ($|x| < 3.75) {
155✔
2292
            $y = ($x / 3.75) * ($x / 3.75);
91✔
2293
            $result = $|x| * (0.5 + $y * (0.87890594 + $y * (0.51498869 + $y * (0.15084934
91✔
2294
                + $y * (0.2658733e-1 + $y * (0.301532e-2 + $y * 0.32411e-3))))));
91✔
2295
        } else {
2296
            $y = 3.75 / $|x|;
64✔
2297
            $result = 0.2282967e-1 + $y * (-0.2895312e-1 + $y * (0.1787654e-1 - $y * 0.420059e-2));
64✔
2298
            $result = 0.39894228 + $y * (-0.3988024e-1 + $y * (-0.362018e-2 + $y * (0.163801e-2 + $y * (-0.1031555e-1 + $y * $result))));
64✔
2299
            $result *= (\exp($|x|) / \sqrt($|x|));
64✔
2300
        }
2301

2302
        return $x < 0.0 ? -$result : $result;
155✔
2303
    }
2304

2305
    /**
2306
     * Modified Bessel function of the first kind, order v - Iᵥ(x)
2307
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2308
     *
2309
     *         ∞
2310
     *        ____         1      ⎛x⎞^(ν+2k)
2311
     * Iᵥ(x) = \     ──────────── ⎜─⎟
2312
     *        /      k! Γ(ν+k+1)  ⎝2⎠
2313
     *        ‾‾‾‾
2314
     *        k=0
2315
     *
2316
     * Recurrence relation:
2317
     *   Iᵥ₊₁(x) = Iᵥ₋₁(x) - (2ν/x)Iᵥ(x)
2318
     *
2319
     * Asymptotic expansion for large x:
2320
     *             eˣ
2321
     *   Iᵥ(x) ≈ ──── [1 + O(1/x)]
2322
     *            √(2πx)
2323
     *
2324
     * Uses series expansion and recurrence relations
2325
     *
2326
     * @param float $ν order (real)
2327
     * @param float $x argument (x ≥ 0 for v ≥ 0)
2328
     *
2329
     * @return float
2330
     */
2331
    public static function besselIv(float $ν, float $x): float
2332
    {
2333
        $|x| = \abs($x);
271✔
2334
        if ($|x| < 1e-10) {
271✔
2335
            return 0.0;
×
2336
        }
2337

2338
        // For very large x relative to ν, use asymptotic expansion (more accurate than I0/I1 approximations)
2339
        // Asymptotic expansion: Iᵥ(x) ≈ eˣ / sqrt(2πx) * Σ (-1)ᵏ * aₖ(ν) / xᵏ
2340
        // Valid when x/ν >= 2 (or x > 15 for ν=0,1)
2341
        $|ν| = \abs($ν);
271✔
2342
        if ($|x| > 15.0 && ($|ν| < 1.0 || $|x| / $|ν| >= 2.0)) {
271✔
2343
            $μ           = 4.0 * $ν * $ν;
81✔
2344
            $coefficient = \exp($|x|) / \sqrt(2.0 * \M_PI * $|x|);
81✔
2345

2346
            $∑    = 1.0;
81✔
2347
            $term = 1.0;
81✔
2348

2349
            // Add asymptotic terms with alternating signs (use more terms for better accuracy)
2350
            for ($k = 1; $k <= 20; $k++) {
81✔
2351
                $term *= -($μ - (2 * $k - 1) * (2 * $k - 1)) / ($k * 8.0 * $|x|);
81✔
2352
                $∑    += $term;
81✔
2353
                if (\abs($term) < \abs($∑) * 1e-15) {
81✔
2354
                    break;
59✔
2355
                }
2356
            }
2357

2358
            return $x < 0.0 && ((int)$ν % 2 === 1) ? -$coefficient * $∑ : $coefficient * $∑;
81✔
2359
        }
2360

2361
        // For ν=0 and ν=1 at medium x, use specialized functions
2362
        if ($ν === 0.0) {
190✔
2363
            return self::besselI0($x);
18✔
2364
        }
2365
        if ($ν === 1.0) {
180✔
2366
            return self::besselI1($x);
26✔
2367
        }
2368

2369
        // For integer orders >= 2, use recurrence for medium x, series for small x
2370
        if ($ν === \floor($ν) && \abs($ν) >= 2) {
171✔
2371
            $n = (int) $ν;
101✔
2372
            if ($n > 0) {
101✔
2373
                // Choose method based on x and ν to minimize error
2374
                // Recurrence accumulates error when n is large relative to x
2375
                // Use series when x < max(5, n) to avoid recurrence error accumulation
2376
                if ($|x| < \max(5.0, $n)) {
91✔
2377
                    // For small x relative to n, use series expansion to avoid error accumulation
2378
                    // Fall through to series expansion below
2379
                } elseif ($|x| <= 15.0 || $|x| / $|ν| < 2.0) {
30✔
2380
                    // For medium x with good x/n ratio, use recurrence
2381
                    $Iₙ₋₁ = self::besselI0($|x|);
30✔
2382
                    $Iₙ = self::besselI1($|x|);
30✔
2383
                    for ($j = 1; $j < $n; $j++) {
30✔
2384
                        $Iₙ₊₁ = $Iₙ₋₁ - ($j * 2.0 / $|x|) * $Iₙ;
30✔
2385
                        $Iₙ₋₁ = $Iₙ;
30✔
2386
                        $Iₙ   = $Iₙ₊₁;
30✔
2387
                    }
2388
                    return $x < 0.0 && ($n % 2 === 1) ? -$Iₙ : $Iₙ;
30✔
2389
                } else {
2390
                    // For large x with x/v >= 2, asymptotic expansion was already tried above
2391
                    // Fall through to series expansion as fallback
2392
                }
2393
            }
2394
        }
2395

2396
        // Use series expansion for non-integer orders, negative orders, or small x
2397
        // $|ν| already defined above
2398
        $∑    = 0.0;
156✔
2399
        $term = \pow($|x| / 2.0, $|ν|) / self::Γ($|ν| + 1);
156✔
2400
        $∑   += $term;
156✔
2401

2402
        for ($k = 1; $k < 200; $k++) {
156✔
2403
            $term *= ($|x| * $|x| / 4.0) / ($k * ($|ν| + $k));
156✔
2404
            $∑    += $term;
156✔
2405
            if (\abs($term) < \abs($∑) * 1e-15) {
156✔
2406
                break;
156✔
2407
            }
2408
        }
2409

2410
        // For negative non-integer orders: I₋ᵥ(x) = Iᵥ(x) + (2/π)sin(νπ)Kᵥ(x)
2411
        if ($ν < 0 && $ν !== \floor($ν)) {
156✔
2412
            $Kᵥ = self::besselKv($|ν|, $|x|);
20✔
2413
            $∑  = $∑ + (2.0 / \M_PI) * \sin($|ν| * \M_PI) * $Kᵥ;
20✔
2414
        }
2415

2416
        return $x < 0.0 && ((int)$ν % 2 === 1) ? -$∑ : $∑;
156✔
2417
    }
2418

2419
    /**
2420
     * Modified Bessel function of the second kind, order 0 - K₀(x)
2421
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2422
     *
2423
     *         π   I₋₀(x) - I₀(x)
2424
     * K₀(x) = ─ ──────────────────
2425
     *         2     sin(0π)
2426
     *
2427
     * Alternative representation:
2428
     *   K₀(x) = -[ln(x/2) + γ] I₀(x) + [series]
2429
     *
2430
     * where γ is Euler-Mascheroni constant.
2431
     *
2432
     * Properties:
2433
     *   K₀(0⁺) = ∞
2434
     *   Kᵥ(x) → 0 as x → ∞
2435
     *   K₀(x) = K₋₀(x)
2436
     *
2437
     * Uses polynomial approximations for numerical stability
2438
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2439
     *
2440
     * @param float $x (x > 0)
2441
     *
2442
     * @return float
2443
     *
2444
     * @throws Exception\OutOfBoundsException if x ≤ 0
2445
     */
2446
    public static function besselK0(float $x): float
2447
    {
2448
        if ($x <= 0.0) {
211✔
2449
            throw new Exception\OutOfBoundsException("K₀(x) requires x > 0");
2✔
2450
        }
2451

2452
        if ($x <= 2.0) {
209✔
2453
            $y = $x * $x / 4.0;
56✔
2454
            $result = (-\log($x / 2.0) * self::besselI0($x)) + (-0.57721566 + $y * (0.42278420
56✔
2455
                + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2
56✔
2456
                + $y * (0.10750e-3 + $y * 0.74e-5))))));
56✔
2457
        } else {
2458
            $y = 2.0 / $x;
153✔
2459
            $result = (\exp(-$x) / \sqrt($x)) * (1.25331414 + $y * (-0.7832358e-1
153✔
2460
                + $y * (0.2189568e-1 + $y * (-0.1062446e-1 + $y * (0.587872e-2
153✔
2461
                + $y * (-0.251540e-2 + $y * 0.53208e-3))))));
153✔
2462
        }
2463

2464
        return $result;
209✔
2465
    }
2466

2467
    /**
2468
     * Modified Bessel function of the second kind, order 1 - K₁(x)
2469
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2470
     *
2471
     *         1   x  /   x       1 \
2472
     * K₁(x) = ─ + ─ | ln ─ + γ − ─  | + O(x³)
2473
     *         x   2  \   2       2 /
2474
     *
2475
     * Recurrence relation:
2476
     *   Kᵥ₊₁(x) = Kᵥ₋₁(x) + (2ν/x)Kᵥ(x)
2477
     *
2478
     * Properties:
2479
     *   K₁(0⁺) = ∞
2480
     *   K₋₁(x) = K₁(x)
2481
     *
2482
     * Uses polynomial approximations for numerical stability
2483
     * Reference: Abramowitz and Stegun, Handbook of Mathematical Functions
2484
     *
2485
     * @param float $x (x > 0)
2486
     *
2487
     * @return float
2488
     *
2489
     * @throws Exception\OutOfBoundsException if x ≤ 0
2490
     */
2491
    public static function besselK1(float $x): float
2492
    {
2493
        if ($x <= 0.0) {
208✔
2494
            throw new Exception\OutOfBoundsException("K₁(x) requires x > 0");
2✔
2495
        }
2496

2497
        if ($x <= 2.0) {
206✔
2498
            $y = $x * $x / 4.0;
56✔
2499
            $result = (\log($x / 2.0) * self::besselI1($x)) + (1.0 / $x) * (1.0 + $y * (0.15443144
56✔
2500
                + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1
56✔
2501
                + $y * (-0.110404e-2 + $y * (-0.4686e-4)))))));
56✔
2502
        } else {
2503
            $y = 2.0 / $x;
150✔
2504
            $result = (\exp(-$x) / \sqrt($x)) * (1.25331414 + $y * (0.23498619
150✔
2505
                + $y * (-0.3655620e-1 + $y * (0.1504268e-1 + $y * (-0.780353e-2
150✔
2506
                + $y * (0.325614e-2 + $y * (-0.68245e-3)))))));
150✔
2507
        }
2508

2509
        return $result;
206✔
2510
    }
2511

2512
    /**
2513
     * Modified Bessel function of the second kind, order v - Kᵥ(x)
2514
     * https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions
2515
     *
2516
     *         π  I₋ᵥ(x) - Iᵥ(x)
2517
     * Kᵥ(x) = ─ ──────────────────
2518
     *         2     sin(νπ)
2519
     *
2520
     * Recurrence relation:
2521
     *   Kᵥ₊₁(x) = Kᵥ₋₁(x) + (2ν/x)Kᵥ(x)
2522
     *
2523
     * Asymptotic expansion for large x:
2524
     *                ___
2525
     *              √ π   -x
2526
     *   Kᵥ(x) ≈  ───── e   [1 + O(1/x)]
2527
     *              √2x
2528
     *
2529
     * Properties:
2530
     *   Kᵥ(x) = K₋ᵥ(x)
2531
     *   Kᵥ(x) > 0 for x > 0
2532
     *
2533
     * @param float $ν order (real)
2534
     * @param float $x argument (x > 0)
2535
     *
2536
     * @return float
2537
     *
2538
     * @throws Exception\OutOfBoundsException if v < 0 or x ≤ 0
2539
     */
2540
    public static function besselKv(float $ν, float $x): float
2541
    {
2542
        if ($ν < 0) {
283✔
2543
            throw new Exception\OutOfBoundsException("Order ν must be non-negative");
1✔
2544
        }
2545
        if ($x <= 0.0) {
282✔
2546
            throw new Exception\OutOfBoundsException("Kᵥ(x) requires x > 0");
2✔
2547
        }
2548

2549
        if ($ν === 0.0) {
280✔
2550
            return self::besselK0($x);
28✔
2551
        }
2552
        if ($ν === 1.0) {
260✔
2553
            return self::besselK1($x);
28✔
2554
        }
2555

2556
        // For integer orders, use recurrence
2557
        if ($ν === \floor($ν)) {
242✔
2558
            $n    = (int) $ν;
131✔
2559
            $Kₙ₋₁ = self::besselK0($x);
131✔
2560
            $Kₙ   = self::besselK1($x);
131✔
2561
            for ($j = 1; $j < $n; $j++) {
131✔
2562
                $Kₙ₊₁ = $Kₙ₋₁ + ($j * 2.0 / $x) * $Kₙ;
131✔
2563
                $Kₙ₋₁ = $Kₙ;
131✔
2564
                $Kₙ = $Kₙ₊₁;
131✔
2565
            }
2566
            return $Kₙ;
131✔
2567
        }
2568

2569
        // For non-integer orders
2570
        // For large x (x > 10), use asymptotic expansion which converges better
2571
        // For small x, use series expansion
2572

2573
        $π = \M_PI;
111✔
2574
        $sin⟮νπ⟯ = \sin($ν * $π);
111✔
2575

2576
        if (\abs($sin⟮νπ⟯) < 1e-10) {
111✔
2577
            // Near integer, use recurrence from nearby values
2578
            $ν_floor = \floor($ν);
×
2579
            return self::besselKv($ν_floor, $x);
×
2580
        }
2581

2582
        // For large x, use asymptotic expansion: Kᵥ(x) ~ sqrt(π/(2x)) * e⁻ˣ * [1 + ...]
2583
        if ($x > 10.0) {
111✔
2584
            // Asymptotic expansion: Kᵥ(x) ≈ sqrt(π/(2x)) * e⁻ˣ * Σ aₖ / xᵏ
2585
            // where aₖ involves coefficients depending on ν
2586
            $coefficient = \sqrt($π / (2.0 * $x)) * \exp(-$x);
36✔
2587

2588
            // Leading term expansion
2589
            $μ    = 4.0 * $ν * $ν;
36✔
2590
            $∑    = 1.0;
36✔
2591
            $term = 1.0;
36✔
2592

2593
            // Add correction terms
2594
            $term *= ($μ - 1.0) / (8.0 * $x);
36✔
2595
            $∑    += $term;
36✔
2596

2597
            $term *= ($μ - 9.0) / (2.0 * 8.0 * $x);
36✔
2598
            $∑    += $term;
36✔
2599

2600
            $term *= ($μ - 25.0) / (3.0 * 8.0 * $x);
36✔
2601
            $∑    += $term;
36✔
2602

2603
            return $coefficient * $∑;
36✔
2604
        }
2605

2606
        // For small x, use series expansion
2607
        // Kᵥ(x) = (π/2) * (I₋ᵥ(x) - Iᵥ(x)) / sin(νπ)
2608

2609
        // Compute Iᵥ directly
2610
        $∑_ν    = 0.0;
75✔
2611
        $term_ν = \pow($x / 2.0, $ν) / self::Γ($ν + 1);
75✔
2612
        $∑_ν   += $term_ν;
75✔
2613
        for ($k = 1; $k < 200; $k++) {
75✔
2614
            $term_ν *= ($x * $x / 4.0) / ($k * ($ν + $k));
75✔
2615
            $∑_ν    += $term_ν;
75✔
2616
            if (\abs($term_ν) < \abs($∑_ν) * 1e-15) {
75✔
2617
                break;
75✔
2618
            }
2619
        }
2620

2621
        // Compute I₋ᵥ
2622
        $∑_neg_ν    = 0.0;
75✔
2623
        $term_neg_ν = \pow($x / 2.0, -$ν) / self::Γ(1.0 - $ν);
75✔
2624
        $∑_neg_ν   += $term_neg_ν;
75✔
2625
        for ($k = 1; $k < 200; $k++) {
75✔
2626
            $term_neg_ν *= ($x * $x / 4.0) / ($k * ($k - $ν));
75✔
2627
            $∑_neg_ν    += $term_neg_ν;
75✔
2628
            if (\abs($term_neg_ν) < \abs($∑_neg_ν) * 1e-15) {
75✔
2629
                break;
75✔
2630
            }
2631
        }
2632

2633
        return ($π / 2.0) * ($∑_neg_ν - $∑_ν) / $sin⟮νπ⟯;
75✔
2634
    }
2635

2636
    /**************************************************************************
2637
     * ORTHOGONAL POLYNOMIALS
2638
     *************************************************************************/
2639

2640
    /**
2641
     * Legendre polynomial Pₙ(x)
2642
     * https://en.wikipedia.org/wiki/Legendre_polynomials
2643
     *
2644
     * Rodrigues' formula:
2645
     *              1    dⁿ
2646
     *   Pₙ(x) = ────── ───── [(x² - 1)ⁿ]
2647
     *            2ⁿ n!  dxⁿ
2648
     *
2649
     * Recurrence relation (used in implementation):
2650
     *   (n+1)Pₙ₊₁(x) = (2n+1)xPₙ(x) - nPₙ₋₁(x)
2651
     *
2652
     * Initial values:
2653
     *   P₀(x) = 1
2654
     *   P₁(x) = x
2655
     *
2656
     * Properties:
2657
     *   Pₙ(1) = 1
2658
     *   Pₙ(-1) = (-1)ⁿ
2659
     *   Pₙ(-x) = (-1)ⁿPₙ(x)
2660
     *
2661
     * @param int   $n order (n ≥ 0)
2662
     * @param float $x (-1 ≤ x ≤ 1 for orthogonality)
2663
     *
2664
     * @return float
2665
     *
2666
     * @throws Exception\OutOfBoundsException if n < 0
2667
     */
2668
    public static function legendreP(int $n, float $x): float
2669
    {
2670
        if ($n < 0) {
108✔
2671
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2672
        }
2673

2674
        if ($n === 0) {
107✔
2675
            return 1.0;
13✔
2676
        }
2677
        if ($n === 1) {
98✔
2678
            return $x;
17✔
2679
        }
2680

2681
        // Three-term recurrence relation
2682
        $Pₙ₋₁ = 1.0;  // P₀
89✔
2683
        $Pₙ   = $x;     // P₁
89✔
2684

2685
        for ($k = 1; $k < $n; $k++) {
89✔
2686
            $Pₙ₊₁ = ((2 * $k + 1) * $x * $Pₙ - $k * $Pₙ₋₁) / ($k + 1);
89✔
2687
            $Pₙ₋₁ = $Pₙ;
89✔
2688
            $Pₙ   = $Pₙ₊₁;
89✔
2689
        }
2690

2691
        return $Pₙ;
89✔
2692
    }
2693

2694
    /**
2695
     * Chebyshev polynomial of the first kind Tₙ(x)
2696
     * https://en.wikipedia.org/wiki/Chebyshev_polynomials
2697
     *
2698
     * Trigonometric representation:
2699
     *   Tₙ(x) = cos(n arccos(x))
2700
     *   Tₙ(cos θ) = cos(nθ)
2701
     *
2702
     * Explicit formula:
2703
     *            n
2704
     *   Tₙ(x) = ─── cos[n · arccos(x)]
2705
     *            1
2706
     *
2707
     * Recurrence relation (used in implementation):
2708
     *   Tₙ₊₁(x) = 2xTₙ(x) - Tₙ₋₁(x)
2709
     *
2710
     * Initial values:
2711
     *   T₀(x) = 1
2712
     *   T₁(x) = x
2713
     *
2714
     * @param int $n order (n ≥ 0)
2715
     * @param float $x
2716
     *
2717
     * @return float
2718
     *
2719
     * @throws Exception\OutOfBoundsException if n < 0
2720
     */
2721
    public static function chebyshevT(int $n, float $x): float
2722
    {
2723
        if ($n < 0) {
89✔
2724
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2725
        }
2726

2727
        if ($n === 0) {
88✔
2728
            return 1.0;
11✔
2729
        }
2730
        if ($n === 1) {
81✔
2731
            return $x;
15✔
2732
        }
2733

2734
        // Three-term recurrence relation
2735
        $Tₙ₋₁ = 1.0;  // T₀
74✔
2736
        $Tₙ   = $x;   // T₁
74✔
2737

2738
        for ($k = 1; $k < $n; $k++) {
74✔
2739
            $Tₙ₊₁ = 2.0 * $x * $Tₙ - $Tₙ₋₁;
74✔
2740
            $Tₙ₋₁ = $Tₙ;
74✔
2741
            $Tₙ   = $Tₙ₊₁;
74✔
2742
        }
2743

2744
        return $Tₙ;
74✔
2745
    }
2746

2747
    /**
2748
     * Chebyshev polynomial of the second kind Uₙ(x)
2749
     * https://en.wikipedia.org/wiki/Chebyshev_polynomials
2750
     *
2751
     *          sin((n + 1) arccos(x))
2752
     *  Uₙ(x) = ──────────────────────
2753
     *               ____________
2754
     *              √  1 - x²
2755
     *
2756
     * Uses three-term recurrence relation:
2757
     * Uₙ₊₁(x) = 2xUₙ(x) - Uₙ₋₁(x)
2758
     *
2759
     * @param int $n order (n ≥ 0)
2760
     * @param float $x
2761
     *
2762
     * @return float
2763
     *
2764
     * @throws Exception\OutOfBoundsException if n < 0
2765
     */
2766
    public static function chebyshevU(int $n, float $x): float
2767
    {
2768
        if ($n < 0) {
45✔
2769
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2770
        }
2771

2772
        if ($n === 0) {
44✔
2773
            return 1.0;
8✔
2774
        }
2775
        if ($n === 1) {
39✔
2776
            return 2.0 * $x;
11✔
2777
        }
2778

2779
        // Three-term recurrence relation
2780
        $Uₙ₋₁ = 1.0;        // U₀
34✔
2781
        $Uₙ   = 2.0 * $x;   // U₁
34✔
2782

2783
        for ($k = 1; $k < $n; $k++) {
34✔
2784
            $Uₙ₊₁ = 2.0 * $x * $Uₙ - $Uₙ₋₁;
34✔
2785
            $Uₙ₋₁ = $Uₙ;
34✔
2786
            $Uₙ   = $Uₙ₊₁;
34✔
2787
        }
2788

2789
        return $Uₙ;
34✔
2790
    }
2791

2792
    /**
2793
     * Hermite polynomial (physicist's version) Hₙ(x)
2794
     * https://en.wikipedia.org/wiki/Hermite_polynomials
2795
     *
2796
     * Rodrigues' formula:
2797
     *                  x²    dⁿ   -x²
2798
     *   Hₙ(x) = (-1)ⁿ e    ───── [e   ]
2799
     *                       dxⁿ
2800
     *
2801
     * Recurrence relation (used in implementation):
2802
     *   Hₙ₊₁(x) = 2xHₙ(x) - 2nHₙ₋₁(x)
2803
     *
2804
     * Initial values:
2805
     *   H₀(x) = 1
2806
     *   H₁(x) = 2x
2807
     *
2808
     * @param int $n order (n ≥ 0)
2809
     * @param float $x
2810
     *
2811
     * @return float
2812
     *
2813
     * @throws Exception\OutOfBoundsException if n < 0
2814
     */
2815
    public static function hermiteH(int $n, float $x): float
2816
    {
2817
        if ($n < 0) {
84✔
2818
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2819
        }
2820

2821
        if ($n === 0) {
83✔
2822
            return 1.0;
11✔
2823
        }
2824
        if ($n === 1) {
76✔
2825
            return 2.0 * $x;
15✔
2826
        }
2827

2828
        // Three-term recurrence relation
2829
        $Hₙ₋₁ = 1.0;        // H₀
69✔
2830
        $Hₙ   = 2.0 * $x;   // H₁
69✔
2831

2832
        for ($k = 1; $k < $n; $k++) {
69✔
2833
            $Hₙ₊₁ = 2.0 * $x * $Hₙ - 2.0 * $k * $Hₙ₋₁;
69✔
2834
            $Hₙ₋₁ = $Hₙ;
69✔
2835
            $Hₙ   = $Hₙ₊₁;
69✔
2836
        }
2837

2838
        return $Hₙ;
69✔
2839
    }
2840

2841
    /**
2842
     * Hermite polynomial (probabilist's version) Heₙ(x)
2843
     * https://en.wikipedia.org/wiki/Hermite_polynomials
2844
     *
2845
     *                  x²/2    dⁿ    -x²/2
2846
     *   Heₙ(x) = (-1)ⁿ e      ───── [e     ]
2847
     *                          dxⁿ
2848
     *
2849
     * Uses three-term recurrence relation:
2850
     * Heₙ₊₁(x) = xHeₙ(x) - nHeₙ₋₁(x)
2851
     *
2852
     * @param int   $n order (n ≥ 0)
2853
     * @param float $x
2854
     *
2855
     * @return float
2856
     *
2857
     * @throws Exception\OutOfBoundsException if n < 0
2858
     */
2859
    public static function hermiteHe(int $n, float $x): float
2860
    {
2861
        if ($n < 0) {
57✔
2862
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2863
        }
2864

2865
        if ($n === 0) {
56✔
2866
            return 1.0;
8✔
2867
        }
2868
        if ($n === 1) {
48✔
2869
            return $x;
8✔
2870
        }
2871

2872
        // Three-term recurrence relation
2873
        $Heₙ₋₁ = 1.0;  // He₀
40✔
2874
        $Heₙ   = $x;     // He₁
40✔
2875

2876
        for ($k = 1; $k < $n; $k++) {
40✔
2877
            $Heₙ₊₁ = $x * $Heₙ - $k * $Heₙ₋₁;
40✔
2878
            $Heₙ₋₁ = $Heₙ;
40✔
2879
            $Heₙ   = $Heₙ₊₁;
40✔
2880
        }
2881

2882
        return $Heₙ;
40✔
2883
    }
2884

2885
    /**
2886
     * Laguerre polynomial Lₙ(x)
2887
     * https://en.wikipedia.org/wiki/Laguerre_polynomials
2888
     *
2889
     * Rodrigues' formula:
2890
     *            eˣ   dⁿ
2891
     *   Lₙ(x) = ──── ───── [xⁿ e⁻ˣ]
2892
     *            n!   dxⁿ
2893
     *
2894
     * Recurrence relation (used in implementation):
2895
     *   (n+1)Lₙ₊₁(x) = (2n+1-x)Lₙ(x) - nLₙ₋₁(x)
2896
     *
2897
     * Initial values:
2898
     *   L₀(x) = 1
2899
     *   L₁(x) = 1 - x
2900
     *
2901
     * @param int   $n order (n ≥ 0)
2902
     * @param float $x
2903
     *
2904
     * @return float
2905
     *
2906
     * @throws Exception\OutOfBoundsException if n < 0
2907
     */
2908
    public static function laguerreL(int $n, float $x): float
2909
    {
2910
        if ($n < 0) {
50✔
2911
            throw new Exception\OutOfBoundsException("Order n must be non-negative");
1✔
2912
        }
2913

2914
        if ($n === 0) {
49✔
2915
            return 1.0;
9✔
2916
        }
2917
        if ($n === 1) {
44✔
2918
            return 1.0 - $x;
13✔
2919
        }
2920

2921
        // Three-term recurrence relation
2922
        $Lₙ₋₁ = 1.0;        // L₀
39✔
2923
        $Lₙ   = 1.0 - $x;   // L₁
39✔
2924

2925
        for ($k = 1; $k < $n; $k++) {
39✔
2926
            $Lₙ₊₁ = ((2.0 * $k + 1.0 - $x) * $Lₙ - $k * $Lₙ₋₁) / ($k + 1.0);
39✔
2927
            $Lₙ₋₁ = $Lₙ;
39✔
2928
            $Lₙ   = $Lₙ₊₁;
39✔
2929
        }
2930

2931
        return $Lₙ;
39✔
2932
    }
2933

2934
    /**************************************************************************
2935
     * AIRY FUNCTIONS
2936
     *************************************************************************/
2937

2938
    /**
2939
     * Airy function Ai(x)
2940
     * https://en.wikipedia.org/wiki/Airy_function
2941
     *
2942
     * DLMF 9.6: Airy functions in terms of Bessel functions
2943
     * Ai(x) and Bi(x) can be expressed in terms of modified Bessel functions:
2944
     * For x > 0: Ai(x) = (1/π)√(x/3) K₁⸝₃(ζ) where ζ = (2/3)x³ᐟ²
2945
     * For x < 0: Ai(-z) = (√z/3) [J₁⸝₃(ζ) + J₋₁⸝₃(ζ)]  // Use trigonometric forms with Bessel functions
2946
     *
2947
     * Airy functions Ai(x) and Bi(x) are solutions to: y'' - xy = 0
2948
     *
2949
     * @param float $x
2950
     *
2951
     * @return float
2952
     */
2953
    public static function airyAi(float $x): float
2954
    {
2955
        if ($x === 0.0) {
55✔
2956
            return 0.35502805388782;  // Ai(0) = 1/(3^(2/3) * Γ(2/3))
1✔
2957
        } elseif ($x > 0) {
54✔
2958
            $ζ    = (2.0 / 3.0) * $x * \sqrt($x);
24✔
2959
            $K₁⸝₃ = self::besselKv(1.0 / 3.0, $ζ);
24✔
2960
            return \sqrt($x / 3.0) * $K₁⸝₃ / \M_PI;
24✔
2961
        } else {
2962
            $|x| = \abs($x);
30✔
2963
            $ζ    = (2.0 / 3.0) * $|x| * \sqrt($|x|);
30✔
2964
            $J₁⸝₃  = self::besselJv(1.0 / 3.0, $ζ);
30✔
2965
            $J₋₁⸝₃ = self::besselJv(-1.0 / 3.0, $ζ);
30✔
2966
            return \sqrt($|x|) / 3.0 * ($J₁⸝₃ + $J₋₁⸝₃);
30✔
2967
        }
2968
    }
2969

2970
    /**
2971
     * Airy function Bi(x)
2972
     * https://en.wikipedia.org/wiki/Airy_function
2973
     *
2974
     * Uses series expansions for small |x| and asymptotic expansions for large |x|
2975
     *
2976
     * DLMF 9.6: Bi(x) = √(x/3) [I₋₁⸝₃(ζ) + I₁⸝₃(ζ)] where ζ = (2/3)x³ᐟ²
2977
     * DLMF 9.6.4: Bi(-z) = √(z/3) [J₋₁⸝₃(ζ) - J₁⸝₃(ζ)]
2978
     *
2979
     * @param float $x
2980
     *
2981
     * @return float
2982
     */
2983
    public static function airyBi(float $x): float
2984
    {
2985
        if ($x === 0.0) {
55✔
2986
            return 0.61492662744600;  // Bi(0) = 1/(3^(1/6) * Γ(2/3))
1✔
2987
        } elseif ($x > 0) {
54✔
2988
            $ζ    = (2.0 / 3.0) * $x * \sqrt($x);
24✔
2989
            $I₁⸝₃  = self::besselIv(1.0 / 3.0, $ζ);
24✔
2990
            $I₋₁⸝₃ = self::besselIv(-1.0 / 3.0, $ζ);
24✔
2991
            return \sqrt($x / 3.0) * ($I₋₁⸝₃ + $I₁⸝₃);
24✔
2992
        } else {
2993
            $|x| = \abs($x);
30✔
2994
            $ζ    = (2.0 / 3.0) * $|x| * \sqrt($|x|);
30✔
2995
            $J₁⸝₃  = self::besselJv(1.0 / 3.0, $ζ);
30✔
2996
            $J₋₁⸝₃ = self::besselJv(-1.0 / 3.0, $ζ);
30✔
2997
            return \sqrt($|x| / 3.0) * ($J₋₁⸝₃ - $J₁⸝₃);
30✔
2998
        }
2999
    }
3000

3001
    /**
3002
     * Airy function derivative Ai'(x)
3003
     * https://en.wikipedia.org/wiki/Airy_function
3004
     *
3005
     * DLMF 9.6: Ai'(x) = -(x/√3) K₂⸝₃(ζ) / π where ζ = (2/3)x³ᐟ²
3006
     * DLMF 9.6.7: Ai'(-z) = (z/3) [J₂⸝₃(ζ) - J₋₂⸝₃(ζ)]
3007
     *
3008
     * Uses series expansions for small |x| and asymptotic expansions for large |x|
3009
     *
3010
     * @param float $x
3011
     *
3012
     * @return float
3013
     */
3014
    public static function airyAip(float $x): float
3015
    {
3016
        if ($x === 0.0) {
55✔
3017
            return -0.25881940379281;  // Ai'(0) = -1/(3^(1/3) * Γ(1/3))
1✔
3018
        } elseif ($x > 0) {
54✔
3019
            $ζ   = (2.0 / 3.0) * $x * \sqrt($x);
24✔
3020
            $K₂⸝₃ = self::besselKv(2.0 / 3.0, $ζ);
24✔
3021
            return -$x / (\sqrt(3.0) * \M_PI) * $K₂⸝₃;
24✔
3022
        } else {
3023
            $|x| = \abs($x);
30✔
3024
            $ζ    = (2.0 / 3.0) * $|x| * \sqrt($|x|);
30✔
3025
            $J₂⸝₃  = self::besselJv(2.0 / 3.0, $ζ);
30✔
3026
            $J₋₂⸝₃ = self::besselJv(-2.0 / 3.0, $ζ);
30✔
3027
            return ($|x| / 3.0) * ($J₂⸝₃ - $J₋₂⸝₃);
30✔
3028
        }
3029
    }
3030

3031
    /**
3032
     * Airy function derivative Bi'(x)
3033
     * https://en.wikipedia.org/wiki/Airy_function
3034
     *
3035
     * DLMF 9.6: Bi'(x) = (x/√3) [I₋₂⸝₃(ζ) + I₂⸝₃(ζ)] where ζ = (2/3)x³ᐟ²
3036
     * DLMF 9.6.8: Bi'(-z) = (z/√3) [J₋₂⸝₃(ζ) + J₂⸝₃(ζ)]
3037
     *
3038
     * Uses series expansions for small |x| and asymptotic expansions for large |x|
3039
     *
3040
     * @param float $x
3041
     *
3042
     * @return float
3043
     */
3044
    public static function airyBip(float $x): float
3045
    {
3046
        if ($x === 0.0) {
55✔
3047
            return 0.44828835735383;  // Bi'(0) = 3^(1/6) / Γ(1/3)
1✔
3048
        } elseif ($x > 0) {
54✔
3049
            $ζ    = (2.0 / 3.0) * $x * \sqrt($x);
24✔
3050
            $I₂⸝₃  = self::besselIv(2.0 / 3.0, $ζ);
24✔
3051
            $I₋₂⸝₃ = self::besselIv(-2.0 / 3.0, $ζ);
24✔
3052
            return $x / \sqrt(3.0) * ($I₋₂⸝₃ + $I₂⸝₃);
24✔
3053
        } else {
3054
            $|x| = \abs($x);
30✔
3055
            $ζ    = (2.0 / 3.0) * $|x| * \sqrt($|x|);
30✔
3056
            $J₂⸝₃  = self::besselJv(2.0 / 3.0, $ζ);
30✔
3057
            $J₋₂⸝₃ = self::besselJv(-2.0 / 3.0, $ζ);
30✔
3058
            return $|x| / \sqrt(3.0) * ($J₋₂⸝₃ + $J₂⸝₃);
30✔
3059
        }
3060
    }
3061
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc