hazdev-webutils
Version:
Utilities commonly used in web applications developed by the EHP HazDev team.
663 lines (608 loc) • 14.6 kB
JavaScript
;
var Vector = require('./Vector');
// static methods that operate on arrays
var __col,
__diagonal,
__get,
__identity,
__index,
__jacobi,
__multiply,
__row,
__set,
__stringify,
__transpose;
/**
* Extract a column from this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param col {Number}
* index of column, in range [0,n)
* @throws Error if column out of range.
* @return {Array<Number>} column elements.
*/
__col = function (data, m, n, col) {
var row,
values = [];
if (col < 0 || col >= n) {
throw new Error('column ' + col + ' out of range [0,' + n + ')');
}
if (n === 1) {
// only one column in matrix
return data;
}
values = [];
for (row = 0; row < m; row++) {
values.push(data[__index(m, n, row, col)]);
}
return values;
};
/**
* Get array of elements on the diagonal.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @return {Array<Number>} elements on the diagonal.
*/
__diagonal = function (data, m, n) {
var len = Math.min(m, n),
diag = [],
i;
for (i = 0; i < len; i++) {
diag.push(data[__index(m, n, i, i)]);
}
return diag;
};
/**
* Get the value of an element of this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param row {Number}
* row of element, in range [0,m)
* @param col {Number}
* column of element, in range [0,n)
* @throws Error if row or col are out of range.
* @return {Number} value.
*/
__get = function (data, m, n, row, col) {
return data[__index(m, n, row, col)];
};
/**
* Create an identity Matrix.
*
* @param n {Number}
* number of rows and columns.
* @return identity matrix of size n.
*/
__identity = function (n) {
var values = [],
row,
col;
for (row = 0; row < n; row++) {
for (col = 0; col < n; col++) {
values.push((row === col) ? 1 : 0);
}
}
return values;
};
/**
* Get the index of an element of this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param row {Number}
* row of element, in range [0,m)
* @param col {Number}
* column of element, in range [0,n)
* @return {Number} index.
*/
__index = function (m, n, row, col) {
return n * row + col;
};
/**
* Jacobi eigenvalue algorithm.
*
* Ported from:
* http://users-phys.au.dk/fedorov/nucltheo/Numeric/now/eigen.pdf
*
* An iterative method for eigenvalues and eigenvectors,
* only works on symmetric matrices.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param maxRotations {Number}
* maximum number of rotations.
* Optional, default 100.
* @return {Array<Vector>} array of eigenvectors, magnitude is eigenvalue.
*/
__jacobi = function (data, m, n, maxRotations) {
var a,
aip,
aiq,
api,
app,
app1,
apq,
aqi,
aqq,
aqq1,
c,
changed,
e,
i,
ip,
iq,
p,
phi,
pi,
q,
qi,
rotations,
s,
v,
vector,
vectors,
vip,
viq;
if (m !== n) {
throw new Error('Jacobi only works on symmetric, square matrices');
}
// set a default max
maxRotations = maxRotations || 100;
a = data.slice(0);
e = __diagonal(data, m, n);
v = __identity(n);
rotations = 0;
do {
changed = false;
for (p=0; p<n; p++) {
for (q=p+1; q<n; q++) {
app = e[p];
aqq = e[q];
apq = a[n * p + q];
phi = 0.5 * Math.atan2(2 * apq, aqq - app);
c = Math.cos(phi);
s = Math.sin(phi);
app1 = c * c * app - 2 * s * c * apq + s * s * aqq;
aqq1 = s * s * app + 2 * s * c * apq + c * c * aqq;
if (app1 !== app || aqq1 !== aqq) {
changed = true;
rotations++;
e[p] = app1;
e[q] = aqq1;
a[n * p + q] = 0;
for (i = 0; i < p; i++) {
ip = n * i + p;
iq = n * i + q;
aip = a[ip];
aiq = a[iq];
a[ip] = c * aip - s * aiq;
a[iq] = c * aiq + s * aip;
}
for (i = p + 1; i < q; i++) {
pi = n * p + i;
iq = n * i + q;
api = a[pi];
aiq = a[iq];
a[pi] = c * api - s * aiq;
a[iq] = c * aiq + s * api;
}
for (i = q + 1; i < n; i++) {
pi = n * p + i;
qi = n * q + i;
api = a[pi];
aqi = a[qi];
a[pi] = c * api - s * aqi;
a[qi] = c * aqi + s * api;
}
for (i = 0; i < n; i++) {
ip = n * i + p;
iq = n * i + q;
vip = v[ip];
viq = v[iq];
v[ip] = c * vip - s * viq;
v[iq] = c * viq + s * vip;
}
}
}
}
} while (changed && (rotations < maxRotations));
if (changed) {
throw new Error('failed to converge');
}
vectors = [];
for (i = 0; i < n; i++) {
// i-th vector is i-th column
vector = Vector(__col(v, m, n, i));
vector.eigenvalue = e[i];
vectors.push(vector);
}
return vectors;
};
/**
* Multiply this matrix by another matrix.
*
* @param data1 {Array<Number>}
* first matrix data.
* @param m1 {Number}
* number of rows in first matrix.
* @param n1 {Number}
* number of columns in first matrix.
* @param data2 {Array<Number>}
* second matrix data.
* @param m2 {Number}
* number of rows in second matrix.
* @param n2 {Number}
* number of columns in second matrix.
* @throws Error if n1 !== m2
* @return result of multiplication (original matrix is unchanged).
*/
__multiply = function (data1, m1, n1, data2, m2, n2) {
var col,
col2,
row,
row1,
values;
if (n1 !== m2) {
throw new Error('wrong combination of rows and cols');
}
values = [];
for (row = 0; row < m1; row++) {
row1 = __row(data1, m1, n1, row);
for (col = 0; col < n2; col++) {
col2 = __col(data2, m2, n2, col);
// result is dot product
values.push(Vector.dot(row1, col2));
}
}
return values;
};
/**
* Extract a row from this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param row {Number}
* index of row, in range [0,m)
* @throws Error if row out of range.
* @return {Array<Number>} row elements.
*/
__row = function (data, m, n, row) {
var col,
values;
if (row < 0 || row >= m) {
throw new Error('row ' + row + ' out of range [0,' + m + ')');
}
values = [];
for (col = 0; col < n; col++) {
values.push(data[__index(m, n, row, col)]);
}
return values;
};
/**
* Set the value of an element of this matrix.
*
* NOTE: this method modifies the contents of this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @param row {Number}
* row of element, in range [0,m)
* @param col {Number}
* column of element, in range [0,n)
* @param value {Number}
* value to set.
* @throws Error if row or col are out of range.
*/
__set = function (data, m, n, row, col, value) {
data[__index(m, n, row, col)] = value;
};
/**
* Display matrix as a string.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @return {String} formatted matrix.
*/
__stringify = function (data, m, n) {
var lastRow = m - 1,
lastCol = n - 1,
buf = [],
row,
col;
buf.push('[');
for (row = 0; row < m; row++) {
for (col = 0; col < n; col++) {
buf.push(
data[n * row + col],
(col !== lastCol || row !== lastRow) ? ', ' : '');
}
if (row !== lastRow) {
buf.push('\n ');
}
}
buf.push(']');
return buf.join('');
};
/**
* Transpose this matrix.
*
* @param data {Array<Number>}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
* @return transposed matrix (original matrix is unchanged).
*/
__transpose = function (data, m, n) {
var values = [],
row,
col;
for (col = 0; col < n; col++) {
for (row = 0; row < m; row++) {
values.push(data[__index(m, n, row, col)]);
}
}
return values;
};
/**
* Construct a new Matrix object.
*
* If m and n are omitted, Matrix is assumed to be square and
* data length is used to compute size.
*
* If m or n are omitted, data length is used to compute omitted value.
*
* @param data {Array}
* matrix data.
* @param m {Number}
* number of rows.
* @param n {Number}
* number of columns.
*/
var Matrix = function (data, m, n) {
var _this,
_initialize,
// variables
_data,
_m,
_n;
_this = {};
_initialize = function (data, m, n) {
_data = data;
_m = m;
_n = n;
if (m && n) {
// done
return;
}
// try to compute size based on data
if (!m && !n) {
var side = Math.sqrt(data.length);
if (side !== parseInt(side, 10)) {
throw new Error('matrix m,n unspecified, and matrix not square');
}
_m = side;
_n = side;
} else if (!m) {
_m = data.length / n;
if (_m !== parseInt(_m, 10)) {
throw new Error('wrong number of data elements');
}
} else if (!n) {
_n = data.length / m;
if (_n !== parseInt(_n, 10)) {
throw new Error('wrong number of data elements');
}
}
};
/**
* Add matrices.
*
* @param that {Matrix}
* matrix to add.
* @throws Error if dimensions do not match.
* @return result of addition (original matrix is unchanged).
*/
_this.add = function (that) {
if (_m !== that.m() || n !== that.n()) {
throw new Error('matrices must be same size');
}
return Matrix(Vector.add(_data, that.data()), _m, _n);
};
/**
* Get a column from this matrix.
*
* @param col {Number}
* zero-based column index.
* @return {Array<Number>} array containing elements from column.
*/
_this.col = function (col) {
return __col(_data, _m, _n, col);
};
/**
* Access the wrapped array.
*/
_this.data = function () {
return _data;
};
/**
* Get the diagonal from this matrix.
*
* @return {Array<Number>} array containing elements from diagonal.
*/
_this.diagonal = function () {
return __diagonal(_data, _m, _n);
};
/**
* Get a value from this matrix.
*
* @param row {Number}
* zero-based index of row.
* @param col {Number}
* zero-based index of column.
* @return {Number} value at (row, col).
*/
_this.get = function (row, col) {
return __get(_data, _m, _n, row, col);
};
/**
* Compute the eigenvectors of this matrix.
*
* NOTE: Matrix should be 3x3 and symmetric.
*
* @param maxRotations {Number}
* default 100.
* maximum number of iterations.
* @return {Array<Vector>} eigenvectors.
* Magnitude of each vector is eigenvalue.
*/
_this.jacobi = function (maxRotations) {
return __jacobi(_data, _m, _n, maxRotations);
};
/**
* Get the number of rows in matrix.
*
* @return {Number}
* number of rows.
*/
_this.m = function () {
return _m;
};
/**
* Multiply matrices.
*
* @param that {Matrix}
* matrix to multiply.
* @return {Matrix} result of multiplication.
*/
_this.multiply = function (that) {
return Matrix(__multiply(_data, _m, _n, that.data(), that.m(), that.n()),
// use that.N
_m, that.n());
};
/**
* Get number of columns in matrix.
*
* @return {Number} number of columns.
*/
_this.n = function () {
return _n;
};
/**
* Multiply each element by -1.
*
* @return {Matrix} result of negation.
*/
_this.negative = function () {
return Matrix(Vector.multiply(_data, -1), _m, _n);
};
/**
* Get a row from this matrix.
*
* @param row {Number}
* zero-based index of row.
* @return {Array<Number>} elements from row.
*/
_this.row = function (row) {
return __row(_data, _m, _n, row);
};
/**
* Set a value in this matrix.
*
* @param row {Number}
* zero-based row index.
* @param col {Number}
* zero-based column index.
* @param value {Number}
* value to set.
*/
_this.set = function (row, col, value) {
__set(_data, _m, _n, row, col, value);
};
/**
* Subtract another matrix from this matrix.
*
* @param that {Matrix}
* matrix to subtract.
* @throws Error if dimensions do not match.
* @return result of subtraction (original matrix is unchanged).
*/
_this.subtract = function (that) {
if (_m !== that.m() || n !== that.n()) {
throw new Error('matrices must be same size');
}
return Matrix(Vector.subtract(_data, that.data()), _m, _n);
};
/**
* Display matrix as a string.
*
* @return {String} formatted matrix.
*/
_this.toString = function () {
return __stringify(_data, _m, _n);
};
/**
* Transpose matrix.
*
* Columns become rows, and rows become columns.
*
* @return {Matrix} result of transpose.
*/
_this.transpose = function () {
return Matrix(__transpose(_data, _m, _n),
// swap M and N
_n, _m);
};
_initialize(data, m, n);
data = null;
return _this;
};
// expose static methods.
Matrix.col = __col;
Matrix.diagonal = __diagonal;
Matrix.get = __get;
Matrix.identity = __identity;
Matrix.index = __index;
Matrix.jacobi = __jacobi;
Matrix.multiply = __multiply;
Matrix.row = __row;
Matrix.set = __set;
Matrix.stringify = __stringify;
Matrix.transpose = __transpose;
module.exports = Matrix;