Passed
Pull Request — master (#3302)
by Mark
12:42
created

GammaBase::logGamma4()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 2
nop 1
dl 0
loc 19
ccs 12
cts 12
cp 1
crap 3
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7
8
abstract class GammaBase
9
{
10
    private const LOG_GAMMA_X_MAX_VALUE = 2.55e305;
11
12
    private const EPS = 2.22e-16;
13
14
    private const MAX_VALUE = 1.2e308;
15
16
    private const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
17
18
    private const MAX_ITERATIONS = 256;
19
20
    /** @return float|string */
21 10
    protected static function calculateDistribution(float $value, float $a, float $b, bool $cumulative)
22
    {
23 10
        if ($cumulative) {
24 7
            return self::incompleteGamma($a, $value / $b) / self::gammaValue($a);
25
        }
26
27 7
        return (1 / ($b ** $a * self::gammaValue($a))) * $value ** ($a - 1) * exp(0 - ($value / $b));
28
    }
29
30
    /** @return float|string */
31 4
    protected static function calculateInverse(float $probability, float $alpha, float $beta)
32
    {
33 4
        $xLo = 0;
34 4
        $xHi = $alpha * $beta * 5;
35
36 4
        $dx = 1024;
37 4
        $x = $xNew = 1;
38 4
        $i = 0;
39
40 4
        while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
41
            // Apply Newton-Raphson step
42 4
            $result = self::calculateDistribution($x, $alpha, $beta, true);
43 4
            if (!is_float($result)) {
44
                return ExcelError::NA();
45
            }
46 4
            $error = $result - $probability;
47
48 4
            if ($error == 0.0) {
49 2
                $dx = 0;
50 4
            } elseif ($error < 0.0) {
51 4
                $xLo = $x;
52
            } else {
53 4
                $xHi = $x;
54
            }
55
56 4
            $pdf = self::calculateDistribution($x, $alpha, $beta, false);
57
            // Avoid division by zero
58 4
            if (!is_float($pdf)) {
59
                return ExcelError::NA();
60
            }
61 4
            if ($pdf !== 0.0) {
62 4
                $dx = $error / $pdf;
63 4
                $xNew = $x - $dx;
64
            }
65
66
            // If the NR fails to converge (which for example may be the
67
            // case if the initial guess is too rough) we apply a bisection
68
            // step to determine a more narrow interval around the root.
69 4
            if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) {
70 4
                $xNew = ($xLo + $xHi) / 2;
71 4
                $dx = $xNew - $x;
72
            }
73 4
            $x = $xNew;
74
        }
75
76 4
        if ($i === self::MAX_ITERATIONS) {
77
            return ExcelError::NA();
78
        }
79
80 4
        return $x;
81
    }
82
83
    //
84
    //    Implementation of the incomplete Gamma function
85
    //
86 36
    public static function incompleteGamma(float $a, float $x): float
87
    {
88 36
        static $max = 32;
89 36
        $summer = 0;
90 36
        for ($n = 0; $n <= $max; ++$n) {
91 36
            $divisor = $a;
92 36
            for ($i = 1; $i <= $n; ++$i) {
93 36
                $divisor *= ($a + $i);
94
            }
95 36
            $summer += ($x ** $n / $divisor);
96
        }
97
98 36
        return $x ** $a * exp(0 - $x) * $summer;
99
    }
100
101
    //
102
    //    Implementation of the Gamma function
103
    //
104 77
    public static function gammaValue(float $value): float
105
    {
106 77
        if ($value == 0.0) {
107
            return 0;
108
        }
109
110 77
        static $p0 = 1.000000000190015;
111 77
        static $p = [
112 77
            1 => 76.18009172947146,
113 77
            2 => -86.50532032941677,
114 77
            3 => 24.01409824083091,
115 77
            4 => -1.231739572450155,
116 77
            5 => 1.208650973866179e-3,
117 77
            6 => -5.395239384953e-6,
118 77
        ];
119
120 77
        $y = $x = $value;
121 77
        $tmp = $x + 5.5;
122 77
        $tmp -= ($x + 0.5) * log($tmp);
123
124 77
        $summer = $p0;
125 77
        for ($j = 1; $j <= 6; ++$j) {
126 77
            $summer += ($p[$j] / ++$y);
127
        }
128
129 77
        return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x));
130
    }
131
132
    private const  LG_D1 = -0.5772156649015328605195174;
133
134
    private const LG_D2 = 0.4227843350984671393993777;
135
136
    private const LG_D4 = 1.791759469228055000094023;
137
138
    private const LG_P1 = [
139
        4.945235359296727046734888,
140
        201.8112620856775083915565,
141
        2290.838373831346393026739,
142
        11319.67205903380828685045,
143
        28557.24635671635335736389,
144
        38484.96228443793359990269,
145
        26377.48787624195437963534,
146
        7225.813979700288197698961,
147
    ];
148
149
    private const LG_P2 = [
150
        4.974607845568932035012064,
151
        542.4138599891070494101986,
152
        15506.93864978364947665077,
153
        184793.2904445632425417223,
154
        1088204.76946882876749847,
155
        3338152.967987029735917223,
156
        5106661.678927352456275255,
157
        3074109.054850539556250927,
158
    ];
159
160
    private const LG_P4 = [
161
        14745.02166059939948905062,
162
        2426813.369486704502836312,
163
        121475557.4045093227939592,
164
        2663432449.630976949898078,
165
        29403789566.34553899906876,
166
        170266573776.5398868392998,
167
        492612579337.743088758812,
168
        560625185622.3951465078242,
169
    ];
170
171
    private const LG_Q1 = [
172
        67.48212550303777196073036,
173
        1113.332393857199323513008,
174
        7738.757056935398733233834,
175
        27639.87074403340708898585,
176
        54993.10206226157329794414,
177
        61611.22180066002127833352,
178
        36351.27591501940507276287,
179
        8785.536302431013170870835,
180
    ];
181
182
    private const LG_Q2 = [
183
        183.0328399370592604055942,
184
        7765.049321445005871323047,
185
        133190.3827966074194402448,
186
        1136705.821321969608938755,
187
        5267964.117437946917577538,
188
        13467014.54311101692290052,
189
        17827365.30353274213975932,
190
        9533095.591844353613395747,
191
    ];
192
193
    private const LG_Q4 = [
194
        2690.530175870899333379843,
195
        639388.5654300092398984238,
196
        41355999.30241388052042842,
197
        1120872109.61614794137657,
198
        14886137286.78813811542398,
199
        101680358627.2438228077304,
200
        341747634550.7377132798597,
201
        446315818741.9713286462081,
202
    ];
203
204
    private const LG_C = [
205
        -0.001910444077728,
206
        8.4171387781295e-4,
207
        -5.952379913043012e-4,
208
        7.93650793500350248e-4,
209
        -0.002777777777777681622553,
210
        0.08333333333333333331554247,
211
        0.0057083835261,
212
    ];
213
214
    // Rough estimate of the fourth root of logGamma_xBig
215
    private const LG_FRTBIG = 2.25e76;
216
217
    private const PNT68 = 0.6796875;
218
219
    // Function cache for logGamma
220
    /** @var float */
221
    private static $logGammaCacheResult = 0.0;
222
223
    /** @var float */
224
    private static $logGammaCacheX = 0.0;
225
226
    /**
227
     * logGamma function.
228
     *
229
     * Original author was Jaco van Kooten. Ported to PHP by Paul Meagher.
230
     *
231
     * The natural logarithm of the gamma function. <br />
232
     * Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz <br />
233
     * Applied Mathematics Division <br />
234
     * Argonne National Laboratory <br />
235
     * Argonne, IL 60439 <br />
236
     * <p>
237
     * References:
238
     * <ol>
239
     * <li>W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural
240
     *     Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.</li>
241
     * <li>K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.</li>
242
     * <li>Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.</li>
243
     * </ol>
244
     * </p>
245
     * <p>
246
     * From the original documentation:
247
     * </p>
248
     * <p>
249
     * This routine calculates the LOG(GAMMA) function for a positive real argument X.
250
     * Computation is based on an algorithm outlined in references 1 and 2.
251
     * The program uses rational functions that theoretically approximate LOG(GAMMA)
252
     * to at least 18 significant decimal digits. The approximation for X > 12 is from
253
     * reference 3, while approximations for X < 12.0 are similar to those in reference
254
     * 1, but are unpublished. The accuracy achieved depends on the arithmetic system,
255
     * the compiler, the intrinsic functions, and proper selection of the
256
     * machine-dependent constants.
257
     * </p>
258
     * <p>
259
     * Error returns: <br />
260
     * The program returns the value XINF for X .LE. 0.0 or when overflow would occur.
261
     * The computation is believed to be free of underflow and overflow.
262
     * </p>
263
     *
264
     * @version 1.1
265
     *
266
     * @author Jaco van Kooten
267
     *
268
     * @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305
269
     */
270 20
    public static function logGamma(float $x): float
271
    {
272 20
        if ($x == self::$logGammaCacheX) {
273 1
            return self::$logGammaCacheResult;
274
        }
275
276 20
        $y = $x;
277 20
        if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) {
278 20
            if ($y <= self::EPS) {
279
                $res = -log($y);
280 20
            } elseif ($y <= 1.5) {
281 3
                $res = self::logGamma1($y);
282 18
            } elseif ($y <= 4.0) {
283 9
                $res = self::logGamma2($y);
284 17
            } elseif ($y <= 12.0) {
285 17
                $res = self::logGamma3($y);
286
            } else {
287 20
                $res = self::logGamma4($y);
288
            }
289
        } else {
290
            // --------------------------
291
            //    Return for bad arguments
292
            // --------------------------
293
            $res = self::MAX_VALUE;
294
        }
295
296
        // ------------------------------
297
        //    Final adjustments and return
298
        // ------------------------------
299 20
        self::$logGammaCacheX = $x;
300 20
        self::$logGammaCacheResult = $res;
301
302 20
        return $res;
303
    }
304
305 3
    private static function logGamma1(float $y): float
306
    {
307
        // ---------------------
308
        //    EPS .LT. X .LE. 1.5
309
        // ---------------------
310 3
        if ($y < self::PNT68) {
311 2
            $corr = -log($y);
312 2
            $xm1 = $y;
313
        } else {
314 3
            $corr = 0.0;
315 3
            $xm1 = $y - 1.0;
316
        }
317
318 3
        $xden = 1.0;
319 3
        $xnum = 0.0;
320 3
        if ($y <= 0.5 || $y >= self::PNT68) {
321 3
            for ($i = 0; $i < 8; ++$i) {
322 3
                $xnum = $xnum * $xm1 + self::LG_P1[$i];
323 3
                $xden = $xden * $xm1 + self::LG_Q1[$i];
324
            }
325
326 3
            return $corr + $xm1 * (self::LG_D1 + $xm1 * ($xnum / $xden));
327
        }
328
329
        $xm2 = $y - 1.0;
330
        for ($i = 0; $i < 8; ++$i) {
331
            $xnum = $xnum * $xm2 + self::LG_P2[$i];
332
            $xden = $xden * $xm2 + self::LG_Q2[$i];
333
        }
334
335
        return $corr + $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden));
336
    }
337
338 9
    private static function logGamma2(float $y): float
339
    {
340
        // ---------------------
341
        //    1.5 .LT. X .LE. 4.0
342
        // ---------------------
343 9
        $xm2 = $y - 2.0;
344 9
        $xden = 1.0;
345 9
        $xnum = 0.0;
346 9
        for ($i = 0; $i < 8; ++$i) {
347 9
            $xnum = $xnum * $xm2 + self::LG_P2[$i];
348 9
            $xden = $xden * $xm2 + self::LG_Q2[$i];
349
        }
350
351 9
        return $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden));
352
    }
353
354 17
    protected static function logGamma3(float $y): float
355
    {
356
        // ----------------------
357
        //    4.0 .LT. X .LE. 12.0
358
        // ----------------------
359 17
        $xm4 = $y - 4.0;
360 17
        $xden = -1.0;
361 17
        $xnum = 0.0;
362 17
        for ($i = 0; $i < 8; ++$i) {
363 17
            $xnum = $xnum * $xm4 + self::LG_P4[$i];
364 17
            $xden = $xden * $xm4 + self::LG_Q4[$i];
365
        }
366
367 17
        return self::LG_D4 + $xm4 * ($xnum / $xden);
368
    }
369
370 9
    protected static function logGamma4(float $y): float
371
    {
372
        // ---------------------------------
373
        //    Evaluate for argument .GE. 12.0
374
        // ---------------------------------
375 9
        $res = 0.0;
376 9
        if ($y <= self::LG_FRTBIG) {
377 9
            $res = self::LG_C[6];
378 9
            $ysq = $y * $y;
379 9
            for ($i = 0; $i < 6; ++$i) {
380 9
                $res = $res / $ysq + self::LG_C[$i];
381
            }
382 9
            $res /= $y;
383 9
            $corr = log($y);
384 9
            $res = $res + log(self::SQRT2PI) - 0.5 * $corr;
385 9
            $res += $y * ($corr - 1.0);
386
        }
387
388 9
        return $res;
389
    }
390
}
391