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

markrogoyski / math-php / 4908566749

pending completion
4908566749

push

github

Mark Rogoyski
Github actions changes for static analysis.

7538 of 7544 relevant lines covered (99.92%)

184.92 hits per line

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

99.87
/src/LinearAlgebra/NumericMatrix.php
1
<?php
2

3
namespace MathPHP\LinearAlgebra;
4

5
use MathPHP\Functions\Map;
6
use MathPHP\Functions\Support;
7
use MathPHP\Exception;
8
use MathPHP\LinearAlgebra\Reduction;
9

10
/**
11
 * m x n Matrix
12
 *
13
 * @extends Matrix<number>
14
 */
15
class NumericMatrix extends Matrix
16
{
17
    /** @var float Error/zero tolerance */
18
    protected $ε;
19

20
    // Default error/zero tolerance
21
    protected const ε = 0.00000000001;
22

23
    // Matrix data direction
24
    public const ROWS    = 'rows';
25
    public const COLUMNS = 'columns';
26

27
    // Matrix solve methods
28
    public const LU      = 'LU';
29
    public const QR      = 'QR';
30
    public const INVERSE = 'Inverse';
31
    public const RREF    = 'RREF';
32
    public const DEFAULT = 'Default';
33

34
    /**
35
     * Constructor
36
     *
37
     * @param array<array<number>> $A of arrays $A m x n matrix
38
     *
39
     * @throws Exception\BadDataException if any rows have a different column count
40
     */
41
    public function __construct(array $A)
42
    {
43
        $this->A       = $A;
7,068✔
44
        $this->m       = \count($A);
7,068✔
45
        $this->n       = $this->m > 0 ? \count($A[0]) : 0;
7,068✔
46
        $this->ε       = self::ε;
7,068✔
47
        $this->catalog = new MatrixCatalog();
7,068✔
48

49
        $this->validateMatrixDimensions();
7,068✔
50
    }
7,068✔
51

52
    /**
53
     * Validate the matrix is entirely m x n
54
     *
55
     * @throws Exception\BadDataException
56
     */
57
    protected function validateMatrixDimensions(): void
58
    {
59
        foreach ($this->A as $i => $row) {
7,068✔
60
            if (\count($row) !== $this->n) {
7,068✔
61
                throw new Exception\BadDataException("Row $i has a different column count: " . \count($row) . "; was expecting {$this->n}.");
1✔
62
            }
63
        }
64
    }
7,068✔
65

66
    /**
67
     * Get the type of objects that are stored in the matrix
68
     *
69
     * @return string The class of the objects
70
     */
71
    public function getObjectType(): string
72
    {
73
        return 'number';
1,788✔
74
    }
75

76
    /**************************************************************************
77
     * BASIC MATRIX GETTERS
78
     *  - getError
79
     **************************************************************************/
80

81
    /**
82
     * Get error / zero tolerance
83
     * @return float
84
     */
85
    public function getError(): float
86
    {
87
        return $this->ε;
2,299✔
88
    }
89

90
    /***************************************************************************
91
     * SETTERS
92
     *  - setError
93
     **************************************************************************/
94

95
    /**
96
     * Set the error/zero tolerance for matrix values
97
     *  - Used to determine tolerance for equality
98
     *  - Used to determine if a value is zero
99
     *
100
     * @param float|null $ε
101
     */
102
    public function setError(?float $ε): void
103
    {
104
        if ($ε === null) {
6,912✔
105
            return;
6,863✔
106
        }
107
        $this->ε = $ε;
5,121✔
108
    }
5,121✔
109

110
    /***************************************************************************
111
     * MATRIX COMPARISONS
112
     *  - isEqual
113
     ***************************************************************************/
114

115
    /**
116
     * Is this matrix equal to some other matrix?
117
     *
118
     * @param NumericMatrix $B
119
     *
120
     * @return bool
121
     */
122
    public function isEqual(NumericMatrix $B): bool
123
    {
124
        if (!$this->isEqualSizeAndType($B)) {
498✔
125
            return false;
1✔
126
        }
127

128
        $m = $this->m;
497✔
129
        $n = $this->n;
497✔
130
        $ε = $this->ε;
497✔
131
        // All elements are the same
132
        for ($i = 0; $i < $m; $i++) {
497✔
133
            for ($j = 0; $j < $n; $j++) {
497✔
134
                if (Support::isNotEqual($this->A[$i][$j], $B[$i][$j], $ε)) {
497✔
135
                    return false;
63✔
136
                }
137
            }
138
        }
139

140
        return true;
445✔
141
    }
142

143
    /**************************************************************************
144
     * MATRIX PROPERTIES
145
     *  - isSymmetric
146
     *  - isSingular
147
     *  - isNonsingular
148
     *  - isInvertible
149
     *  - isPositiveDefinite
150
     *  - isPositiveSemidefinite
151
     *  - isNegativeDefinite
152
     *  - isNegativeSemidefinite
153
     *  - isLowerTriangular
154
     *  - isUpperTriangular
155
     *  - isTriangular
156
     *  - isDiagonal
157
     *  - isRectangularDiagonal
158
     *  - isRef
159
     *  - isRref
160
     *  - isIdempotent
161
     *  - isNilpotent
162
     *  - isInvolutory
163
     *  - isSignature
164
     *  - isUpperBidiagonal
165
     *  - isLowerBidiagonal
166
     *  - isBidiagonal
167
     *  - isTridiagonal
168
     *  - isUpperHessenberg
169
     *  - isLowerHessenberg
170
     *  - isOrthogonal
171
     *  - isNormal
172
     *  - isUnitary
173
     *  - isHermitian
174
     **************************************************************************/
175

176
    /**
177
     * Is the matrix symmetric?
178
     * Does A = Aᵀ
179
     * aᵢⱼ = aⱼᵢ
180
     *
181
     * Algorithm: Iterate on the upper triangular half and compare with corresponding
182
     * values on the lower triangular half. Skips the diagonal as it is symmetric with itself.
183
     *
184
     * @return bool true if symmetric; false otherwise.
185
     *
186
     * @throws Exception\IncorrectTypeException
187
     * @throws Exception\MatrixException
188
     */
189
    public function isSymmetric(): bool
190
    {
191
        if (!$this->isSquare()) {
448✔
192
            return false;
10✔
193
        }
194

195
        for ($i = 0; $i < $this->m - 1; $i++) {
438✔
196
            for ($j = $i + 1; $j < $this->n; $j++) {
430✔
197
                if (Support::isNotEqual($this->A[$i][$j], $this->A[$j][$i], $this->ε)) {
430✔
198
                    return false;
8✔
199
                }
200
            }
201
        }
202

203
        return true;
430✔
204
    }
205

206
    /**
207
     * Is the matrix skew-symmetric? (Antisymmetric matrix)
208
     * Does Aᵀ = −A
209
     * aᵢⱼ = -aⱼᵢ and main diagonal are all zeros
210
     *
211
     * Algorithm: Iterate on the upper triangular half and compare with corresponding
212
     * values on the lower triangular half. Skips the diagonal as it is symmetric with itself.
213
     *
214
     * @return bool true if skew-symmetric; false otherwise.
215
     *
216
     * @throws Exception\BadParameterException
217
     * @throws Exception\IncorrectTypeException
218
     * @throws Exception\MatrixException
219
     */
220
    public function isSkewSymmetric(): bool
221
    {
222
        if (!$this->isSquare()) {
70✔
223
            return false;
6✔
224
        }
225

226
        for ($i = 0; $i < $this->m - 1; $i++) {
64✔
227
            for ($j = $i + 1; $j < $this->n; $j++) {
59✔
228
                if (Support::isNotEqual($this->A[$i][$j], -$this->A[$j][$i], $this->ε)) {
59✔
229
                    return false;
4✔
230
                }
231
            }
232
        }
233
        foreach ($this->getDiagonalElements() as $diagonalElement) {
60✔
234
            if (Support::isNotZero($diagonalElement, $this->ε)) {
60✔
235
                return false;
4✔
236
            }
237
        }
238

239
        return true;
56✔
240
    }
241

242
    /**
243
     * Is the matrix singular?
244
     * A square matrix that does not have an inverse.
245
     * If the determinant is zero, then the matrix is singular.
246
     * http://mathworld.wolfram.com/SingularMatrix.html
247
     *
248
     * @return bool true if singular; false otherwise.
249
     *
250
     * @throws Exception\MatrixException
251
     * @throws Exception\IncorrectTypeException
252
     * @throws Exception\BadParameterException
253
     */
254
    public function isSingular(): bool
255
    {
256
        $│A│ = $this->det();
1,086✔
257

258
        if (Support::isZero($│A│, $this->ε)) {
1,086✔
259
            return true;
21✔
260
        }
261

262
        return false;
1,065✔
263
    }
264

265
    /**
266
     * Is the matrix nonsingular? (Regular matrix)
267
     * A square matrix that is not singular. It has an inverse.
268
     * If the determinant is nonzero, then the matrix is nonsingular.
269
     * http://mathworld.wolfram.com/NonsingularMatrix.html
270
     *
271
     * @return bool true if nonsingular; false otherwise.
272
     *
273
     * @throws Exception\MatrixException
274
     * @throws Exception\IncorrectTypeException
275
     * @throws Exception\BadParameterException
276
     */
277
    public function isNonsingular(): bool
278
    {
279
        $│A│ = $this->det();
335✔
280

281
        if (Support::isNotZero($│A│, $this->ε)) {
335✔
282
            return true;
258✔
283
        }
284

285
        return false;
77✔
286
    }
287

288
    /**
289
     * Is the matrix invertible? (Regular nonsingular matrix)
290
     * Convenience method for isNonsingular.
291
     * https://en.wikipedia.org/wiki/Invertible_matrix
292
     * http://mathworld.wolfram.com/NonsingularMatrix.html
293
     *
294
     * @return bool true if invertible; false otherwise.
295
     *
296
     * @throws Exception\MatrixException
297
     * @throws Exception\IncorrectTypeException
298
     * @throws Exception\BadParameterException
299
     */
300
    public function isInvertible(): bool
301
    {
302
        return $this->isNonsingular();
253✔
303
    }
304

305
    /**
306
     * Is the matrix positive definite?
307
     *  - It is square and symmetric.
308
     *  - All principal minors have strictly positive determinants (> 0)
309
     *
310
     * Other facts:
311
     *  - All its eigenvalues are positive.
312
     *  - All its pivots are positive.
313
     *
314
     * https://en.wikipedia.org/wiki/Positive-definite_matrix
315
     * http://mathworld.wolfram.com/PositiveDefiniteMatrix.html
316
     * http://mat.gsia.cmu.edu/classes/QUANT/NOTES/chap1/node8.html
317
     * https://en.wikipedia.org/wiki/Sylvester%27s_criterion
318
     *
319
     * @return boolean true if positive definite; false otherwise
320
     *
321
     * @throws Exception\MatrixException
322
     * @throws Exception\OutOfBoundsException
323
     * @throws Exception\IncorrectTypeException
324
     * @throws Exception\BadParameterException
325
     */
326
    public function isPositiveDefinite(): bool
327
    {
328
        if (!$this->isSymmetric()) {
221✔
329
            return false;
7✔
330
        }
331

332
        for ($i = 1; $i <= $this->n; $i++) {
214✔
333
            if ($this->leadingPrincipalMinor($i)->det() <= 0) {
214✔
334
                return false;
7✔
335
            }
336
        }
337

338
        return true;
207✔
339
    }
340

341
    /**
342
     * Is the matrix positive semidefinite?
343
     *  - It is square and symmetric.
344
     *  - All principal minors have positive determinants (≥ 0)
345
     *
346
     * http://mathworld.wolfram.com/PositiveSemidefiniteMatrix.html
347
     *
348
     * @return boolean true if positive semidefinite; false otherwise
349
     *
350
     * @throws Exception\MatrixException
351
     * @throws Exception\OutOfBoundsException
352
     * @throws Exception\IncorrectTypeException
353
     * @throws Exception\BadParameterException
354
     */
355
    public function isPositiveSemidefinite(): bool
356
    {
357
        if (!$this->isSymmetric()) {
41✔
358
            return false;
2✔
359
        }
360

361
        for ($i = 1; $i <= $this->n; $i++) {
39✔
362
            if ($this->leadingPrincipalMinor($i)->det() < 0) {
39✔
363
                return false;
3✔
364
            }
365
        }
366

367
        return true;
36✔
368
    }
369

370
    /**
371
     * Is the matrix negative definite?
372
     *  - It is square and symmetric.
373
     *  - All principal minors have nonzero determinants and alternate in signs, starting with det(A₁) < 0
374
     *
375
     * http://mathworld.wolfram.com/NegativeDefiniteMatrix.html
376
     *
377
     * @return boolean true if negative definite; false otherwise
378
     *
379
     * @throws Exception\MatrixException
380
     * @throws Exception\OutOfBoundsException
381
     * @throws Exception\IncorrectTypeException
382
     * @throws Exception\BadParameterException
383
     */
384
    public function isNegativeDefinite(): bool
385
    {
386
        if (!$this->isSymmetric()) {
26✔
387
            return false;
2✔
388
        }
389

390
        for ($i = 1; $i <= $this->n; $i++) {
24✔
391
            switch ($i % 2) {
24✔
392
                case 1:
24✔
393
                    if ($this->leadingPrincipalMinor($i)->det() >= 0) {
24✔
394
                        return false;
3✔
395
                    }
396
                    break;
21✔
397
                case 0:
21✔
398
                    if ($this->leadingPrincipalMinor($i)->det() <= 0) {
21✔
399
                        return false;
1✔
400
                    }
401
                    break;
20✔
402
            }
403
        }
404

405
        return true;
20✔
406
    }
407

408
    /**
409
     * Is the matrix negative semidefinite?
410
     *  - It is square and symmetric.
411
     *  - All principal minors have determinants that alternate signs, starting with det(A₁) ≤ 0
412
     *
413
     * http://mathworld.wolfram.com/NegativeSemidefiniteMatrix.html
414
     *
415
     * @return boolean true if negative semidefinite; false otherwise
416
     *
417
     * @throws Exception\MatrixException
418
     * @throws Exception\OutOfBoundsException
419
     * @throws Exception\IncorrectTypeException
420
     * @throws Exception\BadParameterException
421
     */
422
    public function isNegativeSemidefinite(): bool
423
    {
424
        if (!$this->isSymmetric()) {
21✔
425
            return false;
2✔
426
        }
427

428
        for ($i = 1; $i <= $this->n; $i++) {
19✔
429
            switch ($i % 2) {
19✔
430
                case 1:
19✔
431
                    if ($this->leadingPrincipalMinor($i)->det() > 0) {
19✔
432
                        return false;
2✔
433
                    }
434
                    break;
17✔
435
                case 0:
17✔
436
                    if ($this->leadingPrincipalMinor($i)->det() < 0) {
17✔
437
                        return false;
1✔
438
                    }
439
                    break;
16✔
440
            }
441
        }
442

443
        return true;
16✔
444
    }
445

446
    /**
447
     * Is the matrix lower triangular?
448
     *  - It is a square matrix
449
     *  - All the entries above the main diagonal are zero
450
     *
451
     * https://en.wikipedia.org/wiki/Triangular_matrix
452
     *
453
     * @return boolean true if lower triangular; false otherwise
454
     */
455
    public function isLowerTriangular(): bool
456
    {
457
        if (!$this->isSquare()) {
1,176✔
458
            return false;
3✔
459
        }
460

461
        $m = $this->m;
1,173✔
462
        $n = $this->n;
1,173✔
463

464
        for ($i = 0; $i < $m; $i++) {
1,173✔
465
            for ($j = $i + 1; $j < $n; $j++) {
1,173✔
466
                if (!Support::isZero($this->A[$i][$j])) {
1,114✔
467
                    return false;
392✔
468
                }
469
            }
470
        }
471

472
        return true;
874✔
473
    }
474

475
    /**
476
     * Is the matrix upper triangular?
477
     *  - It is a square matrix
478
     *  - All the entries below the main diagonal are zero
479
     *
480
     * https://en.wikipedia.org/wiki/Triangular_matrix
481
     *
482
     * @return boolean true if upper triangular; false otherwise
483
     */
484
    public function isUpperTriangular(): bool
485
    {
486
        if (!$this->isSquare()) {
1,248✔
487
            return false;
2✔
488
        }
489

490
        $m = $this->m;
1,246✔
491

492
        for ($i = 1; $i < $m; $i++) {
1,246✔
493
            for ($j = 0; $j < $i; $j++) {
1,186✔
494
                if (!Support::isZero($this->A[$i][$j])) {
1,186✔
495
                    return false;
280✔
496
                }
497
            }
498
        }
499

500
        return true;
996✔
501
    }
502

503
    /**
504
     * Is the matrix triangular?
505
     * The matrix is either lower or upper triangular
506
     *
507
     * https://en.wikipedia.org/wiki/Triangular_matrix
508
     *
509
     * @return boolean true if triangular; false otherwise
510
     */
511
    public function isTriangular(): bool
512
    {
513
        return ($this->isLowerTriangular() || $this->isUpperTriangular());
229✔
514
    }
515

516
    /**
517
     * Is the matrix diagonal?
518
     *  - It is a square matrix
519
     *  - All the entries above the main diagonal are zero
520
     *  - All the entries below the main diagonal are zero
521
     *
522
     * http://mathworld.wolfram.com/DiagonalMatrix.html
523
     *
524
     * @return boolean true if diagonal; false otherwise
525
     */
526
    public function isDiagonal(): bool
527
    {
528
        return ($this->isLowerTriangular() && $this->isUpperTriangular());
252✔
529
    }
530

531
    /**
532
     * Is the retangular matrix diagonal?
533
     *  - All the entries below and above the main diagonal are zero
534
     *
535
     * https://en.wikipedia.org/wiki/Diagonal_matrix
536
     *
537
     * @return boolean true if rectangular diagonal
538
     */
539
    public function isRectangularDiagonal(): bool
540
    {
541
        $m = $this->m;
27✔
542
        $n = $this->n;
27✔
543
        for ($i = 0; $i < $m; $i++) {
27✔
544
            for ($j = 0; $j < $n; $j++) {
27✔
545
                if ($i !== $j && !Support::isZero($this->A[$i][$j])) {
27✔
546
                    return false;
4✔
547
                }
548
            }
549
        }
550
        return true;
23✔
551
    }
552

553
    /**
554
     * Is the matrix in row echelon form?
555
     *  - All nonzero rows are above any rows of all zeroes
556
     *  - The leading coefficient of a nonzero row is always strictly to the right of the leading coefficient of the row above it.
557
     *
558
     * https://en.wikipedia.org/wiki/Row_echelon_form
559
     *
560
     * @return boolean true if matrix is in row echelon form; false otherwise
561
     */
562
    public function isRef(): bool
563
    {
564
        $m           = $this->m;
399✔
565
        $n           = $this->n;
399✔
566
        $zero_row_ok = true;
399✔
567

568
        // All nonzero rows are above any rows of all zeroes
569
        for ($i = $m - 1; $i >= 0; $i--) {
399✔
570
            $zero_row = \count(\array_filter(
399✔
571
                $this->A[$i],
399✔
572
                function ($x) {
573
                    return $x != 0;
399✔
574
                }
399✔
575
            )) === 0;
399✔
576

577
            if (!$zero_row) {
399✔
578
                $zero_row_ok = false;
395✔
579
                continue;
395✔
580
            }
581

582
            if (!$zero_row_ok) {
130✔
583
                return false;
12✔
584
            }
585
        }
586

587
        // Leading coefficients to the right of rows above it
588
        $lc = -1;
387✔
589
        for ($i = 0; $i < $m; $i++) {
387✔
590
            for ($j = 0; $j < $n; $j++) {
387✔
591
                if ($this->A[$i][$j] != 0) {
387✔
592
                    if ($j <= $lc) {
383✔
593
                        return false;
33✔
594
                    }
595
                    $lc = $j;
383✔
596
                    continue 2;
383✔
597
                }
598
            }
599
        }
600

601
        return true;
354✔
602
    }
603

604
    /**
605
     * Is the matrix in reduced row echelon form?
606
     *  - It is in row echelon form
607
     *  - Leading coefficients are 1
608
     *  - Leading coefficients are the only nonzero entry in its column
609
     *
610
     * https://en.wikipedia.org/wiki/Row_echelon_form
611
     *
612
     * @return boolean true if matrix is in reduced row echelon form; false otherwise
613
     *
614
     * @throws Exception\MatrixException
615
     */
616
    public function isRref(): bool
617
    {
618
        // Row echelon form
619
        if (!$this->isRef()) {
271✔
620
            return false;
24✔
621
        }
622

623
        $m   = $this->m;
247✔
624
        $n   = $this->n;
247✔
625
        $lcs = [];
247✔
626

627
        // Leading coefficients are 1
628
        for ($i = 0; $i < $m; $i++) {
247✔
629
            for ($j = 0; $j < $n; $j++) {
247✔
630
                if ($this->A[$i][$j] == 0) {
247✔
631
                    continue;
237✔
632
                }
633
                if ($this->A[$i][$j] != 1) {
245✔
634
                    return false;
12✔
635
                }
636
                $lcs[] = $j;
238✔
637
                continue 2;
238✔
638
            }
639
        }
640

641
        // Leading coefficients are the only nonzero entry in its column
642
        foreach ($lcs as $j) {
235✔
643
            $column  = $this->getColumn($j);
233✔
644
            $entries = \array_filter($column);
233✔
645
            if (\count($entries) !== 1) {
233✔
646
                return false;
9✔
647
            }
648
            $entry = \array_shift($entries);
233✔
649
            if ($entry != 1) {
233✔
650
                return false;
×
651
            }
652
        }
653

654
        return true;
226✔
655
    }
656

657
    /**
658
     * Is the matrix idempotent?
659
     * A matrix that equals itself when squared.
660
     * https://en.wikipedia.org/wiki/Idempotent_matrix
661
     *
662
     * @return boolean true if matrix is idempotent; false otherwise
663
     *
664
     * @throws Exception\IncorrectTypeException
665
     * @throws Exception\MatrixException
666
     * @throws Exception\VectorException
667
     */
668
    public function isIdempotent(): bool
669
    {
670
        if (!$this->isSquare()) {
7✔
671
            return false;
1✔
672
        }
673
        $A² = $this->multiply($this);
6✔
674
        return $this->isEqual($A²);
6✔
675
    }
676

677
    /**
678
     * Is the matrix nilpotent?
679
     *
680
     * A square MxM matrix is nilpotent if it becomes the
681
     * zero matrix when raised to some power k ≤ M.
682
     *
683
     * Nilpotent matrices will have a zero trace for all k
684
     * https://en.wikipedia.org/wiki/Nilpotent_matrix
685
     *
686
     * @return boolean true if matrix is nilpotent; false otherwise
687
     *
688
     * @throws Exception\MathException
689
     */
690
    public function isNilpotent(): bool
691
    {
692
        if (!$this->isSquare() || $this->trace() !== 0) {
19✔
693
            return false;
2✔
694
        }
695

696
        $m    = $this->getM();
17✔
697
        $zero = MatrixFactory::zero($m, $m);
17✔
698
        if ($this->isEqual($zero)) {
17✔
699
            return true;
1✔
700
        }
701

702
        $A         = $this;
16✔
703
        $nilpotent = false;
16✔
704

705
        for ($i = 1; $i < $m; $i++) {
16✔
706
            $A = $A->multiply($this);
16✔
707
            if ($A->isEqual($zero)) {
16✔
708
                $nilpotent = true;
11✔
709
                break;
11✔
710
            }
711
            if ($A->trace() !== 0) {
10✔
712
                break;
5✔
713
            }
714
        }
715

716
        return $nilpotent;
16✔
717
    }
718

719
    /**
720
     * Is the matrix involutory?
721
     * A matrix that is its own inverse. That is, multiplication by matrix A is an involution if and only if A² = I
722
     * https://en.wikipedia.org/wiki/Involutory_matrix
723
     *
724
     * @return boolean true if matrix is involutory; false otherwise
725
     *
726
     * @throws Exception\OutOfBoundsException
727
     * @throws Exception\MathException
728
     */
729
    public function isInvolutory(): bool
730
    {
731
        $I  = MatrixFactory::identity($this->m);
70✔
732
        $A² = $this->multiply($this);
70✔
733

734
        return $A²->isEqual($I);
70✔
735
    }
736

737
    /**
738
     * Is the matrix a signature matrix?
739
     * A diagonal matrix whose diagonal elements are plus or minus 1.
740
     * https://en.wikipedia.org/wiki/Signature_matrix
741
     *
742
     *     | ±1  0  0 |
743
     * A = |  0 ±1  0 |
744
     *     |  0  0 ±1 |
745
     *
746
     * @return boolean true if matrix is a signature matrix; false otherwise
747
     */
748
    public function isSignature(): bool
749
    {
750
        for ($i = 0; $i < $this->m; $i++) {
42✔
751
            for ($j = 0; $j < $this->n; $j++) {
42✔
752
                if ($i == $j) {
42✔
753
                    if (!\in_array($this->A[$i][$j], [-1, 1])) {
42✔
754
                        return false;
42✔
755
                    }
756
                } else {
757
                    if ($this->A[$i][$j] != 0) {
34✔
758
                        return false;
7✔
759
                    }
760
                }
761
            }
762
        }
763

764
        return true;
28✔
765
    }
766

767
    /**
768
     * Is the matrix upper bidiagonal?
769
     *  - It is a square matrix
770
     *  - Non-zero entries allowed along the main diagonal
771
     *  - Non-zero entries allowed along the diagonal above the main diagonal
772
     *  - All the other entries are zero
773
     *
774
     * https://en.wikipedia.org/wiki/Bidiagonal_matrix
775
     *
776
     * @return boolean true if upper bidiagonal; false otherwise
777
     */
778
    public function isUpperBidiagonal(): bool
779
    {
780
        if (!$this->isSquare() || !$this->isUpperTriangular()) {
88✔
781
            return false;
40✔
782
        }
783

784
        $m = $this->m;
48✔
785
        $n = $this->n;
48✔
786

787
        // Elements above upper diagonal are zero
788
        for ($i = 0; $i < $m; $i++) {
48✔
789
            for ($j = $i + 2; $j < $n; $j++) {
48✔
790
                if ($this->A[$i][$j] != 0) {
35✔
791
                    return false;
5✔
792
                }
793
            }
794
        }
795

796
        return true;
43✔
797
    }
798

799
    /**
800
     * Is the matrix lower bidiagonal?
801
     *  - It is a square matrix
802
     *  - Non-zero entries allowed along the main diagonal
803
     *  - Non-zero entries allowed along the diagonal below the main diagonal
804
     *  - All the other entries are zero
805
     *
806
     * https://en.wikipedia.org/wiki/Bidiagonal_matrix
807
     *
808
     * @return boolean true if lower bidiagonal; false otherwise
809
     */
810
    public function isLowerBidiagonal(): bool
811
    {
812
        if (!$this->isSquare() || !$this->isLowerTriangular()) {
59✔
813
            return false;
25✔
814
        }
815

816
        // Elements below lower diagonal are non-zero
817
        for ($i = 2; $i < $this->m; $i++) {
34✔
818
            for ($j = 0; $j < $i - 1; $j++) {
26✔
819
                if ($this->A[$i][$j] != 0) {
26✔
820
                    return false;
5✔
821
                }
822
            }
823
        }
824

825
        return true;
29✔
826
    }
827

828
    /**
829
     * Is the matrix bidiagonal?
830
     *  - It is a square matrix
831
     *  - Non-zero entries along the main diagonal
832
     *  - Non-zero entries along either the diagonal above or the diagonal below the main diagonal
833
     *  - All the other entries are zero
834
     *
835
     * https://en.wikipedia.org/wiki/Bidiagonal_matrix
836
     *
837
     * @return boolean true if bidiagonal; false otherwise
838
     */
839
    public function isBidiagonal(): bool
840
    {
841
        return ($this->isUpperBidiagonal() || $this->isLowerBidiagonal());
35✔
842
    }
843

844
    /**
845
     * Is the matrix tridiagonal?
846
     *  - It is a square matrix
847
     *  - Non-zero entries allowed along the main diagonal
848
     *  - Non-zero entries allowed along the diagonal above the main diagonal
849
     *  - Non-zero entries allowed along the diagonal below the main diagonal
850
     *  - All the other entries are zero
851
     *
852
     *  - Is both upper and lower Hessenberg
853
     *
854
     * https://en.wikipedia.org/wiki/Tridiagonal_matrix
855
     *
856
     * @return boolean true if tridiagonal; false otherwise
857
     */
858
    public function isTridiagonal(): bool
859
    {
860
        if (!$this->isSquare()) {
52✔
861
            return false;
2✔
862
        }
863

864
        if (!$this->isUpperHessenberg() || !$this->isLowerHessenberg()) {
50✔
865
            return false;
18✔
866
        }
867

868
        return true;
32✔
869
    }
870

871
    /**
872
     * Is the matrix upper Hessenberg?
873
     *  - It is a square matrix
874
     *  - Has zero entries below the first subdiagonal
875
     *
876
     * https://en.wikipedia.org/wiki/Hessenberg_matrix
877
     *
878
     * @return boolean true if upper Hessenberg; false otherwise
879
     */
880
    public function isUpperHessenberg(): bool
881
    {
882
        if (!$this->isSquare()) {
86✔
883
            return false;
2✔
884
        }
885

886
        // Elements below lower diagonal are zero
887
        for ($i = 2; $i < $this->m; $i++) {
84✔
888
            for ($j = 0; $j < $i - 1; $j++) {
71✔
889
                if ($this->A[$i][$j] != 0) {
71✔
890
                    return false;
19✔
891
                }
892
            }
893
        }
894

895
        return true;
65✔
896
    }
897

898
    /**
899
     * Is the matrix lower Hessenberg?
900
     *  - It is a square matrix
901
     *  - Has zero entries above the first subdiagonal
902
     *
903
     * https://en.wikipedia.org/wiki/Hessenberg_matrix
904
     *
905
     * @return boolean true if lower Hessenberg; false otherwise
906
     */
907
    public function isLowerHessenberg(): bool
908
    {
909
        if (!$this->isSquare()) {
89✔
910
            return false;
2✔
911
        }
912

913
        // Elements above upper diagonal are zero
914
        for ($i = 0; $i < $this->m; $i++) {
87✔
915
            for ($j = $i + 2; $j < $this->n; $j++) {
87✔
916
                if ($this->A[$i][$j] != 0) {
73✔
917
                    return false;
23✔
918
                }
919
            }
920
        }
921

922
        return true;
64✔
923
    }
924

925
    /**
926
     * Is the matrix orthogonal?
927
     *  - It is a square matrix
928
     *  - AAᵀ = AᵀA = I
929
     *
930
     * @return bool
931
     *
932
     * @throws Exception\MathException
933
     */
934
    public function isOrthogonal(): bool
935
    {
936
        if (!$this->isSquare()) {
164✔
937
            return false;
2✔
938
        }
939

940
        // AAᵀ = I
941
        $I   = MatrixFactory::identity($this->m);
162✔
942
        $Aᵀ  = $this->transpose();
162✔
943
        $AAᵀ = $this->multiply($Aᵀ);
162✔
944

945
        return $AAᵀ->isEqual($I);
162✔
946
    }
947

948
    /**
949
     * Is the matrix normal?
950
     *  - It is a square matrix
951
     *  - AAᴴ = AᴴA
952
     *
953
     * https://en.wikipedia.org/wiki/Normal_matrix
954
     * @return bool
955
     *
956
     * @throws Exception\MathException
957
     */
958
    public function isNormal(): bool
959
    {
960
        if (!$this->isSquare()) {
43✔
961
            return false;
2✔
962
        }
963

964
        // AAᴴ = AᴴA
965
        $Aᴴ  = $this->conjugateTranspose();
41✔
966
        $AAᴴ = $this->multiply($Aᴴ);
41✔
967
        $AᴴA = $Aᴴ->multiply($this);
41✔
968

969
        return $AAᴴ->isEqual($AᴴA);
41✔
970
    }
971

972
    /**
973
     * Is the matrix unitary?
974
     *  - It is a square matrix
975
     *  - AAᴴ = AᴴA = I
976
     *
977
     * https://en.wikipedia.org/wiki/Unitary_matrix
978
     * @return bool
979
     *
980
     * @throws Exception\MathException
981
     */
982
    public function isUnitary(): bool
983
    {
984
        if (!$this->isSquare()) {
16✔
985
            return false;
2✔
986
        }
987

988
        // AAᴴ = AᴴA = I
989
        $Aᴴ  = $this->conjugateTranspose();
14✔
990
        $AAᴴ = $this->multiply($Aᴴ);
14✔
991
        $AᴴA = $Aᴴ->multiply($this);
14✔
992

993
        $I = MatrixFactory::identity($this->m);
14✔
994
        return $AAᴴ->isEqual($AᴴA) && $AAᴴ->isEqual($I);
14✔
995
    }
996

997
    /**
998
     * Is the matrix Hermitian?
999
     *  - It is a square matrix
1000
     *  - A = Aᴴ
1001
     *
1002
     * https://en.wikipedia.org/wiki/Hermitian_matrix
1003
     * @return bool
1004
     *
1005
     * @throws Exception\MathException
1006
     */
1007
    public function isHermitian(): bool
1008
    {
1009
        if (!$this->isSquare()) {
20✔
1010
            return false;
6✔
1011
        }
1012

1013
        // A = Aᴴ
1014
        $Aᴴ  = $this->conjugateTranspose();
14✔
1015

1016
        return $this->isEqual($Aᴴ);
14✔
1017
    }
1018

1019
    /**************************************************************************
1020
     * MATRIX AUGMENTATION - Return a Matrix
1021
     *  - augmentIdentity
1022
     **************************************************************************/
1023

1024
    /**
1025
     * Augment a matrix with its identity matrix
1026
     *
1027
     *     [1, 2, 3]
1028
     * C = [2, 3, 4]
1029
     *     [3, 4, 5]
1030
     *
1031
     *         [1, 2, 3 | 1, 0, 0]
1032
     * (C|I) = [2, 3, 4 | 0, 1, 0]
1033
     *         [3, 4, 5 | 0, 0, 1]
1034
     *
1035
     * C must be a square matrix
1036
     *
1037
     * @return NumericMatrix
1038
     *
1039
     * @throws Exception\MatrixException if matrix is not square
1040
     * @throws Exception\IncorrectTypeException
1041
     * @throws Exception\OutOfBoundsException
1042
     */
1043
    public function augmentIdentity(): NumericMatrix
1044
    {
1045
        if (!$this->isSquare()) {
565✔
1046
            throw new Exception\MatrixException('Matrix is not square; cannot augment with the identity matrix');
1✔
1047
        }
1048

1049
        return $this->augment(MatrixFactory::identity($this->getM()));
564✔
1050
    }
1051

1052
    /**************************************************************************
1053
     * MATRIX ARITHMETIC OPERATIONS - Return a Matrix
1054
     *  - add
1055
     *  - directSum
1056
     *  - kroneckerSum
1057
     *  - subtract
1058
     *  - multiply
1059
     *  - scalarMultiply
1060
     *  - scalarDivide
1061
     *  - hadamardProduct
1062
     *  - kroneckerProduct
1063
     **************************************************************************/
1064

1065
    /**
1066
     * Add two matrices - Entrywise sum
1067
     * Adds each element of one matrix to the same element in the other matrix.
1068
     * Returns a new matrix.
1069
     * https://en.wikipedia.org/wiki/Matrix_addition#Entrywise_sum
1070
     *
1071
     * @param NumericMatrix $B Matrix to add to this matrix
1072
     *
1073
     * @return NumericMatrix
1074
     *
1075
     * @throws Exception\MatrixException if matrices have a different number of rows or columns
1076
     * @throws Exception\IncorrectTypeException
1077
     * @throws Exception\MathException
1078
     */
1079
    public function add($B): NumericMatrix
1080
    {
1081
        if ($B->getM() !== $this->m) {
1,090✔
1082
            throw new Exception\MatrixException('Matrices have different number of rows');
1✔
1083
        }
1084
        if ($B->getN() !== $this->n) {
1,089✔
1085
            throw new Exception\MatrixException('Matrices have different number of columns');
1✔
1086
        }
1087

1088
        $R = [];
1,088✔
1089

1090
        for ($i = 0; $i < $this->m; $i++) {
1,088✔
1091
            for ($j = 0; $j < $this->n; $j++) {
1,088✔
1092
                $R[$i][$j] = $this->A[$i][$j] + $B[$i][$j];
1,088✔
1093
            }
1094
        }
1095

1096
        return MatrixFactory::createNumeric($R, $this->ε);
1,088✔
1097
    }
1098

1099
    /**
1100
     * Direct sum of two matrices: A ⊕ B
1101
     * The direct sum of any pair of matrices A of size m × n and B of size p × q
1102
     * is a matrix of size (m + p) × (n + q)
1103
     * https://en.wikipedia.org/wiki/Matrix_addition#Direct_sum
1104
     *
1105
     * @param  NumericMatrix $B Matrix to add to this matrix
1106
     *
1107
     * @return NumericMatrix
1108
     *
1109
     * @throws Exception\IncorrectTypeException
1110
     */
1111
    public function directSum(NumericMatrix $B): NumericMatrix
1112
    {
1113
        $m = $this->m + $B->getM();
2✔
1114
        $n = $this->n + $B->getN();
2✔
1115

1116
        $R = [];
2✔
1117

1118
        for ($i = 0; $i < $m; $i++) {
2✔
1119
            for ($j = 0; $j < $n; $j++) {
2✔
1120
                $R[$i][$j] = 0;
2✔
1121
            }
1122
        }
1123
        for ($i = 0; $i < $this->m; $i++) {
2✔
1124
            for ($j = 0; $j < $this->n; $j++) {
2✔
1125
                $R[$i][$j] = $this->A[$i][$j];
2✔
1126
            }
1127
        }
1128

1129
        $m = $B->getM();
2✔
1130
        $n = $B->getN();
2✔
1131
        for ($i = 0; $i < $m; $i++) {
2✔
1132
            for ($j = 0; $j < $n; $j++) {
2✔
1133
                $R[$i + $this->m][$j + $this->n] = $B[$i][$j];
2✔
1134
            }
1135
        }
1136

1137
        return MatrixFactory::createNumeric($R, $this->ε);
2✔
1138
    }
1139

1140
    /**
1141
     * Kronecker Sum (A⊕B)
1142
     * A⊕B = A⊗Ib + I⊗aB
1143
     * Where A and B are square matrices, Ia and Ib are identity matrixes,
1144
     * and ⊗ is the Kronecker product.
1145
     *
1146
     * https://en.wikipedia.org/wiki/Matrix_addition#Kronecker_sum
1147
     * http://mathworld.wolfram.com/KroneckerSum.html
1148
     *
1149
     * @param NumericMatrix $B Square matrix
1150
     *
1151
     * @return NumericMatrix
1152
     *
1153
     * @throws Exception\MatrixException if either matrix is not a square matrix
1154
     * @throws Exception\OutOfBoundsException
1155
     * @throws Exception\IncorrectTypeException
1156
     * @throws Exception\BadDataException
1157
     */
1158
    public function kroneckerSum(NumericMatrix $B): NumericMatrix
1159
    {
1160
        if (!$this->isSquare() || !$B->isSquare()) {
12✔
1161
            throw new Exception\MatrixException('Matrices A and B must both be square for kroneckerSum');
3✔
1162
        }
1163

1164
        $A  = $this;
9✔
1165
        $m  = $B->getM();
9✔
1166
        $n  = $this->n;
9✔
1167

1168
        $In = MatrixFactory::identity($n);
9✔
1169
        $Im = MatrixFactory::identity($m);
9✔
1170

1171
        $A⊗Im = $A->kroneckerProduct($Im);
9✔
1172
        $In⊗B = $In->kroneckerProduct($B);
9✔
1173
        $A⊕B  = $A⊗Im->add($In⊗B);
9✔
1174

1175
        return $A⊕B;
9✔
1176
    }
1177

1178
    /**
1179
     * Subtract two matrices - Entrywise subtraction
1180
     * Adds each element of one matrix to the same element in the other matrix.
1181
     * Returns a new matrix.
1182
     * https://en.wikipedia.org/wiki/Matrix_addition#Entrywise_sum
1183
     *
1184
     * @param NumericMatrix $B Matrix to subtract from this matrix
1185
     *
1186
     * @return NumericMatrix
1187
     *
1188
     * @throws Exception\MatrixException if matrices have a different number of rows or columns
1189
     * @throws Exception\IncorrectTypeException
1190
     */
1191
    public function subtract($B): NumericMatrix
1192
    {
1193
        if ($B->getM() !== $this->m) {
1,049✔
1194
            throw new Exception\MatrixException('Matrices have different number of rows');
1✔
1195
        }
1196
        if ($B->getN() !== $this->n) {
1,048✔
1197
            throw new Exception\MatrixException('Matrices have different number of columns');
1✔
1198
        }
1199

1200
        $R = [];
1,047✔
1201

1202
        for ($i = 0; $i < $this->m; $i++) {
1,047✔
1203
            for ($j = 0; $j < $this->n; $j++) {
1,047✔
1204
                $R[$i][$j] = $this->A[$i][$j] - $B[$i][$j];
1,047✔
1205
            }
1206
        }
1207
        return MatrixFactory::createNumeric($R, $this->ε);
1,047✔
1208
    }
1209

1210
    /**
1211
     * Matrix multiplication - ijk algorithm using cache-oblivious algorithm optimization
1212
     * https://en.wikipedia.org/wiki/Matrix_multiplication
1213
     * https://en.wikipedia.org/wiki/Cache-oblivious_algorithm
1214
     *
1215
     * Gene H. Golub and Charles F. Van Loan (2013). "Matrix Computations 4th Edition" - The Johns Hopkins University Press
1216
     *   - 1.1.10–15 Matrix-Matrix Multiplication
1217
     *   - 1.5 Vectorization and Locality (1.5.4 Blocking for Data Reuse)
1218
     * Park, Liuy, Prasanna, Raghavendra. "Efficient Matrix Multiplication Using Cache Conscious Data Layouts"
1219
     *   (http://www.cs.technion.ac.il/~itai/Courses/Cache/matrix-mult.pdf)
1220
     *
1221
     * ijk is the classic matrix multiplication algorithm using triply nested loops.
1222
     * Iterate the rows of A; iterate the columns of B; iterate the common dimension columns of A/rows of B.
1223
     *
1224
     * A ∈ ℝᵐˣʳ  B ∈ ℝʳˣⁿ  R ∈ ℝᵐˣ
1225
     * for i = 1:m
1226
     *   for j = 1:n
1227
     *     for k - 1:r
1228
     *       R(i,j) = R(i,j) + A(i,k)⋅B(k,j)
1229
     *
1230
     * Cache-oblivious matrix algorithms recognize the cost of thrashing data between memory to high-speed cache.
1231
     * Matrices are implemented using PHP arrays, as rows of arrays, representing data from each column.
1232
     * Transposing the matrix B and traversing it along its transposed rows rather than down columns will have fewer
1233
     * operations to move values between internal memory hierarchies, leading to significant performance gains for
1234
     * larger matrices on most computer hardware.
1235
     *
1236
     * Consider the standard way to think about matrix-matrix multiplication where each resultant matrix element
1237
     * is computed as the dot product:
1238
     *
1239
     *     A        B                    R
1240
     * [ 1  2 ] [ 5  6 ]     [ 1⋅5 + 2⋅7  1⋅6 + 2⋅8 ]
1241
     * [ 3  4 ] [ 7  8 ]  =  [ 3⋅5 + 4⋅7  3⋅6 + 4⋅8 ]
1242
     *
1243
     * The element of R[0][0] traverses A by row and B by column: 1⋅5 + 2⋅7
1244
     *    A       B                   R
1245
     * [ → → ] [ ↓  ]       [ (1  2) ] [ (5)  6 ]
1246
     * [     ] [ ↓  ]       [  3  4  ] [ (7)  8 ]
1247
     *
1248
     * To traverse B by column, a single element of each array is required. Considering that arrays are implemented
1249
     * with contiguous memory allocations and moved into cache in blocks, it is highly probable to have fewer memory-
1250
     * to-cache movement operations if we could also traverse B by row rather than by column.
1251
     * Therefore, if we transpose B, we will traverse both A and B by row, which may lead to fewer operations to move
1252
     * values between internal memory hierarchies.
1253
     *
1254
     * Then, the element of R[0][0] now traverses A by row and Bᵀ by row (which represents a column of B): 1⋅5 + 2⋅7
1255
     *    A       B                  R
1256
     * [ → → ] [ → → ]     [ (1  2) ] [ (5) (7) ]
1257
     * [     ] [     ]     [  3  4  ] [  6   8  ]
1258
     *
1259
     * @param  NumericMatrix|Vector $B Matrix or Vector to multiply
1260
     *
1261
     * @return NumericMatrix
1262
     *
1263
     * @throws Exception\IncorrectTypeException if parameter B is not a Matrix or Vector
1264
     * @throws Exception\MatrixException if matrix dimensions do not match
1265
     * @throws Exception\MathException
1266
     */
1267
    public function multiply($B): NumericMatrix
1268
    {
1269
        // @phpstan-ignore-next-line
1270
        if ((!$B instanceof NumericMatrix) && (!$B instanceof Vector)) {
2,830✔
1271
            throw new Exception\IncorrectTypeException('Can only do matrix multiplication with a Matrix or Vector');
1✔
1272
        }
1273
        if ($B instanceof Vector) {
2,829✔
1274
            $B = $B->asColumnMatrix();
536✔
1275
        }
1276
        if ($B->getM() !== $this->n) {
2,829✔
1277
            throw new Exception\MatrixException("Matrix dimensions do not match");
1✔
1278
        }
1279

1280
        $R = [];
2,828✔
1281
        $Bᵀ = $B->transpose()->getMatrix();
2,828✔
1282

1283
        foreach ($this->A as $i => $Aʳᵒʷ⟦i⟧) {
2,828✔
1284
            /** @var array<number> $R */
1285
            $R[$i] = \array_fill(0, $B->n, 0);
2,828✔
1286
            foreach ($Bᵀ as $j => $Bᶜᵒˡ⟦j⟧) {
2,828✔
1287
                foreach ($Aʳᵒʷ⟦i⟧ as $k => $A⟦i⟧⟦k⟧) {
2,828✔
1288
                    $R[$i][$j] += $A⟦i⟧⟦k⟧ * $Bᶜᵒˡ⟦j⟧[$k];
2,828✔
1289
                }
1290
            }
1291
        }
1292

1293
        return MatrixFactory::createNumeric($R, $this->ε);
2,828✔
1294
    }
1295

1296
    /**
1297
     * Scalar matrix multiplication
1298
     * https://en.wikipedia.org/wiki/Matrix_multiplication#Scalar_multiplication
1299
     *
1300
     * @param  float $λ
1301
     *
1302
     * @return NumericMatrix
1303
     *
1304
     * @throws Exception\BadParameterException if λ is not a number
1305
     * @throws Exception\IncorrectTypeException
1306
     */
1307
    public function scalarMultiply(float $λ): NumericMatrix
1308
    {
1309
        $R = [];
1,735✔
1310

1311
        for ($i = 0; $i < $this->m; $i++) {
1,735✔
1312
            for ($j = 0; $j < $this->n; $j++) {
1,735✔
1313
                $R[$i][$j] = $this->A[$i][$j] * $λ;
1,735✔
1314
            }
1315
        }
1316

1317
        return MatrixFactory::createNumeric($R, $this->ε);
1,735✔
1318
    }
1319

1320
    /**
1321
     * Negate a matrix
1322
     * −A = −1A
1323
     *
1324
     * @return NumericMatrix
1325
     *
1326
     * @throws Exception\BadParameterException
1327
     * @throws Exception\IncorrectTypeException
1328
     */
1329
    public function negate(): NumericMatrix
1330
    {
1331
        return $this->scalarMultiply(-1);
9✔
1332
    }
1333

1334
    /**
1335
     * Scalar matrix division
1336
     *
1337
     * @param  float $λ
1338
     *
1339
     * @return NumericMatrix
1340
     *
1341
     * @throws Exception\BadParameterException if λ is not a number
1342
     * @throws Exception\BadParameterException if λ is 0
1343
     * @throws Exception\IncorrectTypeException
1344
     */
1345
    public function scalarDivide(float $λ): NumericMatrix
1346
    {
1347
        if ($λ == 0) {
178✔
1348
            throw new Exception\BadParameterException('Parameter λ cannot equal 0');
1✔
1349
        }
1350

1351
        $R = [];
177✔
1352

1353
        for ($i = 0; $i < $this->m; $i++) {
177✔
1354
            for ($j = 0; $j < $this->n; $j++) {
177✔
1355
                $R[$i][$j] = $this->A[$i][$j] / $λ;
177✔
1356
            }
1357
        }
1358

1359
        return MatrixFactory::createNumeric($R, $this->ε);
177✔
1360
    }
1361

1362
    /**
1363
     * Hadamard product (A∘B)
1364
     * Also known as the Schur product, or the entrywise product
1365
     *
1366
     * A binary operation that takes two matrices of the same dimensions,
1367
     * and produces another matrix where each element ij is the product of
1368
     * elements ij of the original two matrices.
1369
     * https://en.wikipedia.org/wiki/Hadamard_product_(matrices)
1370
     *
1371
     * (A∘B)ᵢⱼ = (A)ᵢⱼ(B)ᵢⱼ
1372
     *
1373
     * @param NumericMatrix $B
1374
     *
1375
     * @return NumericMatrix
1376
     *
1377
     * @throws Exception\MatrixException if matrices are not the same dimensions
1378
     * @throws Exception\IncorrectTypeException
1379
     */
1380
    public function hadamardProduct(NumericMatrix $B): NumericMatrix
1381
    {
1382
        if ($B->getM() !== $this->m || $B->getN() !== $this->n) {
4✔
1383
            throw new Exception\MatrixException('Matrices are not the same dimensions');
1✔
1384
        }
1385

1386
        $m   = $this->m;
3✔
1387
        $n   = $this->n;
3✔
1388
        $A   = $this->A;
3✔
1389
        $B   = $B->getMatrix();
3✔
1390
        $A∘B = [];
3✔
1391

1392
        for ($i = 0; $i < $m; $i++) {
3✔
1393
            for ($j = 0; $j < $n; $j++) {
3✔
1394
                $A∘B[$i][$j] = $A[$i][$j] * $B[$i][$j];
3✔
1395
            }
1396
        }
1397

1398
        return MatrixFactory::createNumeric($A∘B, $this->ε);
3✔
1399
    }
1400

1401
    /**
1402
     * Kronecker product (A⊗B)
1403
     *
1404
     * If A is an m × n matrix and B is a p × q matrix,
1405
     * then the Kronecker product A ⊗ B is the mp × nq block matrix:
1406
     *
1407
     *       [a₁₁b₁₁ a₁₁b₁₂ ⋯ a₁₁b₁q ⋯ ⋯ a₁nb₁₁ a₁nb₁₂ ⋯ a₁nb₁q]
1408
     *       [a₁₁b₂₁ a₁₁b₂₂ ⋯ a₁₁b₂q ⋯ ⋯ a₁nb₂₁ a₁nb₂₂ ⋯ a₁nb₂q]
1409
     *       [  ⋮       ⋮    ⋱  ⋮           ⋮      ⋮    ⋱   ⋮   ]
1410
     *       [a₁₁bp₁ a₁₁bp₂ ⋯ a₁₁bpq ⋯ ⋯ a₁nbp₁ a₁nbp₂ ⋯ a₁nbpq]
1411
     * A⊗B = [  ⋮       ⋮       ⋮     ⋱     ⋮      ⋮        ⋮   ]
1412
     *       [  ⋮       ⋮       ⋮       ⋱   ⋮      ⋮        ⋮   ]
1413
     *       [am₁b₁₁ am₁b₁₂ ⋯ am₁b₁q ⋯ ⋯ amnb₁₁ amnb₁₂ ⋯ amnb₁q]
1414
     *       [am₁b₂₁ am₁b₂₂ ⋯ am₁b₂q ⋯ ⋯ amnb₂₁ amnb₂₂ ⋯ amnb₂q]
1415
     *       [  ⋮       ⋮    ⋱  ⋮           ⋮      ⋮    ⋱   ⋮   ]
1416
     *       [am₁bp₁ am₁bp₂ ⋯ am₁bpq ⋯ ⋯ amnbp₁ amnbp₂ ⋯ amnbpq]
1417
     *
1418
     * https://en.wikipedia.org/wiki/Kronecker_product
1419
     *
1420
     * @param NumericMatrix $B
1421
     *
1422
     * @return NumericMatrix
1423
     *
1424
     * @throws Exception\BadDataException
1425
     */
1426
    public function kroneckerProduct(NumericMatrix $B): NumericMatrix
1427
    {
1428
        // Compute each element of the block matrix
1429
        $arrays = [];
83✔
1430
        for ($m = 0; $m < $this->m; $m++) {
83✔
1431
            $row = [];
83✔
1432
            for ($n = 0; $n < $this->n; $n++) {
83✔
1433
                $R = [];
83✔
1434
                for ($p = 0; $p < $B->getM(); $p++) {
83✔
1435
                    for ($q = 0; $q < $B->getN(); $q++) {
83✔
1436
                        $R[$p][$q] = $this->A[$m][$n] * $B[$p][$q];
83✔
1437
                    }
1438
                }
1439
                $row[] = new NumericMatrix($R);
83✔
1440
            }
1441
            $arrays[] = $row;
83✔
1442
        }
1443

1444
        // Augment each aᵢ₁ to aᵢn block
1445
        $matrices = [];
83✔
1446
        foreach ($arrays as $row) {
83✔
1447
            /** @var NumericMatrix $initial_matrix */
1448
            $initial_matrix = \array_shift($row);
83✔
1449
            $matrices[] = \array_reduce(
83✔
1450
                $row,
83✔
1451
                function (NumericMatrix $augmented_matrix, NumericMatrix $matrix) {
1452
                    return $augmented_matrix->augment($matrix);
60✔
1453
                },
83✔
1454
                $initial_matrix
83✔
1455
            );
1456
        }
1457

1458
        // Augment below each row block a₁ to am
1459
        /** @var NumericMatrix $initial_matrix */
1460
        $initial_matrix = \array_shift($matrices);
83✔
1461
        $A⊗B            = \array_reduce(
83✔
1462
            $matrices,
83✔
1463
            function (NumericMatrix $augmented_matrix, NumericMatrix $matrix) {
1464
                return $augmented_matrix->augmentBelow($matrix);
74✔
1465
            },
83✔
1466
            $initial_matrix
83✔
1467
        );
1468

1469
        return $A⊗B;
83✔
1470
    }
1471

1472
    /**************************************************************************
1473
     * MATRIX OPERATIONS - Return a Matrix
1474
     *  - diagonal
1475
     *  - inverse
1476
     *  - cofactorMatrix
1477
     *  - meanDeviation
1478
     *  - covarianceMatrix
1479
     *  - adjugate
1480
     *  - householder
1481
     **************************************************************************/
1482

1483
    /**
1484
     * Diagonal matrix
1485
     * Retains the elements along the main diagonal.
1486
     * All other off-diagonal elements are zeros.
1487
     *
1488
     * @return NumericMatrix
1489
     *
1490
     * @throws Exception\IncorrectTypeException
1491
     */
1492
    public function diagonal(): NumericMatrix
1493
    {
1494
        $m = $this->m;
4✔
1495
        $n = $this->n;
4✔
1496
        $R = [];
4✔
1497

1498
        for ($i = 0; $i < $m; $i++) {
4✔
1499
            for ($j = 0; $j < $n; $j++) {
4✔
1500
                $R[$i][$j] = ($i == $j) ? $this->A[$i][$j] : 0;
4✔
1501
            }
1502
        }
1503

1504
        return MatrixFactory::createNumeric($R, $this->ε);
4✔
1505
    }
1506

1507
    /**
1508
     * Inverse
1509
     *
1510
     * For a 1x1 matrix
1511
     *  A   = [a]
1512
     *  A⁻¹ = [1/a]
1513
     *
1514
     * For a 2x2 matrix:
1515
     *      [a b]
1516
     *  A = [c d]
1517
     *
1518
     *         1
1519
     *  A⁻¹ = --- [d -b]
1520
     *        │A│ [-c a]
1521
     *
1522
     * For a 3x3 matrix or larger:
1523
     * Augment with identity matrix and calculate reduced row echelon form.
1524
     *
1525
     * @return NumericMatrix
1526
     *
1527
     * @throws Exception\MatrixException if not a square matrix
1528
     * @throws Exception\MatrixException if singular matrix
1529
     * @throws Exception\IncorrectTypeException
1530
     * @throws Exception\BadParameterException
1531
     * @throws Exception\OutOfBoundsException
1532
     */
1533
    public function inverse(): NumericMatrix
1534
    {
1535
        if ($this->catalog->hasInverse()) {
973✔
1536
            /** @var NumericMatrix */
1537
            return $this->catalog->getInverse();
14✔
1538
        }
1539

1540
        if (!$this->isSquare()) {
973✔
1541
            throw new Exception\MatrixException('Not a square matrix (required for determinant)');
1✔
1542
        }
1543
        if ($this->isSingular()) {
972✔
1544
            throw new Exception\MatrixException('Singular matrix (determinant = 0); not invertible');
3✔
1545
        }
1546

1547
        $m   = $this->m;
969✔
1548
        $n   = $this->n;
969✔
1549
        $A   = $this->A;
969✔
1550
        $│A│ = $this->det();
969✔
1551

1552
         // 1x1 matrix: A⁻¹ = [1 / a]
1553
        if ($m === 1) {
969✔
1554
            $a   = $A[0][0];
50✔
1555
            $A⁻¹ = MatrixFactory::createNumeric([[1 / $a]], $this->ε);
50✔
1556
            $this->catalog->addInverse($A⁻¹);
50✔
1557
            return $A⁻¹;
50✔
1558
        }
1559

1560
        /*
1561
         * 2x2 matrix:
1562
         *      [a b]
1563
         *  A = [c d]
1564
         *
1565
         *        1
1566
         * A⁻¹ = --- [d -b]
1567
         *       │A│ [-c a]
1568
         */
1569
        if ($m === 2) {
920✔
1570
            $a = $A[0][0];
361✔
1571
            $b = $A[0][1];
361✔
1572
            $c = $A[1][0];
361✔
1573
            $d = $A[1][1];
361✔
1574

1575
            $R = MatrixFactory::createNumeric(
361✔
1576
                [
1577
                    [$d, -$b],
361✔
1578
                    [-$c, $a],
361✔
1579
                ],
1580
                $this->ε
361✔
1581
            );
1582
            $A⁻¹ = $R->scalarMultiply(1 / $│A│);
361✔
1583

1584
            $this->catalog->addInverse($A⁻¹);
361✔
1585
            return $A⁻¹;
361✔
1586
        }
1587

1588
        // nxn matrix 3x3 or larger
1589
        $R   = $this->augmentIdentity()->rref();
561✔
1590
        $A⁻¹ = [];
561✔
1591

1592
        for ($i = 0; $i < $n; $i++) {
561✔
1593
            $A⁻¹[$i] = \array_slice($R[$i], $n);
561✔
1594
        }
1595

1596
        $A⁻¹ = MatrixFactory::createNumeric($A⁻¹, $this->ε);
561✔
1597

1598
        $this->catalog->addInverse($A⁻¹);
561✔
1599
        return $A⁻¹;
561✔
1600
    }
1601

1602
    /**
1603
     * Cofactor matrix
1604
     * A matrix where each element is a cofactor.
1605
     *
1606
     *     [A₀₀ A₀₁ A₀₂]
1607
     * A = [A₁₀ A₁₁ A₁₂]
1608
     *     [A₂₀ A₂₁ A₂₂]
1609
     *
1610
     *      [C₀₀ C₀₁ C₀₂]
1611
     * CM = [C₁₀ C₁₁ C₁₂]
1612
     *      [C₂₀ C₂₁ C₂₂]
1613
     *
1614
     * @return NumericMatrix
1615
     *
1616
     * @throws Exception\MatrixException if matrix is not square
1617
     * @throws Exception\IncorrectTypeException
1618
     * @throws Exception\BadParameterException
1619
     */
1620
    public function cofactorMatrix(): NumericMatrix
1621
    {
1622
        if (!$this->isSquare()) {
456✔
1623
            throw new Exception\MatrixException('Matrix is not square; cannot get cofactor Matrix of a non-square matrix');
1✔
1624
        }
1625
        if ($this->n === 1) {
455✔
1626
            throw new Exception\MatrixException('Matrix must be 2x2 or greater to compute cofactorMatrix');
1✔
1627
        }
1628

1629
        $m = $this->m;
454✔
1630
        $n = $this->n;
454✔
1631
        $R = [];
454✔
1632

1633
        for ($i = 0; $i < $m; $i++) {
454✔
1634
            for ($j = 0; $j < $n; $j++) {
454✔
1635
                $R[$i][$j] = $this->cofactor($i, $j);
454✔
1636
            }
1637
        }
1638

1639
        return MatrixFactory::createNumeric($R, $this->ε);
454✔
1640
    }
1641

1642
    /**
1643
     * Mean deviation matrix
1644
     * Matrix as an array of column vectors, each subtracted by the sample mean.
1645
     *
1646
     * Example:
1647
     *      [1  4 7 8]      [5]
1648
     *  A = [2  2 8 4]  M = [4]
1649
     *      [1 13 1 5]      [5]
1650
     *
1651
     *      |[1] - [5]   [4]  - [5]   [7] - [5]   [8] - [5]|
1652
     *  B = |[2] - [4]   [2]  - [4]   [8] - [4]   [4] - [4]|
1653
     *      |[1] - [5]   [13] - [5]   [1] - [5]   [5] - [5]|
1654
     *
1655
     *      [-4 -1  2 3]
1656
     *  B = [-2 -2  4 0]
1657
     *      [-2  8 -4 0]
1658
     *
1659
     * @param string $direction Optional specification if to calculate along rows or columns
1660
     *
1661
     * @return NumericMatrix
1662
     *
1663
     * @throws Exception\BadParameterException if direction is not rows or columns
1664
     */
1665
    public function meanDeviation(string $direction = 'rows'): NumericMatrix
1666
    {
1667
        if (!\in_array($direction, [self::ROWS, self::COLUMNS])) {
8✔
1668
            throw new Exception\BadParameterException("Direction must be rows or columns, got $direction");
1✔
1669
        }
1670

1671
        return $direction === self::ROWS
7✔
1672
            ? $this->meanDeviationOfRowVariables()
3✔
1673
            : $this->meanDeviationOfColumnVariables();
7✔
1674
    }
1675

1676
    /**
1677
     * Mean deviation matrix
1678
     * Matrix as an array of column vectors, where rows represent variables and columns represent samples.
1679
     * Each column vector is subtracted by the sample mean.
1680
     *
1681
     * Example:
1682
     *      [1  4 7 8]      [5]
1683
     *  A = [2  2 8 4]  M = [4]
1684
     *      [1 13 1 5]      [5]
1685
     *
1686
     *      |[1] - [5]   [4]  - [5]   [7] - [5]   [8] - [5]|
1687
     *  B = |[2] - [4]   [2]  - [4]   [8] - [4]   [4] - [4]|
1688
     *      |[1] - [5]   [13] - [5]   [1] - [5]   [5] - [5]|
1689
     *
1690
     *      [-4 -1  2 3]
1691
     *  B = [-2 -2  4 0]
1692
     *      [-2  8 -4 0]
1693
     *
1694
     * @return NumericMatrix
1695
     *
1696
     * @throws Exception\IncorrectTypeException
1697
     */
1698
    public function meanDeviationOfRowVariables(): NumericMatrix
1699
    {
1700
        $X = $this->asVectors();
26✔
1701
        $M = $this->rowMeans();
26✔
1702

1703
        /** @var Vector[] $B */
1704
        $B = \array_map(
26✔
1705
            function (Vector $Xᵢ) use ($M) {
1706
                return $Xᵢ->subtract($M);
26✔
1707
            },
26✔
1708
            $X
26✔
1709
        );
1710

1711
        return MatrixFactory::createFromVectors($B, $this->ε);
26✔
1712
    }
1713

1714
    /**
1715
     * Mean deviation matrix
1716
     * Matrix as an array of row vectors, where columns represent variables and rows represent samples.
1717
     * Each row vector is subtracted by the sample mean.
1718
     *
1719
     * Example:
1720
     *      [1  4 7 8]      [5]
1721
     *  A = [2  2 8 4]  M = [4]
1722
     *      [1 13 1 5]      [5]
1723
     *
1724
     *  M = [4/3, 19/3, 16/3, 17/3]
1725
     *
1726
     *      |[1] - [4/3]  [4] - [19/3]  7 - [16/3]  [8] - [17/3]|
1727
     *  B = |[2] - [4/3]  [2] - [19/3]  8 - [16/3]  [4] - [17/3]|
1728
     *      |[1] - [4/3] [13] - [19/3]  1 - [16/3]  [5] - [17/3]|
1729
     *
1730
     *      [-1/3  -2.33   1.66  2.33]
1731
     *  B = [2/3   -4.33   2.66 -1.66]
1732
     *      [-1/3   6.66  -4.33  -2/3]
1733
     *
1734
     * @return NumericMatrix
1735
     *
1736
     * @throws Exception\IncorrectTypeException
1737
     */
1738
    public function meanDeviationOfColumnVariables(): NumericMatrix
1739
    {
1740
        $X = $this->asRowVectors();
9✔
1741
        $M = $this->columnMeans();
9✔
1742

1743
        /** @var Vector[] $B */
1744
        $B = \array_map(
9✔
1745
            function (Vector $Xᵢ) use ($M) {
1746
                return $Xᵢ->subtract($M);
9✔
1747
            },
9✔
1748
            $X
9✔
1749
        );
1750

1751
        return MatrixFactory::createFromVectors($B, $this->ε)->transpose();
9✔
1752
    }
1753

1754
    /**
1755
     * Covariance matrix (variance-covariance matrix, sample covariance matrix)
1756
     * https://en.wikipedia.org/wiki/Covariance_matrix
1757
     * https://en.wikipedia.org/wiki/Sample_mean_and_covariance
1758
     *
1759
     * Example:
1760
     *     [var₁  cov₁₂ cov₁₃]
1761
     * S = [cov₁₂ var₂  cov₂₃]
1762
     *     [cov₁₃ cov₂₃ var₃]
1763
     *
1764
     * @param string $direction Optional specification if to calculate along rows or columns
1765
     *                          'rows' (default): rows represent variables and columns represent samples
1766
     *                          'columns': columns represent variables and rows represent samples
1767
     *
1768
     * @return NumericMatrix
1769
     *
1770
     * @throws Exception\IncorrectTypeException
1771
     * @throws Exception\MatrixException
1772
     * @throws Exception\BadParameterException
1773
     * @throws Exception\VectorException
1774
     */
1775
    public function covarianceMatrix(string $direction = 'rows'): NumericMatrix
1776
    {
1777
        if (!\in_array($direction, [self::ROWS, self::COLUMNS])) {
27✔
1778
            throw new Exception\BadParameterException("Direction must be rows or columns, got $direction");
1✔
1779
        }
1780

1781
        $S = $direction === self::ROWS
26✔
1782
            ? $this->covarianceMatrixOfRowVariables()
22✔
1783
            : $this->covarianceMatrixOfColumnVariables();
26✔
1784

1785
        return $S;
26✔
1786
    }
1787

1788
    /**
1789
     * Covariance matrix (variance-covariance matrix, sample covariance matrix)
1790
     * where rows represent variables and columns represent samples
1791
     * https://en.wikipedia.org/wiki/Covariance_matrix
1792
     * https://en.wikipedia.org/wiki/Sample_mean_and_covariance
1793
     *
1794
     *       1
1795
     * S = ----- BBᵀ
1796
     *     N - 1
1797
     *
1798
     *  where B is the mean-deviation form
1799
     *
1800
     * Uses mathematical convention where matrix columns represent observation vectors.
1801
     * Follows formula and method found in Linear Algebra and Its Applications (Lay).
1802
     *
1803
     * @return NumericMatrix
1804
     *
1805
     * @throws Exception\IncorrectTypeException
1806
     * @throws Exception\MatrixException
1807
     * @throws Exception\BadParameterException
1808
     * @throws Exception\VectorException
1809
     */
1810
    protected function covarianceMatrixOfRowVariables(): NumericMatrix
1811
    {
1812
        $n  = $this->n;
22✔
1813
        $B  = $this->meanDeviationOfRowVariables();
22✔
1814
        $Bᵀ = $B->transpose();
22✔
1815

1816
        $S = $B->multiply($Bᵀ)->scalarMultiply((1 / ($n - 1)));
22✔
1817

1818
        return $S;
22✔
1819
    }
1820

1821
    /**
1822
     * Covariance matrix (variance-covariance matrix, sample covariance matrix)
1823
     * where columns represent variables and rows represent samples
1824
     * https://en.wikipedia.org/wiki/Covariance_matrix
1825
     * https://en.wikipedia.org/wiki/Sample_mean_and_covariance
1826
     *
1827
     *       1
1828
     * S = ----- BᵀB
1829
     *     N - 1
1830
     *
1831
     *  where B is the mean-deviation form
1832
     *
1833
     * @return NumericMatrix
1834
     *
1835
     * @throws Exception\IncorrectTypeException
1836
     * @throws Exception\MatrixException
1837
     * @throws Exception\BadParameterException
1838
     * @throws Exception\VectorException
1839
     */
1840
    protected function covarianceMatrixOfColumnVariables(): NumericMatrix
1841
    {
1842
        $n  = $this->m;
4✔
1843
        $B  = $this->meanDeviationOfColumnVariables();
4✔
1844
        $Bᵀ = $B->transpose();
4✔
1845

1846
        $S = $Bᵀ->multiply($B)->scalarMultiply((1 / ($n - 1)));
4✔
1847

1848
        return $S;
4✔
1849
    }
1850

1851
    /**
1852
     * Adjugate matrix (adjoint, adjunct)
1853
     * The transpose of its cofactor matrix.
1854
     * https://en.wikipedia.org/wiki/Adjugate_matrix
1855
     *
1856
     * @return NumericMatrix
1857
     *
1858
     * @throws Exception\MatrixException is matrix is not square
1859
     * @throws Exception\IncorrectTypeException
1860
     * @throws Exception\BadParameterException
1861
     */
1862
    public function adjugate(): NumericMatrix
1863
    {
1864
        if (!$this->isSquare()) {
455✔
1865
            throw new Exception\MatrixException('Matrix is not square; cannot get adjugate Matrix of a non-square matrix');
1✔
1866
        }
1867

1868
        if ($this->n === 1) {
454✔
1869
            return MatrixFactory::createNumeric([[1]], $this->ε);
9✔
1870
        }
1871

1872
        $adj⟮A⟯ = $this->cofactorMatrix()->transpose();
445✔
1873

1874
        return $adj⟮A⟯;
445✔
1875
    }
1876

1877
    /**
1878
     * Householder matrix transformation
1879
     *
1880
     * @return NumericMatrix
1881
     *
1882
     * @throws Exception\MathException
1883
     */
1884
    public function householder(): NumericMatrix
1885
    {
1886
        return Householder::transform($this);
80✔
1887
    }
1888

1889
    /**************************************************************************
1890
     * MATRIX VECTOR OPERATIONS - Return a Vector
1891
     *  - vectorMultiply
1892
     *  - rowSums
1893
     *  - rowMeans
1894
     *  - columnSums
1895
     *  - columnMeans
1896
     **************************************************************************/
1897

1898
    /**
1899
     * Matrix multiplication by a vector
1900
     * m x n matrix multiplied by a 1 x n vector resulting in a new vector.
1901
     * https://en.wikipedia.org/wiki/Matrix_multiplication#Square_matrix_and_column_vector
1902
     *
1903
     * @param  Vector $B Vector to multiply
1904
     *
1905
     * @return Vector
1906
     *
1907
     * @throws Exception\MatrixException if dimensions do not match
1908
     */
1909
    public function vectorMultiply(Vector $B): Vector
1910
    {
1911
        $B = $B->getVector();
8✔
1912
        $n = \count($B);
8✔
1913
        $m = $this->m;
8✔
1914

1915
        if ($n !== $this->n) {
8✔
1916
            throw new Exception\MatrixException("Matrix and vector dimensions do not match");
1✔
1917
        }
1918

1919
        $R = [];
7✔
1920
        for ($i = 0; $i < $m; $i++) {
7✔
1921
            $R[$i] = \array_sum(Map\Multi::multiply($this->getRow($i), $B));
7✔
1922
        }
1923

1924
        return new Vector($R);
7✔
1925
    }
1926

1927
    /**
1928
     * Sums of each row, returned as a Vector
1929
     *
1930
     * @return Vector
1931
     */
1932
    public function rowSums(): Vector
1933
    {
1934
        $sums = \array_map(
5✔
1935
            function (array $row) {
1936
                return \array_sum($row);
5✔
1937
            },
5✔
1938
            $this->A
5✔
1939
        );
1940

1941
        return new Vector($sums);
5✔
1942
    }
1943

1944
    /**
1945
     * Means of each row, returned as a Vector
1946
     * https://en.wikipedia.org/wiki/Sample_mean_and_covariance
1947
     *
1948
     *     1
1949
     * M = - (X₁ + X₂ + ⋯ + Xn)
1950
     *     n
1951
     *
1952
     * Example:
1953
     *      [1  4 7 8]
1954
     *  A = [2  2 8 4]
1955
     *      [1 13 1 5]
1956
     *
1957
     *  Consider each column of observations as a column vector:
1958
     *        [1]       [4]        [7]       [8]
1959
     *   X₁ = [2]  X₂ = [2]   X₃ = [8]  X₄ = [4]
1960
     *        [1]       [13]       [1]       [5]
1961
     *
1962
     *    1  /[1]   [4]    [7]   [8]\     1 [20]   [5]
1963
     *    - | [2] + [2]  + [8] + [4] |  = - [16] = [4]
1964
     *    4  \[1]   [13]   [1]   [5]/     4 [20]   [5]
1965
     *
1966
     * @return Vector
1967
     */
1968
    public function rowMeans(): Vector
1969
    {
1970
        $n = $this->n;
31✔
1971

1972
        $means = \array_map(
31✔
1973
            function (array $row) use ($n) {
1974
                return \array_sum($row) / $n;
31✔
1975
            },
31✔
1976
            $this->A
31✔
1977
        );
1978

1979
        return new Vector($means);
31✔
1980
    }
1981

1982
    /**
1983
     * Sums of each column, returned as a Vector
1984
     *
1985
     * @return Vector
1986
     */
1987
    public function columnSums(): Vector
1988
    {
1989
        $sums = [];
5✔
1990
        for ($i = 0; $i < $this->n; $i++) {
5✔
1991
            $sums[] = \array_sum(\array_column($this->A, $i));
5✔
1992
        }
1993

1994
        return new Vector($sums);
5✔
1995
    }
1996

1997
    /**
1998
     * Means of each column, returned as a Vector
1999
     * https://en.wikipedia.org/wiki/Sample_mean_and_covariance
2000
     *
2001
     *     1
2002
     * M = - (X₁ + X₂ + ⋯ + Xn)
2003
     *     m
2004
     *
2005
     * Example:
2006
     *      [1  4 7 8]
2007
     *  A = [2  2 8 4]
2008
     *      [1 13 1 5]
2009
     *
2010
     *  Consider each row of observations as a row vector:
2011
     *
2012
     *   X₁ = [1  4 7 9]
2013
     *   X₂ = [2  2 8 4]
2014
     *   X₃ = [1 13 1 5]
2015
     *
2016
     *   1  /  1    4    7    9  \      1
2017
     *   - |  +2   +2   +8   +4   |  =  - [4  19  16  18]  =  [1⅓, 6⅓, 5⅓, 5.⅔]
2018
     *   3  \ +1  +13   +1   +5  /      3
2019
     *
2020
     * @return Vector
2021
     */
2022
    public function columnMeans(): Vector
2023
    {
2024
        $m = $this->m;
21✔
2025
        $n = $this->n;
21✔
2026

2027
        $means = [];
21✔
2028
        for ($i = 0; $i < $n; $i++) {
21✔
2029
            $means[] = \array_sum(\array_column($this->A, $i)) / $m;
21✔
2030
        }
2031

2032
        return new Vector($means);
21✔
2033
    }
2034

2035
    /**************************************************************************
2036
     * MATRIX OPERATIONS - Return a value
2037
     *  - trace
2038
     *  - oneNorm
2039
     *  - frobeniusNorm
2040
     *  - infinityNorm
2041
     *  - maxNorm
2042
     *  - det
2043
     *  - cofactor
2044
     *  - rank
2045
     **************************************************************************/
2046

2047
    /**
2048
     * Trace
2049
     * the trace of an n-by-n square matrix A is defined to be
2050
     * the sum of the elements on the main diagonal
2051
     * (the diagonal from the upper left to the lower right).
2052
     * https://en.wikipedia.org/wiki/Trace_(linear_algebra)
2053
     *
2054
     * tr(A) = a₁₁ + a₂₂ + ... ann
2055
     *
2056
     * @return number
2057
     *
2058
     * @throws Exception\MatrixException if the matrix is not a square matrix
2059
     */
2060
    public function trace()
2061
    {
2062
        if (!$this->isSquare()) {
81✔
2063
            throw new Exception\MatrixException('trace only works on a square matrix');
1✔
2064
        }
2065

2066
        $m    = $this->m;
80✔
2067
        $tr⟮A⟯ = 0;
80✔
2068

2069
        for ($i = 0; $i < $m; $i++) {
80✔
2070
            $tr⟮A⟯ += $this->A[$i][$i];
80✔
2071
        }
2072

2073
        return $tr⟮A⟯;
80✔
2074
    }
2075

2076
    /**
2077
     * 1-norm (‖A‖₁)
2078
     * Maximum absolute column sum of the matrix
2079
     *
2080
     * @return number
2081
     */
2082
    public function oneNorm()
2083
    {
2084
        $n = $this->n;
7✔
2085
        $‖A‖₁ = \array_sum(Map\Single::abs(\array_column($this->A, 0)));
7✔
2086

2087
        for ($j = 1; $j < $n; $j++) {
7✔
2088
            $‖A‖₁ = \max($‖A‖₁, \array_sum(Map\Single::abs(\array_column($this->A, $j))));
6✔
2089
        }
2090

2091
        return $‖A‖₁;
7✔
2092
    }
2093

2094
    /**
2095
     * Frobenius norm (Hilbert–Schmidt norm, Euclidean norm) (‖A‖F)
2096
     * Square root of the sum of the square of all elements.
2097
     *
2098
     * https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm
2099
     *
2100
     *          _____________
2101
     *         /ᵐ   ⁿ
2102
     * ‖A‖F = √ Σ   Σ  |aᵢⱼ|²
2103
     *         ᵢ₌₁ ᵢ₌₁
2104
     *
2105
     * @return number
2106
     */
2107
    public function frobeniusNorm()
2108
    {
2109
        $m      = $this->m;
1,036✔
2110
        $n      = $this->n;
1,036✔
2111
        $ΣΣaᵢⱼ² = 0;
1,036✔
2112

2113
        for ($i = 0; $i < $m; $i++) {
1,036✔
2114
            for ($j = 0; $j < $n; $j++) {
1,036✔
2115
                $ΣΣaᵢⱼ² += ($this->A[$i][$j]) ** 2;
1,036✔
2116
            }
2117
        }
2118

2119
        return \sqrt($ΣΣaᵢⱼ²);
1,036✔
2120
    }
2121

2122
    /**
2123
     * Infinity norm (‖A‖∞)
2124
     * Maximum absolute row sum of the matrix
2125
     *
2126
     * @return number
2127
     */
2128
    public function infinityNorm()
2129
    {
2130
        $m = $this->m;
8✔
2131
        $‖A‖∞ = \array_sum(Map\Single::abs($this->A[0]));
8✔
2132

2133
        for ($i = 1; $i < $m; $i++) {
8✔
2134
            $‖A‖∞ = \max($‖A‖∞, \array_sum(Map\Single::abs($this->A[$i])));
7✔
2135
        }
2136

2137
        return $‖A‖∞;
8✔
2138
    }
2139

2140
    /**
2141
     * Max norm (‖A‖max)
2142
     * Elementwise max
2143
     *
2144
     * @return number
2145
     */
2146
    public function maxNorm()
2147
    {
2148
        $m   = $this->m;
8✔
2149
        $n   = $this->n;
8✔
2150
        $max = \abs($this->A[0][0]);
8✔
2151

2152
        for ($i = 0; $i < $m; $i++) {
8✔
2153
            for ($j = 0; $j < $n; $j++) {
8✔
2154
                $max = \max($max, \abs($this->A[$i][$j]));
8✔
2155
            }
2156
        }
2157

2158
        return $max;
8✔
2159
    }
2160

2161
    /**
2162
     * Determinant
2163
     *
2164
     * For a 1x1 matrix:
2165
     *  A = [a]
2166
     *
2167
     * |A| = a
2168
     *
2169
     * For a 2x2 matrix:
2170
     *      [a b]
2171
     *  A = [c d]
2172
     *
2173
     * │A│ = ad - bc
2174
     *
2175
     * For a 3x3 matrix:
2176
     *      [a b c]
2177
     *  A = [d e f]
2178
     *      [g h i]
2179
     *
2180
     * │A│ = a(ei - fh) - b(di - fg) + c(dh - eg)
2181
     *
2182
     * For 4x4 and larger matrices:
2183
     *
2184
     * │A│ = (-1)ⁿ │ref(A)│
2185
     *
2186
     *  where:
2187
     *   │ref(A)│ = determinant of the row echelon form of A
2188
     *   ⁿ        = number of row swaps when computing REF
2189
     *
2190
     * @return number
2191
     *
2192
     * @throws Exception\MatrixException if matrix is not square
2193
     * @throws Exception\IncorrectTypeException
2194
     * @throws Exception\BadParameterException
2195
     */
2196
    public function det()
2197
    {
2198
        if ($this->catalog->hasDeterminant()) {
2,267✔
2199
            /** @var number */
2200
            return $this->catalog->getDeterminant();
971✔
2201
        }
2202

2203
        if (!$this->isSquare()) {
2,267✔
2204
            throw new Exception\MatrixException('Not a square matrix (required for determinant)');
1✔
2205
        }
2206

2207
        $m = $this->m;
2,266✔
2208

2209
        /** @var NumericMatrix $R */
2210
        $R = MatrixFactory::create($this->A);
2,266✔
2211

2212
        /*
2213
         * 1x1 matrix
2214
         *  A = [a]
2215
         *
2216
         * |A| = a
2217
         */
2218
        if ($m === 1) {
2,266✔
2219
            $det = $R[0][0];
403✔
2220
            $this->catalog->addDeterminant($det);
403✔
2221
            return $det;
403✔
2222
        }
2223

2224
        /*
2225
         * 2x2 matrix
2226
         *      [a b]
2227
         *  A = [c d]
2228
         *
2229
         * |A| = ad - bc
2230
         */
2231
        if ($m === 2) {
2,142✔
2232
            $a = $R[0][0];
845✔
2233
            $b = $R[0][1];
845✔
2234
            $c = $R[1][0];
845✔
2235
            $d = $R[1][1];
845✔
2236

2237
            $ad = $a * $d;
845✔
2238
            $bc = $b * $c;
845✔
2239

2240
            $det = $ad - $bc;
845✔
2241
            $this->catalog->addDeterminant($det);
845✔
2242
            return $det;
845✔
2243
        }
2244

2245
        /*
2246
         * 3x3 matrix
2247
         *      [a b c]
2248
         *  A = [d e f]
2249
         *      [g h i]
2250
         *
2251
         * |A| = a(ei - fh) - b(di - fg) + c(dh - eg)
2252
         */
2253
        if ($m === 3) {
1,466✔
2254
            $a = $R[0][0];
531✔
2255
            $b = $R[0][1];
531✔
2256
            $c = $R[0][2];
531✔
2257
            $d = $R[1][0];
531✔
2258
            $e = $R[1][1];
531✔
2259
            $f = $R[1][2];
531✔
2260
            $g = $R[2][0];
531✔
2261
            $h = $R[2][1];
531✔
2262
            $i = $R[2][2];
531✔
2263

2264
            $ei = $e * $i;
531✔
2265
            $fh = $f * $h;
531✔
2266
            $di = $d * $i;
531✔
2267
            $fg = $f * $g;
531✔
2268
            $dh = $d * $h;
531✔
2269
            $eg = $e * $g;
531✔
2270

2271
            $det = $a * ($ei - $fh) - $b * ($di - $fg) + $c * ($dh - $eg);
531✔
2272
            $this->catalog->addDeterminant($det);
531✔
2273
            return $det;
531✔
2274
        }
2275

2276
        /*
2277
         * nxn matrix 4x4 or larger
2278
         * Get row echelon form, then compute determinant of ref.
2279
         * Then plug into formula with swaps.
2280
         * │A│ = (-1)ⁿ │ref(A)│
2281
         */
2282
        $ref⟮A⟯ = $this->ref();
980✔
2283
        $ⁿ     = $ref⟮A⟯->getRowSwaps();
980✔
2284

2285
        // Det(ref(A))
2286
        $│ref⟮A⟯│ = 1;
980✔
2287
        for ($i = 0; $i < $m; $i++) {
980✔
2288
            $│ref⟮A⟯│ *= $ref⟮A⟯[$i][$i];
980✔
2289
        }
2290

2291
        // │A│ = (-1)ⁿ │ref(A)│
2292
        $det = (-1) ** $ⁿ * $│ref⟮A⟯│;
980✔
2293
        $this->catalog->addDeterminant($det);
980✔
2294
        return $det;
980✔
2295
    }
2296

2297
    /**
2298
     * Cofactor
2299
     * Multiply the minor by (-1)ⁱ⁺ʲ.
2300
     *
2301
     * Cᵢⱼ = (-1)ⁱ⁺ʲMᵢⱼ
2302
     *
2303
     * Example:
2304
     *        [1 4  7]
2305
     * If A = [3 0  5]
2306
     *        [1 9 11]
2307
     *
2308
     *                [1 4 -]       [1 4]
2309
     * Then M₁₂ = det [- - -] = det [1 9] = 13
2310
     *                [1 9 -]
2311
     *
2312
     * Therefore C₁₂ = (-1)¹⁺²(13) = -13
2313
     *
2314
     * https://en.wikipedia.org/wiki/Minor_(linear_algebra)
2315
     *
2316
     * @param int $mᵢ Row to exclude
2317
     * @param int $nⱼ Column to exclude
2318
     *
2319
     * @return number
2320
     *
2321
     * @throws Exception\MatrixException if matrix is not square
2322
     * @throws Exception\MatrixException if row to exclude for cofactor does not exist
2323
     * @throws Exception\MatrixException if column to exclude for cofactor does not exist
2324
     * @throws Exception\IncorrectTypeException
2325
     * @throws Exception\BadParameterException
2326
     */
2327
    public function cofactor(int $mᵢ, int $nⱼ)
2328
    {
2329
        if (!$this->isSquare()) {
469✔
2330
            throw new Exception\MatrixException('Matrix is not square; cannot get cofactor of a non-square matrix');
1✔
2331
        }
2332
        if ($mᵢ >= $this->m || $mᵢ < 0) {
468✔
2333
            throw new Exception\MatrixException('Row to exclude for cofactor does not exist');
1✔
2334
        }
2335
        if ($nⱼ >= $this->n || $nⱼ < 0) {
467✔
2336
            throw new Exception\MatrixException('Column to exclude for cofactor does not exist');
1✔
2337
        }
2338

2339
        $Mᵢⱼ    = $this->minor($mᵢ, $nⱼ);
466✔
2340
        $⟮−1⟯ⁱ⁺ʲ = (-1) ** ($mᵢ + $nⱼ);
466✔
2341

2342
        return $⟮−1⟯ⁱ⁺ʲ * $Mᵢⱼ;
466✔
2343
    }
2344

2345
    /**
2346
     * Rank of a matrix
2347
     * Computed by counting number of pivots once in reduced row echelon form
2348
     * https://en.wikipedia.org/wiki/Rank_(linear_algebra)
2349
     *
2350
     * @return int
2351
     *
2352
     * @throws Exception\BadParameterException
2353
     * @throws Exception\IncorrectTypeException
2354
     * @throws Exception\MatrixException
2355
     */
2356
    public function rank(): int
2357
    {
2358
        $rref   = $this->rref();
611✔
2359
        $pivots = 0;
611✔
2360

2361
        for ($i = 0; $i < $this->m; $i++) {
611✔
2362
            for ($j = 0; $j < $this->n; $j++) {
611✔
2363
                if (Support::isNotZero($rref[$i][$j], $this->ε)) {
611✔
2364
                    $pivots++;
576✔
2365
                    continue 2;
576✔
2366
                }
2367
            }
2368
        }
2369

2370
        return $pivots;
611✔
2371
    }
2372

2373
    /**************************************************************************
2374
     * ROW OPERATIONS - Return a Matrix
2375
     *  - rowMultiply
2376
     *  - rowDivide
2377
     *  - rowAdd
2378
     *  - rowAddScalar
2379
     *  - rowAddVector
2380
     *  - rowSubtract
2381
     *  - rowSubtractScalar
2382
     **************************************************************************/
2383

2384
    /**
2385
     * Multiply a row by a factor k
2386
     *
2387
     * Each element of Row mᵢ will be multiplied by k
2388
     *
2389
     * @param int   $mᵢ Row to multiply
2390
     * @param float $k Multiplier
2391
     *
2392
     * @return NumericMatrix
2393
     *
2394
     * @throws Exception\MatrixException if row to multiply does not exist
2395
     * @throws Exception\IncorrectTypeException
2396
     */
2397
    public function rowMultiply(int $mᵢ, float $k): NumericMatrix
2398
    {
2399
        if ($mᵢ >= $this->m) {
7✔
2400
            throw new Exception\MatrixException('Row to multiply does not exist');
1✔
2401
        }
2402

2403
        $n = $this->n;
6✔
2404
        $R = $this->A;
6✔
2405

2406
        for ($j = 0; $j < $n; $j++) {
6✔
2407
            $R[$mᵢ][$j] *= $k;
6✔
2408
        }
2409

2410
        return MatrixFactory::createNumeric($R, $this->ε);
6✔
2411
    }
2412

2413
    /**
2414
     * Divide a row by a divisor k
2415
     *
2416
     * Each element of Row mᵢ will be divided by k
2417
     *
2418
     * @param int   $mᵢ Row to multiply
2419
     * @param float $k divisor
2420
     *
2421
     * @return NumericMatrix
2422
     *
2423
     * @throws Exception\MatrixException if row to multiply does not exist
2424
     * @throws Exception\BadParameterException if k is 0
2425
     * @throws Exception\IncorrectTypeException
2426
     */
2427
    public function rowDivide(int $mᵢ, float $k): NumericMatrix
2428
    {
2429
        if ($mᵢ >= $this->m) {
1,487✔
2430
            throw new Exception\MatrixException('Row to multiply does not exist');
1✔
2431
        }
2432
        if ($k == 0) {
1,486✔
2433
            throw new Exception\BadParameterException('Divisor k must not be 0');
1✔
2434
        }
2435

2436
        $n = $this->n;
1,485✔
2437
        $R = $this->A;
1,485✔
2438

2439
        for ($j = 0; $j < $n; $j++) {
1,485✔
2440
            $R[$mᵢ][$j] /= $k;
1,485✔
2441
        }
2442

2443
        return MatrixFactory::createNumeric($R, $this->ε);
1,485✔
2444
    }
2445

2446
    /**
2447
     * Add k times row mᵢ to row mⱼ
2448
     *
2449
     * @param int   $mᵢ Row to multiply * k to be added to row mⱼ
2450
     * @param int   $mⱼ Row that will have row mⱼ * k added to it
2451
     * @param float $k Multiplier
2452
     *
2453
     * @return NumericMatrix
2454
     *
2455
     * @throws Exception\MatrixException if row to add does not exist
2456
     * @throws Exception\BadParameterException if k is 0
2457
     * @throws Exception\IncorrectTypeException
2458
     */
2459
    public function rowAdd(int $mᵢ, int $mⱼ, float $k): NumericMatrix
2460
    {
2461
        if ($mᵢ >= $this->m || $mⱼ >= $this->m) {
1,379✔
2462
            throw new Exception\MatrixException('Row to add does not exist');
1✔
2463
        }
2464
        if ($k == 0) {
1,378✔
2465
            throw new Exception\BadParameterException('Multiplication factor k must not be 0');
1✔
2466
        }
2467

2468
        $n = $this->n;
1,377✔
2469
        $R = $this->A;
1,377✔
2470

2471
        for ($j = 0; $j < $n; $j++) {
1,377✔
2472
            $R[$mⱼ][$j] += $R[$mᵢ][$j] * $k;
1,377✔
2473
        }
2474

2475
        return MatrixFactory::createNumeric($R, $this->ε);
1,377✔
2476
    }
2477

2478
    /**
2479
     * Add a scalar k to each item of a row
2480
     *
2481
     * Each element of Row mᵢ will have k added to it
2482
     *
2483
     * @param int   $mᵢ Row to add k to
2484
     * @param float $k scalar
2485
     *
2486
     * @return NumericMatrix
2487
     *
2488
     * @throws Exception\MatrixException if row to add does not exist
2489
     * @throws Exception\IncorrectTypeException
2490
     */
2491
    public function rowAddScalar(int $mᵢ, float $k): NumericMatrix
2492
    {
2493
        if ($mᵢ >= $this->m) {
4✔
2494
            throw new Exception\MatrixException('Row to add does not exist');
1✔
2495
        }
2496

2497
        $n = $this->n;
3✔
2498
        $R = $this->A;
3✔
2499

2500
        for ($j = 0; $j < $n; $j++) {
3✔
2501
            $R[$mᵢ][$j] += $k;
3✔
2502
        }
2503

2504
        return MatrixFactory::createNumeric($R, $this->ε);
3✔
2505
    }
2506

2507
    /**
2508
     * Add components of vector V to row mᵢ
2509
     *
2510
     * @param int    $mᵢ Row to add vector $v to
2511
     * @param Vector $V  Vector to add to row mᵢ
2512
     *
2513
     * @return NumericMatrix
2514
     *
2515
     * @throws Exception\MatrixException if row to add does not exist
2516
     * @throws Exception\BadParameterException if the vector has a different # of components to the # of columns
2517
     * @throws Exception\IncorrectTypeException
2518
     */
2519
    public function rowAddVector(int $mᵢ, Vector $V): NumericMatrix
2520
    {
2521
        if ($mᵢ < 0 || $mᵢ >= $this->m) {
12✔
2522
            throw new Exception\MatrixException("Row to add to ($mᵢ) does not exist");
1✔
2523
        }
2524
        if (count($V) !== $this->n) {
11✔
2525
            throw new Exception\BadParameterException('Vector is not the same length as matrix columns');
1✔
2526
        }
2527

2528
        $n = $this->n;
10✔
2529
        $R = $this->A;
10✔
2530

2531
        for ($j = 0; $j < $n; $j++) {
10✔
2532
            $R[$mᵢ][$j] += $V[$j];
10✔
2533
        }
2534

2535
        return MatrixFactory::createNumeric($R, $this->ε);
10✔
2536
    }
2537

2538
    /**
2539
     * Subtract k times row mᵢ to row mⱼ
2540
     *
2541
     * @param int   $mᵢ Row to multiply * k to be subtracted to row mⱼ
2542
     * @param int   $mⱼ Row that will have row mⱼ * k subtracted to it
2543
     * @param float $k Multiplier
2544
     *
2545
     * @return NumericMatrix
2546
     *
2547
     * @throws Exception\MatrixException if row to subtract does not exist
2548
     * @throws Exception\IncorrectTypeException
2549
     */
2550
    public function rowSubtract(int $mᵢ, int $mⱼ, float $k): NumericMatrix
2551
    {
2552
        if ($mᵢ >= $this->m || $mⱼ >= $this->m) {
6✔
2553
            throw new Exception\MatrixException('Row to subtract does not exist');
1✔
2554
        }
2555

2556
        $n = $this->n;
5✔
2557
        $R = $this->A;
5✔
2558

2559
        for ($j = 0; $j < $n; $j++) {
5✔
2560
            $R[$mⱼ][$j] -= $R[$mᵢ][$j] * $k;
5✔
2561
        }
2562

2563
        return MatrixFactory::createNumeric($R, $this->ε);
5✔
2564
    }
2565

2566
    /**
2567
     * Subtract a scalar k to each item of a row
2568
     *
2569
     * Each element of Row mᵢ will have k subtracted from it
2570
     *
2571
     * @param int   $mᵢ Row to add k to
2572
     * @param float $k scalar
2573
     *
2574
     * @return NumericMatrix
2575
     *
2576
     * @throws Exception\MatrixException if row to subtract does not exist
2577
     * @throws Exception\IncorrectTypeException
2578
     */
2579
    public function rowSubtractScalar(int $mᵢ, float $k): NumericMatrix
2580
    {
2581
        if ($mᵢ >= $this->m) {
4✔
2582
            throw new Exception\MatrixException('Row to subtract does not exist');
1✔
2583
        }
2584

2585
        $n = $this->n;
3✔
2586
        $R = $this->A;
3✔
2587

2588
        for ($j = 0; $j < $n; $j++) {
3✔
2589
            $R[$mᵢ][$j] -= $k;
3✔
2590
        }
2591

2592
        return MatrixFactory::createNumeric($R, $this->ε);
3✔
2593
    }
2594

2595
    /**************************************************************************
2596
     * COLUMN OPERATIONS - Return a Matrix
2597
     *  - columnMultiply
2598
     *  - columnAdd
2599
     *  - columnAddVector
2600
     **************************************************************************/
2601

2602
    /**
2603
     * Multiply a column by a factor k
2604
     *
2605
     * Each element of column nᵢ will be multiplied by k
2606
     *
2607
     * @param int   $nᵢ Column to multiply
2608
     * @param float $k Multiplier
2609
     *
2610
     * @return NumericMatrix
2611
     *
2612
     * @throws Exception\MatrixException if column to multiply does not exist
2613
     * @throws Exception\IncorrectTypeException
2614
     */
2615
    public function columnMultiply(int $nᵢ, float $k): NumericMatrix
2616
    {
2617
        if ($nᵢ >= $this->n) {
7✔
2618
            throw new Exception\MatrixException('Column to multiply does not exist');
1✔
2619
        }
2620

2621
        $m = $this->m;
6✔
2622
        $R = $this->A;
6✔
2623

2624
        for ($i = 0; $i < $m; $i++) {
6✔
2625
            $R[$i][$nᵢ] *= $k;
6✔
2626
        }
2627

2628
        return MatrixFactory::createNumeric($R, $this->ε);
6✔
2629
    }
2630

2631
    /**
2632
     * Add k times column nᵢ to column nⱼ
2633
     *
2634
     * @param int   $nᵢ Column to multiply * k to be added to column nⱼ
2635
     * @param int   $nⱼ Column that will have column nⱼ * k added to it
2636
     * @param float $k Multiplier
2637
     *
2638
     * @return NumericMatrix
2639
     *
2640
     * @throws Exception\MatrixException if column to add does not exist
2641
     * @throws Exception\BadParameterException if k is 0
2642
     * @throws Exception\IncorrectTypeException
2643
     */
2644
    public function columnAdd(int $nᵢ, int $nⱼ, float $k): NumericMatrix
2645
    {
2646
        if ($nᵢ >= $this->n || $nⱼ >= $this->n) {
7✔
2647
            throw new Exception\MatrixException('Column to add does not exist');
1✔
2648
        }
2649
        if ($k == 0) {
6✔
2650
            throw new Exception\BadParameterException('Multiplication factor k must not be 0');
1✔
2651
        }
2652

2653
        $m = $this->m;
5✔
2654
        $R = $this->A;
5✔
2655

2656
        for ($i = 0; $i < $m; $i++) {
5✔
2657
            $R[$i][$nⱼ] += $R[$i][$nᵢ] * $k;
5✔
2658
        }
2659

2660
        return MatrixFactory::createNumeric($R, $this->ε);
5✔
2661
    }
2662

2663
    /**
2664
     * Add components of vector V to column nᵢ
2665
     *
2666
     * @param int    $nᵢ Column to add vector $v to
2667
     * @param Vector $V  Vector to add to column nᵢ
2668
     *
2669
     * @return NumericMatrix
2670
     *
2671
     * @throws Exception\MatrixException if column to add does not exist
2672
     * @throws Exception\BadParameterException if the vector has a different # of components to the # of rows
2673
     * @throws Exception\IncorrectTypeException
2674
     */
2675
    public function columnAddVector(int $nᵢ, Vector $V): NumericMatrix
2676
    {
2677
        if ($nᵢ < 0 || $nᵢ >= $this->n) {
12✔
2678
            throw new Exception\MatrixException("Column to add to ($nᵢ) does not exist");
1✔
2679
        }
2680
        if (count($V) !== $this->m) {
11✔
2681
            throw new Exception\BadParameterException('Vector is not the same length as matrix rows');
1✔
2682
        }
2683

2684
        $m = $this->m;
10✔
2685
        $R = $this->A;
10✔
2686

2687
        for ($i = 0; $i < $m; $i++) {
10✔
2688
            $R[$i][$nᵢ] += $V[$i];
10✔
2689
        }
2690

2691
        return MatrixFactory::createNumeric($R, $this->ε);
10✔
2692
    }
2693

2694
    /**************************************************************************
2695
     * MATRIX REDUCTIONS - Return a Matrix in a reduced form
2696
     *  - ref (row echelon form)
2697
     *  - rref (reduced row echelon form)
2698
     **************************************************************************/
2699

2700
    /**
2701
     * Row echelon form - REF
2702
     *
2703
     * @return Reduction\RowEchelonForm
2704
     *
2705
     * @throws Exception\BadDataException
2706
     * @throws Exception\BadParameterException
2707
     * @throws Exception\IncorrectTypeException
2708
     * @throws Exception\MatrixException
2709
     */
2710
    public function ref(): Reduction\RowEchelonForm
2711
    {
2712
        if (!$this->catalog->hasRowEchelonForm()) {
2,234✔
2713
            $this->catalog->addRowEchelonForm(Reduction\RowEchelonForm::reduce($this));
2,234✔
2714
        }
2715

2716
        return $this->catalog->getRowEchelonForm();
2,234✔
2717
    }
2718

2719
    /**
2720
     * Reduced row echelon form (row canonical form) - RREF
2721
     *
2722
     * @return Reduction\ReducedRowEchelonForm
2723
     *
2724
     * @throws Exception\BadDataException
2725
     * @throws Exception\BadParameterException
2726
     * @throws Exception\IncorrectTypeException
2727
     * @throws Exception\MatrixException
2728
     */
2729
    public function rref(): Reduction\ReducedRowEchelonForm
2730
    {
2731
        if (!$this->catalog->hasReducedRowEchelonForm()) {
1,457✔
2732
            $ref = $this->ref();
1,457✔
2733
            $this->catalog->addReducedRowEchelonForm($ref->rref());
1,457✔
2734
        }
2735

2736
        return $this->catalog->getReducedRowEchelonForm();
1,457✔
2737
    }
2738

2739
    /********************************************************************************
2740
     * MATRIX DECOMPOSITIONS - Returns a Decomposition object that contains Matrices
2741
     *  - LU decomposition
2742
     *  - QR decomposition
2743
     *  - Cholesky decomposition
2744
     *  - Crout decomposition
2745
     *  - SVD (Singular Value Decomposition)
2746
     ********************************************************************************/
2747

2748
    /**
2749
     * LU Decomposition (Doolittle decomposition) with pivoting via permutation matrix
2750
     *
2751
     * A = LU(P)
2752
     *
2753
     * L: Lower triangular matrix--all entries above the main diagonal are zero.
2754
     *    The main diagonal will be all ones.
2755
     * U: Upper tirangular matrix--all entries below the main diagonal are zero.
2756
     * P: Permutation matrix--Identity matrix with possible rows interchanged.
2757
     *
2758
     * @return Decomposition\LU
2759
     *
2760
     * @throws Exception\MatrixException if matrix is not square
2761
     * @throws Exception\IncorrectTypeException
2762
     * @throws Exception\OutOfBoundsException
2763
     * @throws Exception\VectorException
2764
     */
2765
    public function luDecomposition(): Decomposition\LU
2766
    {
2767
        if (!$this->catalog->hasLuDecomposition()) {
425✔
2768
            $this->catalog->addLuDecomposition(Decomposition\LU::decompose($this));
425✔
2769
        }
2770

2771
        return $this->catalog->getLuDecomposition();
424✔
2772
    }
2773

2774
    /**
2775
     * QR Decomposition using Householder reflections
2776
     *
2777
     * A = QR
2778
     *
2779
     * Q is an orthogonal matrix
2780
     * R is an upper triangular matrix
2781
     *
2782
     * @return Decomposition\QR
2783
     *
2784
     * @throws Exception\MathException
2785
     */
2786
    public function qrDecomposition(): Decomposition\QR
2787
    {
2788
        if (!$this->catalog->hasQrDecomposition()) {
839✔
2789
            $this->catalog->addQrDecomposition(Decomposition\QR::decompose($this));
839✔
2790
        }
2791

2792
        return $this->catalog->getQrDecomposition();
839✔
2793
    }
2794

2795
    /**
2796
     * Cholesky decomposition
2797
     *
2798
     * A decomposition of a square, positive definitive matrix into the product of a lower triangular matrix and its transpose.
2799
     *
2800
     * A = LLᵀ
2801
     *
2802
     * L:  Lower triangular matrix
2803
     * Lᵀ: Transpose of lower triangular matrix
2804
     *
2805
     * @return Decomposition\Cholesky
2806
     *
2807
     * @throws Exception\MatrixException if the matrix is not positive definite
2808
     * @throws Exception\OutOfBoundsException
2809
     * @throws Exception\IncorrectTypeException
2810
     * @throws Exception\BadParameterException
2811
     */
2812
    public function choleskyDecomposition(): Decomposition\Cholesky
2813
    {
2814
        if (!$this->catalog->hasCholeskyDecomposition()) {
68✔
2815
            $this->catalog->addCholeskyDecomposition(Decomposition\Cholesky::decompose($this));
68✔
2816
        }
2817

2818
        return $this->catalog->getCholeskyDecomposition();
67✔
2819
    }
2820

2821
    /**
2822
     * Crout decomposition
2823
     *
2824
     * Decomposes a matrix into a lower triangular matrix (L), an upper triangular matrix (U).
2825
     *
2826
     * A = LU where L = LD
2827
     * A = (LD)U
2828
     *  - L = lower triangular matrix
2829
     *  - D = diagonal matrix
2830
     *  - U = normalised upper triangular matrix
2831
     *
2832
     * @return Decomposition\Crout
2833
     *
2834
     * @throws Exception\MatrixException if there is division by 0 because of a 0-value determinant
2835
     * @throws Exception\OutOfBoundsException
2836
     * @throws Exception\IncorrectTypeException
2837
     */
2838
    public function croutDecomposition(): Decomposition\Crout
2839
    {
2840
        if (!$this->catalog->hasCroutDecomposition()) {
43✔
2841
            $this->catalog->addCroutDecomposition(Decomposition\Crout::decompose($this));
43✔
2842
        }
2843

2844
        return $this->catalog->getCroutDecomposition();
42✔
2845
    }
2846

2847
    /**
2848
     * Singular Value Decomposition (SVD)
2849
     *
2850
     * A = USVᵀ
2851
     *
2852
     * U is an orthogonal matrix
2853
     * S is a diagonal matrix
2854
     * V is an orthogonal matrix
2855
     *
2856
     * @return Decomposition\SVD
2857
     *
2858
     * @throws Exception\MathException
2859
     */
2860
    public function svd(): Decomposition\SVD
2861
    {
2862
        if (!$this->catalog->hasSVD()) {
36✔
2863
            $this->catalog->addSVD(Decomposition\SVD::decompose($this));
36✔
2864
        }
2865

2866
        return $this->catalog->getSVD();
36✔
2867
    }
2868

2869
    /**************************************************************************
2870
     * SOLVE LINEAR SYSTEM OF EQUATIONS
2871
     * - solve
2872
     **************************************************************************/
2873

2874
    /**
2875
     * Solve linear system of equations
2876
     * Ax = b
2877
     *  where:
2878
     *   A: Matrix
2879
     *   x: unknown to solve for
2880
     *   b: solution to linear system of equations (input to function)
2881
     *
2882
     * If A is nxn invertible matrix,
2883
     * and the inverse is already computed:
2884
     *  x = A⁻¹b
2885
     *
2886
     * If 2x2, just take the inverse and solve:
2887
     *  x = A⁻¹b
2888
     *
2889
     * If 3x3 or higher, check if the RREF is already computed,
2890
     * and if so, then just take the inverse and solve:
2891
     *   x = A⁻¹b
2892
     *
2893
     * Otherwise, it is more efficient to decompose and then solve.
2894
     * Use LU Decomposition and solve Ax = b.
2895
     *
2896
     * @param Vector|array<number> $b solution to Ax = b
2897
     * @param string               $method (optional) Force a specific solve method - defaults to DEFAULT where various methods are tried
2898
     *
2899
     * @return Vector x
2900
     *
2901
     * @throws Exception\IncorrectTypeException if b is not a Vector or array
2902
     * @throws Exception\MatrixException
2903
     * @throws Exception\VectorException
2904
     * @throws Exception\OutOfBoundsException
2905
     * @throws Exception\BadParameterException
2906
     */
2907
    public function solve($b, string $method = self::DEFAULT): Vector
2908
    {
2909
        // Input must be a Vector or array.
2910
        // @phpstan-ignore-next-line
2911
        if (!($b instanceof Vector || \is_array($b))) {
360✔
2912
            throw new Exception\IncorrectTypeException('b in Ax = b must be a Vector or array');
2✔
2913
        }
2914
        if (\is_array($b)) {
358✔
2915
            $b = new Vector($b);
95✔
2916
        }
2917

2918
        switch ($method) {
2919
            case self::LU:
358✔
2920
                $lu = $this->luDecomposition();
31✔
2921
                return $lu->solve($b);
31✔
2922

2923
            case self::QR:
327✔
2924
                $qr = $this->qrDecomposition();
31✔
2925
                return $qr->solve($b);
31✔
2926

2927
            case self::INVERSE:
296✔
2928
                $A⁻¹ = $this->inverse();
31✔
2929
                return new Vector($A⁻¹->multiply($b)->getColumn(0));
31✔
2930

2931
            case self::RREF:
265✔
2932
                return $this->solveRref($b);
32✔
2933

2934
            default:
2935
                // If inverse is already calculated, solve: x = A⁻¹b
2936
                if ($this->catalog->hasInverse()) {
233✔
2937
                    /** @var NumericMatrix $inverse */
2938
                    $inverse = $this->catalog->getInverse();
31✔
2939
                    return new Vector($inverse->multiply($b)->getColumn(0));
31✔
2940
                }
2941

2942
                // If 2x2, just compute the inverse and solve: x = A⁻¹b
2943
                if ($this->m === 2 && $this->n === 2) {
202✔
2944
                    $A⁻¹ = $this->inverse();
82✔
2945
                    return new Vector($A⁻¹->multiply($b)->getColumn(0));
82✔
2946
                }
2947

2948
                // For 3x3 or higher, check if the RREF is already computed.
2949
                // If so, just compute the inverse and solve: x = A⁻¹b
2950
                if ($this->catalog->hasReducedRowEchelonForm()) {
130✔
2951
                    $A⁻¹ = $this->inverse();
21✔
2952
                    return new Vector($A⁻¹->multiply($b)->getColumn(0));
21✔
2953
                }
2954

2955
                try {
2956
                    $lu = $this->luDecomposition();
109✔
2957
                    return $lu->solve($b);
109✔
2958
                } catch (Exception\DivisionByZeroException $e) {
2✔
2959
                    // Not solvable via LU decomposition
2960
                }
2961

2962
                // LU failed, use QR Decomposition.
2963
                try {
2964
                    $qr = $this->qrDecomposition();
2✔
2965
                    return $qr->solve($b);
2✔
2966
                } catch (Exception\MatrixException $e) {
2✔
2967
                    // Not solvable via QR decomposition
2968
                }
2969

2970
                // Last resort, augment A with b (Ab) and solve RREF.
2971
                // x is the rightmost column.
2972
                return $this->solveRref($b);
2✔
2973
        }
2974
    }
2975

2976
    /**
2977
     * Solve Ax = b using RREF
2978
     *
2979
     * As an augmented matrix Ab, the RREF has the x solution to Ax = b as the rightmost column.
2980
     *
2981
     * Edge case: If the matrix is singular, there may be one or more rows of zeros at the bottom. This leads to
2982
     * the ones not being on the diagonal. In this case, the rightmost column will not have the values in the correct
2983
     * order. To deal with this, we look at where the ones are and reorder the column vector.
2984
     *
2985
     * @param Vector $b
2986
     * @return Vector
2987
     */
2988
    private function solveRref(Vector $b): Vector
2989
    {
2990
        $Ab   = $this->augment($b->asColumnMatrix());
34✔
2991
        $rref = $Ab->rref();
34✔
2992

2993
        // Edge case if singular matrix
2994
        if ($this->isSingular()) {
34✔
2995
            $x = [];
3✔
2996
            $i = 0;
3✔
2997
            $j = 0;
3✔
2998
            while ($i < $this->m && $j < $this->n) {
3✔
2999
                if ($rref[$i][$j] == 1) {
3✔
3000
                    $x[] = $rref[$i][$this->n];
3✔
3001
                    $i++;
3✔
3002
                    $j++;
3✔
3003
                } else {
3004
                    $x[] = 0;
2✔
3005
                    $j++;
2✔
3006
                }
3007
            }
3008
            return new Vector($x);
3✔
3009
        }
3010

3011
        // Standard case - rightmost column is the solution
3012
        return new Vector(\array_column($rref->getMatrix(), $rref->getN() - 1));
31✔
3013
    }
3014

3015
    /**************************************************************************
3016
     * EIGEN METHODS
3017
     * - eigenvalues
3018
     * - eigenvectors
3019
     **************************************************************************/
3020

3021
    /**
3022
     * Eigenvalues of the matrix.
3023
     * Various eigenvalue algorithms (methods) are available.
3024
     * Use the $method parameter to control the algorithm used.
3025
     *
3026
     * @param string $method Algorithm used to compute the eigenvalues
3027
     *
3028
     * @return array<number> of eigenvalues
3029
     *
3030
     * @throws Exception\MatrixException if method is not a valid eigenvalue method
3031
     * @throws Exception\MathException
3032
     */
3033
    public function eigenvalues(string $method = null): array
3034
    {
3035
        if (!$this->isSquare()) {
249✔
3036
            throw new Exception\MatrixException('Eigenvalues can only be calculated on square matrices');
1✔
3037
        }
3038
        if ($method === null) {
248✔
3039
            if ($this->isTriangular()) {
142✔
3040
                $diagonal = $this->getDiagonalElements();
25✔
3041
                \usort($diagonal, function ($a, $b) {
3042
                    return \abs($b) <=> \abs($a);
18✔
3043
                });
25✔
3044
                return $diagonal;
25✔
3045
            }
3046
            if ($this->m < 5) {
119✔
3047
                return Eigenvalue::closedFormPolynomialRootMethod($this);
103✔
3048
            }
3049
            if ($this->isSymmetric()) {
18✔
3050
                return Eigenvalue::jacobiMethod($this);
17✔
3051
            }
3052
            throw new Exception\MatrixException("Eigenvalue cannot be calculated");
1✔
3053
        } elseif (Eigenvalue::isAvailableMethod($method)) {
106✔
3054
            return Eigenvalue::$method($this);
104✔
3055
        }
3056
        throw new Exception\MatrixException("$method is not a valid eigenvalue method");
2✔
3057
    }
3058

3059
    /**
3060
     * Eigenvectors of the matrix.
3061
     * Eigenvector computation function takes in an array of eigenvalues as input.
3062
     * Various eigenvalue algorithms (methods) are availbale.
3063
     * Use the $method parameter to control the algorithm used.
3064
     *
3065
     * @param string $method Algorithm used to compute the eigenvalues
3066
     *
3067
     * @return NumericMatrix of eigenvectors
3068
     *
3069
     * @throws Exception\MatrixException if method is not a valid eigenvalue method
3070
     * @throws Exception\MathException
3071
     */
3072
    public function eigenvectors(string $method = null): NumericMatrix
3073
    {
3074
        if ($method === null) {
56✔
3075
            return Eigenvector::eigenvectors($this, $this->eigenvalues());
50✔
3076
        }
3077

3078
        return Eigenvector::eigenvectors($this, $this->eigenvalues($method));
6✔
3079
    }
3080

3081
    /**************************************************************************
3082
     * PHP MAGIC METHODS
3083
     *  - __toString
3084
     *  - __debugInfo
3085
     **************************************************************************/
3086

3087
    /**
3088
     * Print the matrix as a string
3089
     * Format is as a matrix, not as the underlying array structure.
3090
     * Ex:
3091
     *  [1, 2, 3]
3092
     *  [2, 3, 4]
3093
     *  [3, 4, 5]
3094
     *
3095
     * @return string
3096
     */
3097
    public function __toString(): string
3098
    {
3099
        // @phpstan-ignore-next-line
3100
        return \trim(\array_reduce(\array_map(
6✔
3101
            function ($mᵢ) {
3102
                return '[' . \implode(', ', $mᵢ) . ']';
6✔
3103
            },
6✔
3104
            $this->A
6✔
3105
        ), function ($A, $mᵢ) {
3106
            return $A . \PHP_EOL . $mᵢ;
6✔
3107
        }));
6✔
3108
    }
3109

3110
    /**
3111
     * Debug info
3112
     * Ex:
3113
     *   [matrix] => 3x4
3114
     *   [data] =>
3115
     *     [1, 2, 3, 4]
3116
     *     [2, 3, 4, 5]
3117
     *     [3, 4, 5, 6]
3118
     *   [ε] => 1.0E-11
3119
     *
3120
     * @return array{matrix: string, data: string, ε: float}
3121
     */
3122
    public function __debugInfo(): array
3123
    {
3124
        return [
3125
            'matrix' => sprintf('%dx%d', $this->m, $this->n),
1✔
3126
            'data'   => \PHP_EOL . (string) $this,
1✔
3127
            'ε'      => $this->ε,
1✔
3128
        ];
3129
    }
3130
}
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

© 2025 Coveralls, Inc