Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.09% covered (warning)
65.09%
179 / 275
76.19% covered (warning)
76.19%
32 / 42
CRAP
0.00% covered (danger)
0.00%
0 / 1
Matrix
65.09% covered (warning)
65.09%
179 / 275
76.19% covered (warning)
76.19%
32 / 42
885.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fromArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 transpose
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getMatrix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSubMatrix
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getSubMatrixByColumnsRows
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getSubMatrixByColumns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getSubMatrixByRows
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSymmetric
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 rank
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
11
 setMatrix
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 sub
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addMatrix
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 getM
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getN
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addScalar
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 mult
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 multMatrix
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 multScalar
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 upperTriangular
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 upperTrianglize
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
9.24
 inverse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 solve
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 det
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 dot
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
306
 sum
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 isDiagonal
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 pow
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 exp
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 current
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 next
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 rewind
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetSet
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 offsetUnset
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Math\Matrix
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Math\Matrix;
16
17use phpOMS\Math\Matrix\Exception\InvalidDimensionException;
18
19/**
20 * Matrix class
21 *
22 * @package phpOMS\Math\Matrix
23 * @license OMS License 2.0
24 * @link    https://jingga.app
25 * @since   1.0.0
26 *
27 * @phpstan-implements \ArrayAccess<string, mixed>
28 * @phpstan-implements \Iterator<string, mixed>
29 */
30class Matrix implements \ArrayAccess, \Iterator
31{
32    /**
33     * Epsilon for float comparison.
34     *
35     * @var float
36     * @since 1.0.0
37     */
38    public const EPSILON = 4.88e-04;
39
40    /**
41     * Matrix.
42     *
43     * @var array<int, array<int, int|float>>
44     * @since 1.0.0
45     */
46    public array $matrix = [];
47
48    /**
49     * Columns.
50     *
51     * @var int<0, max>
52     * @since 1.0.0
53     */
54    protected int $n = 0;
55
56    /**
57     * Rows.
58     *
59     * @var int<0, max>
60     * @since 1.0.0
61     */
62    protected int $m = 0;
63
64    /**
65     * Iterator position.
66     *
67     * @var int
68     * @since 1.0.0
69     */
70    protected int $position = 0;
71
72    /**
73     * Constructor.
74     *
75     * @param int<0, max> $m Rows
76     * @param int<0, max> $n Columns
77     *
78     * @since 1.0.0
79     */
80    public function __construct(int $m = 1, int $n = 1)
81    {
82        $this->n = $n;
83        $this->m = $m;
84
85        for ($i = 0; $i < $m; ++$i) {
86            $this->matrix[$i] = \array_fill(0, $n, 0);
87        }
88    }
89
90    /**
91     * Create matrix from array
92     *
93     * @param array $matrix Matrix array
94     *
95     * @return self
96     *
97     * @since 1.0.0
98     */
99    public static function fromArray(array $matrix) : self
100    {
101        $m = new self();
102        $m->setMatrix($matrix);
103
104        return $m;
105    }
106
107    /**
108     * Set value.
109     *
110     * @param int       $m     Row
111     * @param int       $n     Column
112     * @param int|float $value Value
113     *
114     * @return void
115     *
116     * @throws InvalidDimensionException
117     *
118     * @since 1.0.0
119     */
120    public function set(int $m, int $n, int | float $value) : void
121    {
122        if (!isset($this->matrix[$m], $this->matrix[$m][$n])) {
123            throw new InvalidDimensionException($m . 'x' . $n);
124        }
125
126        $this->matrix[$m][$n] = $value;
127    }
128
129    /**
130     * Get value.
131     *
132     * @param int $m Row
133     * @param int $n Column
134     *
135     * @return int|float
136     *
137     * @throws InvalidDimensionException
138     *
139     * @since 1.0.0
140     */
141    public function get(int $m, int $n = 0) : int | float
142    {
143        if (!isset($this->matrix[$m], $this->matrix[$m][$n])) {
144            throw new InvalidDimensionException($m . 'x' . $n);
145        }
146
147        return $this->matrix[$m][$n];
148    }
149
150    /**
151     * Transpose matrix.
152     *
153     * @return Matrix
154     *
155     * @since 1.0.0
156     */
157    public function transpose() : self
158    {
159        $matrix = new self($this->n, $this->m);
160        $matrix->setMatrix(\array_map(null, ...$this->matrix));
161
162        return $matrix;
163    }
164
165    /**
166     * Get matrix array.
167     *
168     * @return array<int, array<int, mixed>>
169     *
170     * @since 1.0.0
171     */
172    public function getMatrix() : array
173    {
174        return $this->matrix;
175    }
176
177    /**
178     * Get sub matrix array.
179     *
180     * @param int $iRow Start row
181     * @param int $lRow End row
182     * @param int $iCol Start col
183     * @param int $lCol End col
184     *
185     * @return Matrix
186     *
187     * @since 1.0.0
188     */
189    public function getSubMatrix(int $iRow, int $lRow, int $iCol, int $lCol) : self
190    {
191        $X = [[]];
192        for ($i = $iRow; $i <= $lRow; ++$i) {
193            for ($j = $iCol; $j <= $lCol; ++$j) {
194                $X[$i - $iRow][$j - $iCol] = $this->matrix[$i][$j];
195            }
196        }
197
198        $matrix = new self();
199        $matrix->setMatrix($X);
200
201        return $matrix;
202    }
203
204    /**
205     * Get sub matrix array.
206     *
207     * @param int[] $rows Row indices
208     * @param int[] $cols Row indices
209     *
210     * @return Matrix
211     *
212     * @since 1.0.0
213     */
214    public function getSubMatrixByColumnsRows(array $rows, array $cols) : self
215    {
216        $X       = [[]];
217        $rlength = \count($rows);
218        $clength = \count($cols);
219
220        for ($i = 0; $i < $rlength; ++$i) {
221            for ($j = 0; $j < $clength; ++$j) {
222                $X[$i][$j] = $this->matrix[$rows[$i]][$cols[$j]];
223            }
224        }
225
226        $matrix = new self();
227        $matrix->setMatrix($X);
228
229        return $matrix;
230    }
231
232    /**
233     * Get sub matrix array.
234     *
235     * @param int   $iRow Start row
236     * @param int   $lRow End row
237     * @param int[] $cols Row indices
238     *
239     * @return Matrix
240     *
241     * @since 1.0.0
242     */
243    public function getSubMatrixByColumns(int $iRow, int $lRow, array $cols) : self
244    {
245        $X      = [[]];
246        $length = \count($cols);
247
248        for ($i = $iRow; $i <= $lRow; ++$i) {
249            for ($j = 0; $j < $length; ++$j) {
250                $X[$i - $iRow][$j] = $this->matrix[$i][$cols[$j]];
251            }
252        }
253
254        $matrix = new self();
255        $matrix->setMatrix($X);
256
257        return $matrix;
258    }
259
260    /**
261     * Get sub matrix array.
262     *
263     * @param int[] $rows Row indices
264     * @param int   $iCol Start col
265     * @param int   $lCol End col
266     *
267     * @return Matrix
268     *
269     * @since 1.0.0
270     */
271    public function getSubMatrixByRows(array $rows, int $iCol, int $lCol) : self
272    {
273        $X      = [[]];
274        $length = \count($rows);
275
276        for ($i = 0; $i < $length; ++$i) {
277            for ($j = $iCol; $j <= $lCol; ++$j) {
278                $X[$i][$j - $iCol] = $this->matrix[$rows[$i]][$j];
279            }
280        }
281
282        $matrix = new self();
283        $matrix->setMatrix($X);
284
285        return $matrix;
286    }
287
288    /**
289     * Get matrix array.
290     *
291     * @return array<int, array<int, int|float>>
292     *
293     * @since 1.0.0
294     */
295    public function toArray() : array
296    {
297        return $this->matrix;
298    }
299
300    /**
301     * Is symmetric.
302     *
303     * @return bool
304     *
305     * @since 1.0.0
306     */
307    public function isSymmetric() : bool
308    {
309        $isSymmetric = true;
310        for ($j = 0; ($j < $this->m) & $isSymmetric; ++$j) {
311            for ($i = 0; ($i < $this->n) & $isSymmetric; ++$i) {
312                $isSymmetric = ($this->matrix[$i][$j] === $this->matrix[$j][$i]);
313            }
314        }
315
316        return $isSymmetric;
317    }
318
319    /**
320     * Get matrix rank.
321     *
322     * @return int
323     *
324     * @since 1.0.0
325     */
326    public function rank() : int
327    {
328        $matrix = $this->matrix;
329        $mDim   = $this->m;
330        $nDim   = $this->n;
331
332        $rank     = 0;
333        $selected = \array_fill(0, $mDim, false);
334
335        for ($i = 0; $i < $nDim; ++$i) {
336            for ($j = 0; $j < $mDim; ++$j) {
337                if (!$selected[$j] && \abs($matrix[$j][$i]) > self::EPSILON) {
338                    break;
339                }
340            }
341
342            if ($j === $mDim) {
343                continue;
344            }
345
346            ++$rank;
347            $selected[$j] = true;
348            for ($p = $i + 1; $p < $nDim; ++$p) {
349                $matrix[$j][$p] /= $matrix[$j][$i];
350            }
351
352            for ($k = 0; $k < $mDim; ++$k) {
353                if ($k !== $j && \abs($matrix[$k][$i]) > self::EPSILON) {
354                    for ($p = $i + 1; $p < $nDim; ++$p) {
355                        $matrix[$k][$p] -= $matrix[$j][$p] * $matrix[$k][$i];
356                    }
357                }
358            }
359        }
360
361        return $rank;
362    }
363
364    /**
365     * Set matrix array.
366     *
367     * @param array<int, array<int|float>> $matrix Matrix
368     *
369     * @return Matrix
370     *
371     * @since 1.0.0
372     */
373    public function setMatrix(array $matrix) : self
374    {
375        $this->m      = \count($matrix);
376        $this->n      = \is_array($matrix[0] ?? 1) ? \count($matrix[0]) : 1;
377        $this->matrix = $matrix;
378
379        return $this;
380    }
381
382    /**
383     * Subtract right.
384     *
385     * @param int|float|self $value Value
386     *
387     * @return Matrix
388     *
389     * @since 1.0.0
390     */
391    public function sub(int | float | self $value) : self
392    {
393        if (\is_numeric($value)) {
394            return $this->addScalar(-$value);
395        }
396
397        return $this->add($value->mult(-1));
398    }
399
400    /**
401     * Add right.
402     *
403     * @param int|float|self $value Value
404     *
405     * @return Matrix
406     *
407     * @since 1.0.0
408     */
409    public function add(int | float | self $value) : self
410    {
411        if (\is_numeric($value)) {
412            return $this->addScalar($value);
413        }
414
415        return $this->addMatrix($value);
416    }
417
418    /**
419     * Add matrix.
420     *
421     * @param Matrix $matrix Matrix to add
422     *
423     * @return Matrix
424     *
425     * @throws InvalidDimensionException
426     *
427     * @since 1.0.0
428     */
429    private function addMatrix(self $matrix) : self
430    {
431        if ($this->m !== $matrix->getM() || $this->n !== $matrix->getN()) {
432            throw new InvalidDimensionException($matrix->getM() . 'x' . $matrix->getN());
433        }
434
435        $matrixArr    = $matrix->getMatrix();
436        $newMatrixArr = $this->matrix;
437
438        foreach ($newMatrixArr as $i => $vector) {
439            foreach ($vector as $j => $_) {
440                $newMatrixArr[$i][$j] += $matrixArr[$i][$j];
441            }
442        }
443
444        $newMatrix = new self($this->m, $this->n);
445        $newMatrix->setMatrix($newMatrixArr);
446
447        return $newMatrix;
448    }
449
450    /**
451     * Get matrix rows.
452     *
453     * @return int<0, max>
454     *
455     * @since 1.0.0
456     */
457    public function getM() : int
458    {
459        return $this->m;
460    }
461
462    /**
463     * Get matrix columns.
464     *
465     * @return int<0, max>
466     *
467     * @since 1.0.0
468     */
469    public function getN() : int
470    {
471        return $this->n;
472    }
473
474    /**
475     * Add scalar.
476     *
477     * @param int|float $scalar Scalar
478     *
479     * @return Matrix
480     *
481     * @since 1.0.0
482     */
483    private function addScalar(int | float $scalar) : self
484    {
485        $newMatrixArr = $this->matrix;
486
487        foreach ($newMatrixArr as $i => $vector) {
488            foreach ($vector as $j => $value) {
489                $newMatrixArr[$i][$j] += $scalar;
490            }
491        }
492
493        $newMatrix = new self($this->m, $this->n);
494        $newMatrix->setMatrix($newMatrixArr);
495
496        return $newMatrix;
497    }
498
499    /**
500     * Multiply right.
501     *
502     * @param int|float|self $value Factor
503     *
504     * @return Matrix
505     *
506     * @since 1.0.0
507     */
508    public function mult(int | float | self $value) : self
509    {
510        if (\is_numeric($value)) {
511            return $this->multScalar($value);
512        }
513
514        return $this->multMatrix($value);
515    }
516
517    /**
518     * Multiply matrix.
519     *
520     * @param Matrix $matrix Matrix to multiply with
521     *
522     * @return Matrix
523     *
524     * @throws InvalidDimensionException
525     *
526     * @since 1.0.0
527     */
528    private function multMatrix(self $matrix) : self
529    {
530        $nDim = $matrix->getN();
531        $mDim = $matrix->getM();
532
533        if ($mDim !== $this->n) {
534            throw new InvalidDimensionException($mDim . 'x' . $nDim);
535        }
536
537        $matrixArr    = $matrix->getMatrix();
538        $newMatrix    = new self($this->m, $nDim);
539        $newMatrixArr = $newMatrix->getMatrix();
540
541        for ($i = 0; $i < $this->m; ++$i) { // Row of $this
542            for ($c = 0; $c < $nDim; ++$c) { // Column of $matrix
543                $temp = 0;
544
545                for ($j = 0; $j < $mDim; ++$j) { // Row of $matrix
546                    $temp += ($this->matrix[$i][$j] ?? 0) * ($matrixArr[$j][$c] ?? 0);
547                }
548
549                $newMatrixArr[$i][$c] = $temp;
550            }
551        }
552
553        $newMatrix->setMatrix($newMatrixArr); /* @phpstan-ignore-line */
554
555        return $newMatrix;
556    }
557
558    /**
559     * Multiply matrix.
560     *
561     * @param int|float $scalar Scalar value
562     *
563     * @return Matrix
564     *
565     * @since 1.0.0
566     */
567    private function multScalar(int | float $scalar) : self
568    {
569        $newMatrixArr = $this->matrix;
570
571        foreach ($newMatrixArr as $i => $vector) {
572            foreach ($vector as $j => $value) {
573                $newMatrixArr[$i][$j] *= $scalar;
574            }
575        }
576
577        $newMatrix = new self($this->m, $this->n);
578        $newMatrix->setMatrix($newMatrixArr);
579
580        return $newMatrix;
581    }
582
583    /**
584     * Upper triangulize matrix.
585     *
586     * @return Matrix
587     *
588     * @since 1.0.0
589     */
590    public function upperTriangular() : self
591    {
592        $matrix = new self($this->n, $this->n);
593
594        $matrixArr = $this->matrix;
595        $this->upperTrianglize($matrixArr);
596        $matrix->setMatrix($matrixArr);
597
598        return $matrix;
599    }
600
601    /**
602     * Trianglize matrix.
603     *
604     * @param array<int, array<int|float>> $arr Matrix to trianglize
605     *
606     * @return int Det sign
607     *
608     * @since 1.0.0
609     */
610    private function upperTrianglize(array &$arr) : int
611    {
612        $n    = $this->n;
613        $sign = 1;
614
615        for ($i = 0; $i < $n; ++$i) {
616            $max = 0;
617
618            for ($j = $i; $j < $n; ++$j) {
619                if (\abs($arr[$j][$i]) > \abs($arr[$max][$i])) {
620                    $max = $j;
621                }
622            }
623
624            if ($max !== 0) {
625                $sign      = -$sign;
626                $temp      = $arr[$i];
627                $arr[$i]   = $arr[$max];
628                $arr[$max] = $temp;
629            }
630
631            if (!$arr[$i][$i]) {
632                return 0;
633            }
634
635            for ($j = $i + 1; $j < $n; ++$j) {
636                $r = $arr[$j][$i] / $arr[$i][$i];
637
638                if (!$r) {
639                    continue;
640                }
641
642                for ($c = $i; $c < $n; ++$c) {
643                    $arr[$j][$c] -= $arr[$i][$c] * $r;
644                }
645            }
646        }
647
648        return $sign;
649    }
650
651    /**
652     * Inverse matrix.
653     *
654     * @return Matrix
655     *
656     * @throws InvalidDimensionException
657     *
658     * @since 1.0.0
659     */
660    public function inverse() : self
661    {
662        return $this->solve(new IdentityMatrix($this->m));
663    }
664
665    /**
666     * Solve matrix
667     *
668     * @param Matrix $B Matrix/Vector b
669     *
670     * @return Matrix|Vector
671     *
672     * @since 1.0.0
673     */
674    public function solve(self $B) : self
675    {
676        $M = $this->m === $this->n ? new LUDecomposition($this) : new QRDecomposition($this);
677
678        return $M->solve($B);
679    }
680
681    /**
682     * Calculate det.
683     *
684     * @return float
685     *
686     * @since 1.0.0
687     */
688    public function det() : float
689    {
690        $L = new LUDecomposition($this);
691
692        return $L->det();
693    }
694
695    /**
696     * Dot product
697     *
698     * @param self $B Matrix
699     *
700     * @return self
701     *
702     * @since 1.0.0
703     */
704    public function dot(self $B) : self
705    {
706        $value1 = $this->matrix;
707        $value2 = $B->getMatrix();
708
709        $m1 = \count($value1);
710        $n1 = ($isMatrix1 = \is_array($value1[0])) ? \count($value1[0]) : 1;
711
712        $m2 = \count($value2);
713        $n2 = ($isMatrix2 = \is_array($value2[0])) ? \count($value2[0]) : 1;
714
715        $result = null;
716
717        if ($isMatrix1 && $isMatrix2) {
718            if ($m2 !== $n1) {
719                throw new InvalidDimensionException($m2 . 'x' . $n2 . ' not compatible with ' . $m1 . 'x' . $n1);
720            }
721
722            $result = [[]];
723            for ($i = 0; $i < $m1; ++$i) { // Row of 1
724                for ($c = 0; $c < $n2; ++$c) { // Column of 2
725                    $temp = 0;
726
727                    for ($j = 0; $j < $m2; ++$j) { // Row of 2
728                        $temp += $value1[$i][$j] * $value2[$j][$c];
729                    }
730
731                    $result[$i][$c] = $temp;
732                }
733            }
734
735            return self::fromArray($result);
736        } elseif (!$isMatrix1 && !$isMatrix2) {
737            if ($m1 !== $m2) {
738                throw new InvalidDimensionException($m1 . 'x' . $m2);
739            }
740
741            $result = 0;
742            for ($i = 0; $i < $m1; ++$i) {
743                /** @var array $value1 */
744                /** @var array $value2 */
745                $result += $value1[$i] * $value2[$i];
746            }
747
748            return self::fromArray([[$result]]);
749        } elseif ($isMatrix1 && !$isMatrix2) {
750            $result = [];
751            for ($i = 0; $i < $m1; ++$i) { // Row of 1
752                $temp = 0;
753
754                for ($c = 0; $c < $m2; ++$c) { // Row of 2
755                    /** @var array $value2 */
756                    $temp += $value1[$i][$c] * $value2[$c];
757                }
758
759                $result[$i] = $temp;
760            }
761
762            return self::fromArray($result);
763        }
764
765        throw new \InvalidArgumentException();
766    }
767
768    /**
769     * Sum the elements in the matrix
770     *
771     * @param int $axis Axis (-1 -> all dimensions, 0 -> columns, 1 -> rows)
772     *
773     * @return int|float|self Returns int or float for axis -1
774     *
775     * @since 1.0.0
776     */
777    public function sum(int $axis = -1) : int|float|self
778    {
779        if ($axis === -1) {
780            $sum = 0;
781
782            foreach ($this->matrix as $row) {
783                $sum += \array_sum($row);
784            }
785
786            return $sum;
787        } elseif ($axis === 0) {
788            $sum = [];
789            foreach ($this->matrix as $row) {
790                foreach ($row as $idx2 => $value) {
791                    if (!isset($sum[$idx2])) {
792                        $sum[$idx2] = 0;
793                    }
794
795                    $sum[$idx2] += $value;
796                }
797            }
798
799            return self::fromArray($sum);
800        } elseif ($axis === 1) {
801            $sum = [];
802            foreach ($this->matrix as $idx => $row) {
803                $sum[$idx] = \array_sum($row);
804            }
805
806            return self::fromArray($sum);
807        }
808
809        return new self();
810    }
811
812    /**
813     * Is matrix a diagonal matrix
814     *
815     * @return bool
816     *
817     * @since 1.0.0
818     */
819    public function isDiagonal() : bool
820    {
821        if ($this->m !== $this->n) {
822            return false;
823        }
824
825        for ($i = 0; $i < $this->m; ++$i) {
826            for ($j = 0; $j < $this->n; ++$j) {
827                if ($i !== $j && \abs($this->matrix[$i][$j]) > self::EPSILON) {
828                    return false;
829                }
830            }
831        }
832
833        return true;
834    }
835
836    /**
837     * Calculate the power of a matrix
838     *
839     * @param int|float $exponent Exponent
840     *
841     * @return self
842     *
843     * @throws InvalidDimensionException
844     *
845     * @since 1.0.0
846     */
847    public function pow(int | float $exponent) : self
848    {
849        if ($this->isDiagonal()) {
850            $matrix = [];
851
852            for ($i = 0; $i < $this->m; ++$i) {
853                $row = [];
854                for ($j = 0; $j < $this->m; ++$j) {
855                    $row[] = $i === $j ? \pow($this->matrix[$i][$j], $exponent) : 0;
856                }
857
858                $matrix[] = $row;
859            }
860
861            return self::fromArray($matrix);
862        } elseif (\is_int($exponent)) {
863            if ($this->m !== $this->n) {
864                throw new InvalidDimensionException($this->m . 'x' . $this->n);
865            }
866
867            $matrix = new IdentityMatrix($this->m);
868            for ($i = 0; $i < $exponent; ++$i) {
869                $matrix = $matrix->mult($this);
870            }
871
872            return $matrix;
873        } else {
874            // @todo: implement
875            throw new \Exception('Not yet implemented');
876        }
877    }
878
879    /**
880     * Calculate e^M
881     *
882     * @param int $iterations Iterations for approximation
883     *
884     * @return self
885     *
886     * @since 1.0.0
887     */
888    public function exp(int $iterations = 10) : self
889    {
890        if ($this->m !== $this->n) {
891            throw new InvalidDimensionException($this->m . 'x' . $this->n);
892        }
893
894        $identity = new IdentityMatrix($this->m);
895        $matrix   = $identity;
896
897        $factorial = 1;
898        $pow       = $matrix;
899
900        for ($i = 1; $i <= $iterations; ++$i) {
901            $factorial *= $i;
902            $coeff      = 1 / $factorial;
903
904            $term   = $pow->mult($coeff);
905            $matrix = $matrix->add($term);
906            $pow    = $pow->mult($matrix); // @todo: maybe wrong order?
907        }
908
909        return $matrix;
910    }
911
912    /**
913     * {@inheritdoc}
914     */
915    public function current() : int
916    {
917        $row = (int) ($this->position / $this->m);
918
919        return $this->matrix[$row][$this->position - $row * $this->n];
920    }
921
922    /**
923     * {@inheritdoc}
924     */
925    public function offsetGet(mixed $offset) : mixed
926    {
927        if (!\is_int($offset)) {
928            return 0;
929        }
930
931        $offset = (int) $offset;
932        $row    = (int) ($offset / $this->m);
933
934        return $this->matrix[$row][$offset - $row * $this->n];
935    }
936
937    /**
938     * {@inheritdoc}
939     */
940    public function next() : void
941    {
942        ++$this->position;
943    }
944
945    /**
946     * {@inheritdoc}
947     */
948    public function key() : mixed
949    {
950        return $this->position;
951    }
952
953    /**
954     * {@inheritdoc}
955     */
956    public function valid() : bool
957    {
958        return $this->offsetExists($this->position);
959    }
960
961    /**
962     * {@inheritdoc}
963     */
964    public function offsetExists(mixed $offset) : bool
965    {
966        if (!\is_int($offset)) {
967            return false;
968        }
969
970        $offset = (int) $offset;
971        $row    = (int) ($offset / $this->m);
972
973        return isset($this->matrix[$row][$offset - $row * $this->n]);
974    }
975
976    /**
977     * {@inheritdoc}
978     */
979    public function rewind() : void
980    {
981        $this->position = 0;
982    }
983
984    /**
985     * {@inheritdoc}
986     */
987    public function offsetSet(mixed $offset, mixed $value) : void
988    {
989        if (!\is_int($offset) || !\is_numeric($value)) {
990            return;
991        }
992
993        $offset                                        = (int) $offset;
994        $row                                           = (int) ($offset / $this->m);
995        $this->matrix[$row][$offset - $row * $this->n] = $value; /* @phpstan-ignore-line */
996    }
997
998    /**
999     * {@inheritdoc}
1000     */
1001    public function offsetUnset(mixed $offset) : void
1002    {
1003        if (!\is_int($offset)) {
1004            return;
1005        }
1006
1007        $offset = (int) $offset;
1008        $row    = (int) ($offset / $this->m);
1009        unset($this->matrix[$row][$offset - $row * $this->n]);
1010    }
1011}