See Release Notes
Long Term Support Release
<?php declare(strict_types=1); namespace Phpml\Math; use Phpml\Exception\InvalidArgumentException; use Phpml\Exception\MatrixException; use Phpml\Math\LinearAlgebra\LUDecomposition; class Matrix { /** * @var array */ private $matrix = []; /** * @var int */ private $rows; /** * @var int */ private $columns; /** * @var float */ private $determinant; /** * @throws InvalidArgumentException */ public function __construct(array $matrix, bool $validate = true) { // When a row vector is given if (!is_array($matrix[0])) { $this->rows = 1; $this->columns = count($matrix); $matrix = [$matrix]; } else { $this->rows = count($matrix); $this->columns = count($matrix[0]); } if ($validate) { for ($i = 0; $i < $this->rows; ++$i) { if (count($matrix[$i]) !== $this->columns) { throw new InvalidArgumentException('Matrix dimensions did not match'); } } } $this->matrix = $matrix; } public static function fromFlatArray(array $array): self { $matrix = []; foreach ($array as $value) { $matrix[] = [$value]; } return new self($matrix); } public function toArray(): array { return $this->matrix; } public function toScalar(): float { return $this->matrix[0][0]; } public function getRows(): int { return $this->rows; } public function getColumns(): int { return $this->columns; } /** * @throws MatrixException */ public function getColumnValues(int $column): array { if ($column >= $this->columns) { throw new MatrixException('Column out of range'); } return array_column($this->matrix, $column); } /** * @return float|int * * @throws MatrixException */ public function getDeterminant() { if ($this->determinant !== null) { return $this->determinant; } if (!$this->isSquare()) { throw new MatrixException('Matrix is not square matrix'); } $lu = new LUDecomposition($this); return $this->determinant = $lu->det(); } public function isSquare(): bool { return $this->columns === $this->rows; } public function transpose(): self { if ($this->rows === 1) {< $matrix = array_map(function ($el) {> $matrix = array_map(static function ($el): array {return [$el]; }, $this->matrix[0]); } else { $matrix = array_map(null, ...$this->matrix); } return new self($matrix, false); } public function multiply(self $matrix): self { if ($this->columns !== $matrix->getRows()) { throw new InvalidArgumentException('Inconsistent matrix supplied'); } $array1 = $this->toArray(); $array2 = $matrix->toArray(); $colCount = $matrix->columns; /* - To speed-up multiplication, we need to avoid use of array index operator [ ] as much as possible( See #255 for details) - A combination of "foreach" and "array_column" works much faster then accessing the array via index operator */ $product = []; foreach ($array1 as $row => $rowData) { for ($col = 0; $col < $colCount; ++$col) { $columnData = array_column($array2, $col); $sum = 0; foreach ($rowData as $key => $valueData) { $sum += $valueData * $columnData[$key]; } $product[$row][$col] = $sum; } } return new self($product, false); } /** * @param float|int $value */ public function divideByScalar($value): self { $newMatrix = []; for ($i = 0; $i < $this->rows; ++$i) { for ($j = 0; $j < $this->columns; ++$j) { $newMatrix[$i][$j] = $this->matrix[$i][$j] / $value; } } return new self($newMatrix, false); } /** * @param float|int $value */ public function multiplyByScalar($value): self { $newMatrix = []; for ($i = 0; $i < $this->rows; ++$i) { for ($j = 0; $j < $this->columns; ++$j) { $newMatrix[$i][$j] = $this->matrix[$i][$j] * $value; } } return new self($newMatrix, false); } /** * Element-wise addition of the matrix with another one */ public function add(self $other): self {< return $this->_add($other);> return $this->sum($other);} /** * Element-wise subtracting of another matrix from this one */ public function subtract(self $other): self {< return $this->_add($other, -1);> return $this->sum($other, -1);} public function inverse(): self { if (!$this->isSquare()) { throw new MatrixException('Matrix is not square matrix'); } $LU = new LUDecomposition($this); $identity = $this->getIdentity(); $inverse = $LU->solve($identity); return new self($inverse, false); } public function crossOut(int $row, int $column): self { $newMatrix = []; $r = 0; for ($i = 0; $i < $this->rows; ++$i) { $c = 0; if ($row != $i) { for ($j = 0; $j < $this->columns; ++$j) { if ($column != $j) { $newMatrix[$r][$c] = $this->matrix[$i][$j]; ++$c; } } ++$r; } } return new self($newMatrix, false); } public function isSingular(): bool { return $this->getDeterminant() == 0; } /** * Frobenius norm (Hilbert–Schmidt norm, Euclidean norm) (‖A‖F) * Square root of the sum of the square of all elements. * * https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm * * _____________ * /ᵐ ⁿ * ‖A‖F = √ Σ Σ |aᵢⱼ|² * ᵢ₌₁ ᵢ₌₁ */ public function frobeniusNorm(): float { $squareSum = 0; for ($i = 0; $i < $this->rows; ++$i) { for ($j = 0; $j < $this->columns; ++$j) { $squareSum += $this->matrix[$i][$j] ** 2; } } return $squareSum ** .5; } /** * Returns the transpose of given array */ public static function transposeArray(array $array): array { return (new self($array, false))->transpose()->toArray(); } /** * Returns the dot product of two arrays<br> * Matrix::dot(x, y) ==> x.y' */ public static function dot(array $array1, array $array2): array { $m1 = new self($array1, false); $m2 = new self($array2, false); return $m1->multiply($m2->transpose())->toArray()[0]; } /** * Element-wise addition or substraction depending on the given sign parameter */< private function _add(self $other, int $sign = 1): self> private function sum(self $other, int $sign = 1): self{ $a1 = $this->toArray(); $a2 = $other->toArray(); $newMatrix = []; for ($i = 0; $i < $this->rows; ++$i) { for ($k = 0; $k < $this->columns; ++$k) { $newMatrix[$i][$k] = $a1[$i][$k] + $sign * $a2[$i][$k]; } } return new self($newMatrix, false); } /** * Returns diagonal identity matrix of the same size of this matrix */ private function getIdentity(): self { $array = array_fill(0, $this->rows, array_fill(0, $this->columns, 0)); for ($i = 0; $i < $this->rows; ++$i) { $array[$i][$i] = 1; } return new self($array, false); } }