Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Moore-Penrose inverse #469

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions src/LinearAlgebra/Decomposition/SVD.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace MathPHP\LinearAlgebra\Decomposition;

use MathPHP\Arithmetic;
use MathPHP\Exception;
use MathPHP\Exception\MatrixException;
use MathPHP\LinearAlgebra\NumericMatrix;
use MathPHP\LinearAlgebra\MatrixFactory;
use MathPHP\LinearAlgebra\Vector;
Expand Down Expand Up @@ -107,6 +109,19 @@
// A rectangular diagonal matrix
$S = $U->transpose()->multiply($M)->multiply($V);

// If S is non-diagonal, try permuting S to be diagonal
if (!$S->isRectangularDiagonal()) {
['sort'=>$sort, 'P'=>$P] = self::diagonalize($S);
// Depending on the value of $sort, we either permute the rows or columns of $S
if ($sort === 'm') {
$S = $P->multiply($S); // Permute rows of S
$U = $U->multiply($P->inverse()); // Permute corresponding columns of U
} elseif ($sort === 'n') {
$S = $S->multiply($P); // Permute columns of S
$V = $P->inverse()->multiply($V); // Permute corresponding rows of V
}
}

$diag = $S->getDiagonalElements();

// If there is a negative singular value, we need to adjust the signs of columns in U
Expand All @@ -120,9 +135,226 @@
$S = $signature->multiply($S);
}

// Check the elements are in descending order
if (!self::isDiagonalDescending($S)) {
$P = self::sortDiagonal($S);
/** @var NumericMatrix */
$Pᵀ = $P->transpose();

$S = $Pᵀ->multiply($S)->multiply($P);
$U = $P->multiply($U)->multiply($Pᵀ);
}

return new SVD($U, $S, $V);
}

/**
* Returns a permutation matrix, P, such that the product of SP is diagonal
*
* @param NumericMatrix $S the matrix to diagonalize
*
* @return array{'sort': string, 'P': NumericMatrix} a matrix, P, that will diagonalize S. Multiplication order defined by sort
* If 'm', then pre-multiply
* If 'n', then post-multiply
*/
private static function diagonalize(NumericMatrix $S): array
{
if ($S->isRectangularDiagonal()) {
return MatrixFactory::identity($S->getN());

Check failure on line 163 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Method MathPHP\LinearAlgebra\Decomposition\SVD::diagonalize() should return array{sort: string, P: MathPHP\LinearAlgebra\NumericMatrix} but returns MathPHP\LinearAlgebra\NumericSquareMatrix.
}

$sort = '';
$vecMethod = '';
$max = 0;
$min = 0;

if ($S->getM() >= $S->getN()) {
$sort = 'm'; // rows
$vecMethod = 'asRowVectors';
$max = $S->getM();
$min = $S->getN();
} else {
$sort = 'n'; // columns
$vecMethod = 'asVectors';
$max = $S->getN();
$min = $S->getM();
}

// Create an identity matrix to store permutations in
$P = MatrixFactory::identity($max)->{$vecMethod}();

// Push all zero-columns to the right
$vectors = $S->{$vecMethod}();

$zeroCols = [];

foreach ($vectors as $i => $vector)
{
// Each column should contain 1 non-zero element
$isZero = Arithmetic::almostEqual((float) $vector->l2Norm(), 0);

$zeroCols[$i] = $isZero ? 0 : 1;
}

arsort($zeroCols, SORT_NUMERIC);

$zeroMap = array_keys($zeroCols);

uksort($P, function ($left, $right) use ($zeroMap) {

Check failure on line 203 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Parameter #2 $callback of function uksort expects callable(TKey, TKey): int, Closure(mixed, mixed): bool given.
$leftPos = $zeroMap[$left];
$rightPos = $zeroMap[$right];

return $leftPos >= $rightPos;
});

// Only check the columns that contain diagonal entries
$vectors = $S->submatrix(0,0, $min-1, $min-1)->{$vecMethod}();

$nonDiagonalValues = [];

/** @var Vector */
foreach ($vectors as $i => $vector)

Check failure on line 216 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

PHPDoc tag @var above foreach loop does not specify variable name.
{
$ε = $S->getError();

// Each column should contain up to 1 non-zero element
if (Arithmetic::almostEqual($vector->l2Norm(), 0, $ε)) {

Check failure on line 221 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Cannot call method l2Norm() on float|int.
$j = $i;
} else {
$j = self::getStandardBasisIndex($vector, $ε);

Check failure on line 224 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Parameter #1 $v of static method MathPHP\LinearAlgebra\Decomposition\SVD::getStandardBasisIndex() expects MathPHP\LinearAlgebra\Vector, float|int given.
}

if ($j === -1) {
throw new MatrixException("S Matrix in SVD is not orthogonal:\n" . (string) $S);
}

if ($i === $j) {
continue;
} else {
$nonDiagonalValues[$i] = ['value' => $vector[$j], 'j' => $j];

Check failure on line 234 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Cannot access offset mixed on float|int.
}
}

// Now create a sort order
$order = range(0, $min - 1);

foreach ($nonDiagonalValues as $i => $elem)
{
$entry = $elem['j'];
$order[$entry] = $i;
}

$map = array_flip($order);

// Need to make column ($i of $nonDiagonalValues) = row ($j)
// order = [1=>2, 2=>1, 3=>3]
uksort($P, function ($left, $right) use ($map) {
$leftPos = isset($map[$left]) ? $map[$left] : INF; // sorts in ascending order, so just use inf
$rightPos = isset($map[$right]) ? $map[$right] : INF;

return $leftPos <=> $rightPos;
});

$P = MatrixFactory::createFromVectors($P);

// fromVectors treats the array as column vectors, so the matrix might need to be transposed
if ($sort === 'm') {
$P = $P->transpose();
}

return ['sort'=>$sort, 'P' => $P];
}

/**
* Checks that a vector has a single non-zero entry and returns its index
*
* @param Vector $v
*
* @return int The index of the non-zero entry or -1 if either:
* 1. There are multiple non-zero entries
* 2. The vector is a zero vector
*/
private static function getStandardBasisIndex(Vector $v, float $ε): int
{
if ($v->l2Norm() === 0) {
return false;

Check failure on line 280 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Method MathPHP\LinearAlgebra\Decomposition\SVD::getStandardBasisIndex() should return int but returns false.
}

// Vectors don't have negative indices
$index = -1;

foreach ($v->getVector() as $i => $component)
{
if (!Arithmetic::almostEqual($component, 0, $ε)) {
if ($index === -1) {
$index = $i;
} else { // If we already found a non-zero component, then return -1
return -1;
}
}
}

return $index;
}

/**
* Returns a permutation matrix that sorts its diagonal values in descending order
*
* @param NumericMatrix $S singular matrix
*
* @return NumericMatrix a permutation matrix such that PᵀSP is diagonal
*/
private static function sortDiagonal(NumericMatrix $S): NumericMatrix
{
// Get diagonal, pad it by columns, and sort it
$diagonal = $S->getDiagonalElements();

// Pad
$padLength = $S->getN() - count($diagonal);

$diagonal = array_merge($diagonal, array_fill(0, $padLength, 0)); // Pick 0 because the numbers should all be positive

Check failure on line 315 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Parameter #2 ...$args of function array_merge expects array, array<int, int>|false given.

// arsort preserves the indices
arsort($diagonal, SORT_NUMERIC);

// ... so we can create a position map from the keys
$map = array_keys($diagonal);

$P = MatrixFactory::identity($S->getM())->asVectors();

uksort($P, function ($left, $right) use ($map) {

Check failure on line 325 in src/LinearAlgebra/Decomposition/SVD.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Parameter #2 $callback of function uksort expects callable(int|string, int|string): int, Closure(mixed, mixed): bool given.
$leftPos = $map[$left];
$rightPos = $map[$right];

return $leftPos >= $rightPos;
});

return MatrixFactory::createFromVectors($P);
}

/**
* Checks if the elements of a diagonal matrix are in descending order
*
* @param NumericMatrix $S the matrix to check
*
* @return bool
*/
private static function isDiagonalDescending(NumericMatrix $S): bool
{
$diagonal = $S->getDiagonalElements();
$sorted = array_values($diagonal); rsort($sorted, SORT_NUMERIC);

// Compare sorted using matrix error (in case duplicate, floating-point eigenvalues)
$n = count($diagonal);
for ($i = 0; $i < $n; $i++) {
if (!Arithmetic::almostEqual($diagonal[$i], $sorted[$i], $S->getError())) {
return false;
}
}

return true;
}

/**
* Get U, S, or V matrix, or D vector
*
Expand Down
11 changes: 9 additions & 2 deletions src/LinearAlgebra/Eigenvector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace MathPHP\LinearAlgebra;

use MathPHP\Arithmetic;
use MathPHP\Exception;
use MathPHP\Exception\MatrixException;
use MathPHP\Functions\Map\Single;
Expand Down Expand Up @@ -66,8 +67,14 @@ public static function eigenvectors(NumericMatrix $A, array $eigenvalues = []):
foreach ($eigenvalues as $eigenvalue) {
// If this is a duplicate eigenvalue, and this is the second instance, the first
// pass already found all the vectors.
$key = \array_search($eigenvalue, \array_column($solution_array, 'eigenvalue'));
if (!$key) {
$key = false;
foreach (\array_column($solution_array, 'eigenvalue') as $i => $v) {
if (Arithmetic::almostEqual($v, $eigenvalue, $A->getError())) {
$key = $i;
break;
}
}
if ($key === false) {
$Iλ = MatrixFactory::identity($number)->scalarMultiply($eigenvalue);
$T = $A->subtract($Iλ);

Expand Down
30 changes: 30 additions & 0 deletions src/LinearAlgebra/MatrixCatalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
/** @var Matrix<T> inverse */
private $A⁻¹;

/** @var NumericMatrix pseudoInverse */
private $A⁺;

/** @var Reduction\RowEchelonForm */
private $REF;

Expand Down Expand Up @@ -43,6 +46,7 @@
* DERIVED MATRICES
* - transpose
* - inverse
* - pseudo-inverse
**************************************************************************/

// TRANSPOSE
Expand Down Expand Up @@ -99,6 +103,32 @@
return $this->A⁻¹;
}

// PSEUDO-INVERSE

/**
* @param Matrix $A⁺
*/
public function addPseudoInverse(Matrix $A⁺): void

Check failure on line 111 in src/LinearAlgebra/MatrixCatalog.php

View workflow job for this annotation

GitHub Actions / Static Analysis (7.2)

Method MathPHP\LinearAlgebra\MatrixCatalog::addPseudoInverse() has parameter $A⁺ with generic class MathPHP\LinearAlgebra\Matrix but does not specify its types: T
{
$this->A⁺ = $A⁺;
}

/**
* @return bool
*/
public function hasPseudoInverse(): bool
{
return isset($this->A⁺);
}

/**
* @return Matrix
*/
public function getPseudoInverse(): Matrix
{
return $this->A⁺;
}

/**************************************************************************
* MATRIX REDUCTIONS
* - ref (row echelon form)
Expand Down
10 changes: 10 additions & 0 deletions src/LinearAlgebra/NumericDiagonalMatrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,14 @@ public function inverse(): NumericMatrix
{
return MatrixFactory::diagonal(Single::reciprocal($this->getDiagonalElements()));
}

/**
* pseudoInverse identical to inverse
*
* @return NumericMatrix
*/
public function pseudoInverse(): NumericMatrix
{
return $this->inverse();
}
}
Loading
Loading