UNPKG

mathjs

Version:

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with dif

995 lines (925 loc) 30.3 kB
// deno-lint-ignore-file no-this-alias import { isArray, isBigNumber, isCollection, isIndex, isMatrix, isNumber, isString, typeOf } from '../../utils/is.js'; import { arraySize, getArrayDataType, processSizesWildcard, reshape, resize, unsqueeze, validate, validateIndex, broadcastTo, get } from '../../utils/array.js'; import { format } from '../../utils/string.js'; import { isInteger } from '../../utils/number.js'; import { clone, deepStrictEqual } from '../../utils/object.js'; import { DimensionError } from '../../error/DimensionError.js'; import { factory } from '../../utils/factory.js'; import { optimizeCallback } from '../../utils/optimizeCallback.js'; var name = 'DenseMatrix'; var dependencies = ['Matrix']; export var createDenseMatrixClass = /* #__PURE__ */factory(name, dependencies, _ref => { var { Matrix } = _ref; /** * Dense Matrix implementation. A regular, dense matrix, supporting multi-dimensional matrices. This is the default matrix type. * @class DenseMatrix * @enum {{ value, index: number[] }} */ function DenseMatrix(data, datatype) { if (!(this instanceof DenseMatrix)) { throw new SyntaxError('Constructor must be called with the new operator'); } if (datatype && !isString(datatype)) { throw new Error('Invalid datatype: ' + datatype); } if (isMatrix(data)) { // check data is a DenseMatrix if (data.type === 'DenseMatrix') { // clone data & size this._data = clone(data._data); this._size = clone(data._size); this._datatype = datatype || data._datatype; } else { // build data from existing matrix this._data = data.toArray(); this._size = data.size(); this._datatype = datatype || data._datatype; } } else if (data && isArray(data.data) && isArray(data.size)) { // initialize fields from JSON representation this._data = data.data; this._size = data.size; // verify the dimensions of the array validate(this._data, this._size); this._datatype = datatype || data.datatype; } else if (isArray(data)) { // replace nested Matrices with Arrays this._data = preprocess(data); // get the dimensions of the array this._size = arraySize(this._data); // verify the dimensions of the array, TODO: compute size while processing array validate(this._data, this._size); // data type unknown this._datatype = datatype; } else if (data) { // unsupported type throw new TypeError('Unsupported type of data (' + typeOf(data) + ')'); } else { // nothing provided this._data = []; this._size = [0]; this._datatype = datatype; } } DenseMatrix.prototype = new Matrix(); /** * Create a new DenseMatrix */ DenseMatrix.prototype.createDenseMatrix = function (data, datatype) { return new DenseMatrix(data, datatype); }; /** * Attach type information */ Object.defineProperty(DenseMatrix, 'name', { value: 'DenseMatrix' }); DenseMatrix.prototype.constructor = DenseMatrix; DenseMatrix.prototype.type = 'DenseMatrix'; DenseMatrix.prototype.isDenseMatrix = true; /** * Get the matrix type * * Usage: * const matrixType = matrix.getDataType() // retrieves the matrix type * * @memberOf DenseMatrix * @return {string} type information; if multiple types are found from the Matrix, it will return "mixed" */ DenseMatrix.prototype.getDataType = function () { return getArrayDataType(this._data, typeOf); }; /** * Get the storage format used by the matrix. * * Usage: * const format = matrix.storage() // retrieve storage format * * @memberof DenseMatrix * @return {string} The storage format. */ DenseMatrix.prototype.storage = function () { return 'dense'; }; /** * Get the datatype of the data stored in the matrix. * * Usage: * const format = matrix.datatype() // retrieve matrix datatype * * @memberof DenseMatrix * @return {string} The datatype. */ DenseMatrix.prototype.datatype = function () { return this._datatype; }; /** * Create a new DenseMatrix * @memberof DenseMatrix * @param {Array} data * @param {string} [datatype] */ DenseMatrix.prototype.create = function (data, datatype) { return new DenseMatrix(data, datatype); }; /** * Get a subset of the matrix, or replace a subset of the matrix. * * Usage: * const subset = matrix.subset(index) // retrieve subset * const value = matrix.subset(index, replacement) // replace subset * * @memberof DenseMatrix * @param {Index} index * @param {Array | Matrix | *} [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. * @memberof DenseMatrix * @param {number[]} index Zero-based index * @return {*} value */ DenseMatrix.prototype.get = function (index) { return get(this._data, index); }; /** * Replace a single element in the matrix. * @memberof DenseMatrix * @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, indexI; // 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++) { indexI = index[i]; validateIndex(indexI, data.length); data = data[indexI]; } // set new value indexI = index[index.length - 1]; validateIndex(indexI, data.length); data[indexI] = value; return this; }; /** * Get a submatrix of this matrix * @memberof DenseMatrix * @param {DenseMatrix} matrix * @param {Index} index Zero-based index * @private */ function _get(matrix, index) { if (!isIndex(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), matrix._datatype); } } /** * Recursively get a submatrix of a multi dimensional matrix. * Index is not checked for correct number or length of dimensions. * @memberof DenseMatrix * @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.dimension(dim); if (last) { return range.map(function (i) { validateIndex(i, data.length); return data[i]; }).valueOf(); } else { return range.map(function (i) { validateIndex(i, data.length); var child = data[i]; return _getSubmatrix(child, index, dims, dim + 1); }).valueOf(); } } /** * Replace a submatrix in this matrix * Indexes are zero-based. * @memberof DenseMatrix * @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 || index.isIndex !== true) { throw new TypeError('Invalid index'); } // get index size and check whether the index contains a single value var iSize = index.size(); var isScalar = index.isScalar(); // calculate the size of the submatrix, and convert it into an Array if needed var sSize; if (isMatrix(submatrix)) { sSize = submatrix.size(); submatrix = submatrix.valueOf(); } else { sSize = arraySize(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 // broadcast submatrix if (!deepStrictEqual(sSize, iSize)) { try { if (sSize.length === 0) { submatrix = broadcastTo([submatrix], iSize); } else { submatrix = broadcastTo(submatrix, iSize); } sSize = arraySize(submatrix); } catch (_unused) {} } // 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 = unsqueeze(submatrix, iSize.length, outer, sSize); } // check whether the size of the submatrix matches the index size if (!deepStrictEqual(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; var dim = 0; _setSubmatrix(matrix._data, index, submatrix, dims, dim); } return matrix; } /** * Replace a submatrix of a multi dimensional matrix. * @memberof DenseMatrix * @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; var range = index.dimension(dim); if (last) { range.forEach(function (dataIndex, subIndex) { validateIndex(dataIndex); data[dataIndex] = submatrix[subIndex[0]]; }); } else { range.forEach(function (dataIndex, subIndex) { validateIndex(dataIndex); _setSubmatrix(data[dataIndex], index, submatrix[subIndex[0]], 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). * * @memberof DenseMatrix * @param {number[] || Matrix} 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 (!isCollection(size)) { throw new TypeError('Array or Matrix expected'); } // SparseMatrix input is always 2d, flatten this into 1d if it's indeed a vector var sizeArray = size.valueOf().map(value => { return Array.isArray(value) && value.length === 1 ? value[0] : value; }); // matrix to resize var m = copy ? this.clone() : this; // resize matrix return _resize(m, sizeArray, defaultValue); }; function _resize(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 v; } // resize matrix matrix._size = size.slice(0); // copy the array matrix._data = resize(matrix._data, matrix._size, defaultValue); // return matrix return matrix; } /** * Reshape the matrix to the given size. Returns a copy of the matrix when * `copy=true`, otherwise return the matrix itself (reshape in place). * * NOTE: This might be better suited to copy by default, instead of modifying * in place. For now, it operates in place to remain consistent with * resize(). * * @memberof DenseMatrix * @param {number[]} size The new size the matrix should have. * @param {boolean} [copy] Return a reshaped copy of the matrix * * @return {Matrix} The reshaped matrix */ DenseMatrix.prototype.reshape = function (size, copy) { var m = copy ? this.clone() : this; m._data = reshape(m._data, size); var currentLength = m._size.reduce((length, size) => length * size); m._size = processSizesWildcard(size, currentLength); return m; }; /** * Enlarge the matrix when it is smaller than given size. * If the matrix is larger or equal sized, nothing is done. * @memberof DenseMatrix * @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 // copy the array newSize = matrix._size.slice(0); var 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 * @memberof DenseMatrix * @return {DenseMatrix} clone */ DenseMatrix.prototype.clone = function () { var m = new DenseMatrix({ data: clone(this._data), size: clone(this._size), datatype: this._datatype }); return m; }; /** * Retrieve the size of the matrix. * @memberof DenseMatrix * @returns {number[]} size */ DenseMatrix.prototype.size = function () { return this._size.slice(0); // return a clone of _size }; /** * Applies a callback function to a reference to each element of the matrix * @memberof DenseMatrix * @param {Function} callback The callback function is invoked with three * parameters: the array containing the element, * the index of the element within that array (as an integer), * and for non unarry callbacks copy of the current index (as an array of integers). */ DenseMatrix.prototype._forEach = function (callback) { var isUnary = callback.length === 2; // callback has 2 parameters: value, index var maxDepth = this._size.length - 1; if (maxDepth < 0) return; if (isUnary) { iterateUnary(this._data); return; } if (maxDepth === 0) { for (var i = 0; i < this._data.length; i++) { callback(this._data, i, [i]); } return; } var index = new Array(maxDepth + 1); iterate(this._data); function iterate(data) { var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (depth < maxDepth) { for (var _i = 0; _i < data.length; _i++) { index[depth] = _i; iterate(data[_i], depth + 1); } } else { for (var _i2 = 0; _i2 < data.length; _i2++) { index[depth] = _i2; callback(data, _i2, index.slice()); } } } function iterateUnary(data) { var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (depth < maxDepth) { for (var _i3 = 0; _i3 < data.length; _i3++) { iterateUnary(data[_i3], depth + 1); } } else { for (var _i4 = 0; _i4 < data.length; _i4++) { callback(data, _i4); } } } }; /** * Create a new matrix with the results of the callback function executed on * each entry of the matrix. * @memberof DenseMatrix * @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. * @param {boolean} skipZeros If true, the callback function is invoked only for non-zero entries * @param {boolean} isUnary If true, the callback function is invoked with one parameter * * @return {DenseMatrix} matrix */ DenseMatrix.prototype.map = function (callback) { var skipZeros = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var isUnary = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var me = this; var result = new DenseMatrix(me); var fastCallback = optimizeCallback(callback, me._data, 'map', isUnary); var applyCallback = isUnary || fastCallback.isUnary ? (arr, i) => { arr[i] = fastCallback.fn(arr[i]); } : (arr, i, index) => { arr[i] = fastCallback.fn(arr[i], index, me); }; result._forEach(applyCallback); return result; }; /** * Execute a callback function on each entry of the matrix. * @memberof DenseMatrix * @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. * @param {boolean} skipZeros If true, the callback function is invoked only for non-zero entries * @param {boolean} isUnary If true, the callback function is invoked with one parameter */ DenseMatrix.prototype.forEach = function (callback) { var skipZeros = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var isUnary = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var me = this; var fastCallback = optimizeCallback(callback, me._data, 'map', isUnary); var applyCallback = isUnary || fastCallback.isUnary ? (arr, i) => { fastCallback.fn(arr[i]); } : (arr, i, index) => { fastCallback.fn(arr[i], index, me); }; me._forEach(applyCallback); }; /** * Iterate over the matrix elements * @return {Iterable<{ value, index: number[] }>} */ DenseMatrix.prototype[Symbol.iterator] = function* () { var maxDepth = this._size.length - 1; if (maxDepth < 0) { return; } if (maxDepth === 0) { for (var i = 0; i < this._data.length; i++) { yield { value: this._data[i], index: [i] }; } return; } var index = []; var _recurse = function* recurse(value, depth) { if (depth < maxDepth) { for (var _i5 = 0; _i5 < value.length; _i5++) { index[depth] = _i5; yield* _recurse(value[_i5], depth + 1); } } else { for (var _i6 = 0; _i6 < value.length; _i6++) { index[depth] = _i6; yield { value: value[_i6], index: index.slice() }; } } }; yield* _recurse(this._data, 0); }; /** * Returns an array containing the rows of a 2D matrix * @returns {Array<Matrix>} */ DenseMatrix.prototype.rows = function () { var result = []; var s = this.size(); if (s.length !== 2) { throw new TypeError('Rows can only be returned for a 2D matrix.'); } var data = this._data; for (var row of data) { result.push(new DenseMatrix([row], this._datatype)); } return result; }; /** * Returns an array containing the columns of a 2D matrix * @returns {Array<Matrix>} */ DenseMatrix.prototype.columns = function () { var _this = this; var result = []; var s = this.size(); if (s.length !== 2) { throw new TypeError('Rows can only be returned for a 2D matrix.'); } var data = this._data; var _loop = function _loop(i) { var col = data.map(row => [row[i]]); result.push(new DenseMatrix(col, _this._datatype)); }; for (var i = 0; i < s[1]; i++) { _loop(i); } return result; }; /** * Create an Array with a copy of the data of the DenseMatrix * @memberof DenseMatrix * @returns {Array} array */ DenseMatrix.prototype.toArray = function () { return clone(this._data); }; /** * Get the primitive value of the DenseMatrix: a multidimensional array * @memberof DenseMatrix * @returns {Array} array */ DenseMatrix.prototype.valueOf = function () { return this._data; }; /** * Get a string representation of the matrix, with optional formatting options. * @memberof DenseMatrix * @param {Object | number | Function} [options] Formatting options. See * lib/utils/number:format for a * description of the available * options. * @returns {string} str */ DenseMatrix.prototype.format = function (options) { return format(this._data, options); }; /** * Get a string representation of the matrix * @memberof DenseMatrix * @returns {string} str */ DenseMatrix.prototype.toString = function () { return format(this._data); }; /** * Get a JSON representation of the matrix * @memberof DenseMatrix * @returns {Object} */ DenseMatrix.prototype.toJSON = function () { return { mathjs: 'DenseMatrix', data: this._data, size: this._size, datatype: this._datatype }; }; /** * Get the kth Matrix diagonal. * * @memberof DenseMatrix * @param {number | BigNumber} [k=0] The kth diagonal where the vector will retrieved. * * @returns {Matrix} The matrix with the diagonal values. */ DenseMatrix.prototype.diagonal = function (k) { // validate k if any if (k) { // convert BigNumber to a number if (isBigNumber(k)) { 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 data = []; // loop rows for (var i = 0; i < n; i++) { data[i] = this._data[i + kSub][i + kSuper]; } // create DenseMatrix return new DenseMatrix({ data, size: [n], datatype: this._datatype }); }; /** * Create a diagonal matrix. * * @memberof DenseMatrix * @param {Array} size The matrix size. * @param {number | Matrix | 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 * @param {string} [datatype] The datatype for the 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 (isBigNumber(s)) { // 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 (isBigNumber(k)) { 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 _value(i) { // return value @ i return value[i]; }; } else if (isMatrix(value)) { // matrix size var ms = value.size(); // validate matrix if (ms.length !== 1 || ms[0] !== n) { // number of values in array must be n throw new Error('Invalid matrix length'); } // define function _value = function _value(i) { // return value @ i return value.get([i]); }; } else { // define function _value = function _value() { // return value return value; }; } // discover default value if needed if (!defaultValue) { // check first value in array defaultValue = isBigNumber(_value(0)) ? _value(0).mul(0) // trick to create a BigNumber with value zero : 0; } // empty array var data = []; // check we need to resize array if (size.length > 0) { // resize array data = 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, size: [rows, columns] }); }; /** * Generate a matrix from a JSON object * @memberof DenseMatrix * @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); }; /** * Swap rows i and j in Matrix. * * @memberof DenseMatrix * @param {number} i Matrix row index 1 * @param {number} j Matrix row index 2 * * @return {Matrix} The matrix reference */ DenseMatrix.prototype.swapRows = function (i, j) { // check index if (!isNumber(i) || !isInteger(i) || !isNumber(j) || !isInteger(j)) { throw new Error('Row index must be positive integers'); } // check dimensions if (this._size.length !== 2) { throw new Error('Only two dimensional matrix is supported'); } // validate index validateIndex(i, this._size[0]); validateIndex(j, this._size[0]); // swap rows DenseMatrix._swapRows(i, j, this._data); // return current instance return this; }; /** * Swap rows i and j in Dense Matrix data structure. * * @param {number} i Matrix row index 1 * @param {number} j Matrix row index 2 * @param {Array} data Matrix data */ DenseMatrix._swapRows = function (i, j, data) { // swap values i <-> j var vi = data[i]; data[i] = data[j]; data[j] = vi; }; /** * Preprocess data, which can be an Array or DenseMatrix with nested Arrays and * Matrices. Clones all (nested) Arrays, and replaces all nested Matrices with Arrays * @memberof DenseMatrix * @param {Array | Matrix} data * @return {Array} data */ function preprocess(data) { if (isMatrix(data)) { return preprocess(data.valueOf()); } if (isArray(data)) { return data.map(preprocess); } return data; } return DenseMatrix; }, { isClass: true });