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

Normal::rangeRandom()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 18
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 18
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 ReflectionException;
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\Provider\SequenceProvider;
12
use Samsara\Fermat\Stats\Provider\StatsProvider;
13
use Samsara\Fermat\Core\Types\Base\Interfaces\Numbers\DecimalInterface;
14
use Samsara\Fermat\Expressions\Types\Base\Interfaces\Evaluateables\FunctionInterface;
15
use Samsara\Fermat\Core\Types\Base\Interfaces\Numbers\NumberInterface;
16
use Samsara\Fermat\Core\Types\NumberCollection;
17
use Samsara\Fermat\Core\Values\ImmutableDecimal;
18
19
class Normal extends Distribution
20
{
21
22
    /**
23
     * @var ImmutableDecimal
24
     */
25
    private $mean;
26
27
    /**
28
     * @var ImmutableDecimal
29
     */
30
    private $sd;
31
32
    /**
33
     * Normal constructor.
34
     *
35
     * @param int|float|DecimalInterface $mean
36
     * @param int|float|DecimalInterface $sd
37
     *
38
     * @throws IntegrityConstraint
39
     * @throws ReflectionException
40
     */
41 2
    public function __construct($mean, $sd)
42
    {
43
        /** @var ImmutableDecimal $mean */
44 2
        $mean = Numbers::makeOrDont(Numbers::IMMUTABLE, $mean);
45
        /** @var ImmutableDecimal $sd   */
46 2
        $sd = Numbers::makeOrDont(Numbers::IMMUTABLE, $sd);
47
48 2
        $this->mean = $mean;
49 2
        $this->sd = $sd;
50
    }
51
52
    /**
53
     * @return ImmutableDecimal
54
     */
55
    public function getSD(): ImmutableDecimal
56
    {
57
        return $this->sd;
58
    }
59
60
    /**
61
     * @return ImmutableDecimal
62
     */
63 2
    public function getMean(): ImmutableDecimal
64
    {
65 2
        return $this->mean;
66
    }
67
68
    /**
69
     * @param $x
70
     *
71
     * @return ImmutableDecimal
72
     * @throws IntegrityConstraint
73
     * @throws ReflectionException
74
     */
75
    public function evaluateAt($x, int $scale = 10): ImmutableDecimal
76
    {
77
78
        $one = Numbers::makeOne($scale);
79
        $twoPi = Numbers::make2Pi($scale);
80
        $e = Numbers::makeE($scale);
81
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
82
83
        $internalScale = (new NumberCollection([$scale, $x]))->selectScale() + 2;
84
85
        // $left = 1 / ( sqrt(2pi * SD^2) )
86
        $left =
87
            $one->divide(
88
                $twoPi->multiply(
89
                    $this->getSD()
90
                        ->pow(2)
91
                )->sqrt($internalScale),
92
                $internalScale
93
            );
94
        // $right = e^( -1*((x - SD)^2)/(2*SD^2) )
95
        $right =
96
            $e->pow(
97
                $x->subtract(
98
                    $this->getMean()
99
                )->pow(2)
100
                ->divide(
101
                    $this->getSD()
102
                        ->pow(2)
103
                        ->multiply(2),
104
                    $internalScale
105
                )->multiply(-1)
106
            );
107
108
        // Return value is not inlined to ensure proper return type for IDE
109
        /** @var ImmutableDecimal $value */
110
        $value = $left->multiply($right)->truncateToScale($scale);
0 ignored issues
show
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

110
        $value = $left->multiply($right)->/** @scrutinizer ignore-call */ truncateToScale($scale);
Loading history...
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

110
        $value = $left->multiply($right)->/** @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...
111
112
        return $value;
113
114
    }
115
116
    /**
117
     * @param int|float|DecimalInterface $p
118
     * @param int|float|DecimalInterface $x
119
     * @param int|float|DecimalInterface $mean
120
     *
121
     * @return Normal
122
     * @throws IntegrityConstraint
123
     * @throws OptionalExit
124
     * @throws ReflectionException
125
     */
126
    public static function makeFromMean($p, $x, $mean): Normal
127
    {
128
        $one = Numbers::makeOne(10);
129
        $p = Numbers::makeOrDont(Numbers::IMMUTABLE, $p);
130
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
131
        $mean = Numbers::makeOrDont(Numbers::IMMUTABLE, $mean);
132
133
        $internalScale = (new NumberCollection([$one, $p, $x, $mean]))->selectScale();
134
135
        $internalScale += 2;
136
137
        $z = StatsProvider::inverseNormalCDF($one->subtract($p), $internalScale);
138
139
        $sd =
140
            $x->subtract($mean)
141
                ->divide(
142
                    $z,
143
                    $internalScale
144
                )->abs()
145
                ->truncateToScale($internalScale-2);
146
147
        return new Normal($mean, $sd);
148
    }
149
150
    /**
151
     * @param int|float|DecimalInterface $p
152
     * @param int|float|DecimalInterface $x
153
     * @param int|float|DecimalInterface $sd
154
     *
155
     * @return Normal
156
     * @throws IntegrityConstraint
157
     * @throws OptionalExit
158
     * @throws ReflectionException
159
     */
160
    public static function makeFromSd($p, $x, $sd): Normal
161
    {
162
        $one = Numbers::makeOne(10);
163
        $p = Numbers::makeOrDont(Numbers::IMMUTABLE, $p);
164
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
165
        $sd = Numbers::makeOrDont(Numbers::IMMUTABLE, $sd);
166
167
        $internalScale = (new NumberCollection([$one, $p, $x, $sd]))->selectScale();
168
169
        $internalScale += 2;
170
171
        $z = StatsProvider::inverseNormalCDF($one->subtract($p), $internalScale);
172
173
        $mean =
174
            $x->add(
175
                $z->multiply($sd)
176
            )->truncateToScale($internalScale-2);
177
178
        return new Normal($mean, $sd);
179
    }
180
181
    /**
182
     * @param int|float|DecimalInterface $x
183
     *
184
     * @return ImmutableDecimal
185
     * @throws IntegrityConstraint
186
     * @throws OptionalExit
187
     * @throws ReflectionException
188
     */
189 2
    public function cdf($x): ImmutableDecimal
190
    {
191 2
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
192
193 2
        $oneHalf = Numbers::make(Numbers::IMMUTABLE, '0.5');
194 2
        $one = Numbers::makeOne();
195 2
        $sqrtTwo = Numbers::make(Numbers::IMMUTABLE, 2)->sqrt();
196
197
        /** @var ImmutableDecimal $cdf */
198 2
        $cdf =
199 2
            $oneHalf->multiply(
200 2
                $one->add(
201 2
                    StatsProvider::gaussErrorFunction(
202 2
                        $x->subtract($this->mean)
203 2
                            ->divide(
204 2
                                $this->sd->multiply($sqrtTwo)
205
                            )
206
                    )
207
                )
208
            );
209
210 2
        return $cdf;
211
    }
212
213
    /**
214
     * @param int|float|DecimalInterface $x1
215
     * @param null|int|float|DecimalInterface $x2
216
     *
217
     * @return ImmutableDecimal
218
     * @throws IntegrityConstraint
219
     * @throws ReflectionException
220
     */
221
    public function pdf($x1, $x2 = null): ImmutableDecimal
222
    {
223
224
        $x1 = Numbers::makeOrDont(Numbers::IMMUTABLE, $x1);
225
226
        if (is_null($x2)) {
227
            $separation = $x1->subtract($this->mean)->multiply(2)->abs();
228
229
            if ($this->mean->isLessThan($x1)) {
230
                $x2 = $x1->subtract($separation);
231
            } else {
232
                $x2 = $x1->add($separation);
233
            }
234
        }
235
236
        /** @var ImmutableDecimal $pdf */
237
        $pdf =
238
            $this->cdf($x1)
239
                ->subtract(
240
                    $this->cdf($x2)
241
                )->abs();
242
243
        return $pdf;
244
    }
245
246
    /**
247
     * @param FunctionInterface $function
248
     * @param $x
249
     * @return ImmutableDecimal
250
     * @throws IntegrityConstraint
251
     * @throws ReflectionException
252
     */
253
    public function cdfProduct(FunctionInterface $function, $x): ImmutableDecimal
254
    {
255
256
        $loop = 0;
257
258
        $cdf = Numbers::makeZero();
259
260
        while (true) {
261
            if (count($function->describeShape()) === 0) {
262
                break;
263
            }
264
265
            $cdf = $cdf->add($function->evaluateAt($x)->multiply(SequenceProvider::nthPowerNegativeOne($loop)));
266
267
            $function = $function->derivativeExpression();
268
        }
269
270
        /** @var ImmutableDecimal $cdf */
271
        $cdf = $cdf->multiply($this->cdf($x));
272
273
        return $cdf;
274
275
    }
276
277
    /**
278
     * @param FunctionInterface $function
279
     * @param $x1
280
     * @param $x2
281
     *
282
     * @return ImmutableDecimal
283
     * @throws IntegrityConstraint
284
     * @throws ReflectionException
285
     */
286
    public function pdfProduct(FunctionInterface $function, $x1, $x2): ImmutableDecimal
287
    {
288
        /** @var ImmutableDecimal $pdf */
289
        $pdf = $this->cdfProduct($function, $x2)->subtract($this->cdfProduct($function, $x1));
290
291
        return $pdf;
292
    }
293
294
    /**
295
     * @param int|float|DecimalInterface $x
296
     *
297
     * @return ImmutableDecimal
298
     * @throws IntegrityConstraint
299
     */
300
    public function percentBelowX($x): ImmutableDecimal
301
    {
302
        return $this->cdf($x);
303
    }
304
305
    /**
306
     * @param int|float|DecimalInterface $x
307
     *
308
     * @return ImmutableDecimal
309
     * @throws IntegrityConstraint
310
     * @throws ReflectionException
311
     */
312 2
    public function percentAboveX($x): ImmutableDecimal
313
    {
314 2
        $one = Numbers::makeOne();
315
316
        /** @var ImmutableDecimal $perc */
317 2
        $perc = $one->subtract($this->cdf($x));
318
319 2
        return $perc;
320
    }
321
322
    /**
323
     * @param int|float|DecimalInterface $x
324
     *
325
     * @return ImmutableDecimal
326
     * @throws IntegrityConstraint
327
     * @throws ReflectionException
328
     */
329
    public function zScoreOfX($x): ImmutableDecimal
330
    {
331
        /** @var ImmutableDecimal $x */
332
        $x = Numbers::makeOrDont(Numbers::IMMUTABLE, $x);
333
334
        /** @var ImmutableDecimal $z */
335
        $z = $x->subtract($this->mean)->divide($this->sd);
336
337
        return $z;
338
    }
339
340
    /**
341
     * @param int|float|DecimalInterface $z
342
     *
343
     * @return ImmutableDecimal
344
     * @throws IntegrityConstraint
345
     * @throws ReflectionException
346
     */
347
    public function xFromZScore($z): ImmutableDecimal
348
    {
349
        $z = Numbers::makeOrDont(Numbers::IMMUTABLE, $z);
350
351
        /** @var ImmutableDecimal $x */
352
        $x = $z->multiply($this->sd)->add($this->mean);
353
354
        return $x;
355
    }
356
357
    /**
358
     * @return ImmutableDecimal
359
     * @throws IntegrityConstraint
360
     * @throws ReflectionException
361
     *
362
     * @codeCoverageIgnore
363
     */
364
    public function random(): ImmutableDecimal
365
    {
366
        $rand1 = RandomProvider::randomDecimal(20);
367
        $rand2 = RandomProvider::randomDecimal(20);
368
369
        $randomNumber =
370
            $rand1->ln()
371
                ->multiply(-2)
372
                ->sqrt()
373
                ->multiply(
374
                    $rand2->multiply(Numbers::TAU)
375
                        ->cos(1, false)
376
                );
377
        /** @var ImmutableDecimal $randomNumber */
378
        $randomNumber = $randomNumber->multiply($this->sd)->add($this->mean);
379
380
        return $randomNumber;
381
    }
382
383
    /**
384
     * @param int|float|NumberInterface $min
385
     * @param int|float|NumberInterface $max
386
     * @param int $maxIterations
387
     *
388
     * @return ImmutableDecimal
389
     * @throws OptionalExit
390
     * @throws IntegrityConstraint
391
     * @throws ReflectionException
392
     *
393
     * @codeCoverageIgnore
394
     */
395
    public function rangeRandom($min = 0, $max = PHP_INT_MAX, int $maxIterations = 20): ImmutableDecimal
396
    {
397
        $i = 0;
398
399
        do {
400
            $randomNumber = $this->random();
401
            $i++;
402
        } while (($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) && $i < $maxIterations);
403
404
        if ($randomNumber->isGreaterThan($max) || $randomNumber->isLessThan($min)) {
405
            throw new OptionalExit(
406
                'All random numbers generated were outside of the requested range',
407
                'Widen the acceptable range of random values, or allow the function to perform mor iterations',
408
                '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 $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

408
                'A suitable random number, restricted by the $max ('./** @scrutinizer ignore-type */ $max.') and $min ('.$min.'), could not be found within '.$maxIterations.' iterations'
Loading history...
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

408
                'A suitable random number, restricted by the $max ('.$max.') and $min ('./** @scrutinizer ignore-type */ $min.'), could not be found within '.$maxIterations.' iterations'
Loading history...
409
            );
410
        }
411
412
        return $randomNumber;
413
    }
414
415
}