Matrix   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 50
eloc 106
c 1
b 0
f 0
dl 0
loc 315
rs 8.4

23 Methods

Rating   Name   Duplication   Size   Complexity  
A multiplyByScalar() 0 10 3
A toScalar() 0 3 1
A _add() 0 13 3
A transpose() 0 11 2
A frobeniusNorm() 0 10 3
A transposeArray() 0 3 1
A subtract() 0 3 1
A fromFlatArray() 0 8 2
A __construct() 0 21 5
A divideByScalar() 0 10 3
A getColumnValues() 0 7 2
A add() 0 3 1
A dot() 0 6 1
A getRows() 0 3 1
A getColumns() 0 3 1
A getIdentity() 0 8 2
A inverse() 0 11 2
A crossOut() 0 19 5
A toArray() 0 3 1
A getDeterminant() 0 13 3
A isSingular() 0 3 1
A isSquare() 0 3 1
A multiply() 0 28 5

How to fix   Complexity   

Complex Class

Complex classes like Matrix often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Matrix, and based on these observations, apply Extract Interface, too.

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