diff --git a/src/LinearAlgebra/Eigenvalue.php b/src/LinearAlgebra/Eigenvalue.php index c4db0ef4c..eb374e92a 100644 --- a/src/LinearAlgebra/Eigenvalue.php +++ b/src/LinearAlgebra/Eigenvalue.php @@ -2,20 +2,24 @@ namespace MathPHP\LinearAlgebra; +use MathPHP\Arithmetic; use MathPHP\Exception; use MathPHP\Expression\Polynomial; use MathPHP\Functions\Support; +use MathPHP\LinearAlgebra\Decomposition\QR; class Eigenvalue { public const CLOSED_FORM_POLYNOMIAL_ROOT_METHOD = 'closedFormPolynomialRootMethod'; public const POWER_ITERATION = 'powerIteration'; public const JACOBI_METHOD = 'jacobiMethod'; + public const QR_ALGORITHM = 'qrAlgorithm'; private const METHODS = [ self::CLOSED_FORM_POLYNOMIAL_ROOT_METHOD, self::POWER_ITERATION, self::JACOBI_METHOD, + self::QR_ALGORITHM, ]; /** @@ -261,4 +265,69 @@ public static function powerIteration(NumericMatrix $A, int $iterations = 1000): return [$max_ev]; } + + public static function qrAlgorithm(NumericMatrix $A): array + { + self::checkMatrix($A); + + if ($A->isUpperHessenberg()) { + $H = $A; + } else { + $H = $A->upperHessenberg(); + } + + return self::qrIteration($H); + } + + private static function qrIteration(NumericMatrix $A, array &$values = null): array + { + $values = $values ?? []; + $e = $A->getError(); + $n = $A->getM(); + + if ($A->getM() === 1) { + $values[] = $A[0][0]; + return $values; + } + + $ident = MatrixFactory::identity($n); + + do { + // Pick shift + $shift = self::calcShift($A); + $A = $A->subtract($ident->scalarMultiply($shift)); + + // decompose + $qr = QR::decompose($A); + $A = $qr->R->multiply($qr->Q); + + // shift back + $A = $A->add($ident->scalarMultiply($shift)); + } while (!Arithmetic::almostEqual($A[$n-1][$n-2], 0, $e)); // subdiagonal entry needs to be 0 + + // Check if we can deflate + $eig = $A[$n-1][$n-1]; + $values[] = $eig; + self::qrIteration($A->submatrix(0, 0, $n-2, $n-2), $values); + + return array_reverse($values); + } + + private static function calcShift(NumericMatrix $A): float + { + $n = $A->getM() - 1; + $sigma = .5 * ($A[$n-1][$n-1] - $A[$n][$n]); + $sign = $sigma >= 0 ? 1 : -1; + + $numerator = ($sign * ($A[$n][$n-1])**2); + $denominator = abs($sigma) + sqrt($sigma**2 + ($A[$n-1][$n-1])**2); + + try { + $fraction = $numerator / $denominator; + } catch (\DivisionByZeroError $error) { + $fraction = 0; + } + + return $A[$n][$n] - $fraction; + } } diff --git a/src/LinearAlgebra/Eigenvector.php b/src/LinearAlgebra/Eigenvector.php index abed66e87..af0b110f2 100644 --- a/src/LinearAlgebra/Eigenvector.php +++ b/src/LinearAlgebra/Eigenvector.php @@ -2,6 +2,7 @@ namespace MathPHP\LinearAlgebra; +use MathPHP\Arithmetic; use MathPHP\Exception; use MathPHP\Exception\MatrixException; use MathPHP\Functions\Map\Single; @@ -9,6 +10,13 @@ class Eigenvector { + public static function qrAlgorithm(NumericMatrix $A) + { + $eigenvalues = Eigenvalue::qrAlgorithm($A); + + return self::eigenvectors($A, $eigenvalues); + } + /** * Calculate the Eigenvectors for a matrix * @@ -66,8 +74,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λ); diff --git a/src/LinearAlgebra/NumericMatrix.php b/src/LinearAlgebra/NumericMatrix.php index 52970dced..7d5a2d107 100644 --- a/src/LinearAlgebra/NumericMatrix.php +++ b/src/LinearAlgebra/NumericMatrix.php @@ -2,6 +2,7 @@ namespace MathPHP\LinearAlgebra; +use MathPHP\Arithmetic; use MathPHP\Functions\Map; use MathPHP\Functions\Support; use MathPHP\Exception; @@ -1478,6 +1479,7 @@ function (NumericMatrix $augmented_matrix, NumericMatrix $matrix) { * - covarianceMatrix * - adjugate * - householder + * - upperHessenberg **************************************************************************/ /** @@ -1886,6 +1888,64 @@ public function householder(): NumericMatrix return Householder::transform($this); } + /** + * Uses householder to convert the matrix to upper hessenberg form + * + * @return NumericMatrix + * + * @throws Exception\MathException + */ + public function upperHessenberg(): NumericMatrix + { + $n = $this->getM(); + + $hessenberg = $this; + $identity = MatrixFactory::identity($n); + + for ($i = 0; $i < $n-2; $i++) + { + $slice = $this->submatrix($i+1, $i, $n-1, $i); + $x = $slice->asVectors()[0]; + $sign = $x[0] >= 0 ? 1 : -1; + $x1 = $sign * $x->l2norm(); + if (Arithmetic::almostEqual($x1, 0, $this->getError())) { + continue; + } + $u = \array_fill(0, $n-$i-1, 0); + $u[0] = $x1; + $u = new Vector($u); + + $v = $u->subtract($x); + $v = MatrixFactory::createFromVectors([$v]); + $vt = $v->transpose(); + + $vvt = $v->multiply($vt); + $vtv = $vt->multiply($v)[0][0]; + $P = $vvt->scalarDivide($vtv); + + $H = MatrixFactory::identity($P->getM()); + $H = $H->subtract($P->scalarMultiply(2)); + + // augment + $offset = $n - $H->getM(); + $elems = $identity->getMatrix(); + for ($i = 0; $i < $H->getM(); $i++) + { + for ($j = 0; $j < $H->getN(); $j++) + { + $elems[$i + $offset][$j + $offset] = $H[$i][$j]; + } + } + + $H = MatrixFactory::create($elems); + + // Multiply + $hessenberg = $H->multiply($hessenberg->multiply($H)); + } + + return $hessenberg; + } + /************************************************************************** * MATRIX VECTOR OPERATIONS - Return a Vector * - vectorMultiply diff --git a/tests/LinearAlgebra/Eigen/EigenvalueTest.php b/tests/LinearAlgebra/Eigen/EigenvalueTest.php index c176283c9..af47150dd 100644 --- a/tests/LinearAlgebra/Eigen/EigenvalueTest.php +++ b/tests/LinearAlgebra/Eigen/EigenvalueTest.php @@ -109,6 +109,31 @@ public function testPowerIteration(array $A, array $S, float $max_abs_eigenvalue $this->assertEqualsWithDelta([$max_abs_eigenvalue], $eigenvalues, 0.0001); } + /** + * @test qrAlgorithm returns the expected eigenvalues + * @dataProvider dataProviderForEigenvalues + * @dataProvider dataProviderForLargeMatrixEigenvalues + * @dataProvider dataProviderForSymmetricEigenvalues + * @param array $A + * @param array $S + * @param float $max_abs_eigenvalue maximum absolute eigenvalue + * @throws \Exception + */ + public function testQRAlgorithm(array $A, array $S) + { + // Given + $A = MatrixFactory::create($A); + + // When + $eigenvalues = Eigenvalue::qrAlgorithm($A); + + sort($S); + sort($eigenvalues); + + // Then + $this->assertEqualsWithDelta($S, $eigenvalues, 0.0001); + } + /** * @test Matrix eigenvalues using powerIterationMethod returns the expected eigenvalues * @dataProvider dataProviderForEigenvalues @@ -195,6 +220,24 @@ public function dataProviderForEigenvalues(): array [2, 2, 1], 2, ], + [ + [ + [2, 0, 1], + [2, 1, 2], + [3, 0, 4] + ], + [5, 1, 1], + 5, + ], + [ // Matrix has duplicate eigenvalues. no solution on the axis + [ + [2, 2, -3], + [2, 5, -6], + [3, 6, -8], + ], + [-3, 1, 1], + -3 + ], [ [ [1, 2, 1], @@ -251,6 +294,18 @@ public function dataProviderForLargeMatrixEigenvalues(): array ], [4, 3, 2, -2, 1, -1], 4, + ], + [ // Failing case + [ + [2,0,0,0,0,0], + [0,2,0,0,0,1729.7], + [0,0,2,0,-1729.7,0], + [0,0,0,0,0,0], + [0,0,-1729.7,0,2.82879*10**6,0], + [0,1729.7,0,0,0,2.82879*10**6] + ], + [2828791.05765, 0.94235235527, 0.94235235527, 2828791.05765, 2, 0], + 2828791.05765 ] ]; } diff --git a/tests/LinearAlgebra/Eigen/EigenvectorTest.php b/tests/LinearAlgebra/Eigen/EigenvectorTest.php index 0a6e52817..e52f0a6e6 100644 --- a/tests/LinearAlgebra/Eigen/EigenvectorTest.php +++ b/tests/LinearAlgebra/Eigen/EigenvectorTest.php @@ -234,4 +234,25 @@ public function testMatrixEigenvectorInvalidMethodException() // When $A->eigenvectors($invalidMethod); } + + /** + * @test qrAlgorithm + * @dataProvider dataProviderForEigenvector + * @param array $A + * @param array $B + */ + public function testQRAlgorithm(array $A, array $S): void + { + // Given + $A = MatrixFactory::create($A); + $S = MatrixFactory::create($S); + + // When + $eigenvectors = Eigenvector::qrAlgorithm($A); + + // Then + $this->assertEqualsWithDelta($S, $eigenvectors, 0.0001, sprintf( + "Eigenvectors unequal:\nExpected:\n" . (string) $S . "\nActual:\n" . (string) $eigenvectors + )); + } } diff --git a/tests/LinearAlgebra/Matrix/Numeric/MatrixOperationsTest.php b/tests/LinearAlgebra/Matrix/Numeric/MatrixOperationsTest.php index 5024d942d..b66eb0454 100644 --- a/tests/LinearAlgebra/Matrix/Numeric/MatrixOperationsTest.php +++ b/tests/LinearAlgebra/Matrix/Numeric/MatrixOperationsTest.php @@ -1485,4 +1485,42 @@ public function dataProviderForRank(): array ], ]; } + + /** + * @test upperHessenberg reduction + * @dataProvider dataProviderForUpperHessenberg + * @param array $A + * @throws \Exception + */ + public function testUpperHessenberg(array $A, array $H) + { + // Given + $A = MatrixFactory::create($A); + $H = MatrixFactory::create($H); + + // When + $actual = $A->upperHessenberg(); + + // Then + $this->assertTrue($actual->isUpperHessenberg()); + $this->assertEqualsWithDelta($H, $actual, 0.0000001); + } + + public static function dataProviderForUpperHessenberg(): \Iterator + { + yield [ + 'A' => [ + [3, 0, 0, 0], + [0, 1, 0, 1], + [0, 0, 2, 0], + [0, 1, 0, 1], + ], + 'H' => [ + [3, 0, 0, 0], + [0, 1, 1, 0], + [0, 1, 1, 0], + [0, 0, 0, 2], + ] + ]; + } }