setValue($currentRow, $currentColumn, $currentColumnData[$currentRow]); } } return $result; } public static function fromRowsColumns(...$args): Matrix { $rows = $args[0]; $cols = $args[1]; $result = new Matrix($rows, $cols); $currentIndex = 2; for ($currentRow = 0; $currentRow < $rows; $currentRow++) { for ($currentCol = 0; $currentCol < $cols; $currentCol++) { $result->setValue($currentRow, $currentCol, $args[$currentIndex++]); } } return $result; } public function getRowCount(): int { return $this->rowCount; } public function getColumnCount(): int { return $this->columnCount; } public function getValue(int $row, int $col): float|int { return $this->matrixRowData[$row][$col]; } public function setValue(int $row, int $col, float|int $value) { $this->matrixRowData[$row][$col] = $value; } public function getTranspose(): self { // Just flip everything $transposeMatrix = []; $rowMatrixData = $this->matrixRowData; for ( $currentRowTransposeMatrix = 0; $currentRowTransposeMatrix < $this->columnCount; $currentRowTransposeMatrix++ ) { for ( $currentColumnTransposeMatrix = 0; $currentColumnTransposeMatrix < $this->rowCount; $currentColumnTransposeMatrix++ ) { $transposeMatrix[$currentRowTransposeMatrix][$currentColumnTransposeMatrix] = $rowMatrixData[$currentColumnTransposeMatrix][$currentRowTransposeMatrix]; } } return new Matrix($this->columnCount, $this->rowCount, $transposeMatrix); } private function isSquare(): bool { return ($this->rowCount == $this->columnCount) && ($this->rowCount > 0); } public function getDeterminant(): float { // Basic argument checking if (! $this->isSquare()) { throw new Exception('Matrix must be square!'); } if ($this->rowCount == 1) { // Really happy path :) return $this->matrixRowData[0][0]; } if ($this->rowCount == 2) { // 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]; return $a * $d - $b * $c; } // 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]; $cofactor = $this->getCofactor(0, $currentColumn); $itemToAdd = $firstRowColValue * $cofactor; $result += $itemToAdd; } return $result; } public function getAdjugate(): SquareMatrix|self { if (! $this->isSquare()) { throw new Exception('Matrix must be square!'); } // See http://en.wikipedia.org/wiki/Adjugate_matrix if ($this->rowCount == 2) { // 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]; return new SquareMatrix( $d, -$b, -$c, $a ); } // The idea is that it's the transpose of the cofactors $result = []; for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) { for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) { $result[$currentColumn][$currentRow] = $this->getCofactor($currentRow, $currentColumn); } } return new Matrix($this->columnCount, $this->rowCount, $result); } public function getInverse(): Matrix|SquareMatrix { if (($this->rowCount == 1) && ($this->columnCount == 1)) { return new SquareMatrix(1.0 / $this->matrixRowData[0][0]); } // 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|int $scalarValue, Matrix $matrix): Matrix { $rows = $matrix->getRowCount(); $columns = $matrix->getColumnCount(); $newValues = []; for ($currentRow = 0; $currentRow < $rows; $currentRow++) { for ($currentColumn = 0; $currentColumn < $columns; $currentColumn++) { $newValues[$currentRow][$currentColumn] = $scalarValue * $matrix->getValue($currentRow, $currentColumn); } } return new Matrix($rows, $columns, $newValues); } public static function add(Matrix $left, Matrix $right): Matrix { if ( ($left->getRowCount() != $right->getRowCount()) || ($left->getColumnCount() != $right->getColumnCount()) ) { throw new Exception('Matrices must be of the same size'); } // simple addition of each item $resultMatrix = []; for ($currentRow = 0; $currentRow < $left->getRowCount(); $currentRow++) { for ($currentColumn = 0; $currentColumn < $right->getColumnCount(); $currentColumn++) { $resultMatrix[$currentRow][$currentColumn] = $left->getValue($currentRow, $currentColumn) + $right->getValue($currentRow, $currentColumn); } } return new Matrix($left->getRowCount(), $right->getColumnCount(), $resultMatrix); } public static function multiply(Matrix $left, Matrix $right): Matrix { // Just your standard matrix multiplication. // See http://en.wikipedia.org/wiki/Matrix_multiplication for details if ($left->getColumnCount() != $right->getRowCount()) { throw new Exception('The width of the left matrix must match the height of the right matrix'); } $resultRows = $left->getRowCount(); $resultColumns = $right->getColumnCount(); $resultMatrix = []; for ($currentRow = 0; $currentRow < $resultRows; $currentRow++) { for ($currentColumn = 0; $currentColumn < $resultColumns; $currentColumn++) { $productValue = 0; for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); $vectorIndex++) { $leftValue = $left->getValue($currentRow, $vectorIndex); $rightValue = $right->getValue($vectorIndex, $currentColumn); $vectorIndexProduct = $leftValue * $rightValue; $productValue += $vectorIndexProduct; } $resultMatrix[$currentRow][$currentColumn] = $productValue; } } return new Matrix($resultRows, $resultColumns, $resultMatrix); } private function getMinorMatrix(int $rowToRemove, int $columnToRemove): Matrix { // See http://en.wikipedia.org/wiki/Minor_(linear_algebra) // I'm going to use a horribly naïve algorithm... because I can :) $result = []; $actualRow = 0; for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) { if ($currentRow == $rowToRemove) { continue; } $actualCol = 0; for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) { if ($currentColumn == $columnToRemove) { continue; } $result[$actualRow][$actualCol] = $this->matrixRowData[$currentRow][$currentColumn]; $actualCol++; } $actualRow++; } return new Matrix($this->rowCount - 1, $this->columnCount - 1, $result); } public function getCofactor(int $rowToRemove, int $columnToRemove): float { // 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); if ($isEven) { return $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant(); } else { return -1.0 * $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant(); } } public function equals(Matrix $otherMatrix): bool { // If one is null, but not both, return false. if ($otherMatrix == null) { return false; } if (($this->rowCount != $otherMatrix->getRowCount()) || ($this->columnCount != $otherMatrix->getColumnCount())) { return false; } for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) { for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) { $delta = abs( $this->matrixRowData[$currentRow][$currentColumn] - $otherMatrix->getValue($currentRow, $currentColumn) ); if ($delta > self::ERROR_TOLERANCE) { return false; } } } return true; } }