2016-04-29 22:58:54 +00:00
|
|
|
|
<?php
|
2016-04-29 22:59:10 +00:00
|
|
|
|
|
2016-11-20 21:53:17 +00:00
|
|
|
|
declare(strict_types=1);
|
2016-04-29 22:58:54 +00:00
|
|
|
|
|
|
|
|
|
namespace Phpml\Math;
|
|
|
|
|
|
|
|
|
|
use Phpml\Exception\InvalidArgumentException;
|
|
|
|
|
use Phpml\Exception\MatrixException;
|
2017-11-06 07:56:37 +00:00
|
|
|
|
use Phpml\Math\LinearAlgebra\LUDecomposition;
|
2016-04-29 22:58:54 +00:00
|
|
|
|
|
|
|
|
|
class Matrix
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @var array
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
private $matrix = [];
|
2016-04-29 22:58:54 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var int
|
|
|
|
|
*/
|
|
|
|
|
private $rows;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var int
|
|
|
|
|
*/
|
|
|
|
|
private $columns;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var float
|
|
|
|
|
*/
|
|
|
|
|
private $determinant;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
*/
|
|
|
|
|
public function __construct(array $matrix, bool $validate = true)
|
|
|
|
|
{
|
2017-04-23 07:03:30 +00:00
|
|
|
|
// 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]);
|
|
|
|
|
}
|
2016-04-29 22:58:54 +00:00
|
|
|
|
|
2016-04-29 22:59:10 +00:00
|
|
|
|
if ($validate) {
|
|
|
|
|
for ($i = 0; $i < $this->rows; ++$i) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
if (count($matrix[$i]) !== $this->columns) {
|
2018-03-03 15:03:53 +00:00
|
|
|
|
throw new InvalidArgumentException('Matrix dimensions did not match');
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->matrix = $matrix;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public static function fromFlatArray(array $array): self
|
2016-04-30 11:54:01 +00:00
|
|
|
|
{
|
|
|
|
|
$matrix = [];
|
|
|
|
|
foreach ($array as $value) {
|
|
|
|
|
$matrix[] = [$value];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new self($matrix);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function toArray(): array
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->matrix;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function toScalar(): float
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->matrix[0][0];
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-06 07:56:37 +00:00
|
|
|
|
public function getRows(): int
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->rows;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-06 07:56:37 +00:00
|
|
|
|
public function getColumns(): int
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->columns;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @throws MatrixException
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function getColumnValues($column): array
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
2016-04-29 22:59:10 +00:00
|
|
|
|
if ($column >= $this->columns) {
|
2018-03-03 15:03:53 +00:00
|
|
|
|
throw new MatrixException('Column out of range');
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-23 07:03:30 +00:00
|
|
|
|
return array_column($this->matrix, $column);
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return float|int
|
2016-11-20 21:53:17 +00:00
|
|
|
|
*
|
2016-04-29 22:58:54 +00:00
|
|
|
|
* @throws MatrixException
|
|
|
|
|
*/
|
|
|
|
|
public function getDeterminant()
|
|
|
|
|
{
|
2018-02-16 06:25:24 +00:00
|
|
|
|
if ($this->determinant !== null) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
return $this->determinant;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$this->isSquare()) {
|
2018-03-03 15:03:53 +00:00
|
|
|
|
throw new MatrixException('Matrix is not square matrix');
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-25 06:58:02 +00:00
|
|
|
|
$lu = new LUDecomposition($this);
|
2017-05-17 07:03:25 +00:00
|
|
|
|
|
2017-04-25 06:58:02 +00:00
|
|
|
|
return $this->determinant = $lu->det();
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-06 07:56:37 +00:00
|
|
|
|
public function isSquare(): bool
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->columns === $this->rows;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function transpose(): self
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
2017-04-23 07:03:30 +00:00
|
|
|
|
if ($this->rows == 1) {
|
|
|
|
|
$matrix = array_map(function ($el) {
|
|
|
|
|
return [$el];
|
|
|
|
|
}, $this->matrix[0]);
|
|
|
|
|
} else {
|
|
|
|
|
$matrix = array_map(null, ...$this->matrix);
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-23 07:03:30 +00:00
|
|
|
|
return new self($matrix, false);
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function multiply(self $matrix): self
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
if ($this->columns != $matrix->getRows()) {
|
2018-03-03 15:03:53 +00:00
|
|
|
|
throw new InvalidArgumentException('Inconsistent matrix supplied');
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$product = [];
|
|
|
|
|
$multiplier = $matrix->toArray();
|
2016-04-29 22:59:10 +00:00
|
|
|
|
for ($i = 0; $i < $this->rows; ++$i) {
|
2016-08-02 11:23:58 +00:00
|
|
|
|
$columns = $matrix->getColumns();
|
|
|
|
|
for ($j = 0; $j < $columns; ++$j) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
$product[$i][$j] = 0;
|
2016-04-29 22:59:10 +00:00
|
|
|
|
for ($k = 0; $k < $this->columns; ++$k) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
$product[$i][$j] += $this->matrix[$i][$k] * $multiplier[$k][$j];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-04-29 22:59:10 +00:00
|
|
|
|
|
2016-04-29 22:58:54 +00:00
|
|
|
|
return new self($product, false);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function divideByScalar($value): self
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
2017-01-31 19:33:08 +00:00
|
|
|
|
$newMatrix = [];
|
2016-04-29 22:59:10 +00:00
|
|
|
|
for ($i = 0; $i < $this->rows; ++$i) {
|
|
|
|
|
for ($j = 0; $j < $this->columns; ++$j) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
$newMatrix[$i][$j] = $this->matrix[$i][$j] / $value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new self($newMatrix, false);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function multiplyByScalar($value): self
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
|
|
|
|
$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
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function add(self $other): self
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->_add($other);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Element-wise subtracting of another matrix from this one
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function subtract(self $other): self
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
|
|
|
|
return $this->_add($other, -1);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function inverse(): self
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
if (!$this->isSquare()) {
|
2018-03-03 15:03:53 +00:00
|
|
|
|
throw new MatrixException('Matrix is not square matrix');
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-25 06:58:02 +00:00
|
|
|
|
$LU = new LUDecomposition($this);
|
|
|
|
|
$identity = $this->getIdentity();
|
|
|
|
|
$inverse = $LU->solve($identity);
|
2017-02-15 09:09:16 +00:00
|
|
|
|
|
2017-04-25 06:58:02 +00:00
|
|
|
|
return new self($inverse, false);
|
|
|
|
|
}
|
2016-04-29 22:58:54 +00:00
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function crossOut(int $row, int $column): self
|
2016-04-29 22:58:54 +00:00
|
|
|
|
{
|
|
|
|
|
$newMatrix = [];
|
|
|
|
|
$r = 0;
|
2016-04-29 22:59:10 +00:00
|
|
|
|
for ($i = 0; $i < $this->rows; ++$i) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
$c = 0;
|
|
|
|
|
if ($row != $i) {
|
2016-04-29 22:59:10 +00:00
|
|
|
|
for ($j = 0; $j < $this->columns; ++$j) {
|
2016-04-29 22:58:54 +00:00
|
|
|
|
if ($column != $j) {
|
|
|
|
|
$newMatrix[$r][$c] = $this->matrix[$i][$j];
|
2016-04-29 22:59:10 +00:00
|
|
|
|
++$c;
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-11-22 21:16:10 +00:00
|
|
|
|
|
2016-04-29 22:59:10 +00:00
|
|
|
|
++$r;
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new self($newMatrix, false);
|
|
|
|
|
}
|
2017-02-15 09:09:16 +00:00
|
|
|
|
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public function isSingular(): bool
|
2017-02-15 09:09:16 +00:00
|
|
|
|
{
|
2017-11-22 21:16:10 +00:00
|
|
|
|
return $this->getDeterminant() == 0;
|
2017-02-15 09:09:16 +00:00
|
|
|
|
}
|
2017-04-23 07:03:30 +00:00
|
|
|
|
|
2018-02-11 23:26:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* 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 sqrt($squareSum);
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-23 07:03:30 +00:00
|
|
|
|
/**
|
|
|
|
|
* Returns the transpose of given array
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public static function transposeArray(array $array): array
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
2017-05-17 07:03:25 +00:00
|
|
|
|
return (new self($array, false))->transpose()->toArray();
|
2017-04-23 07:03:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the dot product of two arrays<br>
|
|
|
|
|
* Matrix::dot(x, y) ==> x.y'
|
|
|
|
|
*/
|
2017-11-22 21:16:10 +00:00
|
|
|
|
public static function dot(array $array1, array $array2): array
|
2017-04-23 07:03:30 +00:00
|
|
|
|
{
|
2017-05-17 07:03:25 +00:00
|
|
|
|
$m1 = new self($array1, false);
|
|
|
|
|
$m2 = new self($array2, false);
|
2017-04-23 07:03:30 +00:00
|
|
|
|
|
|
|
|
|
return $m1->multiply($m2->transpose())->toArray()[0];
|
|
|
|
|
}
|
2017-11-22 21:16:10 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Element-wise addition or substraction depending on the given sign parameter
|
|
|
|
|
*/
|
2018-02-11 23:26:34 +00:00
|
|
|
|
private function _add(self $other, int $sign = 1): self
|
2017-11-22 21:16:10 +00:00
|
|
|
|
{
|
|
|
|
|
$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
|
|
|
|
|
*/
|
2018-02-11 23:26:34 +00:00
|
|
|
|
private function getIdentity(): self
|
2017-11-22 21:16:10 +00:00
|
|
|
|
{
|
|
|
|
|
$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);
|
|
|
|
|
}
|
2016-04-29 22:58:54 +00:00
|
|
|
|
}
|