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 @@ public static function decompose(NumericMatrix $M): SVD
// 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 @@ public static function decompose(NumericMatrix $M): SVD
$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());
}

$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) {
$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)
{
$ε = $S->getError();

// Each column should contain up to 1 non-zero element
if (Arithmetic::almostEqual($vector->l2Norm(), 0, $ε)) {
$j = $i;
} else {
$j = self::getStandardBasisIndex($vector, $ε);
}

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];
}
}

// 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;
}

// 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

// 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) {
$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 @@ class MatrixCatalog
/** @var Matrix<T> inverse */
private $A⁻¹;

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

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

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

// TRANSPOSE
Expand Down Expand Up @@ -99,6 +103,32 @@ public function getInverse(): Matrix
return $this->A⁻¹;
}

// PSEUDO-INVERSE

/**
* @param Matrix $A⁺
*/
public function addPseudoInverse(Matrix $A⁺): void
{
$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