Files
trueskill/src/Numerics/Matrix.php

330 lines
11 KiB
PHP
Raw Normal View History

2022-07-05 15:55:47 +02:00
<?php
namespace DNW\Skills\Numerics;
2010-08-31 08:24:12 -04: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
*/
2023-08-02 10:10:57 +00:00
public function __construct(private int $rowCount = 0, private int $columnCount = 0, private array $matrixRowData = array())
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);
for ($currentColumn = 0; $currentColumn < $columns; $currentColumn++) {
$currentColumnData = $columnValues[$currentColumn];
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
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
}
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
{
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->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-01-31 14:15:55 +00:00
for ($currentRowTransposeMatrix = 0; $currentRowTransposeMatrix < $this->columnCount; $currentRowTransposeMatrix++) {
for ($currentColumnTransposeMatrix = 0; $currentColumnTransposeMatrix < $this->rowCount; $currentColumnTransposeMatrix++) {
2010-08-31 22:24:05 -04:00
$transposeMatrix[$currentRowTransposeMatrix][$currentColumnTransposeMatrix] =
$rowMatrixData[$currentColumnTransposeMatrix][$currentRowTransposeMatrix];
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
{
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->matrixRowData[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
$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
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
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
$firstRowColValue = $this->matrixRowData[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->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
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)) {
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
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-01-31 14:15:55 +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
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] =
$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
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
for ($currentRow = 0; $currentRow < $resultRows; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $resultColumns; $currentColumn++) {
2010-08-31 08:24:12 -04:00
$productValue = 0;
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);
$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
{
// 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
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
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
if ($currentColumn == $columnToRemove) {
2010-08-31 08:24:12 -04:00
continue;
}
$result[$actualRow][$actualCol] = $this->matrixRowData[$currentRow][$currentColumn];
2010-08-31 22:24:05 -04:00
$actualCol++;
}
$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
{
// 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();
} else {
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
{
if (($this->rowCount != $otherMatrix->getRowCount()) || ($this->columnCount != $otherMatrix->getColumnCount())) {
2010-09-01 22:17:00 -04:00
return false;
}
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(
$this->matrixRowData[$currentRow][$currentColumn] -
2023-08-01 13:53:19 +00:00
$otherMatrix->getValue($currentRow, $currentColumn)
);
2010-09-01 22:17:00 -04:00
if ($delta > self::ERROR_TOLERANCE) {
2010-09-01 22:17:00 -04:00
return false;
}
}
}
return true;
}
2022-07-05 15:55:47 +02:00
}