Completed
Push — master ( 5d186e...d62f94 )
by Jordan
16s queued 13s
created

Poisson::rangeRandom()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
dl 0
loc 16
ccs 0
cts 0
cp 0
crap 42
rs 9.2222
c 1
b 0
f 0
eloc 11
nc 2
nop 3
1
<?php
2
3
namespace Samsara\Fermat\Stats\Values\Distribution;
4
5
use Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState;
6
use Samsara\Exceptions\UsageError\IntegrityConstraint;
7
use Samsara\Exceptions\UsageError\OptionalExit;
8
use Samsara\Fermat\Core\Numbers;
9
use Samsara\Fermat\Stats\Types\Distribution;
10
use Samsara\Fermat\Core\Provider\RandomProvider;
11
use Samsara\Fermat\Core\Types\Base\Interfaces\Numbers\DecimalInterface;
12
use Samsara\Fermat\Core\Types\Base\Interfaces\Numbers\NumberInterface;
13
use Samsara\Fermat\Core\Values\ImmutableDecimal;
14
15
class Poisson extends Distribution
16
{
17
18
    /**
19
     * @var ImmutableDecimal
20
     */
21
    private $lambda;
22
23
    /**
24
     * Poisson constructor.
25
     *
26
     * @param int|float|DecimalInterface $lambda
27
     *
28
     * @throws IntegrityConstraint
29
     */
30 2
    public function __construct($lambda)
31
    {
32 2
        $lambda = Numbers::makeOrDont(Numbers::IMMUTABLE, $lambda);
33
34 2
        if (!$lambda->isPositive()) {
0 ignored issues
show
Bug introduced by
The method isPositive() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Type...s\SimpleNumberInterface or Samsara\Fermat\Core\Types\Fraction or Samsara\Fermat\Core\Types\Decimal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

34
        if (!$lambda->/** @scrutinizer ignore-call */ isPositive()) {
Loading history...
35
            throw new IntegrityConstraint(
36
                'Lambda must be positive',
37
                'Provide a positive lambda',
38
                'Poisson distributions work on time to occurrence; the mean time to occurrence (lambda) must be positive'
39
            );
40
        }
41
42 2
        $this->lambda = $lambda;
0 ignored issues
show
Documentation Bug introduced by
It seems like $lambda can also be of type Samsara\Fermat\Core\Type...Numbers\NumberInterface. However, the property $lambda is declared as type Samsara\Fermat\Core\Values\ImmutableDecimal. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
43
    }
44
45
    /**
46
     * @param int|float|DecimalInterface $k
47
     *
48
     * @return ImmutableDecimal
49
     * @throws IntegrityConstraint
50
     * @throws IncompatibleObjectState
51
     */
52
    public function probabilityOfKEvents($k, int $scale = 10): ImmutableDecimal
53
    {
54
55
        return $this->pmf($k, $scale);
56
        
57
    }
58
59
    /**
60
     * @param int|float|DecimalInterface $x
61
     *
62
     * @return ImmutableDecimal
63
     * @throws IntegrityConstraint
64
     * @throws IncompatibleObjectState
65
     */
66
    public function cdf(int|float|DecimalInterface $x, int $scale = 10): ImmutableDecimal
67
    {
68
69
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
70
71
        if (!$x->isNatural()) {
0 ignored issues
show
Bug introduced by
The method isNatural() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Types\Fraction or Samsara\Fermat\Core\Types\Decimal or Samsara\Fermat\Core\Type...umbers\DecimalInterface or Samsara\Fermat\Core\Types\Fraction. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

71
        if (!$x->/** @scrutinizer ignore-call */ isNatural()) {
Loading history...
72
            throw new IntegrityConstraint(
73
                'Only integers are valid x values for Poisson distributions',
74
                'Provide an integer value to calculate the CDF',
75
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
76
            );
77
        }
78
79
        $internalScale = $scale + 2;
80
81
        $cumulative = Numbers::makeZero();
82
83
        for ($i = 0;$x->isGreaterThanOrEqualTo($i);$i++) {
0 ignored issues
show
Bug introduced by
The method isGreaterThanOrEqualTo() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Type...s\SimpleNumberInterface or Samsara\Fermat\Core\Types\Fraction or Samsara\Fermat\Core\Types\Decimal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

83
        for ($i = 0;$x->/** @scrutinizer ignore-call */ isGreaterThanOrEqualTo($i);$i++) {
Loading history...
84
            $cumulative =
85
                $cumulative->add(
86
                    $this->pmf(
87
                        $i,
88
                        $internalScale
89
                    )
90
                )->truncateToScale($scale);
91
        }
92
93
        return $cumulative;
94
95
    }
96
97
    /**
98
     * @param float|int|DecimalInterface $x
99
     *
100
     * @return ImmutableDecimal
101
     * @throws IntegrityConstraint
102
     * @throws IncompatibleObjectState
103
     */
104
    public function pmf(float|int|DecimalInterface $x, int $scale = 10): ImmutableDecimal
105
    {
106
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
107
108
        if (!$x->isNatural()) {
109
            throw new IntegrityConstraint(
110
                'Only integers are valid x values for Poisson distributions',
111
                'Provide an integer value to calculate the PMF',
112
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
113
            );
114
        }
115
116
        $internalScale = $scale + 2;
117
118
        $e = Numbers::makeE($internalScale);
119
120
        /** @var ImmutableDecimal $pmf */
121
        $pmf =
122
            $this->lambda
123
            ->pow($x)
124
            ->multiply(
125
                $e->pow(
126
                    $this
127
                        ->lambda
128
                        ->multiply(-1)
129
                )
130
            )->divide(
131
                $x->factorial(),
0 ignored issues
show
Bug introduced by
The method factorial() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Types\Decimal or Samsara\Fermat\Core\Type...umbers\DecimalInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

131
                $x->/** @scrutinizer ignore-call */ 
132
                    factorial(),
Loading history...
132
                $internalScale
133
            )->truncateToScale($scale);
0 ignored issues
show
Bug introduced by
The method truncateToScale() does not exist on Samsara\Fermat\Core\Type...mbers\FractionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

133
            )->/** @scrutinizer ignore-call */ truncateToScale($scale);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method truncateToScale() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Types\Decimal or Samsara\Fermat\Core\Type...umbers\DecimalInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

133
            )->/** @scrutinizer ignore-call */ truncateToScale($scale);
Loading history...
134
135
        return $pmf;
136
    }
137
138
    /**
139
     * @param int|float|DecimalInterface $x1
140
     * @param int|float|DecimalInterface $x2
141
     *
142
     * @return ImmutableDecimal
143
     * @throws IntegrityConstraint
144
     */
145
    public function rangePmf($x1, $x2): ImmutableDecimal
146
    {
147
        $x1 = Numbers::makeOrDont(Numbers::IMMUTABLE, $x1);
148
        $x2 = Numbers::makeOrDont(Numbers::IMMUTABLE, $x2);
149
150
        if ($x1->equals($x2)) {
0 ignored issues
show
Bug introduced by
The method equals() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Types\Base\Number or Samsara\Fermat\Core\Types\Fraction or Samsara\Fermat\Core\Types\Decimal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

150
        if ($x1->/** @scrutinizer ignore-call */ equals($x2)) {
Loading history...
151
            return Numbers::makeZero();
152
        } elseif ($x1->isGreaterThan($x2)) {
0 ignored issues
show
Bug introduced by
The method isGreaterThan() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Type...s\SimpleNumberInterface or Samsara\Fermat\Core\Types\Fraction or Samsara\Fermat\Core\Types\Decimal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        } elseif ($x1->/** @scrutinizer ignore-call */ isGreaterThan($x2)) {
Loading history...
153
            $larger = $x1;
154
            $smaller = $x2;
155
        } else {
156
            $larger = $x2;
157
            $smaller = $x1;
158
        }
159
160
        if (!$larger->isNatural() || !$smaller->isNatural()) {
161
            throw new IntegrityConstraint(
162
                'Only integers are valid x values for Poisson distributions',
163
                'Provide integer values to calculate the range PMF',
164
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
165
            );
166
        }
167
168
        $cumulative = Numbers::makeZero();
169
170
        for (;$larger->isGreaterThanOrEqualTo($smaller);$smaller = $smaller->add(1)) {
171
            $cumulative =
172
                $cumulative->add(
173
                    $this->pmf($smaller)
174
                );
175
        }
176
177
        return $cumulative;
178
    }
179
180
    /**
181
     * @return ImmutableDecimal
182
     * @throws IntegrityConstraint
183
     * @throws IncompatibleObjectState
184
     *
185
     * @codeCoverageIgnore
186
     */
187
    public function random(): ImmutableDecimal
188
    {
189
        if ($this->lambda->isLessThanOrEqualTo(30)) {
190
            return $this->knuthRandom();
191
        } else {
192
            return $this->methodPARandom();
193
        }
194
    }
195
196
    /**
197
     * WARNING: This function is of very limited use with Poisson distributions, and may represent a SIGNIFICANT
198
     * performance hit for certain values of $min, $max, $lambda, and $maxIterations
199
     *
200
     * @param int|float|NumberInterface $min
201
     * @param int|float|NumberInterface $max
202
     * @param int $maxIterations
203
     *
204
     * @return ImmutableDecimal
205
     * @throws OptionalExit
206
     * @throws IntegrityConstraint
207
     * @throws IncompatibleObjectState
208
     *
209
     * @codeCoverageIgnore
210
     */
211
    public function rangeRandom($min = 0, $max = PHP_INT_MAX, int $maxIterations = 20): ImmutableDecimal
212
    {
213
        $i = 0;
214
215
        do {
216
            $randomNumber = $this->random();
217
            $i++;
218
        } while (($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) && $i < $maxIterations);
219
220
        if ($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) {
221
            throw new OptionalExit(
222
                'All random numbers generated were outside of the requested range',
223
                'A suitable random number, restricted by the $max ('.$max.') and $min ('.$min.'), could not be found within '.$maxIterations.' iterations'
0 ignored issues
show
Bug introduced by
Are you sure $min of type Samsara\Fermat\Core\Type...nterface|double|integer can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

223
                'A suitable random number, restricted by the $max ('.$max.') and $min ('./** @scrutinizer ignore-type */ $min.'), could not be found within '.$maxIterations.' iterations'
Loading history...
Bug introduced by
Are you sure $max of type Samsara\Fermat\Core\Type...nterface|double|integer can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

223
                'A suitable random number, restricted by the $max ('./** @scrutinizer ignore-type */ $max.') and $min ('.$min.'), could not be found within '.$maxIterations.' iterations'
Loading history...
224
            );
225
        } else {
226
            return $randomNumber;
227
        }
228
    }
229
230
    /**
231
     * Method PA from The Computer Generation of Poisson Random Variables by A. C. Atkinson, 1979
232
     * Journal of the Royal Statistical Society Series C, Vol. 28, No. 1, Pages 29-35
233
     *
234
     * As described by John D. Cook: http://www.johndcook.com/blog/2010/06/14/generating-poisson-random-values/
235
     *
236
     * @return ImmutableDecimal
237
     * @throws IntegrityConstraint
238
     * @throws IncompatibleObjectState
239
     *
240
     * @codeCoverageIgnore
241
     */
242
    protected function methodPARandom(): ImmutableDecimal
243
    {
244
        /** @var ImmutableDecimal $c */
245
        $c = $this->lambda->pow(-1)->multiply(3.36)->multiply(-1)->add(0.767);
246
        /** @var ImmutableDecimal $beta */
247
        $beta = Numbers::makePi()->divide($this->lambda->multiply(3)->sqrt());
248
        /** @var ImmutableDecimal $alpha */
249
        $alpha = $this->lambda->multiply($beta);
250
        /** @var ImmutableDecimal $k */
251
        $k = $c->ln(20)->subtract($this->lambda)->subtract($beta->ln(20));
252
        /** @var ImmutableDecimal $one */
253
        $one = Numbers::makeOne();
254
        /** @var ImmutableDecimal $oneHalf */
255
        $oneHalf = Numbers::make(Numbers::IMMUTABLE, '0.5');
256
        /** @var ImmutableDecimal $e */
257
        $e = Numbers::makeE();
258
259
        while (true) {
260
            /** @var ImmutableDecimal $u */
261
            $u = PolyfillProvider::randomInt(0, PHP_INT_MAX) / PHP_INT_MAX;
0 ignored issues
show
Bug introduced by
The type Samsara\Fermat\Stats\Val...bution\PolyfillProvider was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
262
            /** @var ImmutableDecimal $x */
263
            $x = $alpha->subtract($one->subtract($u)->divide($u)->ln(20)->divide($beta));
0 ignored issues
show
Bug introduced by
The method ln() does not exist on Samsara\Fermat\Core\Type...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Core\Type...Numbers\NumberInterface such as Samsara\Fermat\Core\Types\Decimal or Samsara\Fermat\Core\Type...umbers\DecimalInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

263
            $x = $alpha->subtract($one->subtract($u)->divide($u)->/** @scrutinizer ignore-call */ ln(20)->divide($beta));
Loading history...
Bug introduced by
The method ln() does not exist on Samsara\Fermat\Core\Type...mbers\FractionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

263
            $x = $alpha->subtract($one->subtract($u)->divide($u)->/** @scrutinizer ignore-call */ ln(20)->divide($beta));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
264
            /** @var ImmutableDecimal $n */
265
            $n = $x->add($oneHalf)->floor();
266
267
            if ($n->isNegative()) {
268
                continue;
269
            }
270
271
            /** @var ImmutableDecimal $v */
272
            $v = Numbers::make(Numbers::IMMUTABLE, PolyfillProvider::randomInt(0, PHP_INT_MAX))->divide(PHP_INT_MAX);
273
            /** @var ImmutableDecimal $y */
274
            $y = $alpha->subtract($beta->multiply($x));
275
            /** @var ImmutableDecimal $lhs */
276
            $lhs = $y->add($v->divide($e->pow($y)->add($one)->pow(2)));
277
            /** @var ImmutableDecimal $rhs */
278
            $rhs = $k->add($n->multiply($this->lambda->ln(20)))->subtract($n->factorial()->ln(20));
279
280
            if ($lhs->isLessThanOrEqualTo($rhs)) {
281
                return $n;
282
            }
283
284
            /*
285
             * At least attempt to free up some memory, since this particular method is extra hard on object instantiation
286
             */
287
            unset($u);
288
            unset($x);
289
            unset($n);
290
            unset($v);
291
            unset($y);
292
            unset($lhs);
293
            unset($rhs);
294
        }
295
    }
296
297
    /**
298
     * @return ImmutableDecimal
299
     * @throws IntegrityConstraint
300
     *
301
     * @codeCoverageIgnore
302
     */
303
    protected function knuthRandom(): ImmutableDecimal
304
    {
305
        /** @var ImmutableDecimal $L */
306
        $L = Numbers::makeE()->pow($this->lambda->multiply(-1));
307
        /** @var ImmutableDecimal $k */
308
        $k = Numbers::makeZero();
309
        /** @var ImmutableDecimal $p */
310
        $p = Numbers::makeOne();
311
312
        do {
313
            $k = $k->add(1);
314
            /** @var ImmutableDecimal $u */
315
            $u = RandomProvider::randomDecimal(10);
316
            $p = $p->multiply($u);
317
        } while ($p->isGreaterThan($L));
318
319
        return $k->subtract(1);
320
    }
321
322
}