Passed
Push — master ( a639aa...cb526f )
by Jordan
02:41
created

Poisson::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace Samsara\Fermat\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\Numbers;
9
use Samsara\Fermat\Types\Distribution;
10
use Samsara\Fermat\Provider\PolyfillProvider;
11
use Samsara\Fermat\Types\Base\Interfaces\Numbers\DecimalInterface;
12
use Samsara\Fermat\Types\Base\Interfaces\Numbers\NumberInterface;
13
use Samsara\Fermat\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 9
    public function __construct($lambda)
31
    {
32 9
        $lambda = Numbers::makeOrDont(Numbers::IMMUTABLE, $lambda);
33
34 9
        if (!$lambda->isPositive()) {
0 ignored issues
show
Bug introduced by
The method isPositive() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Samsara\Fermat\Types\Base\Number. Are you sure you never get one of those? ( 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 1
            throw new IntegrityConstraint(
36 1
                'Lambda must be positive',
37 1
                'Provide a positive lambda',
38 1
                'Poisson distributions work on time to occurrence; the mean time to occurrence (lambda) must be positive'
39
            );
40
        }
41
42 9
        $this->lambda = $lambda;
0 ignored issues
show
Documentation Bug introduced by
It seems like $lambda can also be of type Samsara\Fermat\Values\MutableDecimal. However, the property $lambda is declared as type Samsara\Fermat\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 9
    }
44
45
    /**
46
     * @param int|float|DecimalInterface $k
47
     *
48
     * @return ImmutableDecimal
49
     * @throws IntegrityConstraint
50
     * @throws IncompatibleObjectState
51
     */
52 1
    public function probabilityOfKEvents($k, int $scale = 10): ImmutableDecimal
53
    {
54
55 1
        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 2
    public function cdf($x, int $scale = 10): ImmutableDecimal
67
    {
68
69 2
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
70
71 2
        if (!$x->isNatural()) {
0 ignored issues
show
Bug introduced by
The method isNatural() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Samsara\Fermat\Types\Bas...s\SimpleNumberInterface or Samsara\Fermat\Types\Base\Number or Samsara\Fermat\Types\Bas...mbers\FractionInterface. Are you sure you never get one of those? ( 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 */ 
72
                 isNatural()) {
Loading history...
72 1
            throw new IntegrityConstraint(
73 1
                'Only integers are valid x values for Poisson distributions',
74 1
                'Provide an integer value to calculate the CDF',
75 1
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
76
            );
77
        }
78
79 1
        $internalScale = $scale + 2;
80
81 1
        $cumulative = Numbers::makeZero();
82
83 1
        for ($i = 0;$x->isGreaterThanOrEqualTo($i);$i++) {
0 ignored issues
show
Bug introduced by
The method isGreaterThanOrEqualTo() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Samsara\Fermat\Types\Base\Number. Are you sure you never get one of those? ( 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 1
            $cumulative = $cumulative->add($this->pmf($i, $internalScale))->truncateToScale($scale);
85
        }
86
87 1
        return $cumulative;
88
89
    }
90
91
    /**
92
     * @param float|int|DecimalInterface $x
93
     *
94
     * @return ImmutableDecimal
95
     * @throws IntegrityConstraint
96
     * @throws IncompatibleObjectState
97
     */
98 4
    public function pmf(float|int|DecimalInterface $x, int $scale = 10): ImmutableDecimal
99
    {
100 4
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
101
102 4
        if (!$x->isNatural()) {
103 1
            throw new IntegrityConstraint(
104 1
                'Only integers are valid x values for Poisson distributions',
105 1
                'Provide an integer value to calculate the PMF',
106 1
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
107
            );
108
        }
109
110 3
        $internalScale = $scale + 2;
111
112 3
        $e = Numbers::makeE($internalScale);
113
114
        /** @var ImmutableDecimal $pmf */
115 3
        $pmf = $this->lambda->pow($x)->multiply($e->pow($this->lambda->multiply(-1)))->divide($x->factorial(), $internalScale)->truncateToScale($scale);
0 ignored issues
show
Bug introduced by
The method factorial() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Types\Bas...Numbers\NumberInterface such as Samsara\Fermat\Types\Bas...umbers\DecimalInterface or Samsara\Fermat\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

115
        $pmf = $this->lambda->pow($x)->multiply($e->pow($this->lambda->multiply(-1)))->divide($x->/** @scrutinizer ignore-call */ factorial(), $internalScale)->truncateToScale($scale);
Loading history...
116
117 3
        return $pmf;
118
    }
119
120
    /**
121
     * @param int|float|DecimalInterface $x1
122
     * @param int|float|DecimalInterface $x2
123
     *
124
     * @return ImmutableDecimal
125
     * @throws IntegrityConstraint
126
     */
127 4
    public function rangePmf($x1, $x2): ImmutableDecimal
128
    {
129 4
        $x1 = Numbers::makeOrDont(Numbers::IMMUTABLE, $x1);
130 4
        $x2 = Numbers::makeOrDont(Numbers::IMMUTABLE, $x2);
131
132 4
        if ($x1->equals($x2)) {
0 ignored issues
show
Bug introduced by
The method equals() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of Samsara\Fermat\Types\Bas...Numbers\NumberInterface such as Samsara\Fermat\Types\Base\Number or Samsara\Fermat\Types\Fraction or Samsara\Fermat\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

132
        if ($x1->/** @scrutinizer ignore-call */ equals($x2)) {
Loading history...
133 1
            return Numbers::makeZero();
134 4
        } elseif ($x1->isGreaterThan($x2)) {
0 ignored issues
show
Bug introduced by
The method isGreaterThan() does not exist on Samsara\Fermat\Types\Bas...Numbers\NumberInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Samsara\Fermat\Types\Base\Number. Are you sure you never get one of those? ( Ignorable by Annotation )

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

134
        } elseif ($x1->/** @scrutinizer ignore-call */ isGreaterThan($x2)) {
Loading history...
135 1
            $larger = $x1;
136 1
            $smaller = $x2;
137
        } else {
138 4
            $larger = $x2;
139 4
            $smaller = $x1;
140
        }
141
142 4
        if (!$larger->isNatural() || !$smaller->isNatural()) {
143 3
            throw new IntegrityConstraint(
144 3
                'Only integers are valid x values for Poisson distributions',
145 3
                'Provide integer values to calculate the range PMF',
146 3
                'Poisson distributions describe discrete occurrences; only integers are valid x values'
147
            );
148
        }
149
150 1
        $cumulative = Numbers::makeZero();
151
152 1
        for (;$larger->isGreaterThanOrEqualTo($smaller);$smaller = $smaller->add(1)) {
153 1
            $cumulative = $cumulative->add($this->pmf($smaller));
154
        }
155
156 1
        return $cumulative;
157
    }
158
159
    /**
160
     * @return ImmutableDecimal
161
     * @throws IntegrityConstraint
162
     * @throws IncompatibleObjectState
163
     *
164
     * @codeCoverageIgnore
165
     */
166
    public function random(): ImmutableDecimal
167
    {
168
        if ($this->lambda->isLessThanOrEqualTo(30)) {
169
            return $this->knuthRandom();
170
        } else {
171
            return $this->methodPARandom();
172
        }
173
    }
174
175
    /**
176
     * WARNING: This function is of very limited use with Poisson distributions, and may represent a SIGNIFICANT
177
     * performance hit for certain values of $min, $max, $lambda, and $maxIterations
178
     *
179
     * @param int|float|NumberInterface $min
180
     * @param int|float|NumberInterface $max
181
     * @param int $maxIterations
182
     *
183
     * @return ImmutableDecimal
184
     * @throws OptionalExit
185
     * @throws IntegrityConstraint
186
     * @throws IncompatibleObjectState
187
     *
188
     * @codeCoverageIgnore
189
     */
190
    public function rangeRandom($min = 0, $max = PHP_INT_MAX, int $maxIterations = 20): ImmutableDecimal
191
    {
192
        $i = 0;
193
194
        do {
195
            $randomNumber = $this->random();
196
            $i++;
197
        } while (($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) && $i < $maxIterations);
198
199
        if ($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) {
200
            throw new OptionalExit(
201
                'All random numbers generated were outside of the requested range',
202
                '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\Types\Bas...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

202
                '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\Types\Bas...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

202
                'A suitable random number, restricted by the $max ('./** @scrutinizer ignore-type */ $max.') and $min ('.$min.'), could not be found within '.$maxIterations.' iterations'
Loading history...
203
            );
204
        } else {
205
            return $randomNumber;
206
        }
207
    }
208
209
    /**
210
     * Method PA from The Computer Generation of Poisson Random Variables by A. C. Atkinson, 1979
211
     * Journal of the Royal Statistical Society Series C, Vol. 28, No. 1, Pages 29-35
212
     *
213
     * As described by John D. Cook: http://www.johndcook.com/blog/2010/06/14/generating-poisson-random-values/
214
     *
215
     * @return ImmutableDecimal
216
     * @throws IntegrityConstraint
217
     * @throws IncompatibleObjectState
218
     *
219
     * @codeCoverageIgnore
220
     */
221
    protected function methodPARandom(): ImmutableDecimal
222
    {
223
        /** @var ImmutableDecimal $c */
224
        $c = $this->lambda->pow(-1)->multiply(3.36)->multiply(-1)->add(0.767);
225
        /** @var ImmutableDecimal $beta */
226
        $beta = Numbers::makePi()->divide($this->lambda->multiply(3)->sqrt());
227
        /** @var ImmutableDecimal $alpha */
228
        $alpha = $this->lambda->multiply($beta);
229
        /** @var ImmutableDecimal $k */
230
        $k = $c->ln(20)->subtract($this->lambda)->subtract($beta->ln(20));
231
        /** @var ImmutableDecimal $one */
232
        $one = Numbers::makeOne();
233
        /** @var ImmutableDecimal $oneHalf */
234
        $oneHalf = Numbers::make(Numbers::IMMUTABLE, '0.5');
235
        /** @var ImmutableDecimal $e */
236
        $e = Numbers::makeE();
237
238
        while (true) {
239
            /** @var ImmutableDecimal $u */
240
            $u = PolyfillProvider::randomInt(0, PHP_INT_MAX) / PHP_INT_MAX;
241
            /** @var ImmutableDecimal $x */
242
            $x = $alpha->subtract($one->subtract($u)->divide($u)->ln(20)->divide($beta));
243
            /** @var ImmutableDecimal $n */
244
            $n = $x->add($oneHalf)->floor();
245
246
            if ($n->isNegative()) {
247
                continue;
248
            }
249
250
            /** @var ImmutableDecimal $v */
251
            $v = Numbers::make(Numbers::IMMUTABLE, PolyfillProvider::randomInt(0, PHP_INT_MAX))->divide(PHP_INT_MAX);
252
            /** @var ImmutableDecimal $y */
253
            $y = $alpha->subtract($beta->multiply($x));
254
            /** @var ImmutableDecimal $lhs */
255
            $lhs = $y->add($v->divide($e->pow($y)->add($one)->pow(2)));
256
            /** @var ImmutableDecimal $rhs */
257
            $rhs = $k->add($n->multiply($this->lambda->ln(20)))->subtract($n->factorial()->ln(20));
258
259
            if ($lhs->isLessThanOrEqualTo($rhs)) {
260
                return $n;
261
            }
262
263
            /*
264
             * At least attempt to free up some memory, since this particular method is extra hard on object instantiation
265
             */
266
            unset($u);
267
            unset($x);
268
            unset($n);
269
            unset($v);
270
            unset($y);
271
            unset($lhs);
272
            unset($rhs);
273
        }
274
    }
275
276
    /**
277
     * @return ImmutableDecimal
278
     * @throws IntegrityConstraint
279
     *
280
     * @codeCoverageIgnore
281
     */
282
    protected function knuthRandom(): ImmutableDecimal
283
    {
284
        /** @var ImmutableDecimal $L */
285
        $L = Numbers::makeE()->pow($this->lambda->multiply(-1));
286
        /** @var ImmutableDecimal $k */
287
        $k = Numbers::makeZero();
288
        /** @var ImmutableDecimal $p */
289
        $p = Numbers::makeOne();
290
291
        do {
292
            $k = $k->add(1);
293
            /** @var ImmutableDecimal $u */
294
            $u = PolyfillProvider::randomInt(0, PHP_INT_MAX) / PHP_INT_MAX;
295
            $p = $p->multiply($u);
296
        } while ($p->isGreaterThan($L));
297
298
        return $k->subtract(1);
299
    }
300
301
}