| // +----------------------------------------------------------------------+ // // Matrix definition and manipulation package // // $Id: Matrix.php,v 1.6 2003/04/14 11:27:52 jmcastagnetto Exp $ // require_once 'PEAR.php'; require_once 'Math/Vector/Vector.php'; /** * Defines a matrix object, conceptualized as an array of arrays such that: * *
 * [0][0] [0][1] [0][2] ... [0][M]
* [1][0] [1][1] [1][2] ... [1][M]
* ...
* [N][0] [n][1] [n][2] ... [n][M]
*
* * i.e. N rows, M colums * * Originally this class was part of NumPHP (Numeric PHP package) * * @author Jesus M. Castagnetto * @access public * @version 1.0 * @package Math_Matrix */ class Math_Matrix {/*{{{*/ // Properties /*{{{*/ /** * Contains the array of arrays defining the matrix * * @access private * @var array * @see getData() */ var $_data = null; /** * The number of rows in the matrix * * @access private * @var integer * @see getSize() */ var $_num_rows = null; /** * The number of columns in the matrix * * @access private * @var integer * @see getSize() */ var $_num_cols = null; /** * The smallest value of all matrix cells * * @access private * @var float * @see getMin() * @see getMinMax() */ var $_min = null; /** * The biggest value of all matrix cells * * @access private * @var float * @see getMax() * @see getMinMax() */ var $_max = null; /** * A flag indicating if the matrix is square * i.e. if $this->_num_cols == $this->_num_rows * * @access private * @var boolean * @see isSquare() */ var $_square = false; /** * The Euclidean norm for the matrix: sqrt(sum(e[i][j]^2)) * * @access private * @var float * @see norm() */ var $_norm = null; /** * The matrix determinant * * @access private * @var float * @see determinant() */ var $_det = null; /** * Cutoff error used to test for singular or ill-conditioned matrices * * @access private * @var float * @see determinant(); * @see invert() */ var $_epsilon = 1E-18; /*}}}*/ /** * Constructor for the matrix object * * @access public * @param array $data * @return object Math_Matrix * @see $_data * @see setData() */ function Math_Matrix ($data=null) {/*{{{*/ if (!is_null($data)) $this->setData($data); }/*}}}*/ /** * Validates the data and initializes the internal variables (except for the determinant). * * The validation is performed by by checking that * each row (first dimension in the array of arrays) * contains the same number of colums (e.g. arrays of the * same size) * * @access public * @param array $data array of arrays to create a matrix object * @return mixed true on success, a PEAR_Error object otherwise */ function setData($data) {/*{{{*/ $errorObj = PEAR::raiseError('Invalid data, cannot create/modify matrix'); if (!is_array($data) || !is_array($data[0])) { return $errorObj; } // check that we got a numeric bidimensional array // and that all rows are of the same size $nc = count($data[0]); $nr = count($data); $eucnorm = 0; for ($i=0; $i < $nr; $i++) { if (count($data[$i]) != $nc) { return $errorObj; } for ($j=0; $j < $nc; $j++) { if (!is_numeric($data[$i][$j])) { return $errorObj; } $data[$i][$j] = (float) $data[$i][$j]; $tmp[] = $data[$i][$j]; $eucnorm += $data[$i][$j] * $data[$i][$j]; } } $this->_num_rows = $nr; $this->_num_cols = $nc; $this->_square = ($nr == $nc); $this->_min = min($tmp); $this->_max = max($tmp); $this->_norm = sqrt($eucnorm); $this->_data = $data; $this->_det = null; // lazy initialization ;-) return true; }/*}}}*/ /** * Returns the array of arrays. * * @access public * @return mixed an array of array of numbers on success, a PEAR_Error otherwise */ function getData () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->_data; } }/*}}}*/ /** * Checks if the matrix has been initialized. * * @access public * @return boolean TRUE on success, FALSE otherwise */ function isEmpty() {/*{{{*/ return ( empty($this->_data) || is_null($this->_data) ); }/*}}}*/ /** * Returns an array with the number of rows and columns in the matrix * * @access public * @return mixed an array of integers on success, a PEAR_Error object otherwise */ function getSize() {/*{{{*/ if ($this->isEmpty()) return PEAR::raiseError('Matrix has not been populated'); else return array($this->_num_rows, $this->_num_cols); }/*}}}*/ /** * Checks if it is a square matrix (i.e. num rows == num cols) * * @access public * @return boolean TRUE on success, FALSE otherwise */ function isSquare () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->_square; } }/*}}}*/ /** * Returns the Euclidean norm of the matrix. * * Euclidean norm = sqrt( sum( e[i][j]^2 ) ) * * @access public * @return mixed a number on success, a PEAR_Error otherwise */ function norm() {/*{{{*/ if (!is_null($this->_norm)) { return $this->_norm; } else { return PEAR::raiseError('Uninitialized Math_Matrix object'); } }/*}}}*/ /** * Returns a new Math_Matrix object with the same data as the current one * * @access public * @return object Math_Matrix */ function &clone() {/*{{{*/ return new Math_Matrix($this->_data); }/*}}}*/ /** * Sets the value of the element at (row,col) * * @param integer $row * @param integer $col * @param numeric $value * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function setElement($row, $col, $value) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if ($row >= $this->_num_rows && $col >= $this->_num_cols) { return PEAR::raiseError('Incorrect row and column values'); } if (!is_numeric($value)) { return PEAR::raiseError('Incorrect value, expecting a number'); } $this->_data[$row][$col] = $value; return true; }/*}}}*/ /** * Returns the value of the element at (row,col) * * @param integer $row * @param integer $col * @access public * @return mixed a number on success, a PEAR_Error otherwise */ function getElement($row, $col) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if ($row >= $this->_num_rows && $col >= $this->_num_cols) { return PEAR::raiseError('Incorrect row and column values'); } return $this->_data[$row][$col]; }/*}}}*/ /** * Returns the row with the given index * * This method checks that matrix has been initialized and that the * row requested is not outside the range of rows. * * @param integer $row * @param optional boolean $asVector whether to return a Math_Vector or a simple array. Default = false. * @access public * @return mixed an array number on success, a PEAR_Error otherwise */ function getRow ($row, $asVector = false) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if (is_integer($row) && $row >= $this->_num_rows) { return PEAR::raiseError('Incorrect row value'); } if ($asVector) { $classes = get_declared_classes(); if (!in_array("math_vector", $classes) || !in_array("math_vectopop", $classes)) { return PEAR::raiseError ("Classes Math_Vector and Math_VectorOp undefined". " add \"require_once 'Math/Vector/Vector.php'\" to your script"); } return new Math_Vector($this->_data[$row]); } else { return $this->_data[$row]; } }/*}}}*/ /** * Sets the row with the given index to the array * * This method checks that the row is less than the size of the matrix * rows, and that the array size equals the number of columns in the * matrix. * * @param integer $row index of the row * @param array $arr array of numbers * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function setRow ($row, $arr) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if ($row >= $this->_num_rows) { return PEAR::raiseError('Row index out of bounds'); } if (count($arr) != $this->_num_cols) { return PEAR::raiseError('Incorrect size for matrix row: expecting '.$this->_num_cols .' columns, got '.count($arr).' columns'); } for ($i=0; $i < $this->_num_cols; $i++) { if (!is_numeric($arr[$i])) { return PEAR::raiseError('Incorrect values, expecting numbers'); } } $this->_data[$row] = $arr; return true; }/*}}}*/ /** * Returns the column with the given index * * This method checks that matrix has been initialized and that the * column requested is not outside the range of column. * * @param integer $col * @param optional boolean $asVector whether to return a Math_Vector or a simple array. Default = false. * @access public * @return mixed an array number on success, a PEAR_Error otherwise */ function getCol ($col, $asVector=false) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if (is_integer($col) && $col >= $this->_num_cols) { return PEAR::raiseError('Incorrect column value'); } for ($i=0; $i < $this->_num_rows; $i++) { $ret[$i] = $this->getElement($i,$col); } if ($asVector) { $classes = get_declared_classes(); if (!in_array("math_vector", $classes) || !in_array("math_vectopop", $classes)) { return PEAR::raiseError ("Classes Math_Vector and Math_VectorOp undefined". " add \"require_once 'Math/Vector/Vector.php'\" to your script"); } return new Math_Vector($ret); } else { return $ret; } }/*}}}*/ /** * Sets the column with the given index to the array * * This method checks that the column is less than the size of the matrix * columns, and that the array size equals the number of rows in the * matrix. * * @param integer $col index of the column * @param array $arr array of numbers * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function setCol ($col, $arr) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if ($col >= $this->_num_cols) { return PEAR::raiseError('Incorrect column value'); } if (count($arr) != $this->_num_cols) { return PEAR::raiseError('Incorrect size for matrix column'); } for ($i=0; $i < $this->_num_rows; $i++) { if (!is_numeric($arr[$i])) { return PEAR::raiseError('Incorrect values, expecting numbers'); } else { $err = $this->setElement($i, $col, $arr[$i]); if (PEAR::isError($err)) { return $err; } } } return true; }/*}}}*/ /** * Swaps the rows with the given indices * * @param integer $i * @param integer $j * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function swapRows($i, $j) {/*{{{*/ $r1 = $this->getRow($i); if (PEAR::isError($r1)) { return $r1; } $r2 = $this->getRow($j); if (PEAR::isError($r2)) { return $r2; } $e = $this->setRow($j, $r1); if (PEAR::isError($e)) { return $e; } $e = $this->setRow($i, $r2); if (PEAR::isError($e)) { return $e; } return true; }/*}}}*/ /** * Swaps the columns with the given indices * * @param integer $i * @param integer $j * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function swapCols($i, $j) {/*{{{*/ $r1 = $this->getCol($i); if (PEAR::isError($r1)) { return $r1; } $r2 = $this->getCol($j); if (PEAR::isError($r2)) { return $r2; } $e = $this->setCol($j, $r1); if (PEAR::isError($e)) { return $e; } $e = $this->setCol($i, $r2); if (PEAR::isError($e)) { return $e; } return true; }/*}}}*/ /** * Swaps a given row with a given column. Only valid for square matrices. * * @param integer $row index of row * @param integer $col index of column * @access public * @return mixed TRUE on success, a PEAR_Error otherwise */ function swapRowCol ($row, $col) {/*{{{*/ if (!$this->isSquare() || !is_int($row) || !is_int($col)) { return PEAR::raiseError("Parameters must be row and a column indices"); } $c = $this->getCol($col); if (PEAR::isError($c)) { return $c; } $r = $this->getRow($row); if (PEAR::isError($r)) { return $r; } $e = $this->setCol($col, $r); if (PEAR::isError($e)) { return $e; } $e = $this->setRow($row, $c); if (PEAR::isError($e)) { return $e; } return true; }/*}}}*/ /** * Returns the minimum value of the elements in the matrix * * @access public * @return mixed a number on success, a PEAR_Error otherwise */ function getMin () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->_min; } }/*}}}*/ /** * Returns the maximum value of the elements in the matrix * * @access public * @return mixed a number on success, a PEAR_Error otherwise */ function getMax () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->_max; } }/*}}}*/ /** * Gets the position of the first element with the given value * * @param numeric $val * @access public * @return mixed an array of two numbers on success, FALSE if value is not found, and PEAR_Error otherwise */ function getValueIndex ($val) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } for ($i=0; $i < $this->_num_rows; $i++) { for ($j=0; $j < $this->_num_cols; $j++) { if ($this->_data[$i][$j] == $val) { return array($i, $j); } } } return false; }/*}}}*/ /** * Gets the position of the element with the minimum value * * @access public * @return mixed an array of two numbers on success, FALSE if value is not found, and PEAR_Error otherwise * @see getValueIndex() */ function getMinIndex () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->getValueIndex($this->_min); } }/*}}}*/ /** * Gets the position of the element with the maximum value * * @access public * @return mixed an array of two numbers on success, FALSE if value is not found, and PEAR_Error otherwise * @see getValueIndex() */ function getMaxIndex () {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } else { return $this->getValueIndex($this->_max); } }/*}}}*/ /** * Transpose the matrix rows and columns * * @access public * @return mixed TRUE on success, PEAR_Error otherwise */ function transpose () {/*{{{*/ if (!$this->isSquare()) { return PEAR::raiseError("Transpose is undefined for non-sqaure matrices"); } list($nr, $nc) = $this->getSize(); $data = array(); for ($i=0; $i < $nc; $i++) { $col = $this->getCol($i); if (PEAR::isError($col)) { return $col; } else { $data[] = $col; } } return $this->setData($data); }/*}}}*/ /** * Returns the trace of the matrix. Trace = sum(e[i][j]), for all i == j * * @access public * @return mixed a number on success, PEAR_Error otherwise */ function trace() {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if (!$this->isSquare()) { return PEAR::raiseError('Trace undefined for non-square matrices'); } $trace = 0; for ($i=0; $i < $this->_num_rows; $i++) { $trace += $this->getElement($i, $i); } return $trace; }/*}}}*/ /** * Calculates the matrix determinant using Gaussian elimination with partial pivoting. * * At each step of the pivoting proccess, it checks that the normalized * determinant calculated so far is less than 10^-18, trying to detect * singular or ill-conditioned matrices * * @access public * @return mixed a number on success, a PEAR_Error otherwise */ function determinant() {/*{{{*/ if (!is_null($this->_det) && is_numeric($this->_det)) { return $this->_det; } if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if (!$this->isSquare()) { return PEAR::raiseError('Determinant undefined for non-square matrices'); } $norm = $this->norm(); if (PEAR::isError($norm)) { return $norm; } $det = 1.0; $sign = 1; // work on a copy $m = $this->clone(); list($nr, $nc) = $m->getSize(); for ($r=0; $r<$nr; $r++) { // find the maximum element in the column under the current diagonal element $ridx = $m->_maxElementIndex($r); if (PEAR::isError($ridx)) { return $ridx; } if ($ridx != $r) { $sign = -$sign; $e = $m->swapRows($r, $ridx); if (PEAR::isError($e)) { return $e; } } // pivoting element $pelement = $m->getElement($r, $r); if (PEAR::isError($pelement)) { return $pelement; } $det *= $pelement; // Is this an singular or ill-conditioned matrix? // i.e. is the normalized determinant << 1 and -> 0? if ((abs($det)/$norm) < $this->_epsilon) { return PEAR::raiseError('Probable singular or ill-conditioned matrix, normalized determinant = ' .(abs($det)/$norm)); } if ($pelement == 0) { return PEAR::raiseError('Cannot continue, pivoting element is zero'); } // zero all elements in column below the pivoting element for ($i = $r + 1; $i < $nr; $i++) { $factor = $m->getElement($i, $r) / $pelement; for ($j=$r; $j < $nc; $j++) { $val = $m->getElement($i, $j) - $factor*$m->getElement($r, $j); $e = $m->setElement($i, $j, $val); if (PEAR::isError($e)) { return $e; } } } // for debugging //echo "COLUMN: $r\n"; //echo $m->toString()."\n"; } unset($m); if ($sign < 0) { $det = -$det; } // save the value $this->_det = $det; return $det; }/*}}}*/ /** * Returns the normalized determinant = abs(determinant)/(euclidean norm) * * @access public * @return mixed a positive number on success, a PEAR_Error otherwise */ function normalizedDeterminant() {/*{{{*/ $det = $this->determinant(); if (PEAR::isError($det)) { return $det; } $norm = $this->norm(); if (PEAR::isError($norm)) { return $norm; } if ($norm == 0) { return PEAR::raiseError('Undefined normalized determinant, euclidean norm is zero'); } return abs($det / $norm); }/*}}}*/ /** * Returns the index of the row with the maximum value under column of the element e[i][i] * * @access protected * @return an integer */ function _maxElementIndex($r) {/*{{{*/ $max = 0; $idx = -1; list($nr, $nc) = $this->getSize(); $arr = array(); for ($i=$r; $i<$nr; $i++) { $val = abs($this->_data[$i][$r]); if ($val > $max) { $max = $val; $idx = $i; } } if ($idx == -1) { $idx = $r; } return $idx; }/*}}}*/ /** * Inverts a matrix using Gauss-Jordan elimination with partial pivoting * * @access public * @return mixed the value of the matrix determinant on success, PEAR_Error otherwise * @see scaleRow() */ function invert() { if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } if (!$this->isSquare()) { return PEAR::raiseError('Determinant undefined for non-square matrices'); } $norm = $this->norm(); $sign = 1; $det = 1.0; // work on a copy to be safe $m = $this->clone(); list($nr, $nc) = $m->getSize(); // Unit matrix to use as target $q = Math_Matrix::makeUnit($nr); for ($i=0; $i<$nr; $i++) { $ridx = $this->_maxElementIndex($i); if ($i != $ridx) { $sign = -$sign; $e = $m->swapRows($i, $ridx); if (PEAR::isError($e)) { return $e; } $e = $q->swapRows($i, $ridx); if (PEAR::isError($e)) { return $e; } } $pelement = $m->getElement($i, $i); if (PEAR::isError($pelement)) { return $pelement; } if ($pelement == 0) { return PEAR::raiseError('Cannot continue inversion, pivoting element is zero'); } $det *= $pelement; if ((abs($det)/$norm) < $this->_epsilon) { return PEAR::raiseError('Probable singular or ill-conditioned matrix, normalized determinant = ' .(abs($det)/$norm)); } $e = $m->scaleRow($i, 1/$pelement); if (PEAR::isError($e)) { return $e; } $e = $q->scaleRow($i, 1/$pelement); if (PEAR::isError($e)) { return $e; } // zero all column elements execpt for the current one $tpelement = $m->getElement($i, $i); for ($j=0; $j<$nr; $j++) { if ($j == $i) { continue; } $factor = $m->getElement($j, $i) / $tpelement; for ($k=0; $k<$nc; $k++) { $vm = $m->getElement($j, $k) - $factor * $m->getElement($i, $k); $vq = $q->getElement($j, $k) - $factor * $q->getElement($i, $k); $m->setElement($j, $k, $vm); $q->setElement($j, $k, $vq); } } // for debugging /* echo "COLUMN: $i\n"; echo $m->toString()."\n"; echo $q->toString()."\n"; */ } $data = $q->getData(); /* // for debugging echo $m->toString()."\n"; echo $q->toString()."\n"; */ unset($m); unset($q); $e = $this->setData($data); if (PEAR::isError($e)) { return $e; } if ($sign < 0) { $det = -$det; } $this->_det = $det; return $det; } /** * Returns a submatrix from the position (row, col), with nrows and ncols * * @access public * @return object Math_Matrix on success, PEAR_Error otherwise */ function &getSubMatrix ($row, $col, $nrows, $ncols) {/*{{{*/ if (!is_numeric($row) || !is_numeric($col) || !is_numeric($nrows) || !is_numeric($ncols)) { return PEAR::raiseError('Parameters must be a initial row and column, and number of rows and columns in submatrix'); } list($nr, $nc) = $this->getSize(); if ($rows + $nrows > $nr) { return PEAR::raiseError('Rows in submatrix more than in original matrix'); } if ($cols + $ncols > $nr) { return PEAR::raiseError('Columns in submatrix more than in original matrix'); } $data = array(); for ($i=0; $i < $nrows; $i++) { for ($j=0; $j < $ncols; $j++) { $data[$i][$j] = $this->getElement($i + $row, $j + $col); } } return new Math_Matrix($data); }/*}}}*/ /** * Returns a simple string representation of the matrix * * @param optional string $format a sprintf() format used to print the matrix elements (default = '%6.2f') * @return mixed a string on success, PEAR_Error otherwise */ function toString ($format='%6.2f') {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } $out = ""; for ($i=0; $i < $this->_num_rows; $i++) { for ($j=0; $j < $this->_num_cols; $j++) { $out .= sprintf($format, $this->_data[$i][$j]); } $out .= "\n"; } return $out; }/*}}}*/ /** * Returns an HTML table representation of the matrix elements * * @access public * @return a string on success, PEAR_Error otherwise */ function toHTML() {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Matrix has not been populated'); } $out = "\n\t\n\t\n\t\t"; for ($i=0; $i < $this->_num_cols; $i++) { $out .= ""; } $out .= "\n\t\n"; for ($i=0; $i < $this->_num_rows; $i++) { $out .= "\t\n\t\t"; for ($j=0; $j < $this->_num_cols; $j++) { $out .= ""; } $out .= "\n\t"; } return $out."\n
Matrix"; $out .= "
"; $out .= $this->_num_rows."x".$this->_num_cols."".$i."
".$i."".$this->_data[$i][$j]."
\n"; }/*}}}*/ // Binary operations /** * Adds a matrix to this one * * @param object Math_Matrix $m1 * @return mixed TRUE on success, PEAR_Error otherwise * @see getSize() * @see getElement() * @see setData() */ function add ($m1) {/*{{{*/ if (!Math_Matrix::isMatrix($m1)) { return PEAR::raiseError("Parameter must be a Math_Matrix object"); } if ($this->getSize() != $m1->getSize()) { return PEAR::raiseError("Matrices must have the same dimensions"); } list($nr, $nc) = $m1->getSize(); $data = array(); for ($i=0; $i < $nr; $i++) { for ($j=0; $j < $nc; $j++) { $el1 = $m1->getElement($i,$j); if (PEAR::isError($el1)) { return $el1; } $el = $this->getElement($i,$j); if (PEAR::isError($el)) { return $el; } $data[$i][$j] = $el + $el1; } } if (!empty($data)) { return $this->setData($data); } else { return PEAR::raiseError('Undefined error'); } }/*}}}*/ /** * Substracts a matrix from this one * * @param object Math_Matrix $m1 * @return mixed TRUE on success, PEAR_Error otherwise * @see getSize() * @see getElement() * @see setData() */ function sub (&$m1) {/*{{{*/ if (!Math_Matrix::isMatrix($m1)) { return PEAR::raiseError("Parameter must be a Math_Matrix object"); } if ($this->getSize() != $m1->getSize()) { return PEAR::raiseError("Matrices must have the same dimensions"); } list($nr, $nc) = $m1->getSize(); $data = array(); for ($i=0; $i < $nr; $i++) { for ($j=0; $j < $nc; $j++) { $el1 = $m1->getElement($i,$j); if (PEAR::isError($el1)) { return $el1; } $el = $this->getElement($i,$j); if (PEAR::isError($el)) { return $el; } $data[$i][$j] = $el - $el1; } } if (!empty($data)) { return $this->setData($data); } else { return PEAR::raiseError('Undefined error'); } }/*}}}*/ /** * Scales the matrix by a given factor * * @param numeric $scale the scaling factor * @return mixed TRUE on success, PEAR_Error otherwise * @see getSize() * @see getElement() * @see setData() */ function scale ($scale) {/*{{{*/ if (!is_numeric($scale)) { return PEAR::raiseError("Parameter must be a number"); } list($nr, $nc) = $this->getSize(); $data = array(); for ($i=0; $i < $nr; $i++) { for ($j=0; $j < $nc; $j++) { $data[$i][$j] = $scale * $this->getElement($i,$j); } } if (!empty($data)) { return $this->setData($data); } else { return PEAR::raiseError('Undefined error'); } }/*}}}*/ /** * Multiplies (scales) a row by the given factor * * @access public * @param integer $row the row index * @param numeric $factor the scaling factor * @return mixed TRUE on success, a PEAR_Error otherwise * @see invert() */ function scaleRow($row, $factor) {/*{{{*/ if ($this->isEmpty()) { return PEAR::raiseError('Uninitialized Math_Matrix object'); } if (!is_integer($row) || !is_numeric($factor)) { return PEAR::raiseError('Row index must be an integer, and factor a valid number'); } if ($row >= $this->_num_rows) { return PEAR::raiseError('Row index out of bounds'); } $r = $this->getRow($row); if (PEAR::isError($r)) { return $r; } $nr = count($r); for ($i=0; $i<$nr; $i++) { $r[$i] *= $factor; } return $this->setRow($row, $r); }/*}}}*/ /** * Multiplies a matrix by this one * * @param object Math_Matrix $m1 * @return mixed TRUE on success, PEAR_Error otherwise * @see getSize() * @see getRow() * @see getCol() * @see setData() */ function multiply(&$m1) {/*{{{*/ $epsilon = 1E-10; if (!Math_Matrix::isMatrix($m1)) { return PEAR::raiseError ('Wrong parameter, expected a Math_Matrix object'); } list($nr, $nc) = $this->getSize(); list($nr1, $nc1) = $m1->getSize(); if ($nc1 != $nr) { return PEAR::raiseError('Incompatible sizes columns in matrix must be the same as rows in parameter matrix'); } $data = array(); for ($i=0; $i < $nr; $i++) { for ($j=0; $j < $nc1; $j++) { $data[$i][$j] = 0; for ($k=0; $k < $nc; $k++) { $data[$i][$j] += $this->getElement($i,$k) * $m1->getElement($k, $j); } // take care of some round-off errors if ($data[$i][$j] <= $epsilon) { $data[$i][$j] = 0.0; } } } if (!empty($data)) { return $this->setData($data); } else { return PEAR::raiseError('Undefined error'); } }/*}}}*/ /** * Multiplies a vector by this matrix * * @param object Math_Vector $v1 * @return object an instance of Math_Vector on success, PEAR_Error otherwise * @see getSize() * @see getRow() * @see Math_Vector::get() */ function &vectorMultiply(&$v1) {/*{{{*/ if (!Math_VectorOp::isVector($v1)) { return PEAR::raiseError ("Wrong parameter, a Math_Vector object"); } list($nr, $nc) = $this->getSize(); $nv = $v1->size(); if ($nc != $nv) { return PEAR::raiseError("Incompatible number of columns in matrix ($nc) must ". "be the same as the number of elements ($nv) in the vector"); } $data = array(); for ($i=0; $i < $nr; $i++) { $data[$i] = 0; for ($j=0; $j < $nv; $j++) { $data[$i] += $this->getElement($i,$j) * $v1->get($j); } } return new Math_Vector($data); }/*}}}*/ // Static operations /** * Create a matrix from a file, using data stored in the given format * * Lines starting with '#' will be assumed to be comments and will be skipped * * @static * @access public * @param string $filename name of file containing matrix data * @param optional string $format one of 'serialized' (default) or 'csv' * @return object a Math_Matrix instance on success, a PEAR_Error otherwise */ function &readFromFile ($filename, $format='serialized') {/*{{{*/ if (!file_exists($filename) || !is_readable($filename)) { return PEAR::raiseError('File cannot be opened for reading'); } if (filesize($filename) == 0) { return PEAR::raiseError('File is empty'); } if ($format == 'serialized') { if (function_exists("file_get_contents")) { $objser = file_get_contents($filename); } else { $objser = implode("",file($filename)); } $obj = unserialize($objser); if (Math_Matrix::isMatrix($obj)) { return $obj; } else { return PEAR::raiseError('File did not contain a Math_Matrix object'); } } else { // assume CSV data $data = array(); $lines = file($filename); foreach ($lines as $line) { if (preg_match('/^#/', $line)) { continue; } else { $data[] = explode(',',trim($line)); } } $m =& new Math_Matrix(); $e = $m->setData($data); if (PEAR::isError($e)) { return $e; } else { return $m; } } }/*}}}*/ /** * Writes matrix object to a file using the given format * * @static * @access public * @param object Math_Matrix $matrix the matrix object to store * @param string $filename name of file to contain the matrix data * @param optional string $format one of 'serialized' (default) or 'csv' * @return mixed TRUE on success, a PEAR_Error otherwise */ function writeToFile (&$matrix, $filename, $format='serialized') {/*{{{*/ if (!Math_Matrix::isMatrix($matrix)) { return PEAR::raiseError("Parameter must be a Math_Matrix object"); } if ($matrix->isEmpty()) { return PEAR::raiseError("Math_Matrix object is empty"); } if ($format == 'serialized') { $data = serialize($matrix); } else { $data = ''; list($nr, $nc) = $matrix->getSize(); for ($i=0; $i<$nr; $i++) { $row = $matrix->getRow($i); if (PEAR::isError($row)) { return $row; } $data .= implode(',', $row)."\n"; } } $fp = fopen($filename, "w"); if (!$fp) { return PEAR::raiseError("Cannot write matrix to file $filename"); } fwrite($fp, $data); fclose($fp); return true; }/*}}}*/ /** * Checks if the object is a Math_Matrix instance * * @static * @access public * @param object Math_Matrix $matrix * @return boolean TRUE on success, FALSE otherwise */ function isMatrix (&$matrix) {/*{{{*/ if (function_exists("is_a")) { return is_a($matrix, "Math_Matrix"); } else { return (get_class($matrix) == "math_matrix"); } }/*}}}*/ /** * Returns a Math_Matrix object of size (nrows, ncols) filled with a value * * @static * @access public * @param integer $nrows number of rows in the generated matrix * @param integer $ncols number of columns in the generated matrix * @param numeric $value the fill value * @return object a Math_Matrix instance on success, PEAR_Error otherwise */ function &makeMatrix ($nrows, $ncols, $value) {/*{{{*/ if (!is_int($nrows) && is_int($ncols) && !is_numeric($value)) { return PEAR::raiseError('Number of rows, columns, and a numeric fill value expected'); } for ($i=0; $i<$nrows; $i++) { $m[$i] = explode(":",substr(str_repeat($value.":",$ncols),0,-1)); } return new Math_Matrix($m); }/*}}}*/ /** * Returns the Math_Matrix object of size (nrows, ncols), filled with the value 1 (one) * * @static * @access public * @param integer $nrows number of rows in the generated matrix * @param integer $ncols number of columns in the generated matrix * @return object a Math_Matrix instance on success, PEAR_Error otherwise * @see Math_Matrix::makeMatrix() */ function &makeOne ($nrows, $ncols) {/*{{{*/ return Math_Matrix::makeMatrix ($nrows, $ncols, 1); }/*}}}*/ /** * Returns the Math_Matrix object of size (nrows, ncols), filled with the value 0 (zero) * * @static * @access public * @param integer $nrows number of rows in the generated matrix * @param integer $ncols number of columns in the generated matrix * @return object a Math_Matrix instance on success, PEAR_Error otherwise * @see Math_Matrix::makeMatrix() */ function &makeZero ($nrows, $ncols) {/*{{{*/ return Math_Matrix::makeMatrix ($nrows, $ncols, 0); }/*}}}*/ /** * Returns a square unit Math_Matrix object of the given size * * A unit matrix is one in which the elements follow the rules: * e[i][j] = 1, if i == j * e[i][j] = 0, if i != j * Such a matrix is also called an 'identity matrix' * * @static * @access public * @param integer $size number of rows and columns in the generated matrix * @return object a Math_Matrix instance * @see Math_Matrix::makeIdentity() */ function &makeUnit ($size) {/*{{{*/ for ($i=0; $i<$size; $i++) { for ($j=0; $j<$size; $j++) { if ($i == $j) { $data[$i][$j] = (float) 1.0; } else { $data[$i][$j] = (float) 0.0; } } } return new Math_Matrix($data); }/*}}}*/ /** * Returns the identity matrix of the given size. An alias of Math_Matrix::makeUnit() * * @static * @access public * @param integer $size number of rows and columns in the generated matrix * @return object a Math_Matrix instance * @see Math_Matrix::makeUnit() */ function &makeIdentity($size) {/*{{{*/ return Math_Matrix::makeUnit($size); }/*}}}*/ /** * Solves a system of linear equations: Ax = b * * A system such as: *
     *     a11*x1 + a12*x2 + ... + a1n*xn = b1
     *     a21*x1 + a22*x2 + ... + a2n*xn = b2
     *     ...
     *     ak1*x1 + ak2*x2 + ... + akn*xn = bk
     * 
* can be rewritten as: *
     *     Ax = b
     * 
* where: * - A is matrix of coefficients (aij, i=1..k, j=1..n), * - b a vector of values (bi, i=1..k), * - x the vector of unkowns (xi, i=1..n) * Using: x = (Ainv)*b * where: * - Ainv is the inverse of A * * @static * @access public * @param object Math_Matrix $a the matrix of coefficients * @param object Math_Vector $b the vector of values * @return mixed a Math_Vector object on succcess, PEAR_Error otherwise * @see vectorMultiply() */ function solve($a, $b) { // check that the vector classes are defined if (!Math_Matrix::isMatrix($a) && !Math_VectorOp::isVector($b)) { return PEAR::raiseError('Incorrect parameters, expecting a Math_Matrix and a Math_Vector'); } $e = $a->invert(); if (PEAR::isError($e)) { return $e; } return $a->vectorMultiply($b); } /** * Solves a system of linear equations: Ax = b, using an iterative error correction algorithm * * A system such as: *
     *     a11*x1 + a12*x2 + ... + a1n*xn = b1
     *     a21*x1 + a22*x2 + ... + a2n*xn = b2
     *     ...
     *     ak1*x1 + ak2*x2 + ... + akn*xn = bk
     * 
* can be rewritten as: *
     *     Ax = b
     * 
* where: * - A is matrix of coefficients (aij, i=1..k, j=1..n), * - b a vector of values (bi, i=1..k), * - x the vector of unkowns (xi, i=1..n) * Using: x = (Ainv)*b * where: * - Ainv is the inverse of A * * The error correction algorithm uses the approach that if: * - xp is the approximate solution * - bp the values obtained from pluging xp into the original equation * We obtain *
     *     A(x - xp) = (b - bp),
     *     or
     *     A*xadj = (b - bp)
     * 
     * where:
     * - xadj is the adjusted value (= Ainv*(b - bp))
     * therefore, we calculate iteratively new values of x using the estimated
     * xadj and testing to check if we have decreased the error.
     *
     * @static
     * @access public
     * @param object Math_Matrix $a the matrix of coefficients
     * @param object Math_Vector $b the vector of values
     * @return mixed a Math_Vector object on succcess, PEAR_Error otherwise
     * @see vectorMultiply()
     * @see invert()
     * @see Math_VectorOp::add()
     * @see Math_VectorOp::substract()
     * @see Math_VectorOp::length()
     */
    function solveEC($a, $b) {/*{{{*/
        $ainv = $a->clone();
        $e = $ainv->invert();
        if (PEAR::isError($e)) {
            return $e;
        }
        $x = $ainv->vectorMultiply($b);
        if (PEAR::isError($x)) {
            return $x;
        }
        // initial guesses
        $bprime = $a->vectorMultiply($x);
        if (PEAR::isError($bprime)) {
            return $bprime;
        }
        $err = Math_VectorOp::substract($b, $bprime);
        $adj = $ainv->vectorMultiply($err);
        if (PEAR::isError($adj)) {
            return $adj;
        }
        $adjnorm = $adj->length();
        $xnew = $x;
        
        // compute new solutions and test for accuracy
        // iterate no more than 10 times
        for ($i=0; $i<10; $i++) {
            $xnew = Math_VectorOp::add($x, $adj);
            $bprime = $a->vectorMultiply($xnew);
            $err = Math_VectorOp::substract($b, $bprime);
            $newadj = $ainv->vectorMultiply($err);
            $newadjnorm = $newadj->length();
            // did we improve the accuracy?
            if ($newadjnorm < $adjnorm) {
                $adjnorm = $newadjnorm;
                $x = $xnew;
                $adj = $newadj;
            } else { // we did improve the accuracy, so break;
                break;
            }
        }
        return $x;
    }/*}}}*/
   
} // end of Math_Matrix class /*}}}*/

// vim: ts=4:sw=4:et:
// vim6: fdl=1:

?>