2022-07-05 15:55:47 +02:00
|
|
|
<?php
|
|
|
|
|
2024-02-02 14:53:38 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2022-07-05 15:55:47 +02:00
|
|
|
namespace DNW\Skills\Numerics;
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
use Exception;
|
|
|
|
|
2010-08-31 08:24:12 -04:00
|
|
|
class Matrix
|
|
|
|
{
|
2022-07-05 16:21:06 +02:00
|
|
|
public const ERROR_TOLERANCE = 0.0000000001;
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2023-08-03 13:08:04 +00:00
|
|
|
/**
|
|
|
|
* @param array<int,array<int,float>> $matrixRowData
|
|
|
|
*/
|
2024-02-20 14:21:44 +00:00
|
|
|
public function __construct(private readonly int $rowCount = 0, private readonly int $columnCount = 0, private array $matrixRowData = [])
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2023-08-03 13:08:04 +00:00
|
|
|
/**
|
|
|
|
* @param array<int,array<int,float>> $columnValues
|
|
|
|
*/
|
2023-08-01 12:13:24 +00:00
|
|
|
public static function fromColumnValues(int $rows, int $columns, array $columnValues): self
|
2010-10-02 23:22:01 -04:00
|
|
|
{
|
2022-07-05 15:55:47 +02:00
|
|
|
$data = [];
|
2010-10-02 23:22:01 -04:00
|
|
|
$result = new Matrix($rows, $columns, $data);
|
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentColumn = 0; $currentColumn < $columns; ++$currentColumn) {
|
2010-10-02 23:22:01 -04:00
|
|
|
$currentColumnData = $columnValues[$currentColumn];
|
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
|
2010-10-02 23:22:01 -04:00
|
|
|
$result->setValue($currentRow, $currentColumn, $currentColumnData[$currentRow]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2023-08-02 10:10:57 +00:00
|
|
|
public static function fromRowsColumns(int $rows, int $cols, float|int ...$args): Matrix
|
2010-09-01 22:17:00 -04:00
|
|
|
{
|
|
|
|
$result = new Matrix($rows, $cols);
|
2023-08-02 10:10:57 +00:00
|
|
|
$currentIndex = 0;
|
2010-09-01 22:17:00 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
|
|
|
|
for ($currentCol = 0; $currentCol < $cols; ++$currentCol) {
|
2010-09-01 22:17:00 -04:00
|
|
|
$result->setValue($currentRow, $currentCol, $args[$currentIndex++]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getRowCount(): int
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2023-08-02 09:15:01 +00:00
|
|
|
return $this->rowCount;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getColumnCount(): int
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2023-08-02 09:15:01 +00:00
|
|
|
return $this->columnCount;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getValue(int $row, int $col): float|int
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2023-08-02 09:15:01 +00:00
|
|
|
return $this->matrixRowData[$row][$col];
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-02 10:59:15 +00:00
|
|
|
public function setValue(int $row, int $col, float|int $value): void
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2023-08-02 09:15:01 +00:00
|
|
|
$this->matrixRowData[$row][$col] = $value;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getTranspose(): self
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
// Just flip everything
|
2022-07-05 15:55:47 +02:00
|
|
|
$transposeMatrix = [];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
$rowMatrixData = $this->matrixRowData;
|
2024-07-04 09:38:03 +00:00
|
|
|
for ($curRowTransposeMx = 0; $curRowTransposeMx < $this->columnCount; ++$curRowTransposeMx) {
|
|
|
|
for ($curColTransposeMx = 0; $curColTransposeMx < $this->rowCount; ++$curColTransposeMx) {
|
|
|
|
$transposeMatrix[$curRowTransposeMx][$curColTransposeMx] =
|
|
|
|
$rowMatrixData[$curColTransposeMx][$curRowTransposeMx];
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
return new Matrix($this->columnCount, $this->rowCount, $transposeMatrix);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
private function isSquare(): bool
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2024-02-21 13:40:20 +00:00
|
|
|
return ($this->rowCount === $this->columnCount) && ($this->rowCount > 0);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getDeterminant(): float
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
// Basic argument checking
|
2022-07-05 15:55:47 +02:00
|
|
|
if (! $this->isSquare()) {
|
|
|
|
throw new Exception('Matrix must be square!');
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
if ($this->rowCount == 1) {
|
2010-08-31 08:24:12 -04:00
|
|
|
// Really happy path :)
|
2023-08-02 09:15:01 +00:00
|
|
|
return $this->matrixRowData[0][0];
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
if ($this->rowCount == 2) {
|
2010-08-31 08:24:12 -04:00
|
|
|
// Happy path!
|
|
|
|
// Given:
|
|
|
|
// | a b |
|
|
|
|
// | c d |
|
|
|
|
// The determinant is ad - bc
|
2023-08-02 09:15:01 +00:00
|
|
|
$a = $this->matrixRowData[0][0];
|
|
|
|
$b = $this->matrixRowData[0][1];
|
|
|
|
$c = $this->matrixRowData[1][0];
|
|
|
|
$d = $this->matrixRowData[1][1];
|
2022-07-05 15:55:47 +02:00
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
return $a * $d - $b * $c;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// I use the Laplace expansion here since it's straightforward to implement.
|
|
|
|
// It's O(n^2) and my implementation is especially poor performing, but the
|
|
|
|
// core idea is there. Perhaps I should replace it with a better algorithm
|
|
|
|
// later.
|
|
|
|
// See http://en.wikipedia.org/wiki/Laplace_expansion for details
|
|
|
|
|
|
|
|
$result = 0.0;
|
|
|
|
|
|
|
|
// I expand along the first row
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
|
2023-08-02 09:15:01 +00:00
|
|
|
$firstRowColValue = $this->matrixRowData[0][$currentColumn];
|
2010-08-31 08:24:12 -04:00
|
|
|
$cofactor = $this->getCofactor(0, $currentColumn);
|
2016-05-24 16:31:21 +02:00
|
|
|
$itemToAdd = $firstRowColValue * $cofactor;
|
2022-07-05 16:21:06 +02:00
|
|
|
$result += $itemToAdd;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getAdjugate(): SquareMatrix|self
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2022-07-05 15:55:47 +02:00
|
|
|
if (! $this->isSquare()) {
|
|
|
|
throw new Exception('Matrix must be square!');
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// See http://en.wikipedia.org/wiki/Adjugate_matrix
|
2023-08-02 09:15:01 +00:00
|
|
|
if ($this->rowCount == 2) {
|
2010-08-31 08:24:12 -04:00
|
|
|
// Happy path!
|
|
|
|
// Adjugate of:
|
|
|
|
// | a b |
|
|
|
|
// | c d |
|
|
|
|
// is
|
|
|
|
// | d -b |
|
|
|
|
// | -c a |
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
$a = $this->matrixRowData[0][0];
|
|
|
|
$b = $this->matrixRowData[0][1];
|
|
|
|
$c = $this->matrixRowData[1][0];
|
|
|
|
$d = $this->matrixRowData[1][1];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2023-08-01 13:53:19 +00:00
|
|
|
return new SquareMatrix(
|
|
|
|
$d,
|
|
|
|
-$b,
|
|
|
|
-$c,
|
|
|
|
$a
|
|
|
|
);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// The idea is that it's the transpose of the cofactors
|
2022-07-05 15:55:47 +02:00
|
|
|
$result = [];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
|
|
|
|
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
|
2010-08-31 22:24:05 -04:00
|
|
|
$result[$currentColumn][$currentRow] = $this->getCofactor($currentRow, $currentColumn);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
return new Matrix($this->columnCount, $this->rowCount, $result);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public function getInverse(): Matrix|SquareMatrix
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2023-08-02 09:15:01 +00:00
|
|
|
if (($this->rowCount == 1) && ($this->columnCount == 1)) {
|
|
|
|
return new SquareMatrix(1.0 / $this->matrixRowData[0][0]);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Take the simple approach:
|
|
|
|
// http://en.wikipedia.org/wiki/Cramer%27s_rule#Finding_inverse_matrix
|
|
|
|
$determinantInverse = 1.0 / $this->getDeterminant();
|
|
|
|
$adjugate = $this->getAdjugate();
|
|
|
|
|
|
|
|
return self::scalarMultiply($determinantInverse, $adjugate);
|
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public static function scalarMultiply(float|int $scalarValue, Matrix $matrix): Matrix
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
$rows = $matrix->getRowCount();
|
|
|
|
$columns = $matrix->getColumnCount();
|
2022-07-05 15:55:47 +02:00
|
|
|
$newValues = [];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
|
|
|
|
for ($currentColumn = 0; $currentColumn < $columns; ++$currentColumn) {
|
2016-05-24 16:31:21 +02:00
|
|
|
$newValues[$currentRow][$currentColumn] = $scalarValue * $matrix->getValue($currentRow, $currentColumn);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Matrix($rows, $columns, $newValues);
|
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public static function add(Matrix $left, Matrix $right): Matrix
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
2024-02-21 13:40:20 +00:00
|
|
|
if (($left->getRowCount() !== $right->getRowCount()) || ($left->getColumnCount() !== $right->getColumnCount())) {
|
2022-07-05 15:55:47 +02:00
|
|
|
throw new Exception('Matrices must be of the same size');
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// simple addition of each item
|
|
|
|
|
2022-07-05 15:55:47 +02:00
|
|
|
$resultMatrix = [];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $left->getRowCount(); ++$currentRow) {
|
|
|
|
for ($currentColumn = 0; $currentColumn < $right->getColumnCount(); ++$currentColumn) {
|
2010-08-31 22:24:05 -04:00
|
|
|
$resultMatrix[$currentRow][$currentColumn] =
|
2016-05-24 16:31:21 +02:00
|
|
|
$left->getValue($currentRow, $currentColumn)
|
|
|
|
+
|
|
|
|
$right->getValue($currentRow, $currentColumn);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Matrix($left->getRowCount(), $right->getColumnCount(), $resultMatrix);
|
|
|
|
}
|
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
public static function multiply(Matrix $left, Matrix $right): Matrix
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
// Just your standard matrix multiplication.
|
|
|
|
// See http://en.wikipedia.org/wiki/Matrix_multiplication for details
|
|
|
|
|
2024-02-21 13:40:20 +00:00
|
|
|
if ($left->getColumnCount() !== $right->getRowCount()) {
|
2022-07-05 15:55:47 +02:00
|
|
|
throw new Exception('The width of the left matrix must match the height of the right matrix');
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
$resultRows = $left->getRowCount();
|
|
|
|
$resultColumns = $right->getColumnCount();
|
|
|
|
|
2022-07-05 15:55:47 +02:00
|
|
|
$resultMatrix = [];
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $resultRows; ++$currentRow) {
|
|
|
|
for ($currentColumn = 0; $currentColumn < $resultColumns; ++$currentColumn) {
|
2010-08-31 08:24:12 -04:00
|
|
|
$productValue = 0;
|
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); ++$vectorIndex) {
|
2010-08-31 08:24:12 -04:00
|
|
|
$leftValue = $left->getValue($currentRow, $vectorIndex);
|
|
|
|
$rightValue = $right->getValue($vectorIndex, $currentColumn);
|
2016-05-24 16:31:21 +02:00
|
|
|
$vectorIndexProduct = $leftValue * $rightValue;
|
2022-07-05 16:21:06 +02:00
|
|
|
$productValue += $vectorIndexProduct;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2010-08-31 22:24:05 -04:00
|
|
|
$resultMatrix[$currentRow][$currentColumn] = $productValue;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Matrix($resultRows, $resultColumns, $resultMatrix);
|
2016-05-24 16:31:21 +02:00
|
|
|
}
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2023-08-01 12:13:24 +00:00
|
|
|
private function getMinorMatrix(int $rowToRemove, int $columnToRemove): Matrix
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
// See http://en.wikipedia.org/wiki/Minor_(linear_algebra)
|
|
|
|
|
|
|
|
// I'm going to use a horribly naïve algorithm... because I can :)
|
2022-07-05 15:55:47 +02:00
|
|
|
$result = [];
|
2010-08-31 22:24:05 -04:00
|
|
|
|
|
|
|
$actualRow = 0;
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
|
2016-05-24 16:31:21 +02:00
|
|
|
if ($currentRow == $rowToRemove) {
|
2010-08-31 08:24:12 -04:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2010-08-31 22:24:05 -04:00
|
|
|
$actualCol = 0;
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
|
2016-05-24 16:31:21 +02:00
|
|
|
if ($currentColumn == $columnToRemove) {
|
2010-08-31 08:24:12 -04:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
$result[$actualRow][$actualCol] = $this->matrixRowData[$currentRow][$currentColumn];
|
2010-08-31 22:24:05 -04:00
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
++$actualCol;
|
2010-08-31 22:24:05 -04:00
|
|
|
}
|
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
++$actualRow;
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-02 09:15:01 +00:00
|
|
|
return new Matrix($this->rowCount - 1, $this->columnCount - 1, $result);
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
|
|
|
|
2023-08-02 09:36:44 +00:00
|
|
|
public function getCofactor(int $rowToRemove, int $columnToRemove): float
|
2010-08-31 08:24:12 -04:00
|
|
|
{
|
|
|
|
// See http://en.wikipedia.org/wiki/Cofactor_(linear_algebra) for details
|
|
|
|
// REVIEW: should things be reversed since I'm 0 indexed?
|
|
|
|
$sum = $rowToRemove + $columnToRemove;
|
2016-05-24 16:31:21 +02:00
|
|
|
$isEven = ($sum % 2 == 0);
|
2010-08-31 08:24:12 -04:00
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
if ($isEven) {
|
2010-08-31 08:24:12 -04:00
|
|
|
return $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant();
|
|
|
|
}
|
2024-02-21 14:10:01 +00:00
|
|
|
|
2024-02-21 14:02:35 +00:00
|
|
|
return -1.0 * $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant();
|
2010-08-31 08:24:12 -04:00
|
|
|
}
|
2010-09-01 22:17:00 -04:00
|
|
|
|
2023-08-02 09:36:44 +00:00
|
|
|
public function equals(Matrix $otherMatrix): bool
|
2010-09-01 22:17:00 -04:00
|
|
|
{
|
2024-02-21 13:40:20 +00:00
|
|
|
if (($this->rowCount !== $otherMatrix->getRowCount()) || ($this->columnCount !== $otherMatrix->getColumnCount())) {
|
2024-02-02 14:53:38 +00:00
|
|
|
return FALSE;
|
2010-09-01 22:17:00 -04:00
|
|
|
}
|
|
|
|
|
2024-02-21 13:48:37 +00:00
|
|
|
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
|
|
|
|
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
|
2010-09-01 22:17:00 -04:00
|
|
|
$delta =
|
2023-08-01 13:53:19 +00:00
|
|
|
abs(
|
2023-08-02 09:15:01 +00:00
|
|
|
$this->matrixRowData[$currentRow][$currentColumn] -
|
2023-08-01 13:53:19 +00:00
|
|
|
$otherMatrix->getValue($currentRow, $currentColumn)
|
|
|
|
);
|
2010-09-01 22:17:00 -04:00
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
if ($delta > self::ERROR_TOLERANCE) {
|
2024-02-02 14:53:38 +00:00
|
|
|
return FALSE;
|
2010-09-01 22:17:00 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-02 14:53:38 +00:00
|
|
|
return TRUE;
|
2010-09-01 22:17:00 -04:00
|
|
|
}
|
2022-07-05 15:55:47 +02:00
|
|
|
}
|