if (typeof define !== 'function') { var define = require('amdefine')(module); } define(function(require, exports, module) { /* * MdArray * https://github.com/michaelw/MdArray * * A multi-dimensional array implementation for use in machine learning and * other calculations. * * Copyright (c) 2016 Michael Webster * Licensed under the MIT license. */ /* * FIXME: Need to add scalar multiplication/division, add etc. * */ var _us = require("underscore"); 'use strict'; /** * @public * @class * @alias MdArray * @classdesc The MdArray class provides a multi-dimensional array like * abstraction for javascript. * * Construct an Ml_ndarray object from it's shape in terms of rows,columns, * and any other dimensions it has. * * The parameter to the constructor is a javascript object containing keyword * arguments. Eg.: * * { * data: [1, 2, 3, 4], * shape: [2, 2] * } * * @param arrayArgs An object containing the keyword args for the constructor. * @constructor */ var MdArray = function(arrayArgs) { 'use strict'; assert(arrayArgs.data, "MdArray constructor must be called with an object having at " + "least one of the shape or data fields."); this.data = arrayArgs.data; this.dSize = this.data.length; var dims; var tSize; var strides; if (arrayArgs.shape) { dims = arrayArgs.shape; tSize = _us.reduce(dims, function(memo, val) { return memo * val; }, 1); strides = getStrides(dims); } else { dims = [arrayArgs.data.length]; tSize = arrayArgs.data.length; strides = [1]; } this.dims = dims; this.tSize = tSize; this.strides = strides; assert(this.tSize == this.dSize, "Data must by the right size for the supplied dimenstions."); // Return this for chaining? return this; }; MdArray.prototype = { constructor: MdArray, /** * Get the value of the element indexed by the coordinate values in the * arguments. * * @param arguments indices - eg. X.get(0,1,2); * @returns The value at the array position specified. * @method */ get: function() { 'use strict'; //var args = _us.toArray(arguments); var idx = this.findIndex(arguments); return this.data[idx]; }, /** * Set the value in the array element indexed by the remaining values in * arguments. * * @param val The value to be assigned into the array. * @param rest A list of indices, eg. X.set(x, 0,1,2); * * @method */ set: function(val) { 'use strict'; var idx = this.findIndex(_us.rest(arguments)); this.data[idx] = val; }, /** * Return the required 1-d javascript array index indicated by the supplied * index coordinates. * * @param idx An array containing the coordinate of the requested element. * * @returns The index into data indicated by the supplied idx parameter. * @method */ findIndex: function(idx) { 'use strict'; if (typeof idx[0] != 'number') { idx = idx[0]; } assert(idx.length == this.strides.length, "Wrong number of arguments to index array with " + this.strides.length + " dimensions."); var md_idx = _us.zip(idx, this.strides); return _us.reduce(md_idx, function(memo, val) { return memo + val[0] * val[1]; }, 0); }, /** * Return a string representation of the the multi-Dimensional array. * * @returns A readable string for the array. * * @method */ toString: function() { var idxInfo = _us.zip(this.dims.slice(0), this.strides.slice(0)); function ts(dimStrides, idx, data) { var s = "["; if (dimStrides.length == 1) { s += "\t"; var first = dimStrides[0]; for (var i = 0; i < first[0]; i++) { s += " " + data[idx + i * first[1]]; } s += " ]"; } else { var first = _us.first(dimStrides); for (var i = 0; i < first[0]; i++) { s += ts(_us.rest(dimStrides), idx + i * first[1], data); if (i < first[0] - 1) { s += "\n"; } } s += "]"; } if (dimStrides.length == idxInfo.length) { s += "\n"; } return s; } return ts(idxInfo, 0, this.data); }, /** * Return the data of this MdArray as a flat array. * * @returns A flattened array version of this. * @method */ flatten: function() { return this.data; }, /** * Apply the function opFn to all pairs of matching elements in this and that. * * @param that The other object that is part of this operation. * @param opFn The operation to be applied to pairs of elements from this * and that. * @param dim The dimension along which to apply opFn. For example, dim=1 to * apply down columns of a 2D array. * * @returns A new MdArray containing the results of applying opFn to * this and that. * * @method */ applyOp: function(that, opFn, dim) { if (typeof that === 'number') { // Scalar that case. var newArray = new MdArray({data : this.data.slice(0), shape : this.dims}); _us.each(newArray.data, function(val, idx) { newArray.data[idx] = opFn(val, that); }); return newArray; } var rowsEqual = this.dims[0] === that.dims[0]; var colsEqual = this.dims[1] === that.dims[1]; if (rowsEqual && colsEqual && typeof dim === 'undefined') { // Apply the operation accross very element of the two arrays. var zipped = _us.zip(this.data, that.data); var newData = _us.map(zipped, function(val) { return opFn(val[0], val[1]); }); return new MdArray({data: newData, shape: this.dims}); } else { var d = (dim || (rowsEqual && "cols") || (colsEqual && "rows")); assert(d, "Cannot apply operation across dimensions of different size."); if (d == "rows" && !colsEqual || d == "cols" && !rowsEqual) { assert(false, "Cannot apply operation accross the requested dimension."); } if (d == "rows") { /* * if d == "rows" and this and that have the same number of rows, * then we're applying the operator as follows: * * - apply op to row0 of this against row0 of that * - apply op to row1 of this against row1 of that * : * - apply op to last row of this against last row of that * * If this has a lesser number of rows than that, then first we * create a new array of the same size as that, which is a copy * of this, with the last row of this repeated the required number of * times. We do a similar operation if that has less rows than this. */ var finalThis = this; var finalThat = that; if (finalThis.dims[0] < finalThat.dims[0]) { // extend finalThis along rows. finalThis = finalThis.extendDims(finalThat.dims.slice(0)); } else { // extend final That along rows. finalThat = finalThat.extendDims(finalThis.dims.slice(0)); } var newData = _us.map(finalThis.data, function(val, idx) { return opFn(val, finalThat.data[idx]); }); return new MdArray({data: newData, shape : finalThis.dims.slice(0)}); } else { // By columns. var finalThis = this; var finalThat = that; if (finalThis.dims[1] < finalThat.dims[1]) { // extend finalThis along rows. finalThis = finalThis.extendDims(finalThat.dims.slice(0)); } else { // extend final That along rows. finalThat = finalThat.extendDims(finalThis.dims.slice(0)); } var newData = _us.map(finalThis.data, function(val, idx) { return opFn(val, finalThat.data[idx]); }); return new MdArray({data: newData, shape : finalThis.dims.slice(0)}); } } }, /** * Test whether the MdArray that is compatible with this one, so that * element wise operations are able to be applied between this and that. * * @param that An MdArray object to have it's dimensions checked against * this. * * @returns true => this and that are compatible, false otherwise. * * @method */ compatible: function(that) { var dims = _us.zip(this.dims, that.dims); return _us.every(dims, function(x) { return x[0] === x[1]; }); }, /** * Add that to this, and return a new array containing the result. * * @param that The array to be added to this one. * * @returns A new MdArray containing the values of that + this. * * @method */ add: function(that) { return this.applyOp(that, function(x, y) { return x+y; }); }, /** * Subtract that from this, and return a new MdArray containing the result. * * @param that The array to be subtracted from this one. * * @returns A new MdArray containing the values of this - that. * * @method */ sub: function(that) { return this.applyOp(that, function(x, y) { return x-y; }); }, /** * Multiply this by that and return a new MdArray containing the result. * * @param that The array to be multiplied with this one. * * @returns A new MdArray containing the values of this * that. * * @method */ mul: function(that) { return this.applyOp(that, function(x, y) { return x*y; }); }, /** * Divide this by that and return a new MdArray containing the result. * * @param that The array to divide this by. * * @returns A new MdArray containing the values of this / that. * * @method */ div: function(that) { if (that instanceof MdArray) { if (_us.some(that.data, function(x) { return x == 0; })) { throw new Error("MdArray.div: divisor contains a zero."); } } else { if (that == 0) { throw new Error("MdArray.div: Attempt to divide by zero."); } } return this.applyOp(that, function(x, y) { return x/y; }); }, /** * Return the dot product of this and that. * * Note: This method only runs for 1 or 2 dimensional MdArrays. * * @param that An MdArray to be dotted with this one. * * @returns A value if this and that are 1-dimensional arrays, or a m x p * size MdArray where m is the number of rows in this, and p is the * number of columns in that. */ dot: function(that) { assert(this.dims.length <= 2, "dot is only defined for 1 or 2 dimensional MdArrays."); assert(this.dims.length === that.dims.length, "dot is only defined between MdArray's with the same number of dimensions."); if (this.dims.length === 1) { assert(this.dSize === that.dSize, "dot is only defined for 1 dimensional MdArray's of the same size."); return _us.reduce(_us.zip(this.data, that.data), function(memo, val) { return memo + val[0] * val[1]; }, 0); } else { var dotProd = MdArray.zeros({shape: [this.dims[0], that.dims[1]]}); for (var i = 0; i < this.dims[0]; i++) { for (var j = 0; j < that.dims[1]; j++) { var rowData = this.slice([i.toString(), ":"]).flatten(); var colData = that.slice([":", j.toString()]).flatten(); var val = _us.reduce(_us.zip(rowData, colData), function(memo, v) { return memo + v[0] * v[1]; }, 0); dotProd.set(val, i, j); } } return dotProd; } }, /** * Return a new array containing the transpose of this array or, if inPlace * is true, modify this array to be Transposed. * * @param args An optional object containing a boolean valued key inPlace. * * @returns A new array, or this. * * @method */ T: function(args /* inPlace == true/false => default false*/) { var inPlace = (args && args.inPlace) || false; if (inPlace) { this.strides = this.strides.reverse(); return this; } else { var transposed = new MdArray( {data : this.data.slice(0), shape: this.dims.slice(0).reverse()} ); return transposed; } }, /** * Apply function f to each element of the MdArray. * * @param f A function that can be applied to elements of this. * * @method */ foreach: function(f) { var indices = enumerateDims(this.dims); for (var i = 0; i < indices.length; i++) { f(this.data[this.findIndex(indices[i])]); } }, /** * Raise the values in this array to power exp. * * NOTE: This operation occurs in place. * * @param exp The exponent. */ pow: function(exp) { var t = this; var indices = enumerateDims(t.dims); _us.each(indices, function(idx) { var dIndex = t.findIndex(idx); var val = Math.pow(t.data[dIndex], exp); t.data[dIndex] = val; }); return t; }, /** * Create and return an ArrayView object representing a slice of this * array. * * The sliceInfo parameter is an array of strings that follow the python * numpy array slice syntax. So for example: * * - "2:5" would be taken to mean take just all the values between 2 and 5, * - ":" would mean take all possible values for this dimension. * - ":5" would mean take all values up to 5, * and so on. * * @param sliceInfo An array containing strings of sliceInfo. * * @return A new ArrayView object appropriately created for the slice. * * @method */ slice: function(sliceInfo) { return new ArrayView ( this.data, this.dims.slice(0), this.strides.slice(0), sliceInfo ); }, /** * Create and return a new MdArray object containing a copy of a slice of * this array. * * The sliceInfo parameter is an array of strings that follow the python * numpy array slice syntax. So for example: * * - "2:5" would be taken to mean take just all the values between 2 and 5, * - ":" would mean take all possible values for this dimension. * - ":5" would mean take all values up to 5, * and so on. * * @param sliceInfo An array containing strings of sliceInfo. * * @return A new MdArrayObject appropriately created for the slice. * * @method */ newSlice: function(sliceInfo) { var view = new ArrayView ( this.data, this.dims.slice(0), this.strides.slice(0), sliceInfo ); var new_data = view.flatten(); var newDims = _us.map(view.slices, function(sl) { return sl.end - sl.start; }); return new MdArray({data: new_data, shape: newDims}); }, /** * Add a column of ones as the first column in the MdArray. * * NOTE: This only works for 2 dimensional arrays. * * @returns A new MdArray with the 0'th column being all 1s. * * @method */ addOnes: function() { assert(this.dims.length == 2, "addOnes only defined for 2 dimensional arrays."); var dims = this.dims; var stride = this.strides[0] + 1; var newData = this.data.slice(0); //var rows = _us.range(0, dims.length); var rows = _us.range(0, dims[0]); _us.each(rows, function(val) { newData.splice(val * stride, 0, 1.0); }); return new MdArray({data: newData, shape: [dims[0], dims[1] + 1]}); }, /** * Return the sum of the values in this MdArray. If a dimension is supplied, * the sum occurs down the requested dimension. * * @param dimension The dimension to sum along. * * @returns A new MdArray containing the sum, or if no dimension is * supplied, a scalar value set to the sum over the entire * MdArray. */ sum: function(dimension) { if (typeof dimension === 'undefined') { return _us.reduce(this.data, function(memo, val) { return memo + val; }, 0); } var dim = this.dims[dimension]; var data = []; var sliceInfo = _us.map(_us.range(this.dims.length), function(val) { return ":"; }); for (var i = 0; i < dim; i++) { sliceInfo[dimension] = i.toString(); var newView = this.slice(sliceInfo); var newSum = 0; newView.foreach(function(x) { newSum += x;}); data.push(newSum); } if (dimension == 1) { // Return as a row. return new MdArray({data: data, shape: [1, dim]}); } else { // Return as a column. return new MdArray({data: data, shape: [dim, 1]}); } }, /** * Return the maximum of the values in this MdArray. If a dimension is * supplied, the max is down the requested dimension. * * @param dimension The dimension to find the max along. * * @returns A new MdArray containing the max, or if no dimension is supplied * a scalar with the value of max over the entire MdArray. */ max: function(dimension) { if (typeof dimension === 'undefined') { return _us.reduce(this.data, function(memo, val) { if (val > memo) { return val; } }, Number.MIN_SAFE_INTEGER); } var data = []; var dim = this.dims[dimension]; var sliceInfo = _us.map(_us.range(this.dims.length), function(val) { return ":"; }); for (var i = 0; i < dim; i++) { var max = Number.MIN_SAFE_INTEGER; sliceInfo[dimension] = i.toString(); var newView = this.slice(sliceInfo); var newSum = 0; newView.foreach(function(x) { if (x > max) { max = x; } }); data.push(max); } if (dimension == 1) { // Return as a row. return new MdArray({data: data, shape: [1, dim]}); } else { // Return as a column. return new MdArray({data: data, shape: [dim, 1]}); } }, /** * Return the minimum of the values in this MdArray. If a dimension is * supplied, the min is down the requested dimension. * * @param dimension The dimension to find the min along. * * @returns A new MdArray containing the min, or if no dimension is supplied * a scalar with the value of min over the entire MdArray. */ min: function(dimension) { if (typeof dimension === 'undefined') { return _us.reduce(this.data, function(memo, val) { if (val < memo) { return val; } }, Number.MAX_SAFE_INTEGER); } var data = []; var dim = this.dims[dimension]; var sliceInfo = _us.map(_us.range(this.dims.length), function(val) { return ":"; }); for (var i = 0; i < dim; i++) { var min = Number.MAX_SAFE_INTEGER; sliceInfo[dimension] = i.toString(); var newView = this.slice(sliceInfo); var newSum = 0; newView.foreach(function(x) { if (x < min) { min = x; } }); data.push(min); } if (dimension == 1) { // Return as a row. return new MdArray({data: data, shape: [1, dim]}); } else { // Return as a column. return new MdArray({data: data, shape: [dim, 1]}); } }, /** * Return the mean of the values in this MdArray. If a dimension is * supplied, the mean is down the requested dimension. * * @param dimension The dimension to find the mean along. * * @returns A new MdArray containing the mean, or if no dimension is * supplied, a scalar with the value of mean over the entire * MdArray. */ mean: function(dimension) { if (typeof dimension === 'undefined') { var sum = _us.reduce(this.data, function(memo, val) { return memo + val; }, 0); return sum / this.data.length; } var data = []; var dimSize = _us.reduce(this.dims, function(memo, val, idx) { if (idx != dimension) { return memo * val; } else { return memo; } }, 1); return this.sum(dimension).div(dimSize); }, /** * Return the standard deviation of the values in this MdArray. If a * dimension is supplied, the standard deviation is calculated down the * requested dimension. * * @param dimension The dimension to find the standard deviation along. * * @returns A new MdArray containing the std dev, or if no dimension is * supplied, a scalar with the value of std dev over the entire * MdArray. */ std: function(dim) { var d = dim || 1; var XSq = this.copy().pow(2); var EXSq = XSq.mean(d); var muSq = (this.mean(d)).pow(2); return EXSq.sub(muSq).pow(0.5); }, /** * Extend the dimensions of this array to fit the values passed in in * newDims. The extension happens by repeating the last value, row, column * or other dimension, until the MdArray is of the requested size. * * @param newDims The new dimensions the returned MdArray is to fill. * * @return This MdArray extended to have the requested dimensions. */ extendDims: function(newDims) { var newMdArray = MdArray.extendDims(this, newDims); return newMdArray; }, /** * Create a copy of this MdArray, and return it. * * @param A new MdArray object that is a copy of this one. */ copy: function() { 'use strict'; var newA = Object.create(MdArray.prototype); newA.data = this.data.slice(0); newA.tSize = this.tSize; newA.dSize = this.dSize; newA.dims = this.dims.slice(0); newA.strides = this.strides.slice(0); return newA; } }; /** * Extend arrayInst to meet the dimensions in nDims. * * @param arrayInst An instance of MdArray. * @param nDims An array containing the required dims after extension. * * @returns A new array that is a copy of arrayInst, but extended to satisfy the * extended dimensions in nDims. */ MdArray.extendDims = function(arrayInst, nDims) { var bothDims = _us.zip(arrayInst.dims.slice(0), nDims.slice(0)); var diffElement = _us.find(bothDims, function(val) { return val[0] !== val[1]; }); var diffIndex = _us.indexOf(bothDims, diffElement); // Create a slice of arrayInst containing the last row/column or whatever // the different dimension is of arrayInst. var sliceInfo = _us.map(arrayInst.dims, function(dimVal, idx) { if (idx == diffIndex) { return (diffElement[0] - 1).toString(); } else { return ":"; } }); var orgView = arrayInst.slice(sliceInfo); var lastData = []; orgView.foreach(function (x) { lastData.push(x); }); var newArray = MdArray.zeros({shape: nDims}); var allCoords = enumerateDims(newArray.dims); _us.each(allCoords, function(idx) { if (idx[diffIndex] < diffElement[0]) { newArray.set(arrayInst.get(idx), idx); } else { newArray.set(lastData[idx[1 - diffIndex]], idx); } }); return newArray; }; /* * Factory functions. * * Includes: * - arange Create an MdArray containing a range of numbers. * - zeros Create an MdArray containing 0s. * - ones Create an MdArray containing 1s. */ /** * Return an MdArray of zeros. Args are the shape of the array. * * @param args An object containing a shape field with an array of * dimensions. * * @returns An MdArray of the requested shape containing all zeros. */ MdArray.zeros = function(args /* args.shape = [d1, d2, ...dn] */) { 'use strict'; args.fill = 0; return MdArray.createFilled(args); }; /** * Return an MdArray of ones. Args are the shape of the array. * * @param args An object containing a shape field with an array of * dimensions. * * @returns An MdArray of the requested shape containing all ones. */ MdArray.ones = function(args /* args.shape = [d1, d2, ...dn] */) { 'use strict'; args.fill = 1; return MdArray.createFilled(args); }; /** * Return an MdArray of containing the requested fill value. * * @param args An object containing a shape field with an array of * dimensions, and a fill field. * * @returns An MdArray of the requested shape containing the repeated fill value. */ MdArray.createFilled = function(args/* args.shape = [d1, d2, ...dn], args.val = n */) { 'use strict'; var fillVal = args.fill || 0; var ma = Object.create(MdArray.prototype); var tSize = _us.reduce(args.shape, function(memo, val) { return memo * val; }, 1); var data = new Array(tSize); for (var i = 0; i < tSize; i++) { data[i] = fillVal; } ma.data = data; ma.tSize = tSize; ma.dSize = ma.data.length; ma.dims = args.shape; ma.strides = getStrides(ma.dims); return ma; }; /** * Create an MdArray containing a range of numeric values. * * @param args An object containing a shape field with an array of dimensions, * an end field, and an optional start and by field. * * @return An MdArray containing the requested range of values. */ MdArray.arange = function(args /* [start,] end [, by] [, shape] */) { 'use strict'; var start = args.start || 0; var end = args.end; var by = args.by || 1; var rangeVal = _us.range(start, end, by); var shapeVal = args.shape || [rangeVal.length]; return new MdArray({data : rangeVal, shape : shapeVal}); }; MdArray.createFromRows = function(rows) { 'use strict'; var numRows = rows.length; var numCols = rows[0].length; var d = [] _us.each(rows, function(rowVal) { d.push(rowVal[0]); d.push(rowVal[1]); }); return new MdArray({data: d, shape: [numRows, numCols]}); }; /** * Given the dimensions of an MdArray, return an array of strides for the * array. * * @param dims An array of dimensions for the MdArray. * * @returns An array containing the strides for the MdArray. */ function getStrides(dims) { 'use strict'; if (dims.length == 1) { return [1]; } var reversed = dims.slice(0).reverse(); var strides = []; _us.each(reversed, function(val, idx, arr) { if (idx == 0) { strides.unshift(1); strides.unshift(val); } else if (idx != arr.length - 1) { strides.unshift(val * strides[0]); } }); return strides; } /** * @public * @class * @alias ArrayView * @classdesc A view on an MdArray. */ function ArrayView(orgData, orgDims, orgStrides, sliceInfo) { this.data = orgData; this.dims = orgDims; this.strides = orgStrides; this.slices = processSlices(orgDims, sliceInfo); this.indices = enumerateSlices(this.slices); return this; } ArrayView.prototype = Object.create(MdArray.prototype); _us.extend (ArrayView.prototype, { constructor: ArrayView, /** * Return the required 1-d javascript array index indicated by the supplied * index coordinates. * * @param idx An array containing the coordinate of the requested element. * @method */ findIndex: function(idx) { 'use strict'; var myThis = this; if (idx[0] instanceof Array) { idx = idx[0]; } assert(idx.length == this.strides.length, "Wrong number of arguments to index array with " + this.strides.length + " dimensions."); var md_idx = _us.zip(idx, this.slices, this.strides); assert(_us.every(md_idx, function(val, idx) { return (val[0] + val[1].start) < myThis.dims[idx]; }), "ArrayView: Index out of range."); return _us.reduce(md_idx, function(memo, val) { return memo + (val[0] + val[1].start) * val[2]; }, 0); }, /** * Return a string representation of the the multi-Dimensional array. * * @returns A readable string for the array. * @method */ toString: function() { var idxInfo = _us.zip(this.slices.slice(0), this.strides.slice(0)); function ts(ss, idx, data) { var s = "["; if (ss.length == 1) { s += "\t"; var firstDim = ss[0]; for (var i = firstDim[0].start; i < firstDim[0].end; i++) { s += " " + data[idx + i * firstDim[1]]; } s += " ]"; } else { var firstDim = _us.first(ss); for (var i = firstDim[0].start; i < firstDim[0].end; i++) { s += ts(_us.rest(ss), idx + i * firstDim[1], data); if (i < firstDim[0].end - 1) { s += "\n"; } } s += "]"; } if (ss.length == idxInfo.length) { s += "\n"; } return s; } return ts(idxInfo, 0, this.data); }, /** * Return a new array containing the data of the view. * @method */ flatten: function() { var flattened = []; this.foreach(function(x) {flattened.push(x); }); return flattened; }, /** * Apply function f to each element in this ArrayView sequentially. * * @param f A function to apply to each element in this. * @method */ foreach: function(f) { var indices = this.indices; for (var i = 0; i < indices.length; i++) { f(this.data[this.findIndex(indices[i])]); } }, /** * @alias enumerateIndices * Enumerate the indices of this ArrayView object. * * @return An array containing the indices for each element in this array * view. * @method */ enumerateIndices: function() { return this.indices; }, /** * Set the values in this slice from vals. * * @param vals An array of values. */ setSlice : function(vals) { var sliceIndices = this.indices; for (var i = 0; i < vals.length; i++) { this.set.apply(this, [vals[i]].concat(sliceIndices.shift())); } return; }, /** * Assign this view's values to be the same as that's values. * * @param that A compatible MdArray or ArrayView. */ assign: function(that) { var sliceIndices = this.indices; for (var i = 0; i < sliceIndices.length; i++) { this.set(that.get(sliceIndices[i]), sliceIndices[i]); } return; }, /** * Apply opFn to the values of this and that. If a dim is requested, then * apply accross that dim. * * @param that Another array view object - or possibly an MdArray object. * @param opFn A function to apply to combinations of elements from this * and that. * @param dim If present, indicates a dimension along with to apply the * opFn. * * @return A new MdArray containing the results of applying opFn to * this and that as requested. * @method */ applyOp: function(that, opFn, dim) { var rowsEqual = this.dims[0] === that.dims[0]; var colsEqual = this.dims[1] === that.dims[1]; if (rowsEqual && colsEqual && typeof dim === 'undefined') { // Apply the operation accross very element of the two views. var newData = _us.map(this.indices, function(idx) { return opFn(this.get(idx), that.get(idx)); }); return new MdArray({data: newData, shape: this.dims}); } else { var d = (dim || (rowsEqual && "rows") || (colsEqual && "cols")); assert(d, "Cannot apply operation across dimensions of different size."); var idcs = []; if (d == "rows") { //var range = _us.range(this.dims[0]); for (var i = 0; i < this.dims[0]; i++) { var rowIndices = []; rowIndices = _us.filter(this.indices, function(idxVal) { return idxVal[0] === i; }); idcs = idcs.concat(rowIndices); } } else { // By columns. //var range = _us.range(this.dims[1]); var colIndices = []; for (var i = 0; i < this.dims[1]; i++) { colIndices = _us.filter(this.indices, function(idxVal) { return idxVal[1] === i; }); idcs = idcs.concat(colIndices); } } console.log("idcs is: " + idcs.toString()); var values = []; var myThis = this; _us.each(idcs, function(idx) { values.push(opFn(myThis.get.apply(myThis, idx), that.get.apply(that, idx))); }); if (d == "rows") { return new MdArray({ data: values, shape: [this.dims[0], 1]}); } else { return new MdArray({ data: values, shape: [1, this.dims[0]]}); } } }, /** * Add that to this, and return a new array containing the result. * * @param that The array to be added to this one. * * @returns A new MdArray containing the values of that + this. * * @method */ add: function(that, dim) { return this.applyOp(that, function(x, y) { return x+y; }, dim); }, /** * Subtract that from this, and return a new MdArray containing the result. * * @param that The array to be subtracted from this one. * * @returns A new MdArray containing the values of this - that. * * @method */ sub: function(that, dim) { return this.applyOp(that, function(x, y) { return x-y; }, dim); }, /** * @alias mul * Multiply this by that and return a new MdArray containing the result. * * @param that The array to be multiplied with this one. * * @returns A new MdArray containing the values of this * that. * * @method */ mul: function(that, dim) { return this.applyOp(that, function(x, y) { return x*y; }, dim); }, /** * Divide this by that and return a new MdArray containing the result. * * @param that The array to divide this by. * * @returns A new MdArray containing the values of this / that. * * @method */ div: function(that, dim) { if (that instanceof MdArray) { // FIXME: Need to fix this to work for views. if (_us.some(that.data, function(x) { return x == 0; })) { throw new Error("MdArray.div: divisor contains a zero."); } } else { if (that == 0) { throw new Error("MdArray.div: Attempt to divide by zero."); } } return this.applyOp(that, function(x, y) { return x/y; }, dim); } }); /** * Return an array of objects with start and end index values calculated * from the values in dims and sliceInfo. * * @param dims An array containing the dimensions of the original MdArray. * @param sliceInfo An array of slice strings. * * @returns An array of objects, each of which has a start and end indicating * the start and end of a slice for a particular dimension. */ function processSlices(dims, sliceInfo) { assert(dims.length === sliceInfo.length, "ArrayView: dims and sliceInfo parameters must be of the same length."); var ds = _us.zip(dims, sliceInfo); var processedSlices = _us.map(ds, function(val) { var dim = val[0]; var si = val[1].replace(/\s/g, ""); var siSplits = si.split(":"); var splitObj = {}; if (siSplits.length === 1) { // Just an index splitObj.start = parseInt(siSplits[0]); splitObj.end = Math.min(splitObj.start + 1, dim); } else { splitObj.start = (siSplits[0].length > 0) ? parseInt(siSplits[0]) : 0; splitObj.end = (siSplits[1].length > 0) ? parseInt(siSplits[1]) : dim; } assert(splitObj.start >= 0 && splitObj.start < splitObj.end, "ArrayView: First slice index must be > 0 and < the second index."); assert(splitObj.end <= dim, "ArrayView: Second slice index must be less than dim"); return splitObj; }); return processedSlices; } /** * Return a list of all coordinates for the given MdArray. * * @param dims An array of dimensions for an MdArray. * * @returns An ordered list containing a coordinate for every element of the * MdArray. */ function enumerateDims(dims) { if (dims.length == 0) { return []; } var firstRange = _us.map(_us.range(_us.first(dims)), function(val) { return [val]; }); if (dims.length == 1) { return firstRange; } else { var restDims = enumerateDims(_us.rest(dims)); var final = []; _us.each(firstRange, function(val) { var final1 = _us.map(restDims, function(rVal) { return val.slice(0).concat(rVal.slice(0)); }); final = final.concat(final1); }); return final; } } /** * Return a list of all coordinates for the given MdArray. * * @param dims An array of dimensions for an MdArray. * * @returns An ordered list containing a coordinate for every element of the * MdArray. */ function enumerateSlices(slices) { if (slices.length == 0) { return []; } var firstSlice = _us.first(slices); var firstRange = _us.map(_us.range(firstSlice.start, firstSlice.end), function(val) { return [val - firstSlice.start]; }); if (slices.length == 1) { return firstRange; } else { var restSlices = enumerateSlices(_us.rest(slices)); var final = []; _us.each(firstRange, function(val) { var final1 = _us.map(restSlices, function(rVal) { return val.slice(0).concat(rVal.slice(0)); }); final = final.concat(final1); }); return final; } } /** * Raise an error if possible, when there is a violation of the expectations * of the ml-ndarray module. */ function assert(condition, message) { 'use strict'; if (!condition) { message = message || "Assertion failed"; if (typeof Error !== "undefined") { throw new Error(message); } throw message; } } module.exports = MdArray; });