// Copyright (c) 2011, Chris Umbel, James Coglan // Matrix class - depends on Vector. var fs = require('fs'); var Sylvester = require('./sylvester'); var Vector = require('./vector'); // augment a matrix M with identity rows/cols function identSize(M, m, n, k) { var e = M.elements; var i = k - 1; while(i--) { var row = []; for(var j = 0; j < n; j++) row.push(j == i ? 1 : 0); e.unshift(row); } for(var i = k - 1; i < m; i++) { while(e[i].length < n) e[i].unshift(0); } return $M(e); } function pca(X) { var Sigma = X.transpose().x(X).x(1 / X.rows()); var svd = Sigma.svd(); return {U: svd.U, S: svd.S}; } function Matrix() {} Matrix.prototype = { pcaProject: function(k, U) { var U = U || pca(this).U; var Ureduce= U.slice(1, U.rows(), 1, k); return {Z: this.x(Ureduce), U: U}; }, pcaRecover: function(U) { var k = this.cols(); var Ureduce = U.slice(1, U.rows(), 1, k); return this.x(Ureduce.transpose()); }, triu: function(k) { if(!k) k = 0; return this.map(function(x, i, j) { return j - i >= k ? x : 0; }); }, svd: function() { var A = this; var U = Matrix.I(A.rows()); var S = A.transpose(); var V = Matrix.I(A.cols()); var err = Number.MAX_VALUE; var i = 0; var maxLoop = 100; while(err > 2.2737e-13 && i < maxLoop) { var qr = S.transpose().qr(); S = qr.R; U = U.x(qr.Q); qr = S.transpose().qr(); V = V.x(qr.Q); S = qr.R; var e = S.triu(1).unroll().norm(); var f = S.diagonal().norm(); if(f == 0) f = 1; err = e / f; i++; } var ss = S.diagonal(); var s = []; for(var i = 1; i <= ss.cols(); i++) { var ssn = ss.e(i); s.push(Math.abs(ssn)); if(ssn < 0) { for(var j = 0; j < U.rows(); j++) { U.elements[j][i - 1] = -(U.elements[j][i - 1]); } } } return {U: U, S: $V(s).toDiagonalMatrix(), V: V}; }, unroll: function() { var v = []; for(var i = 1; i <= this.cols(); i++) { for(var j = 1; j <= this.rows(); j++) { v.push(this.e(j, i)); } } return $V(v); }, qr: function() { var m = this.rows(); var n = this.cols(); var Q = Matrix.I(m); var A = this; for(var k = 1; k < Math.min(m, n); k++) { var ak = A.slice(k, 0, k, k).col(1); var oneZero = [1]; while(oneZero.length <= m - k) oneZero.push(0); oneZero = $V(oneZero); var vk = ak.add(oneZero.x(ak.norm() * Math.sign(ak.e(1)))); var Vk = $M(vk); var Hk = Matrix.I(m - k + 1).subtract(Vk.x(2).x(Vk.transpose()).div(Vk.transpose().x(Vk).e(1, 1))); var Qk = identSize(Hk, m, n, k); A = Qk.x(A); Q = Q.x(Qk); } return {Q: Q, R: A}; }, slice: function(startRow, endRow, startCol, endCol) { var x = []; if(endRow == 0) endRow = this.rows(); if(endCol == 0) endCol = this.cols(); for(i = startRow; i <= endRow; i++) { var row = []; for(j = startCol; j <= endCol; j++) { row.push(this.e(i, j)); } x.push(row); } return $M(x); }, // Returns element (i,j) of the matrix e: function(i,j) { if (i < 1 || i > this.elements.length || j < 1 || j > this.elements[0].length) { return null; } return this.elements[i - 1][j - 1]; }, // Returns row k of the matrix as a vector row: function(i) { if (i > this.elements.length) { return null; } return $V(this.elements[i - 1]); }, // Returns column k of the matrix as a vector col: function(j) { if (j > this.elements[0].length) { return null; } var col = [], n = this.elements.length; for (var i = 0; i < n; i++) { col.push(this.elements[i][j - 1]); } return $V(col); }, // Returns the number of rows/columns the matrix has dimensions: function() { return {rows: this.elements.length, cols: this.elements[0].length}; }, // Returns the number of rows in the matrix rows: function() { return this.elements.length; }, // Returns the number of columns in the matrix cols: function() { return this.elements[0].length; }, // Returns true iff the matrix is equal to the argument. You can supply // a vector as the argument, in which case the receiver must be a // one-column matrix equal to the vector. eql: function(matrix) { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } if (this.elements.length != M.length || this.elements[0].length != M[0].length) { return false; } var i = this.elements.length, nj = this.elements[0].length, j; while (i--) { j = nj; while (j--) { if (Math.abs(this.elements[i][j] - M[i][j]) > Sylvester.precision) { return false; } } } return true; }, // Returns a copy of the matrix dup: function() { return Matrix.create(this.elements); }, // Maps the matrix to another matrix (of the same dimensions) according to the given function map: function(fn) { var els = [], i = this.elements.length, nj = this.elements[0].length, j; while (i--) { j = nj; els[i] = []; while (j--) { els[i][j] = fn(this.elements[i][j], i + 1, j + 1); } } return Matrix.create(els); }, // Returns true iff the argument has the same dimensions as the matrix isSameSizeAs: function(matrix) { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } return (this.elements.length == M.length && this.elements[0].length == M[0].length); }, // Returns the result of adding the argument to the matrix add: function(matrix) { if(typeof(matrix) == 'number') { return this.map(function(x, i, j) { return x + matrix}); } else { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } if (!this.isSameSizeAs(M)) { return null; } return this.map(function(x, i, j) { return x + M[i - 1][j - 1]; }); } }, // Returns the result of subtracting the argument from the matrix subtract: function(matrix) { if(typeof(matrix) == 'number') { return this.map(function(x, i, j) { return x - matrix}); } else { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } if (!this.isSameSizeAs(M)) { return null; } return this.map(function(x, i, j) { return x - M[i - 1][j - 1]; }); } }, // Returns true iff the matrix can multiply the argument from the left canMultiplyFromLeft: function(matrix) { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } // this.columns should equal matrix.rows return (this.elements[0].length == M.length); }, // Returns the result of a multiplication-style operation the matrix from the right by the argument. // If the argument is a scalar then just operate on all the elements. If the argument is // a vector, a vector is returned, which saves you having to remember calling // col(1) on the result. mulOp: function(matrix, op) { if (!matrix.elements) { return this.map(function(x) { return op(x, matrix); }); } var returnVector = matrix.modulus ? true : false; var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') M = Matrix.create(M).elements; if (!this.canMultiplyFromLeft(M)) return null; var e = this.elements, rowThis, rowElem, elements = [], sum, m = e.length, n = M[0].length, o = e[0].length, i = m, j, k; while (i--) { rowElem = []; rowThis = e[i]; j = n; while (j--) { sum = 0; k = o; while (k--) { sum += op(rowThis[k], M[k][j]); } rowElem[j] = sum; } elements[i] = rowElem; } var M = Matrix.create(elements); return returnVector ? M.col(1) : M; }, // Returns the result of dividing the matrix from the right by the argument. // If the argument is a scalar then just divide all the elements. If the argument is // a vector, a vector is returned, which saves you having to remember calling // col(1) on the result. div: function(matrix) { return this.mulOp(matrix, function(x, y) { return x / y}); }, // Returns the result of multiplying the matrix from the right by the argument. // If the argument is a scalar then just multiply all the elements. If the argument is // a vector, a vector is returned, which saves you having to remember calling // col(1) on the result. multiply: function(matrix) { return this.mulOp(matrix, function(x, y) { return x * y}); }, x: function(matrix) { return this.multiply(matrix); }, elementMultiply: function(v) { return this.map(function(k, i, j) { return v.e(i, j) * k; }); }, sum: function() { var sum = 0; this.map(function(x) { sum += x;}); return sum; }, // Returns a Vector of each colum averaged. mean: function() { var dim = this.dimensions(); var r = []; for (var i = 1; i <= dim.cols; i++) { r.push(this.col(i).sum() / dim.rows); } return $V(r); }, column: function(n) { return this.col(n); }, log: function() { return this.map(function(x) { return Math.log(x); }); }, // Returns a submatrix taken from the matrix // Argument order is: start row, start col, nrows, ncols // Element selection wraps if the required index is outside the matrix's bounds, so you could // use this to perform row/column cycling or copy-augmenting. minor: function(a, b, c, d) { var elements = [], ni = c, i, nj, j; var rows = this.elements.length, cols = this.elements[0].length; while (ni--) { i = c - ni - 1; elements[i] = []; nj = d; while (nj--) { j = d - nj - 1; elements[i][j] = this.elements[(a + i - 1) % rows][(b + j - 1) % cols]; } } return Matrix.create(elements); }, // Returns the transpose of the matrix transpose: function() { var rows = this.elements.length, i, cols = this.elements[0].length, j; var elements = [], i = cols; while (i--) { j = rows; elements[i] = []; while (j--) { elements[i][j] = this.elements[j][i]; } } return Matrix.create(elements); }, // Returns true iff the matrix is square isSquare: function() { return (this.elements.length == this.elements[0].length); }, // Returns the (absolute) largest element of the matrix max: function() { var m = 0, i = this.elements.length, nj = this.elements[0].length, j; while (i--) { j = nj; while (j--) { if (Math.abs(this.elements[i][j]) > Math.abs(m)) { m = this.elements[i][j]; } } } return m; }, // Returns the indeces of the first match found by reading row-by-row from left to right indexOf: function(x) { var index = null, ni = this.elements.length, i, nj = this.elements[0].length, j; for (i = 0; i < ni; i++) { for (j = 0; j < nj; j++) { if (this.elements[i][j] == x) { return {i: i + 1, j: j + 1}; } } } return null; }, // If the matrix is square, returns the diagonal elements as a vector. // Otherwise, returns null. diagonal: function() { if (!this.isSquare) { return null; } var els = [], n = this.elements.length; for (var i = 0; i < n; i++) { els.push(this.elements[i][i]); } return $V(els); }, // Make the matrix upper (right) triangular by Gaussian elimination. // This method only adds multiples of rows to other rows. No rows are // scaled up or switched, and the determinant is preserved. toRightTriangular: function() { var M = this.dup(), els; var n = this.elements.length, i, j, np = this.elements[0].length, p; for (i = 0; i < n; i++) { if (M.elements[i][i] == 0) { for (j = i + 1; j < n; j++) { if (M.elements[j][i] != 0) { els = []; for (p = 0; p < np; p++) { els.push(M.elements[i][p] + M.elements[j][p]); } M.elements[i] = els; break; } } } if (M.elements[i][i] != 0) { for (j = i + 1; j < n; j++) { var multiplier = M.elements[j][i] / M.elements[i][i]; els = []; for (p = 0; p < np; p++) { // Elements with column numbers up to an including the number // of the row that we're subtracting can safely be set straight to // zero, since that's the point of this routine and it avoids having // to loop over and correct rounding errors later els.push(p <= i ? 0 : M.elements[j][p] - M.elements[i][p] * multiplier); } M.elements[j] = els; } } } return M; }, toUpperTriangular: function() { return this.toRightTriangular(); }, // Returns the determinant for square matrices determinant: function() { if (!this.isSquare()) { return null; } if (this.cols == 1 && this.rows == 1) { return this.row(1); } if (this.cols == 0 && this.rows == 0) { return 1; } var M = this.toRightTriangular(); var det = M.elements[0][0], n = M.elements.length; for (var i = 1; i < n; i++) { det = det * M.elements[i][i]; } return det; }, det: function() { return this.determinant(); }, // Returns true iff the matrix is singular isSingular: function() { return (this.isSquare() && this.determinant() === 0); }, // Returns the trace for square matrices trace: function() { if (!this.isSquare()) { return null; } var tr = this.elements[0][0], n = this.elements.length; for (var i = 1; i < n; i++) { tr += this.elements[i][i]; } return tr; }, tr: function() { return this.trace(); }, // Returns the rank of the matrix rank: function() { var M = this.toRightTriangular(), rank = 0; var i = this.elements.length, nj = this.elements[0].length, j; while (i--) { j = nj; while (j--) { if (Math.abs(M.elements[i][j]) > Sylvester.precision) { rank++; break; } } } return rank; }, rk: function() { return this.rank(); }, // Returns the result of attaching the given argument to the right-hand side of the matrix augment: function(matrix) { var M = matrix.elements || matrix; if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; } var T = this.dup(), cols = T.elements[0].length; var i = T.elements.length, nj = M[0].length, j; if (i != M.length) { return null; } while (i--) { j = nj; while (j--) { T.elements[i][cols + j] = M[i][j]; } } return T; }, // Returns the inverse (if one exists) using Gauss-Jordan inverse: function() { if (!this.isSquare() || this.isSingular()) { return null; } var n = this.elements.length, i = n, j; var M = this.augment(Matrix.I(n)).toRightTriangular(); var np = M.elements[0].length, p, els, divisor; var inverse_elements = [], new_element; // Matrix is non-singular so there will be no zeros on the diagonal // Cycle through rows from last to first while (i--) { // First, normalise diagonal elements to 1 els = []; inverse_elements[i] = []; divisor = M.elements[i][i]; for (p = 0; p < np; p++) { new_element = M.elements[i][p] / divisor; els.push(new_element); // Shuffle off the current row of the right hand side into the results // array as it will not be modified by later runs through this loop if (p >= n) { inverse_elements[i].push(new_element); } } M.elements[i] = els; // Then, subtract this row from those above it to // give the identity matrix on the left hand side j = i; while (j--) { els = []; for (p = 0; p < np; p++) { els.push(M.elements[j][p] - M.elements[i][p] * M.elements[j][i]); } M.elements[j] = els; } } return Matrix.create(inverse_elements); }, inv: function() { return this.inverse(); }, // Returns the result of rounding all the elements round: function() { return this.map(function(x) { return Math.round(x); }); }, // Returns a copy of the matrix with elements set to the given value if they // differ from it by less than Sylvester.precision snapTo: function(x) { return this.map(function(p) { return (Math.abs(p - x) <= Sylvester.precision) ? x : p; }); }, // Returns a string representation of the matrix inspect: function() { var matrix_rows = []; var n = this.elements.length; for (var i = 0; i < n; i++) { matrix_rows.push($V(this.elements[i]).inspect()); } return matrix_rows.join('\n'); }, // Returns a array representation of the matrix toArray: function() { var matrix_rows = []; var n = this.elements.length; for (var i = 0; i < n; i++) { matrix_rows.push(this.elements[i]); } return matrix_rows; }, // Set the matrix's elements from an array. If the argument passed // is a vector, the resulting matrix will be a single column. setElements: function(els) { var i, j, elements = els.elements || els; if (typeof(elements[0][0]) != 'undefined') { i = elements.length; this.elements = []; while (i--) { j = elements[i].length; this.elements[i] = []; while (j--) { this.elements[i][j] = elements[i][j]; } } return this; } var n = elements.length; this.elements = []; for (i = 0; i < n; i++) { this.elements.push([elements[i]]); } return this; }, maxColumnIndexes: function() { var maxes = []; for(var i = 1; i <= this.rows(); i++) { var max = null; var maxIndex = -1; for(var j = 1; j <= this.cols(); j++) { if(max === null || this.e(i, j) > max) { max = this.e(i, j); maxIndex = j; } } maxes.push(maxIndex); } return $V(maxes); }, maxColumns: function() { var maxes = []; for(var i = 1; i <= this.rows(); i++) { var max = null; for(var j = 1; j <= this.cols(); j++) { if(max === null || this.e(i, j) > max) { max = this.e(i, j); } } maxes.push(max); } return $V(maxes); }, minColumnIndexes: function() { var mins = []; for(var i = 1; i <= this.rows(); i++) { var min = null; var minIndex = -1; for(var j = 1; j <= this.cols(); j++) { if(min === null || this.e(i, j) < min) { min = this.e(i, j); minIndex = j; } } mins.push(minIndex); } return $V(mins); }, minColumns: function() { var mins = []; for(var i = 1; i <= this.rows(); i++) { var min = null; for(var j = 1; j <= this.cols(); j++) { if(min === null || this.e(i, j) < min) { min = this.e(i, j); } } mins.push(min); } return $V(mins); } }; // Constructor function Matrix.create = function(elements) { var M = new Matrix(); return M.setElements(elements); }; // Identity matrix of size n Matrix.I = function(n) { var els = [], i = n, j; while (i--) { j = n; els[i] = []; while (j--) { els[i][j] = (i == j) ? 1 : 0; } } return Matrix.create(els); }; Matrix.loadFile = function(file) { var contents = fs.readFileSync(file, 'utf-8'); var matrix = []; var rowArray = contents.split('\n'); for (var i = 0; i < rowArray.length; i++) { var d = rowArray[i].split(','); if (d.length > 1) { matrix.push(d); } } var M = new Matrix(); return M.setElements(matrix); }; // Diagonal matrix - all off-diagonal elements are zero Matrix.Diagonal = function(elements) { var i = elements.length; var M = Matrix.I(i); while (i--) { M.elements[i][i] = elements[i]; } return M; }; // Rotation matrix about some axis. If no axis is // supplied, assume we're after a 2D transform Matrix.Rotation = function(theta, a) { if (!a) { return Matrix.create([ [Math.cos(theta), -Math.sin(theta)], [Math.sin(theta), Math.cos(theta)] ]); } var axis = a.dup(); if (axis.elements.length != 3) { return null; } var mod = axis.modulus(); var x = axis.elements[0] / mod, y = axis.elements[1] / mod, z = axis.elements[2] / mod; var s = Math.sin(theta), c = Math.cos(theta), t = 1 - c; // Formula derived here: http://www.gamedev.net/reference/articles/article1199.asp // That proof rotates the co-ordinate system so theta // becomes -theta and sin becomes -sin here. return Matrix.create([ [t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c] ]); }; // Special case rotations Matrix.RotationX = function(t) { var c = Math.cos(t), s = Math.sin(t); return Matrix.create([ [1, 0, 0], [0, c, -s], [0, s, c] ]); }; Matrix.RotationY = function(t) { var c = Math.cos(t), s = Math.sin(t); return Matrix.create([ [c, 0, s], [0, 1, 0], [-s, 0, c] ]); }; Matrix.RotationZ = function(t) { var c = Math.cos(t), s = Math.sin(t); return Matrix.create([ [c, -s, 0], [s, c, 0], [0, 0, 1] ]); }; // Random matrix of n rows, m columns Matrix.Random = function(n, m) { if (arguments.length === 1) m = n; return Matrix.Zero(n, m).map( function() { return Math.random(); } ); }; Matrix.Fill = function(n, m, v) { if (arguments.length === 2) { v = m; m = n; } var els = [], i = n, j; while (i--) { j = m; els[i] = []; while (j--) { els[i][j] = v; } } return Matrix.create(els); }; // Matrix filled with zeros Matrix.Zero = function(n, m) { return Matrix.Fill(n, m, 0); }; // Matrix filled with zeros Matrix.Zeros = function(n, m) { return Matrix.Zero(n, m); }; // Matrix filled with ones Matrix.One = function(n, m) { return Matrix.Fill(n, m, 1); }; // Matrix filled with ones Matrix.Ones = function(n, m) { return Matrix.One(n, m); }; module.exports = Matrix;