From c68d38e8a930945b2eab464bc8000adfd3e02406 Mon Sep 17 00:00:00 2001 From: rjbaucells Date: Sun, 15 Mar 2015 23:35:25 -0400 Subject: [PATCH] CRS updates --- lib/type/matrix/CrsMatrix.js | 1092 ++++++++++++++++++++++------ test/type/matrix/CrsMatrix.test.js | 411 +++-------- 2 files changed, 994 insertions(+), 509 deletions(-) diff --git a/lib/type/matrix/CrsMatrix.js b/lib/type/matrix/CrsMatrix.js index c8851d5b3..1d89d7f2f 100644 --- a/lib/type/matrix/CrsMatrix.js +++ b/lib/type/matrix/CrsMatrix.js @@ -3,30 +3,42 @@ var util = require('../../util/index'); var DimensionError = require('../../error/DimensionError'); -var array = util.array, +var array = util.array; +var object = util.object; +var string = util.string; +var number = util.number; + +var isArray = Array.isArray, isNumber = util.number.isNumber, - isBoolean = util.boolean.isBoolean, + isInteger = util.number.isInteger, - object = util.object, - string = util.string; - -var isArray = Array.isArray; -var validateIndex = array.validateIndex; + validateIndex = array.validateIndex; module.exports = function (math) { - - var isComplex = math.type.Complex.isComplex, - BigNumber = math.type.BigNumber; + + var Index = math.type.Index, + BigNumber = math.type.BigNumber, + Matrix = math.type.Matrix; function CrsMatrix(data) { if (!(this instanceof CrsMatrix)) throw new SyntaxError('Constructor must be called with the new operator'); - // initialize format - this._format = 'crs'; - - // check data is a serialized json - if (data && isArray(data.values) && isArray(data.index) && isArray(data.ptr)) { + if (data instanceof Matrix) { + // check data is a CrsMatrix + if (data.type === 'CrsMatrix') { + // clone arrays + this._values = object.clone(data._values); + this._index = object.clone(data._index); + this._ptr = object.clone(data._ptr); + this._size = object.clone(data._size); + } + else { + // build from matrix data + _createFromArray(this, data.valueOf()); + } + } + else if (data && isArray(data.values) && isArray(data.index) && isArray(data.ptr) && isArray(data.size)) { // initialize fields this._values = data.values; this._index = data.index; @@ -34,20 +46,39 @@ module.exports = function (math) { this._size = data.size; } else if (isArray(data)) { - // initialize fields + // create from array + _createFromArray(this, data); + } + else if (data) { + // unsupported type + throw new TypeError('Unsupported type of data (' + util.types.type(data) + ')'); + } + else { + // nothing provided this._values = []; this._index = []; - this._ptr = []; - // discover rows & columns, do not use math.size() to avoid looping array twice - var rows = data.length; - var columns = 0; + this._ptr = [0]; + this._size = [0]; + } + } - // loop rows - for (var i = 0; i < rows; i++) { - // current row - var row = data[i]; - // store value index in ptr - this._ptr.push(this._values.length); + var _createFromArray = function (matrix, data) { + // initialize fields + matrix._values = []; + matrix._index = []; + matrix._ptr = []; + // discover rows & columns, do not use math.size() to avoid looping array twice + var rows = data.length; + var columns = 0; + + // loop rows + for (var i = 0; i < rows; i++) { + // store value index in ptr + matrix._ptr.push(matrix._values.length); + // current row + var row = data[i]; + // check row is an array + if (isArray(row)) { // update columns if needed if (row.length > columns) columns = row.length; @@ -58,95 +89,197 @@ module.exports = function (math) { // check value != 0 if (!math.equal(v, 0)) { // store value - this._values.push(v); + matrix._values.push(v); // add column index - this._index.push(j); + matrix._index.push(j); } } } - // store number of valus in ptr - this._ptr.push(this._values.length); - // size - this._size = [rows, columns]; + else { + // update columns if needed (only on first row) + if (i === 0 && columns < 1) + columns = 1; + // check value != 0 (row is a scalar) + if (!math.equal(row, 0)) { + // store value + matrix._values.push(row); + // index + matrix._index.push(0); + } + } } - else - throw new SyntaxError('data must be an array'); - } + // store number of values in ptr + matrix._ptr.push(matrix._values.length); + // size + matrix._size = [rows, columns]; + }; CrsMatrix.prototype = new math.type.Matrix(); CrsMatrix.prototype.type = 'CrsMatrix'; - - CrsMatrix.prototype.toArray = function () { - // result - var a = []; - // values index - var k = 0; - // rows and columns - var rows = this._size[0]; - var columns = this._size[1]; - // loop rows - for (var i = 0; i < rows; i++) { - // push row - var r = a[i] = []; - // k0 <= k < k1 where k0 = _ptr[i] && k1 = _ptr[i+1] - var k0 = this._ptr[i]; - var k1 = this._ptr[i + 1]; - // column pointer - var p = 0; - // check k is within [k0, k1[ - while (k >= k0 && k < k1) { - // column index - var j = this._index[k]; - // zero values - for (var x = p; x < j; x++) - r[x] = 0; - // set value - r[j] = this._values[k]; - // increment k - k++; - // update pointer - p = j + 1; - } - // zero values - for (var y = p; y < columns; y++) - r[y] = 0; - } - return a; + + /** + * Get the storage format used by the matrix. + * + * Usage: + * var format = matrix.storage() // retrieve storage format + * + * @return {string} The storage format. + */ + CrsMatrix.prototype.storage = function () { + return 'crs'; }; - CrsMatrix.prototype.toJSON = function () { - return { - format: this._format, - values: this._values, - index: this._index, - ptr: this._ptr, - size: this._size + /** + * 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 | Maytrix | *} [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. + */ + CrsMatrix.prototype.subset = function (index, replacement, defaultValue) { + // check arguments + switch (arguments.length) { + case 1: + return _getsubset(this, index); + + // intentional fall through + case 2: + case 3: + return _setsubset(this, index, replacement, defaultValue); + + default: + throw new SyntaxError('Wrong number of arguments'); + } + }; + + var _getsubset = function (matrix, index) { + // check index + if (!(index instanceof Index)) { + throw new TypeError('Invalid index'); + } + + var isScalar = index.isScalar(); + if (isScalar) { + // return a scalar + return matrix.get(index.min()); + } + // 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]); + } + + // map callback + var callback = function (v) { + // return value + return v; }; + // get sub-matrix + return _map(matrix, min[0], max[0], min[1], max[1], callback, false); }; - var _getValueIndex = function(j, left, right, index) { - // check column is on the right side - if (right - left === 0 || j > index[right - 1]) - return right; - // loop until we find column - while (left < right) { - // point in the middle (fast integer division) - var p = ~~((left + right) / 2); - // column @ p - var c = index[p]; - // check we have to look on the left side, right side or we found the column - if (j < c) - right = p; - else if (j > c) - left = p + 1; - else - return p; + var _setsubset = function (matrix, index, submatrix, defaultValue) { + // check index + if (!(index instanceof Index)) { + throw new TypeError('Invalid index'); } - return left; + + // 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 Matrix) { + // submatrix size + sSize = submatrix.size(); + // use array representation + submatrix = submatrix.toArray(); + } + else { + // get submatrix size (array, scalar) + sSize = array.size(submatrix); + } + + // check index is a scalar + if (isScalar) { + // verify submatrix is a scalar + if (sSize.length !== 0) { + throw new TypeError('Scalar expected'); + } + // set value + matrix.set(index.min(), submatrix, defaultValue); + } + else { + // validate dimensions, index size must be one or two dimensions + if (iSize.length !== 1 && iSize.length !== 2) { + throw new DimensionError(iSize.length, matrix._size.length, '<'); + } + + // check submatrix and index have the same dimensions + 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, '>'); + } + + // offsets + var x0 = index.min()[0]; + var y0 = index.min()[1]; + + // submatrix rows and columns + var m = sSize[0]; + var n = sSize[1]; + + // loop submatrix + for (var x = 0; x < m; x++) { + // loop columns + for (var y = 0; y < n; y++) { + // value at i, j + var v = submatrix[x][y]; + // invoke set (zero value will remove entry from matrix) + matrix.set([x + x0, y + y0], v, defaultValue); + } + } + } + return matrix; }; - CrsMatrix.prototype.get = function (index, d) { + /** + * Get a single element from the matrix. + * @param {Number[]} index Zero-based index + * @return {*} value + */ + CrsMatrix.prototype.get = function (index) { if (!isArray(index)) throw new TypeError('Array expected'); if (index.length != this._size.length) @@ -155,57 +288,60 @@ module.exports = function (math) { // row and column var i = index[0]; var j = index[1]; - - // check i, j are valid - validateIndex(i, this._size[0]); - validateIndex(j, this._size[1]); - // find value index - var k = _getValueIndex(j, this._ptr[i], this._ptr[i + 1], this._index); - // check k is prior to next row k and it is in the correct column - if (k < this._ptr[i + 1] && this._index[k] === j) - return object.clone(this._values[k]); - - return d || 0; - }; - - var _remove = function (k, i, values, index, ptr) { - // remove value @ k - values.splice(k, 1); - index.splice(k, 1); - // update pointers - for (var x = i + 1; x < ptr.length; x++) - ptr[x]--; - }; - - var _insert = function (k, i, j, v, values, index, ptr) { - // insert value - values.splice(k, 0, v); - // update column for k - index.splice(k, 0, j); - // update row pointers - for (var x = i + 1; x < ptr.length; x++) - ptr[x]++; - }; - - CrsMatrix.prototype.set = function (index, v) { - if (!isArray(index)) - throw new TypeError('Array expected'); - if (index.length != this._size.length) - throw new DimensionError(index.length, this._size.length); - - // row and column - var i = index[0]; - var j = index[1]; - // check i, j are valid validateIndex(i, this._size[0]); validateIndex(j, this._size[1]); // find value index - var k = _getValueIndex(j, this._ptr[i], this._ptr[i + 1], this._index); - // check k is prior to next row k and it is in the correct column - if (k < this._ptr[i + 1] && this._index[k] === j) { + var k = _getValueIndex(i, this._ptr[j], this._ptr[j + 1], this._index); + // check k is prior to next column k and it is in the correct row + if (k < this._ptr[j + 1] && this._index[k] === i) + return object.clone(this._values[k]); + + return 0; + }; + + /** + * 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 set to zero. + * @return {CrsMatrix} self + */ + CrsMatrix.prototype.set = function (index, v, defaultValue) { + if (!isArray(index)) + throw new TypeError('Array expected'); + if (index.length != this._size.length) + throw new DimensionError(index.length, this._size.length); + + // row and column + var i = index[0]; + var j = index[1]; + + // rows & columns + var rows = this._size[0]; + var columns = this._size[1]; + + // check we need to resize matrix + if (i > rows - 1 || j > columns - 1) { + // resize matrix + _resize(this, Math.max(i + 1, rows), Math.max(j + 1, columns), defaultValue); + // update rows & columns + rows = this._size[0]; + columns = this._size[1]; + } + + // check i, j are valid + validateIndex(i, rows); + validateIndex(j, columns); + + // find value index + var k = _getValueIndex(i, this._ptr[j], this._ptr[j + 1], this._index); + // check k is prior to next column k and it is in the correct row + if (k < this._ptr[j + 1] && this._index[k] === i) { // check value != 0 if (!math.equal(v, 0)) { // update value @@ -213,17 +349,199 @@ module.exports = function (math) { } else { // remove value from matrix - _remove(k, i, this._values, this._index, this._ptr); + _remove(k, j, this._values, this._index, this._ptr); } } else { // insert value @ (i, j) _insert(k, i, j, v, this._values, this._index, this._ptr); } - + return this; }; + var _getValueIndex = function(i, top, bottom, index) { + // check row is on the bottom side + if (bottom - top === 0 || i > index[bottom - 1]) + return bottom; + // loop until we find row index + while (top < bottom) { + // point in the middle (fast integer division) + var p = ~~((top + bottom) / 2); + // row @ p + var r = index[p]; + // check we have to look on the top side, bottom side or we found the row + if (i < r) + bottom = p; + else if (i > r) + top = p + 1; + else + return p; + } + return top; + }; + + var _remove = function (k, j, values, index, ptr) { + // remove value @ k + values.splice(k, 1); + index.splice(k, 1); + // update pointers + for (var x = j + 1; x < ptr.length; x++) + ptr[x]--; + }; + + var _insert = function (k, i, j, v, values, index, ptr) { + // insert value + values.splice(k, 0, v); + // update row for k + index.splice(k, 0, i); + // update column pointers + for (var x = j + 1; x < ptr.length; x++) + ptr[x]++; + }; + + /** + * 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 + */ + CrsMatrix.prototype.resize = function (size, defaultValue, copy) { + // validate arguments + if (!isArray(size)) + throw new TypeError('Array expected'); + if (size.length !== 2) + throw new Error('Only two dimensions matrix are supported'); + + // check sizes + size.forEach(function (value) { + if (!number.isNumber(value) || !number.isInteger(value) || value < 0) { + throw new TypeError('Invalid size, must contain positive integers ' + + '(size: ' + string.format(size) + ')'); + } + }); + + // matrix to resize + var m = copy ? this.clone() : this; + // resize matrix + return _resize(m, size[0], size[1], defaultValue); + }; + + var _resize = function (matrix, rows, columns, defaultValue) { + // value to insert at the time of growing matrix + var value = defaultValue || 0; + // should we insert the value? + var ins = !math.equal(value, 0); + + // old columns and rows + var r = matrix._size[0]; + var c = matrix._size[1]; + + var i, j, k; + + // check we need to increase columns + if (columns > c) { + // loop new columns + for (j = c; j < columns; j++) { + // update matrix._ptr for current column + matrix._ptr[j] = matrix._values.length; + // check we need to insert matrix._values + if (ins) { + // loop rows + for (i = 0; i < r; i++) { + // add new matrix._values + matrix._values.push(value); + // update matrix._index + matrix._index.push(i); + } + } + } + // store number of matrix._values in matrix._ptr + matrix._ptr[columns] = matrix._values.length; + } + else if (columns < c) { + // truncate matrix._ptr + matrix._ptr.splice(columns + 1, c - columns); + // truncate matrix._values and matrix._index + matrix._values.splice(matrix._ptr[columns], matrix._values.length); + matrix._index.splice(matrix._ptr[columns], matrix._index.length); + } + // update columns + c = columns; + + // check we need to increase rows + if (rows > r) { + // check we have to insert values + if (ins) { + // inserts + var n = 0; + // loop columns + for (j = 0; j < c; j++) { + // update matrix._ptr for current column + matrix._ptr[j] = matrix._ptr[j] + n; + // where to insert matrix._values + k = matrix._ptr[j + 1] + n; + // pointer + var p = 0; + // loop new rows, initialize pointer + for (i = r; i < rows; i++, p++) { + // add value + matrix._values.splice(k + p, 0, value); + // update matrix._index + matrix._index.splice(k + p, 0, i); + // increment inserts + n++; + } + } + // store number of matrix._values in matrix._ptr + matrix._ptr[c] = matrix._values.length; + } + } + else if (rows < r) { + // deletes + var d = 0; + // loop columns + for (j = 0; j < c; j++) { + // update matrix._ptr for current column + matrix._ptr[j] = matrix._ptr[j] - d; + // where matrix._values start for next column + var k0 = matrix._ptr[j]; + var k1 = matrix._ptr[j + 1] - d; + // loop matrix._index + for (k = k0; k < k1; k++) { + // row + i = matrix._index[k]; + // check we need to delete value and matrix._index + if (i > rows - 1) { + // remove value + matrix._values.splice(k, 1); + // remove item from matrix._index + matrix._index.splice(k, 1); + // increase deletes + d++; + } + } + } + // update matrix._ptr for current column + matrix._ptr[j] = matrix._values.length; + } + // update matrix._size + matrix._size[0] = rows; + matrix._size[1] = columns; + // return matrix + return matrix; + }; + + /** + * Create a clone of the matrix + * @return {CrsMatrix} clone + */ CrsMatrix.prototype.clone = function () { var m = new CrsMatrix({ values: object.clone(this._values), @@ -239,7 +557,185 @@ module.exports = function (math) { * @returns {Number[]} size */ CrsMatrix.prototype.size = function() { - return this._size; + return object.clone(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. + * @param {boolean} [skipZeros] Invoke callback function for non-zero values only. + * + * @return {Matrix} matrix + */ + CrsMatrix.prototype.map = function (callback, skipZeros) { + // matrix instance + var me = this; + // rows and columns + var rows = this._size[0]; + var columns = this._size[1]; + // invoke callback + var invoke = function (v, i, j) { + // invoke callback + return callback(v, [i, j], me); + }; + // invoke _map + return _map(this, 0, rows - 1, 0, columns - 1, invoke, skipZeros); + }; + + /** + * Create a new matrix with the results of the callback function executed on the interval + * [minRow..maxRow, minColumn..maxColumn]. + */ + var _map = function (matrix, minRow, maxRow, minColumn, maxColumn, callback, skipZeros) { + // result arrays + var values = []; + var index = []; + var ptr = []; + // invoke callback + var invoke = function (v, x, y) { + // invoke callback + v = callback(v, x, y); + // check value != 0 + if (!math.equal(v, 0)) { + // store value + values.push(v); + // index + index.push(x); + } + }; + // loop rows + for (var i = minRow; i <= maxRow; i++) { + // store pointer to values index + ptr.push(values.length); + // k0 <= k < k1 where k0 = _ptr[j] && k1 = _ptr[j+1] + var k0 = matrix._ptr[i]; + var k1 = matrix._ptr[i + 1]; + // column pointer + var p = minRow; + // loop k within [k0, k1[ + for (var k = k0; k < k1; k++) { + // column index + var j = matrix._index[k]; + // check j is in range + if (j >= minColumn && j <= maxColumn) { + // zero values + if (!skipZeros) { + // write zeros from column p to j + for (var x = p; x < j; x++) + invoke(0, i - minRow, x - minColumn); + } + // value @ k + invoke(matrix._values[k], i - minRow, j - minColumn); + } + // update pointer + p = j + 1; + } + // zero values + if (!skipZeros) { + // write zeros from column p to maxColumn + for (var y = p; y <= maxColumn; y++) + invoke(0, i - minRow, y - minColumn); + } + } + // store number of values in ptr + ptr.push(values.length); + // return ccs + return new CrsMatrix({ + values: values, + index: index, + ptr: ptr, + size: [maxRow - minRow + 1, maxColumn - minColumn + 1] + }); + }; + + /** + * 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. + */ + CrsMatrix.prototype.forEach = function (callback) { + // matrix instance + var me = this; + // rows and columns + var rows = this._size[0]; + var columns = this._size[1]; + // loop rows + for (var i = 0; i < rows; i++) { + // k0 <= k < k1 where k0 = _ptr[i] && k1 = _ptr[i+1] + var k0 = this._ptr[i]; + var k1 = this._ptr[i + 1]; + // column pointer + var p = 0; + // loop k within [k0, k1[ + for (var k = k0; k < k1; k++) { + // column index + var j = this._index[k]; + // zero values + for (var x = p; x < j; x++) + callback(0, [i, x], me); + // value @ k + callback(this._values[k], [i, j], me); + // update pointer + p = j + 1; + } + // zero values + for (var y = p; y < columns; y++) + callback(0, [i, y], me); + } + }; + + /** + * Create an Array with a copy of the data of the CrsMatrix + * @returns {Array} array + */ + CrsMatrix.prototype.toArray = function () { + return _toArray(this, true); + }; + + /** + * Get the primitive value of the CrsMatrix: a two dimensions array + * @returns {Array} array + */ + CrsMatrix.prototype.valueOf = function () { + return _toArray(this, false); + }; + + var _toArray = function (matrix, copy) { + // result + var a = []; + // rows and columns + var rows = matrix._size[0]; + var columns = matrix._size[1]; + // loop rows + for (var i = 0; i < rows; i++) { + // push row + var r = a[i] = []; + // k0 <= k < k1 where k0 = _ptr[i] && k1 = _ptr[i+1] + var k0 = matrix._ptr[i]; + var k1 = matrix._ptr[i + 1]; + // column pointer + var p = 0; + // loop k is within [k0, k1[ + for (var k = k0; k < k1; k++) { + // column index + var j = matrix._index[k]; + // zero values + for (var x = p; x < j; x++) + r[x] = 0; + // set value + r[j] = copy ? object.clone(matrix._values[k]) : matrix._values[k]; + // update pointer + p = j + 1; + } + // zero values + for (var y = p; y < columns; y++) + r[y] = 0; + } + return a; }; /** @@ -255,72 +751,45 @@ module.exports = function (math) { var rows = this._size[0]; var columns = this._size[1]; // rows & columns - var str = 'CRS [' + string.format(rows) + ' x ' + string.format(columns) + ']\n'; - // values index - var k = 0; + var str = 'CRS [' + string.format(rows, options) + ' x ' + string.format(columns, options) + '] density: ' + string.format(this._values.length / (rows * columns), options) + '\n'; // loop rows for (var i = 0; i < rows; i++) { - // value indexes on current row + // k0 <= k < k1 where k0 = _ptr[i] && k1 = _ptr[i+1] var k0 = this._ptr[i]; var k1 = this._ptr[i + 1]; - // check k is within [k0, k1[ - while (k >= k0 && k < k1) { + // loop k within [k0, k1[ + for (var k = k0; k < k1; k++) { // column index var j = this._index[k]; // append value str += '\n (' + string.format(i, options) + ', ' + string.format(j, options) + ') ==> ' + string.format(this._values[k], options); - // increment k - k++; } } return str; }; - + /** * Get a string representation of the matrix * @returns {String} str */ - CrsMatrix.prototype.toString = function () { + CrsMatrix.prototype.toString = function () { return string.format(this.toArray()); }; - CrsMatrix.prototype.multiply = function (x) { - if (arguments.length != 1) { - throw new math.error.ArgumentsError('multiply', arguments.length, 1); - } - - // scalar - if (isNumber(x) || isComplex(x)|| isBoolean(x) || x instanceof BigNumber) { - // check it is zero - if (math.equal(x, 0)) { - // zero ptr - var ptr = []; - for (var i = 0; i < this._ptr.length; i++) - ptr[i] = 0; - // empty CRS - return new CrsMatrix({ - mathjs: 'CrsMatrix', - values: [], - index: [], - ptr: ptr, - size: this._size - }); - } - // multiply values - var values = []; - for (var j = 0; j < this._values.length; j++) - values[j] = math.multiply(this._values[j], x); - // create storage - return new CrsMatrix({ - mathjs: 'CrsMatrix', - values: values, - index: object.clone(this._index), - ptr: object.clone(this._ptr), - size: this._size - }); - } + /** + * Get a JSON representation of the matrix + * @returns {Object} + */ + CrsMatrix.prototype.toJSON = function () { + return { + mathjs: 'CrsMatrix', + values: this._values, + index: this._index, + ptr: this._ptr, + size: this._size + }; }; - + /** * Calculates the transpose of the matrix * @returns {Matrix} @@ -334,7 +803,7 @@ module.exports = function (math) { // throw exception throw new RangeError('Cannot transpose a 2D matrix with no columns (size: ' + string.format(this._size) + ')'); } - // crs transpose is a crs matrix with the same structure + // crs transpose is a ccs matrix with the same structure return new math.type.CcsMatrix({ values: object.clone(this._values), index: object.clone(this._index), @@ -343,27 +812,187 @@ module.exports = function (math) { }); }; + /** + * 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. + */ + CrsMatrix.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); + + // diagonal + var values = []; + // loop columns + for (var j = kSuper; j < columns && values.length < n; j++) { + // k0 <= k < k1 where k0 = _ptr[j] && k1 = _ptr[j+1] + var k0 = this._ptr[j]; + var k1 = this._ptr[j + 1]; + // column value flag + var cv = false; + // loop x within [k0, k1[ + for (var x = k0; k < k1; k++) { + // row index + var i = this._index[x]; + // check row + if (i === j - kSuper + kSub) { + // set flag + cv = true; + // zero on this column + values.push(object.clone(this._values[x])); + // exit loop + break; + } + else if (i > j - kSuper + kSub) { + // exit loop, no value on the diagonal for column j + break; + } + } + // check this column has a value set + if (!cv && values.length < n) { + // zero on this column + values.push(0); + } + } + return values; + }; + + /** + * Generate a matrix from a JSON object + * @param {Object} json An object structured like + * `{"mathjs": "CrsMatrix", "values": [], "index": [], "ptr": [], "size": []}`, + * where mathjs is optional + * @returns {CrsMatrix} + */ CrsMatrix.fromJSON = function (json) { return new CrsMatrix(json); }; - CrsMatrix.diagonal = function (rows, columns, value) { + /** + * 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. + * + * @returns {CrsMatrix} + */ + CrsMatrix.diagonal = function (size, value, k) { + 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; + }; + } + // create arrays var values = []; var index = []; var ptr = []; - // number of non-zero items - var n = Math.min(rows, columns); - // value to store - var v = value || 1; + // loop items - for (var i = 0; i < n; i++) { - // first column with data - ptr.push(i); - // column - index.push(i); - // add value - values.push(v); + for (var j = 0; j < columns; j++) { + // number of rows with value + ptr.push(values.length); + // diagonal index + var i = j - kSuper; + // check we need to set diagonal value + if (i >= 0 && i < n) { + // get value @ i + var v = _value(i); + // check for zero + if (!math.equal(v, 0)) { + // column + index.push(i + kSub); + // add value + values.push(v); + } + } } // last value should be number of values ptr.push(values.length); @@ -376,5 +1005,56 @@ module.exports = function (math) { }); }; + /** + * 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 + */ + CrsMatrix.prototype.trace = function () { + // size + var size = this._size; + // check dimensions + var rows = size[0]; + var columns = size[1]; + // matrix must be square + if (rows === columns) { + // calulate sum + var sum = 0; + // check we have data (avoid looping rows) + if (this._values.length > 0) { + // loop rows + for (var i = 0; i < rows; i++) { + // k0 <= k < k1 where k0 = _ptr[i] && k1 = _ptr[i+1] + var k0 = this._ptr[i]; + var k1 = this._ptr[i + 1]; + // loop k within [k0, k1[ + for (var k = k0; k < k1; k++) { + // column index + var j = this._index[k]; + // check row + if (i === j) { + // accumulate value + sum = math.add(sum, this._values[k]); + // exit loop + break; + } + if (j > i) { + // exit loop, no value on the diagonal for column j + break; + } + } + } + } + // return trace + return sum; + } + throw new RangeError('Matrix must be square (size: ' + string.format(size) + ')'); + }; + return CrsMatrix; -}; +}; \ No newline at end of file diff --git a/test/type/matrix/CrsMatrix.test.js b/test/type/matrix/CrsMatrix.test.js index 7d0b37e29..bed2f00dd 100644 --- a/test/type/matrix/CrsMatrix.test.js +++ b/test/type/matrix/CrsMatrix.test.js @@ -1,17 +1,24 @@ var assert = require('assert'); var math = require('../../../index'); +var index = math.index; +var Matrix = math.type.Matrix; var CrsMatrix = math.type.CrsMatrix; +var DenseMatrix = math.type.DenseMatrix; var Complex = math.type.Complex; describe('CrsMatrix', function() { describe('constructor', function() { - it('should throw exception if called with no argument', function() { - assert.throws(function () { new CrsMatrix(); }, /data must be an array/); + it('should create empty matrix if called with no argument', function() { + var m = new CrsMatrix(); + assert.deepEqual(m._size, [0]); + assert.deepEqual(m._values, []); + assert.deepEqual(m._index, []); + assert.deepEqual(m._ptr, [0]); }); - it('should create a CRS from an array', function () { + it('should create a CCS from an array', function () { var m = new CrsMatrix( [ [10, 0, 0, 0, -2, 0], @@ -21,54 +28,86 @@ describe('CrsMatrix', function() { [0, 8, 0, 9, 9, 13], [0, 4, 0, 0, 2, -1] ]); - assert.equal(m._format, 'crs'); assert.deepEqual(m._size, [6, 6]); assert.deepEqual(m._values, [10, -2, 3, 9, 3, 7, 8, 7, 3, 8, 7, 5, 8, 9, 9, 13, 4, 2, -1]); assert.deepEqual(m._index, [0, 4, 0, 1, 5, 1, 2, 3, 0, 2, 3, 4, 1, 3, 4, 5, 1, 4, 5]); assert.deepEqual(m._ptr, [0, 2, 5, 8, 12, 16, 19]); }); - it('should create a CRS from an array, empty column', function () { + it('should create a CCS from an array, empty column', function () { var m = new CrsMatrix( [ [1, 0, 0], [0, 0, 1] ]); - assert.equal(m._format, 'crs'); assert.deepEqual(m._size, [2, 3]); assert.deepEqual(m._values, [1, 1]); assert.deepEqual(m._index, [0, 2]); assert.deepEqual(m._ptr, [0, 1, 2]); }); - - it('should create a CRS from an array, empty row', function () { + + it('should create a CCS from an array, empty row', function () { var m = new CrsMatrix( [ [1, 0], [0, 0], [0, 1] ]); - assert.equal(m._format, 'crs'); assert.deepEqual(m._size, [3, 2]); assert.deepEqual(m._values, [1, 1]); assert.deepEqual(m._index, [0, 1]); assert.deepEqual(m._ptr, [0, 1, 1, 2]); }); - it('should create an empty CRS from an array', function () { + it('should create an empty CCS from an array', function () { var m = new CrsMatrix([]); - assert.equal(m._format, 'crs'); assert.deepEqual(m._size, [0, 0]); assert.deepEqual(m._values, []); assert.deepEqual(m._index, []); assert.deepEqual(m._ptr, [0]); }); + it('should create a CCS from a vector', function () { + var m = new CrsMatrix([1, 2, 3]); + assert.deepEqual(m._size, [3, 1]); + assert.deepEqual(m._values, [1, 2, 3]); + assert.deepEqual(m._index, [0, 0, 0]); + assert.deepEqual(m._ptr, [0, 1, 2, 3]); + }); + + it('should create a CrsMatrix from another CrsMatrix', function () { + var m1 = new CrsMatrix( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12] + ]); + var m2 = new CrsMatrix(m1); + assert.deepEqual(m1._size, m2._size); + assert.deepEqual(m1._values, m2._values); + assert.deepEqual(m1._index, m2._index); + assert.deepEqual(m1._ptr, m2._ptr); + }); + + it('should create a CrsMatrix from a DenseMatrix', function () { + var m1 = new DenseMatrix( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12] + ]); + var m2 = new CrsMatrix(m1); + assert.deepEqual(m1.size(), m2.size()); + assert.deepEqual(m1.toArray(), m2.toArray()); + }); + it('should throw an error when called without new keyword', function () { assert.throws(function () { CrsMatrix(); }, /Constructor must be called with the new operator/); }); }); - + describe('size', function() { it('should return the expected size', function() { @@ -79,284 +118,6 @@ describe('CrsMatrix', function() { }); }); - describe('toArray', function () { - - it('should return array', function () { - var m = new CrsMatrix({ - values: [10, -2, 3, 9, 3, 7, 8, 7, 3, 8, 7, 5, 8, 9, 9, 13, 4, 2, -1], - index: [0, 4, 0, 1, 5, 1, 2, 3, 0, 2, 3, 4, 1, 3, 4, 5, 1, 4, 5], - ptr: [0, 2, 5, 8, 12, 16, 19], - size: [6, 6] - }); - - var a = m.toArray(); - - assert.deepEqual( - a, - [ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - }); - - it('should return array, empty row', function () { - var m = new CrsMatrix({ - values: [1, 1], - index: [0, 1], - ptr: [0, 1, 1, 2], - size: [3, 2] - }); - - var a = m.toArray(); - - assert.deepEqual( - a, - [ - [1, 0], - [0, 0], - [0, 1] - ]); - }); - - it('should return array, complex numbers', function () { - var m = new CrsMatrix({ - values: [new Complex(1, 1), new Complex(2, 2), new Complex(3, 3), new Complex(4, 4), new Complex(5, 5), new Complex(6, 6)], - index: [0, 2, 2, 0, 1, 2], - ptr: [0, 2, 3, 6], - size: [3, 3] - }); - - var a = m.toArray(); - - assert.deepEqual( - a, - [ - [new Complex(1, 1), 0, new Complex(2, 2)], - [0, 0, new Complex(3, 3)], - [new Complex(4, 4), new Complex(5, 5), new Complex(6, 6)] - ]); - }); - }); - - describe('diagonal', function () { - - it('should create CRS matrix (n x n)', function () { - - var m = CrsMatrix.diagonal(3, 3, 1); - - assert.deepEqual(m._size, [3, 3]); - assert.deepEqual(m._values, [1, 1, 1]); - assert.deepEqual(m._index, [0, 1, 2]); - assert.deepEqual(m._ptr, [0, 1, 2, 3]); - - assert.deepEqual( - m.toArray(), - [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1] - ]); - }); - - it('should create CRS matrix (n x n), complex number', function () { - - var m = CrsMatrix.diagonal(3, 3, new Complex(1, 1)); - - assert.deepEqual(m._size, [3, 3]); - assert.deepEqual(m._values, [new Complex(1, 1), new Complex(1, 1), new Complex(1, 1)]); - assert.deepEqual(m._index, [0, 1, 2]); - assert.deepEqual(m._ptr, [0, 1, 2, 3]); - }); - - it('should create CRS matrix (m x n), m > n', function () { - - var m = CrsMatrix.diagonal(4, 3, 1); - - assert.deepEqual(m._size, [4, 3]); - assert.deepEqual(m._values, [1, 1, 1]); - assert.deepEqual(m._index, [0, 1, 2]); - assert.deepEqual(m._ptr, [0, 1, 2, 3]); - - assert.deepEqual( - m.toArray(), - [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1], - [0, 0, 0] - ]); - }); - - it('should create CRS matrix (m x n), m < n', function () { - - var m = CrsMatrix.diagonal(3, 4, 1); - - assert.deepEqual(m._size, [3, 4]); - assert.deepEqual(m._values, [1, 1, 1]); - assert.deepEqual(m._index, [0, 1, 2]); - assert.deepEqual(m._ptr, [0, 1, 2, 3]); - - assert.deepEqual( - m.toArray(), - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0] - ]); - }); - }); - - describe('get', function () { - - it('should throw on invalid element position', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - assert.throws(function () { m.get([-1, 0]); }, /Index out of range \(-1 < 0\)/); - assert.throws(function () { m.get([10, 0]); }, /Index out of range \(10 > 5\)/); - assert.throws(function () { m.get([0, -1]); }, /Index out of range \(-1 < 0\)/); - assert.throws(function () { m.get([0, 10]); }, /Index out of range \(10 > 5\)/); - }); - - it('should get matrix element', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - assert.equal(m.get([0, 0]), 10); - assert.equal(m.get([3, 1]), 0); - assert.equal(m.get([5, 1]), 4); - assert.equal(m.get([5, 5]), -1); - }); - }); - - describe('set', function () { - - it('should throw on invalid element position', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - assert.throws(function () { m.set([-1, 0]); }, /Index out of range \(-1 < 0\)/); - assert.throws(function () { m.set([10, 0]); }, /Index out of range \(10 > 5\)/); - assert.throws(function () { m.set([0, -1]); }, /Index out of range \(-1 < 0\)/); - assert.throws(function () { m.set([0, 10]); }, /Index out of range \(10 > 5\)/); - }); - - it('should remove matrix element', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - m.set([0, 0], 0); - m.set([0, 4], 0); - m.set([5, 1], 0); - - assert.deepEqual( - m.toArray(), - [ - [0, 0, 0, 0, 0, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 0, 0, 0, 2, -1] - ]); - }); - - it('should update matrix element (non zero)', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - m.set([0, 0], 15); - m.set([0, 4], 10); - m.set([5, 1], 20); - - assert.deepEqual( - m.toArray(), - [ - [15, 0, 0, 0, 10, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 20, 0, 0, 2, -1] - ]); - }); - - it('should update matrix element (zero)', function () { - var m = new CrsMatrix([ - [10, 0, 0, 0, -2, 0], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [0, 4, 0, 0, 2, -1] - ]); - - m.set([0, 1], 15); - m.set([0, 5], 10); - m.set([5, 0], 20); - - assert.deepEqual( - m.toArray(), - [ - [10, 15, 0, 0, -2, 10], - [3, 9, 0, 0, 0, 3], - [0, 7, 8, 7, 0, 0], - [3, 0, 8, 7, 5, 0], - [0, 8, 0, 9, 9, 13], - [20, 4, 0, 0, 2, -1] - ]); - }); - }); - - describe('clone', function() { - - it('should clone the matrix properly', function() { - var m1 = new CrsMatrix( - [ - [1, 2, 3], - [4, 5, 6] - ]); - - var m2 = m1.clone(); - - assert.deepEqual(m1.toArray(), m2.toArray()); - }); - }); - describe('toString', function() { it('should return string representation of matrix', function() { assert.equal(new CrsMatrix([[1,2],[3,4]]).toString(), '[[1, 2], [3, 4]]'); @@ -364,22 +125,66 @@ describe('CrsMatrix', function() { }); }); - describe('transpose', function () { + describe('toJSON', function () { - it('should transpose a 2d matrix', function() { - var m = new CrsMatrix([[1,2,3],[4,5,6]]); - assert.deepEqual(m.transpose().toArray(), [[1,4],[2,5],[3,6]]); - - m = new CrsMatrix([[1,2],[3,4]]); - assert.deepEqual(m.transpose().toArray(), [[1,3],[2,4]]); - - m = new CrsMatrix([[1,2,3,4]]); - assert.deepEqual(m.transpose().toArray(), [[1],[2],[3],[4]]); + it('should serialize Matrix', function() { + assert.deepEqual( + new CrsMatrix([[1, 2], [3, 4]]).toJSON(), + { + mathjs: 'CrsMatrix', + values: [1, 2, 3, 4], + index: [0, 1, 0, 1], + ptr: [0, 2, 4], + size: [2, 2] + }); }); + }); + + describe('fromJSON', function () { - it('should throw an error for invalid matrix transpose', function() { - var m = new CrsMatrix([[]]); - assert.throws(function () { m.transpose(); }); + it('should deserialize Matrix', function() { + var json = { + mathjs: 'CrsMatrix', + values: [1, 2, 3, 4], + index: [0, 1, 0, 1], + ptr: [0, 2, 4], + size: [2, 2] + }; + var m = CrsMatrix.fromJSON(json); + assert.ok(m instanceof Matrix); + + assert.deepEqual(m._size, [2, 2]); + assert.deepEqual( + m.valueOf(), + [ + [1, 2], + [3, 4] + ]); + }); + }); + + describe('format', function () { + it('should format matrix', function() { + var m = new CrsMatrix( + [ + [0, 0], + [0, 1/3] + ]); + assert.equal(m.format(), 'CRS [2 x 2] density: 0.25\n\n (1, 1) ==> 0.3333333333333333'); + + m = new CrsMatrix( + [ + [0, 0], + [0, 1/3] + ]); + assert.equal(m.format(3), 'CRS [2 x 2] density: 0.25\n\n (1, 1) ==> 0.333'); + + m = new CrsMatrix( + [ + [0, 0], + [0, 1/3] + ]); + assert.equal(m.format(4), 'CRS [2 x 2] density: 0.25\n\n (1, 1) ==> 0.3333'); }); }); }); \ No newline at end of file