Passed
Pull Request — master (#287)
by Marcin
02:51
created

Matrix::fromFlatArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpml\Math;
6
7
use Phpml\Exception\InvalidArgumentException;
8
use Phpml\Exception\MatrixException;
9
use Phpml\Math\LinearAlgebra\LUDecomposition;
10
11
class Matrix
12
{
13
    /**
14
     * @var array
15
     */
16
    private $matrix = [];
17
18
    /**
19
     * @var int
20
     */
21
    private $rows;
22
23
    /**
24
     * @var int
25
     */
26
    private $columns;
27
28
    /**
29
     * @var float
30
     */
31
    private $determinant;
32
33
    /**
34
     * @throws InvalidArgumentException
35
     */
36
    public function __construct(array $matrix, bool $validate = true)
37
    {
38
        // When a row vector is given
39
        if (!is_array($matrix[0])) {
40
            $this->rows = 1;
41
            $this->columns = count($matrix);
42
            $matrix = [$matrix];
43
        } else {
44
            $this->rows = count($matrix);
45
            $this->columns = count($matrix[0]);
46
        }
47
48
        if ($validate) {
49
            for ($i = 0; $i < $this->rows; ++$i) {
50
                if (count($matrix[$i]) !== $this->columns) {
51
                    throw new InvalidArgumentException('Matrix dimensions did not match');
52
                }
53
            }
54
        }
55
56
        $this->matrix = $matrix;
57
    }
58
59
    public static function fromFlatArray(array $array): self
60
    {
61
        $matrix = [];
62
        foreach ($array as $value) {
63
            $matrix[] = [$value];
64
        }
65
66
        return new self($matrix);
67
    }
68
69
    public function toArray(): array
70
    {
71
        return $this->matrix;
72
    }
73
74
    public function toScalar(): float
75
    {
76
        return $this->matrix[0][0];
77
    }
78
79
    public function getRows(): int
80
    {
81
        return $this->rows;
82
    }
83
84
    public function getColumns(): int
85
    {
86
        return $this->columns;
87
    }
88
89
    /**
90
     * @throws MatrixException
91
     */
92
    public function getColumnValues($column): array
93
    {
94
        if ($column >= $this->columns) {
95
            throw new MatrixException('Column out of range');
96
        }
97
98
        return array_column($this->matrix, $column);
99
    }
100
101
    /**
102
     * @return float|int
103
     *
104
     * @throws MatrixException
105
     */
106
    public function getDeterminant()
107
    {
108
        if ($this->determinant !== null) {
109
            return $this->determinant;
110
        }
111
112
        if (!$this->isSquare()) {
113
            throw new MatrixException('Matrix is not square matrix');
114
        }
115
116
        $lu = new LUDecomposition($this);
117
118
        return $this->determinant = $lu->det();
119
    }
120
121
    public function isSquare(): bool
122
    {
123
        return $this->columns === $this->rows;
124
    }
125
126
    public function transpose(): self
127
    {
128
        if ($this->rows == 1) {
129
            $matrix = array_map(function ($el) {
130
                return [$el];
131
            }, $this->matrix[0]);
132
        } else {
133
            $matrix = array_map(null, ...$this->matrix);
134
        }
135
136
        return new self($matrix, false);
137
    }
138
139
    public function multiply(self $matrix): self
140
    {
141
        if ($this->columns != $matrix->getRows()) {
142
            throw new InvalidArgumentException('Inconsistent matrix supplied');
143
        }
144
145
        $array1 = $this->toArray();
146
        $array2 = $matrix->toArray();
147
        $colCount = $matrix->columns;
148
149
        /*
150
         - To speed-up multiplication, we need to avoid use of array index operator [ ] as much as possible( See #255 for details)
151
         - A combination of "foreach" and "array_column" works much faster then accessing the array via index operator
152
        */
153
        $product = [];
154
        foreach ($array1 as $row => $rowData) {
155
            for ($col = 0; $col < $colCount; ++$col) {
156
                $columnData = array_column($array2, $col);
157
                $sum = 0;
158
                foreach ($rowData as $key => $valueData) {
159
                    $sum += $valueData * $columnData[$key];
160
                }
161
162
                $product[$row][$col] = $sum;
163
            }
164
        }
165
166
        return new self($product, false);
167
    }
168
169 View Code Duplication
    public function divideByScalar($value): self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
170
    {
171
        $newMatrix = [];
172
        for ($i = 0; $i < $this->rows; ++$i) {
173
            for ($j = 0; $j < $this->columns; ++$j) {
174
                $newMatrix[$i][$j] = $this->matrix[$i][$j] / $value;
175
            }
176
        }
177
178
        return new self($newMatrix, false);
179
    }
180
181 View Code Duplication
    public function multiplyByScalar($value): self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
    {
183
        $newMatrix = [];
184
        for ($i = 0; $i < $this->rows; ++$i) {
185
            for ($j = 0; $j < $this->columns; ++$j) {
186
                $newMatrix[$i][$j] = $this->matrix[$i][$j] * $value;
187
            }
188
        }
189
190
        return new self($newMatrix, false);
191
    }
192
193
    /**
194
     * Element-wise addition of the matrix with another one
195
     */
196
    public function add(self $other): self
197
    {
198
        return $this->_add($other);
199
    }
200
201
    /**
202
     * Element-wise subtracting of another matrix from this one
203
     */
204
    public function subtract(self $other): self
205
    {
206
        return $this->_add($other, -1);
207
    }
208
209
    public function inverse(): self
210
    {
211
        if (!$this->isSquare()) {
212
            throw new MatrixException('Matrix is not square matrix');
213
        }
214
215
        $LU = new LUDecomposition($this);
216
        $identity = $this->getIdentity();
217
        $inverse = $LU->solve($identity);
218
219
        return new self($inverse, false);
220
    }
221
222
    public function crossOut(int $row, int $column): self
223
    {
224
        $newMatrix = [];
225
        $r = 0;
226
        for ($i = 0; $i < $this->rows; ++$i) {
227
            $c = 0;
228
            if ($row != $i) {
229
                for ($j = 0; $j < $this->columns; ++$j) {
230
                    if ($column != $j) {
231
                        $newMatrix[$r][$c] = $this->matrix[$i][$j];
232
                        ++$c;
233
                    }
234
                }
235
236
                ++$r;
237
            }
238
        }
239
240
        return new self($newMatrix, false);
241
    }
242
243
    public function isSingular(): bool
244
    {
245
        return $this->getDeterminant() == 0;
246
    }
247
248
    /**
249
     * Frobenius norm (Hilbert–Schmidt norm, Euclidean norm) (‖A‖F)
250
     * Square root of the sum of the square of all elements.
251
     *
252
     * https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm
253
     *
254
     *          _____________
255
     *         /ᵐ   ⁿ
256
     * ‖A‖F = √ Σ   Σ  |aᵢⱼ|²
257
     *         ᵢ₌₁ ᵢ₌₁
258
     */
259
    public function frobeniusNorm(): float
260
    {
261
        $squareSum = 0;
262
        for ($i = 0; $i < $this->rows; ++$i) {
263
            for ($j = 0; $j < $this->columns; ++$j) {
264
                $squareSum += ($this->matrix[$i][$j]) ** 2;
265
            }
266
        }
267
268
        return sqrt($squareSum);
269
    }
270
271
    /**
272
     * Returns the transpose of given array
273
     */
274
    public static function transposeArray(array $array): array
275
    {
276
        return (new self($array, false))->transpose()->toArray();
277
    }
278
279
    /**
280
     * Returns the dot product of two arrays<br>
281
     * Matrix::dot(x, y) ==> x.y'
282
     */
283
    public static function dot(array $array1, array $array2): array
284
    {
285
        $m1 = new self($array1, false);
286
        $m2 = new self($array2, false);
287
288
        return $m1->multiply($m2->transpose())->toArray()[0];
289
    }
290
291
    /**
292
     * Element-wise addition or substraction depending on the given sign parameter
293
     */
294
    private function _add(self $other, int $sign = 1): self
295
    {
296
        $a1 = $this->toArray();
297
        $a2 = $other->toArray();
298
299
        $newMatrix = [];
300
        for ($i = 0; $i < $this->rows; ++$i) {
301
            for ($k = 0; $k < $this->columns; ++$k) {
302
                $newMatrix[$i][$k] = $a1[$i][$k] + $sign * $a2[$i][$k];
303
            }
304
        }
305
306
        return new self($newMatrix, false);
307
    }
308
309
    /**
310
     * Returns diagonal identity matrix of the same size of this matrix
311
     */
312
    private function getIdentity(): self
313
    {
314
        $array = array_fill(0, $this->rows, array_fill(0, $this->columns, 0));
315
        for ($i = 0; $i < $this->rows; ++$i) {
316
            $array[$i][$i] = 1;
317
        }
318
319
        return new self($array, false);
320
    }
321
}
322