Files
trueskill/src/Numerics/Matrix.php

355 lines
11 KiB
PHP
Raw Normal View History

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
use Exception;
2010-08-31 08:24:12 -04:00
class Matrix
{
public const float 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
*/
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
{
2022-07-05 15:55:47 +02:00
$data = [];
$result = new Matrix($rows, $columns, $data);
2024-02-21 13:48:37 +00:00
for ($currentColumn = 0; $currentColumn < $columns; ++$currentColumn) {
$currentColumnData = $columnValues[$currentColumn];
2024-02-21 13:48:37 +00:00
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
$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
{
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
{
return $this->columnCount;
2010-08-31 08:24:12 -04:00
}
private function checkRowCol(int $row, int $col): void
{
if ($row < 0) {
throw new Exception("Row negative");
}
if ($row >= $this->getRowCount()) {
throw new Exception("Row beyond range");
}
if ($col < 0) {
throw new Exception("Column negative");
}
if ($col >= $this->getColumnCount()) {
throw new Exception("Column beyond range");
}
}
public function getValue(int $row, int $col): float
2010-08-31 08:24:12 -04:00
{
$this->checkRowCol($row, $col);
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
{
$this->checkRowCol($row, $col);
$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
$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
}
}
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
}
if ($this->rowCount == 1) {
2010-08-31 08:24:12 -04:00
// Really happy path :)
return $this->getValue(0, 0);
2010-08-31 08:24:12 -04: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
2025-05-12 09:40:44 +00:00
$a = $this->getValue(0, 0);
$b = $this->getValue(0, 1);
$c = $this->getValue(1, 0);
$d = $this->getValue(1, 1);
2022-07-05 15:55:47 +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) {
2025-05-12 09:40:44 +00:00
$firstRowColValue = $this->getValue(0, $currentColumn);
2010-08-31 08:24:12 -04:00
$cofactor = $this->getCofactor(0, $currentColumn);
$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
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 |
$a = $this->getValue(0, 0);
$b = $this->getValue(0, 1);
$c = $this->getValue(1, 0);
$d = $this->getValue(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
}
}
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
{
if (($this->rowCount == 1) && ($this->columnCount == 1)) {
2025-05-12 09:40:44 +00:00
return new SquareMatrix(1.0 / $this->getValue(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);
}
public static function scalarMultiply(float $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) {
$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] =
2025-05-12 09:40:44 +00:00
$left->getValue($currentRow, $currentColumn)
+
2025-05-12 09:40:44 +00:00
$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) {
$productValue = 0.0;
2010-08-31 08:24:12 -04:00
2024-02-21 13:48:37 +00:00
for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); ++$vectorIndex) {
2025-05-12 09:40:44 +00:00
$leftValue = $left->getValue($currentRow, $vectorIndex);
$rightValue = $right->getValue($vectorIndex, $currentColumn);
$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);
}
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
{
$this->checkRowCol($rowToRemove, $columnToRemove);
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) {
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) {
if ($currentColumn == $columnToRemove) {
2010-08-31 08:24:12 -04:00
continue;
}
$result[$actualRow][$actualCol] = $this->getValue($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
}
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
{
$this->checkRowCol($rowToRemove, $columnToRemove);
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;
$isEven = ($sum % 2 == 0);
2010-08-31 08:24:12 -04: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
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(
2025-05-12 09:40:44 +00:00
$this->getValue($currentRow, $currentColumn) -
$otherMatrix->getValue($currentRow, $currentColumn)
2023-08-01 13:53:19 +00:00
);
2010-09-01 22:17:00 -04: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
}