250 lines
7.1 KiB
PHP
250 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA;
|
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
|
|
|
/**
|
|
* For an m-by-n matrix A with m >= n, the QR decomposition is an m-by-n
|
|
* orthogonal matrix Q and an n-by-n upper triangular matrix R so that
|
|
* A = Q*R.
|
|
*
|
|
* The QR decompostion always exists, even if the matrix does not have
|
|
* full rank, so the constructor will never fail. The primary use of the
|
|
* QR decomposition is in the least squares solution of nonsquare systems
|
|
* of simultaneous linear equations. This will fail if isFullRank()
|
|
* returns false.
|
|
*
|
|
* @author Paul Meagher
|
|
*
|
|
* @version 1.1
|
|
*/
|
|
class QRDecomposition
|
|
{
|
|
const MATRIX_RANK_EXCEPTION = 'Can only perform operation on full-rank matrix.';
|
|
|
|
/**
|
|
* Array for internal storage of decomposition.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $QR = [];
|
|
|
|
/**
|
|
* Row dimension.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $m;
|
|
|
|
/**
|
|
* Column dimension.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $n;
|
|
|
|
/**
|
|
* Array for internal storage of diagonal of R.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $Rdiag = [];
|
|
|
|
/**
|
|
* QR Decomposition computed by Householder reflections.
|
|
*
|
|
* @param matrix $A Rectangular matrix
|
|
*/
|
|
public function __construct($A)
|
|
{
|
|
if ($A instanceof Matrix) {
|
|
// Initialize.
|
|
$this->QR = $A->getArray();
|
|
$this->m = $A->getRowDimension();
|
|
$this->n = $A->getColumnDimension();
|
|
// Main loop.
|
|
for ($k = 0; $k < $this->n; ++$k) {
|
|
// Compute 2-norm of k-th column without under/overflow.
|
|
$nrm = 0.0;
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$nrm = hypo($nrm, $this->QR[$i][$k]);
|
|
}
|
|
if ($nrm != 0.0) {
|
|
// Form k-th Householder vector.
|
|
if ($this->QR[$k][$k] < 0) {
|
|
$nrm = -$nrm;
|
|
}
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$this->QR[$i][$k] /= $nrm;
|
|
}
|
|
$this->QR[$k][$k] += 1.0;
|
|
// Apply transformation to remaining columns.
|
|
for ($j = $k + 1; $j < $this->n; ++$j) {
|
|
$s = 0.0;
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$s += $this->QR[$i][$k] * $this->QR[$i][$j];
|
|
}
|
|
$s = -$s / $this->QR[$k][$k];
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$this->QR[$i][$j] += $s * $this->QR[$i][$k];
|
|
}
|
|
}
|
|
}
|
|
$this->Rdiag[$k] = -$nrm;
|
|
}
|
|
} else {
|
|
throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION);
|
|
}
|
|
}
|
|
|
|
// function __construct()
|
|
|
|
/**
|
|
* Is the matrix full rank?
|
|
*
|
|
* @return bool true if R, and hence A, has full rank, else false
|
|
*/
|
|
public function isFullRank()
|
|
{
|
|
for ($j = 0; $j < $this->n; ++$j) {
|
|
if ($this->Rdiag[$j] == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// function isFullRank()
|
|
|
|
/**
|
|
* Return the Householder vectors.
|
|
*
|
|
* @return Matrix Lower trapezoidal matrix whose columns define the reflections
|
|
*/
|
|
public function getH()
|
|
{
|
|
$H = [];
|
|
for ($i = 0; $i < $this->m; ++$i) {
|
|
for ($j = 0; $j < $this->n; ++$j) {
|
|
if ($i >= $j) {
|
|
$H[$i][$j] = $this->QR[$i][$j];
|
|
} else {
|
|
$H[$i][$j] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Matrix($H);
|
|
}
|
|
|
|
// function getH()
|
|
|
|
/**
|
|
* Return the upper triangular factor.
|
|
*
|
|
* @return Matrix upper triangular factor
|
|
*/
|
|
public function getR()
|
|
{
|
|
$R = [];
|
|
for ($i = 0; $i < $this->n; ++$i) {
|
|
for ($j = 0; $j < $this->n; ++$j) {
|
|
if ($i < $j) {
|
|
$R[$i][$j] = $this->QR[$i][$j];
|
|
} elseif ($i == $j) {
|
|
$R[$i][$j] = $this->Rdiag[$i];
|
|
} else {
|
|
$R[$i][$j] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Matrix($R);
|
|
}
|
|
|
|
// function getR()
|
|
|
|
/**
|
|
* Generate and return the (economy-sized) orthogonal factor.
|
|
*
|
|
* @return Matrix orthogonal factor
|
|
*/
|
|
public function getQ()
|
|
{
|
|
$Q = [];
|
|
for ($k = $this->n - 1; $k >= 0; --$k) {
|
|
for ($i = 0; $i < $this->m; ++$i) {
|
|
$Q[$i][$k] = 0.0;
|
|
}
|
|
$Q[$k][$k] = 1.0;
|
|
for ($j = $k; $j < $this->n; ++$j) {
|
|
if ($this->QR[$k][$k] != 0) {
|
|
$s = 0.0;
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$s += $this->QR[$i][$k] * $Q[$i][$j];
|
|
}
|
|
$s = -$s / $this->QR[$k][$k];
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$Q[$i][$j] += $s * $this->QR[$i][$k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Matrix($Q);
|
|
}
|
|
|
|
// function getQ()
|
|
|
|
/**
|
|
* Least squares solution of A*X = B.
|
|
*
|
|
* @param Matrix $B a Matrix with as many rows as A and any number of columns
|
|
*
|
|
* @return Matrix matrix that minimizes the two norm of Q*R*X-B
|
|
*/
|
|
public function solve($B)
|
|
{
|
|
if ($B->getRowDimension() == $this->m) {
|
|
if ($this->isFullRank()) {
|
|
// Copy right hand side
|
|
$nx = $B->getColumnDimension();
|
|
$X = $B->getArrayCopy();
|
|
// Compute Y = transpose(Q)*B
|
|
for ($k = 0; $k < $this->n; ++$k) {
|
|
for ($j = 0; $j < $nx; ++$j) {
|
|
$s = 0.0;
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$s += $this->QR[$i][$k] * $X[$i][$j];
|
|
}
|
|
$s = -$s / $this->QR[$k][$k];
|
|
for ($i = $k; $i < $this->m; ++$i) {
|
|
$X[$i][$j] += $s * $this->QR[$i][$k];
|
|
}
|
|
}
|
|
}
|
|
// Solve R*X = Y;
|
|
for ($k = $this->n - 1; $k >= 0; --$k) {
|
|
for ($j = 0; $j < $nx; ++$j) {
|
|
$X[$k][$j] /= $this->Rdiag[$k];
|
|
}
|
|
for ($i = 0; $i < $k; ++$i) {
|
|
for ($j = 0; $j < $nx; ++$j) {
|
|
$X[$i][$j] -= $X[$k][$j] * $this->QR[$i][$k];
|
|
}
|
|
}
|
|
}
|
|
$X = new Matrix($X);
|
|
|
|
return $X->getMatrix(0, $this->n - 1, 0, $nx);
|
|
}
|
|
|
|
throw new CalculationException(self::MATRIX_RANK_EXCEPTION);
|
|
}
|
|
|
|
throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION);
|
|
}
|
|
}
|