UNPKG

mathjs

Version:

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser and offers an integrated solution to work with numbers, big numbers, complex numbers, units, and matrices.

1,057 lines (961 loc) 30.8 kB
'use strict'; var util = require('../../util/index'); var DimensionError = require('../../error/DimensionError'); var string = util.string; var array = util.array; var object = util.object; var isArray = Array.isArray; var isNumber = util.number.isNumber; var isInteger = util.number.isInteger; var validateIndex = array.validateIndex; module.exports = function (math) { var Index = math.type.Index, BigNumber = math.type.BigNumber, Matrix = math.type.Matrix; function DenseMatrix(data) { if (!(this instanceof DenseMatrix)) throw new SyntaxError('Constructor must be called with the new operator'); if (data instanceof Matrix) { // check data is a DenseMatrix if (data.type === 'DenseMatrix') { // clone data & size this._data = object.clone(data._data); this._size = object.clone(data._size); } else { // build data from existing matrix this._data = data.toArray(); this._size = data.size(); } } else if (data && isArray(data.data) && isArray(data.size)) { // initialize fields from JSON representation this._data = data.data; this._size = data.size; } else if (isArray(data)) { // replace nested Matrices with Arrays this._data = preprocess(data); // verify the size of the array, TODO: compute size while processing array this._size = array.size(this._data); } else if (data) { // unsupported type throw new TypeError('Unsupported type of data (' + util.types.type(data) + ')'); } else { // nothing provided this._data = []; this._size = [0]; } } DenseMatrix.prototype = new math.type.Matrix(); DenseMatrix.prototype.type = 'DenseMatrix'; /** * Get the storage format used by the matrix. * * Usage: * var format = matrix.storage() // retrieve storage format * * @return {string} The storage format. */ DenseMatrix.prototype.storage = function () { return 'dense'; }; /** * Get a subset of the matrix, or replace a subset of the matrix. * * Usage: * var subset = matrix.subset(index) // retrieve subset * var value = matrix.subset(index, replacement) // replace subset * * @param {Index} index * @param {Array | DenseMatrix | *} [replacement] * @param {*} [defaultValue=0] Default value, filled in on new entries when * the matrix is resized. If not provided, * new matrix elements will be filled with zeros. */ DenseMatrix.prototype.subset = function (index, replacement, defaultValue) { switch (arguments.length) { case 1: return _get(this, index); // intentional fall through case 2: case 3: return _set(this, index, replacement, defaultValue); default: throw new SyntaxError('Wrong number of arguments'); } }; /** * Get a single element from the matrix. * @param {Number[]} index Zero-based index * @return {*} value */ DenseMatrix.prototype.get = function (index) { if (!isArray(index)) throw new TypeError('Array expected'); if (index.length != this._size.length) throw new DimensionError(index.length, this._size.length); // check index for (var x = 0; x < index.length; x++) validateIndex(index[x], this._size[x]); var data = this._data; for (var i = 0, ii = index.length; i < ii; i++) { var index_i = index[i]; validateIndex(index_i, data.length); data = data[index_i]; } return object.clone(data); }; /** * Replace a single element in the matrix. * @param {Number[]} index Zero-based index * @param {*} value * @param {*} [defaultValue] Default value, filled in on new entries when * the matrix is resized. If not provided, * new matrix elements will be left undefined. * @return {DenseMatrix} self */ DenseMatrix.prototype.set = function (index, value, defaultValue) { if (!isArray(index)) throw new TypeError('Array expected'); if (index.length < this._size.length) throw new DimensionError(index.length, this._size.length, '<'); var i, ii, index_i; // enlarge matrix when needed var size = index.map(function (i) { return i + 1; }); _fit(this, size, defaultValue); // traverse over the dimensions var data = this._data; for (i = 0, ii = index.length - 1; i < ii; i++) { index_i = index[i]; validateIndex(index_i, data.length); data = data[index_i]; } // set new value index_i = index[index.length - 1]; validateIndex(index_i, data.length); data[index_i] = value; return this; }; /** * Get a submatrix of this matrix * @param {DenseMatrix} matrix * @param {Index} index Zero-based index * @private */ function _get (matrix, index) { if (!(index instanceof Index)) { throw new TypeError('Invalid index'); } var isScalar = index.isScalar(); if (isScalar) { // return a scalar return matrix.get(index.min()); } else { // validate dimensions var size = index.size(); if (size.length != matrix._size.length) { throw new DimensionError(size.length, matrix._size.length); } // validate if any of the ranges in the index is out of range var min = index.min(); var max = index.max(); for (var i = 0, ii = matrix._size.length; i < ii; i++) { validateIndex(min[i], matrix._size[i]); validateIndex(max[i], matrix._size[i]); } // retrieve submatrix // TODO: more efficient when creating an empty matrix and setting _data and _size manually return new DenseMatrix(_getSubmatrix(matrix._data, index, size.length, 0)); } } /** * Recursively get a submatrix of a multi dimensional matrix. * Index is not checked for correct number or length of dimensions. * @param {Array} data * @param {Index} index * @param {number} dims Total number of dimensions * @param {number} dim Current dimension * @return {Array} submatrix * @private */ function _getSubmatrix (data, index, dims, dim) { var last = (dim == dims - 1); var range = index.range(dim); if (last) { return range.map(function (i) { return data[i]; }); } else { return range.map(function (i) { var child = data[i]; return _getSubmatrix(child, index, dims, dim + 1); }); } } /** * Replace a submatrix in this matrix * Indexes are zero-based. * @param {DenseMatrix} matrix * @param {Index} index * @param {DenseMatrix | Array | *} submatrix * @param {*} defaultValue Default value, filled in on new entries when * the matrix is resized. * @return {DenseMatrix} matrix * @private */ function _set (matrix, index, submatrix, defaultValue) { if (!(index instanceof Index)) { throw new TypeError('Invalid index'); } // get index size and check whether the index contains a single value var iSize = index.size(), isScalar = index.isScalar(); // calculate the size of the submatrix, and convert it into an Array if needed var sSize; if (submatrix instanceof math.type.Matrix) { sSize = submatrix.size(); submatrix = submatrix.valueOf(); } else { sSize = array.size(submatrix); } if (isScalar) { // set a scalar // check whether submatrix is a scalar if (sSize.length !== 0) { throw new TypeError('Scalar expected'); } matrix.set(index.min(), submatrix, defaultValue); } else { // set a submatrix // validate dimensions if (iSize.length < matrix._size.length) { throw new DimensionError(iSize.length, matrix._size.length, '<'); } if (sSize.length < iSize.length) { // calculate number of missing outer dimensions var i = 0; var outer = 0; while (iSize[i] === 1 && sSize[i] === 1) { i++; } while (iSize[i] === 1) { outer++; i++; } // unsqueeze both outer and inner dimensions submatrix = array.unsqueeze(submatrix, iSize.length, outer, sSize); } // check whether the size of the submatrix matches the index size if (!object.deepEqual(iSize, sSize)) { throw new DimensionError(iSize, sSize, '>'); } // enlarge matrix when needed var size = index.max().map(function (i) { return i + 1; }); _fit(matrix, size, defaultValue); // insert the sub matrix var dims = iSize.length, dim = 0; _setSubmatrix (matrix._data, index, submatrix, dims, dim); } return matrix; } /** * Replace a submatrix of a multi dimensional matrix. * @param {Array} data * @param {Index} index * @param {Array} submatrix * @param {number} dims Total number of dimensions * @param {number} dim * @private */ function _setSubmatrix (data, index, submatrix, dims, dim) { var last = (dim == dims - 1), range = index.range(dim); if (last) { range.forEach(function (dataIndex, subIndex) { validateIndex(dataIndex); data[dataIndex] = submatrix[subIndex]; }); } else { range.forEach(function (dataIndex, subIndex) { validateIndex(dataIndex); _setSubmatrix(data[dataIndex], index, submatrix[subIndex], dims, dim + 1); }); } } /** * Resize the matrix to the given size. Returns a copy of the matrix when * `copy=true`, otherwise return the matrix itself (resize in place). * * @param {Number[]} size The new size the matrix should have. * @param {*} [defaultValue=0] Default value, filled in on new entries. * If not provided, the matrix elements will * be filled with zeros. * @param {boolean} [copy] Return a resized copy of the matrix * * @return {Matrix} The resized matrix */ DenseMatrix.prototype.resize = function (size, defaultValue, copy) { // validate arguments if (!isArray(size)) throw new TypeError('Array expected'); // matrix to resize var m = copy ? this.clone() : this; // resize matrix return _resize(m, size, defaultValue); }; var _resize = function (matrix, size, defaultValue) { // check size if (size.length === 0) { // first value in matrix var v = matrix._data; // go deep while (isArray(v)) { v = v[0]; } return object.clone(v); } // resize matrix matrix._size = object.clone(size); matrix._data = array.resize(matrix._data, matrix._size, defaultValue); // return matrix return matrix; }; /** * Enlarge the matrix when it is smaller than given size. * If the matrix is larger or equal sized, nothing is done. * @param {DenseMatrix} matrix The matrix to be resized * @param {Number[]} size * @param {*} defaultValue Default value, filled in on new entries. * @private */ function _fit(matrix, size, defaultValue) { var newSize = object.clone(matrix._size), changed = false; // add dimensions when needed while (newSize.length < size.length) { newSize.push(0); changed = true; } // enlarge size when needed for (var i = 0, ii = size.length; i < ii; i++) { if (size[i] > newSize[i]) { newSize[i] = size[i]; changed = true; } } if (changed) { // resize only when size is changed _resize(matrix, newSize, defaultValue); } } /** * Create a clone of the matrix * @return {DenseMatrix} clone */ DenseMatrix.prototype.clone = function () { var m = new DenseMatrix({ data: object.clone(this._data), size: object.clone(this._size) }); return m; }; /** * Retrieve the size of the matrix. * @returns {Number[]} size */ DenseMatrix.prototype.size = function() { return this._size; }; /** * Create a new matrix with the results of the callback function executed on * each entry of the matrix. * @param {function} callback The callback function is invoked with three * parameters: the value of the element, the index * of the element, and the Matrix being traversed. * * @return {DenseMatrix} matrix */ DenseMatrix.prototype.map = function (callback) { // matrix instance var me = this; var recurse = function (value, index) { if (isArray(value)) { return value.map(function (child, i) { return recurse(child, index.concat(i)); }); } else { return callback(value, index, me); } }; // return dense format return new DenseMatrix({ data: recurse(this._data, []), size: object.clone(this._size) }); }; /** * Execute a callback function on each entry of the matrix. * @param {function} callback The callback function is invoked with three * parameters: the value of the element, the index * of the element, and the Matrix being traversed. */ DenseMatrix.prototype.forEach = function (callback) { // matrix instance var me = this; var recurse = function (value, index) { if (isArray(value)) { value.forEach(function (child, i) { recurse(child, index.concat(i)); }); } else { callback(value, index, me); } }; recurse(this._data, []); }; /** * Create an Array with a copy of the data of the DenseMatrix * @returns {Array} array */ DenseMatrix.prototype.toArray = function () { return object.clone(this._data); }; /** * Get the primitive value of the DenseMatrix: a multidimensional array * @returns {Array} array */ DenseMatrix.prototype.valueOf = function () { return this._data; }; /** * Get a string representation of the matrix, with optional formatting options. * @param {Object | Number | Function} [options] Formatting options. See * lib/util/number:format for a * description of the available * options. * @returns {String} str */ DenseMatrix.prototype.format = function (options) { return string.format(this._data, options); }; /** * Get a string representation of the matrix * @returns {String} str */ DenseMatrix.prototype.toString = function () { return string.format(this._data); }; /** * Get a JSON representation of the matrix * @returns {Object} */ DenseMatrix.prototype.toJSON = function () { return { mathjs: 'DenseMatrix', data: this._data, size: this._size }; }; /** * Calculates the transpose of the matrix * @returns {Matrix} */ DenseMatrix.prototype.transpose = function () { // check dimensions switch (this._size.length) { case 1: // vector return this.clone(); case 2: // rows and columns var rows = this._size[0]; var columns = this._size[1]; // check columns if (columns === 0) { // throw exception throw new RangeError('Cannot transpose a 2D matrix with no columns (size: ' + string.format(this._size) + ')'); } // transposed matrix data var transposed = []; var transposedRow; // loop columns for (var j = 0; j < columns; j++) { // initialize row transposedRow = transposed[j] = []; // loop rows for (var i = 0; i < rows; i++) { // set data transposedRow[i] = object.clone(this._data[i][j]); } } // return matrix return new DenseMatrix({ data: transposed, size: [columns, rows] }); default: // multi dimensional throw new RangeError('Matrix must be two dimensional (size: ' + string.format(this._size) + ')'); } }; /** * Get the kth Matrix diagonal. * * @param {Number | BigNumber} [k=0] The kth diagonal where the vector will retrieved. * * @returns {Array} The array vector with the diagonal values. */ DenseMatrix.prototype.diagonal = function(k) { // validate k if any if (k) { // convert BigNumber to a number if (k instanceof BigNumber) k = k.toNumber(); // is must be an integer if (!isNumber(k) || !isInteger(k)) { throw new TypeError ('The parameter k must be an integer number'); } } else { // default value k = 0; } var kSuper = k > 0 ? k : 0; var kSub = k < 0 ? -k : 0; // rows & columns var rows = this._size[0]; var columns = this._size[1]; // number diagonal values var n = Math.min(rows - kSub, columns - kSuper); // x is a matrix get diagonal from matrix var vector = []; // loop rows for (var i = 0; i < n; i++) { vector[i] = object.clone(this._data[i + kSub][i + kSuper]); } return vector; }; /** * Create a diagonal matrix. * * @param {Array} size The matrix size. * @param {Number, Array} value The values for the diagonal. * @param {Number | BigNumber} [k=0] The kth diagonal where the vector will be filled in. * @param {Number} [defaultValue] The default value for non-diagonal * * @returns {DenseMatrix} */ DenseMatrix.diagonal = function (size, value, k, defaultValue) { if (!isArray(size)) throw new TypeError('Array expected, size parameter'); if (size.length !== 2) throw new Error('Only two dimensions matrix are supported'); // map size & validate size = size.map(function (s) { // check it is a big number if (s instanceof BigNumber) { // convert it s = s.toNumber(); } // validate arguments if (!isNumber(s) || !isInteger(s) || s < 1) { throw new Error('Size values must be positive integers'); } return s; }); // validate k if any if (k) { // convert BigNumber to a number if (k instanceof BigNumber) k = k.toNumber(); // is must be an integer if (!isNumber(k) || !isInteger(k)) { throw new TypeError ('The parameter k must be an integer number'); } } else { // default value k = 0; } var kSuper = k > 0 ? k : 0; var kSub = k < 0 ? -k : 0; // rows and columns var rows = size[0]; var columns = size[1]; // number of non-zero items var n = Math.min(rows - kSub, columns - kSuper); // value extraction function var _value; // check value if (isArray(value)) { // validate array if (value.length !== n) { // number of values in array must be n throw new Error('Invalid value array length'); } // define function _value = function (i) { // return value @ i return value[i]; }; } else { // define function _value = function () { // return value return value; }; } // empty array var data = []; // check we need to resize array if (size.length > 0) { // resize array data = array.resize(data, size, defaultValue); // fill diagonal for (var d = 0; d < n; d++) { data[d + kSub][d + kSuper] = _value(d); } } // create DenseMatrix return new DenseMatrix({ data: data, size: [rows, columns] }); }; /** * Calculate the trace of a matrix: the sum of the elements on the main * diagonal of a square matrix. * * See also: * * diagonal * * @returns {Number} The matrix trace */ DenseMatrix.prototype.trace = function () { // size & data var size = this._size; var data = this._data; // check dimensions switch (size.length) { case 1: // vector if (size[0] == 1) { // return data[0] return object.clone(data[0]); } throw new RangeError('Matrix must be square (size: ' + string.format(size) + ')'); case 2: // two dimensional array var rows = size[0]; var cols = size[1]; if (rows === cols) { // calulate sum var sum = 0; // loop diagonal for (var i = 0; i < rows; i++) sum = math.add(sum, data[i][i]); // return trace return sum; } throw new RangeError('Matrix must be square (size: ' + string.format(size) + ')'); default: // multi dimensional array throw new RangeError('Matrix must be two dimensional (size: ' + string.format(size) + ')'); } }; /** * Generate a matrix from a JSON object * @param {Object} json An object structured like * `{"mathjs": "DenseMatrix", data: [], size: []}`, * where mathjs is optional * @returns {DenseMatrix} */ DenseMatrix.fromJSON = function (json) { return new DenseMatrix(json); }; /** * Multiply the matrix values times the argument. * * @param {Number | BigNumber | Boolean | Complex | Unit | Array | Matrix | null} Value to multiply. * * @return {Number | BigNumber | Complex | Unit | Matrix} */ DenseMatrix.prototype.multiply = function (value) { // process matrix size switch(this._size.length) { case 1: // multiply vector return _multiplyVector(this, this._size[0], value); case 2: // multiply matrix return _multiplyMatrix(this, this._size[0], this._size[1], value); default: throw new Error('Can only multiply a 1 or 2 dimensional matrix ' + '(matrix has ' + this._size.length + ' dimensions)'); } }; var _multiplyVector = function (matrix, m, value) { // check value is a matrix if (value instanceof Matrix) { // matrix size var z = value.size(); // check value is a vector if (z.length === 1) { // vectors must have same length if (z[0] !== m) throw new RangeError('Dimension mismatch in multiplication. Vectors must have the same length.'); // multiply vector x vector return _multiplyVectorVector(matrix, m, function (i) { // value[i] return value.get([i]); }); } // check two dimensions matrix if (z.length === 2) { // vector length must be equal rows in matrix if (z[0] !== m) throw new RangeError('Dimension mismatch in multiplication. Matrix rows and Vector length must be equal.'); // mutiply vector x matrix return _multiplyVectorMatrix(matrix, m, z[1], function (i, j) { // value[i] return value.get([i, j]); }); } throw new Error('Can only multiply a 1 or 2 dimensional matrix ' + '(value has ' + z.length + ' dimensions)'); } // check value is an array if (isArray(value)) { // array size var s = array.size(value); // check value is a vector if (s.length === 1) { // vectors must have same length if (s[0] !== m) throw new RangeError('Dimension mismatch in multiplication. Vectors must have the same length.'); // multiply vector x vector return _multiplyVectorVector(matrix, m, function (i) { // value[i] return value[i]; }); } if (s.length === 2) { // vector length must be equal rows in matrix if (s[0] !== m) throw new RangeError('Dimension mismatch in multiplication. Matrix rows and Vector length must be equal.'); // mutiply vector x matrix return _multiplyVectorMatrix(matrix, m, s[1], function (i, j) { // value[i] return value[i][j]; }); } throw new Error('Can only multiply a 1 or 2 dimensional matrix ' + '(value has ' + s.length + ' dimensions)'); } // value is a scalar return matrix.map(function (v) { return math.multiply(value, v); }); }; var _multiplyVectorVector = function (matrix, m, get) { // check empty vector if (m === 0) throw new Error('Cannot multiply two empty vectors'); // result var result = 0; // loop data for (var i = 0; i < m; i++) { // multiply and accumulate result = math.add(result, math.multiply(matrix._data[i], get(i))); } return result; }; var _multiplyVectorMatrix = function (matrix, m, n, get) { // result var result = []; // loop columns in matrix for (var j = 0; j < n; j++) { // sum var sum = 0; // loop vector for (var i = 0; i < m; i++) { // multiply and accumulate sum = math.add(sum, math.multiply(matrix._data[i], get(i, j))); } result[j] = sum; } // check we need to squeeze the result into a scalar if (n === 1) return result[0]; // return matrix return new DenseMatrix({ data: result, size: [n] }); }; var _multiplyMatrix = function (matrix, m, n, value) { // check value is a matrix if (value instanceof Matrix) { // matrix size var z = value.size(); // check value is a vector if (z.length === 1) { // vectors must have same length if (z[0] !== n) throw new RangeError('Dimension mismatch in multiplication. Matrix columns must match vector length.'); // multiply matrix vector return _multiplyMatrixVector(matrix, m, n, function (i) { // value[i] return value.get([i]); }); } // check two dimensions matrix if (z.length === 2) { // vector length must be equal rows in matrix if (z[0] !== n) { throw new RangeError('Dimension mismatch in multiplication. ' + 'Columns of A must match length of B ' + '(A is ' + m + 'x' + n + ', B is ' + z[0] + ', ' + n + ' != ' + z[0] + ')'); } // mutiply vector x matrix return _multiplyMatrixMatrix(matrix, m, n, z[1], function (i, j) { // value[i, j] return value.get([i, j]); }); } throw new Error('Can only multiply a 1 or 2 dimensional matrix ' + '(value has ' + z.length + ' dimensions)'); } // check value is an array if (isArray(value)) { // array size var s = array.size(value); // check value is a vector if (s.length === 1) { // vectors must have same length if (s[0] !== n) throw new RangeError('Dimension mismatch in multiplication. Matrix columns must match vector length.'); // multiply matrix vector return _multiplyMatrixVector(matrix, m, n, function (i) { // value[i] return value[i]; }); } if (s.length === 2) { // vector length must be equal rows in matrix if (s[0] !== n) { throw new RangeError('Dimension mismatch in multiplication. ' + 'Columns of A must match length of B ' + '(A is ' + m + 'x' + n + ', B is ' + s[0] + ', ' + n + ' != ' + s[0] + ')'); } // mutiply vector x matrix return _multiplyMatrixMatrix(matrix, m, n, s[1], function (i, j) { // value[i, j] return value[i][j]; }); } throw new Error('Can only multiply a 1 or 2 dimensional matrix ' + '(value has ' + s.length + ' dimensions)'); } // value is a scalar return matrix.map(function (v) { return math.multiply(value, v); }); }; var _multiplyMatrixVector = function (matrix, m, n, get) { // result var result = []; // loop matrix rows for (var i = 0; i < m; i++) { // current row var row = matrix._data[i]; // sum var sum = 0; // loop matrix columns for (var j = 0; j < n; j++) { // multiply & accumulate sum = math.add(sum, math.multiply(row[j], get(j))); } result[i] = sum; } // check we need to squeeze the result into a scalar if (m === 1) return result[0]; // return matrix return new DenseMatrix({ data: result, size: [m] }); }; var _multiplyMatrixMatrix = function (matrix, m, n, c, get) { // result var result = []; // loop matrix rows for (var i = 0; i < m; i++) { // current row var row = matrix._data[i]; // initialize row array result[i] = []; // loop other matrix columns for (var j = 0; j < c; j++) { // sum var sum = 0; // loop matrix columns for (var x = 0; x < n; x++) { // multiply & accumulate sum = math.add(sum, math.multiply(row[x], get(x, j))); } result[i][j] = sum; } } // check we need to squeeze the result into a scalar if (m === 1 && c === 1) return result[0][0]; // return matrix return new DenseMatrix({ data: result, size: [m, c] }); }; /** * Preprocess data, which can be an Array or DenseMatrix with nested Arrays and * Matrices. Replaces all nested Matrices with Arrays * @param {Array} data * @return {Array} data */ function preprocess(data) { for (var i = 0, ii = data.length; i < ii; i++) { var elem = data[i]; if (isArray(elem)) { data[i] = preprocess(elem); } else if (elem instanceof math.type.Matrix) { data[i] = preprocess(elem.valueOf()); } } return data; } // exports return DenseMatrix; };