UNPKG

lomath

Version:

Lomath is a tensorial math library extended from lodash.

1,711 lines (1,684 loc) 87.3 kB
//////////// // lomath // //////////// // Prepare lodash for extension and export var _ = require('lodash').runInContext(); // the module: lodash extended with math mixins var lomath = _.mixin({ AUTHOR: "kengz", VERSION: "0.2.9", ////////////////////////////// // Function builder backend // ////////////////////////////// // We employ clearer terminologies to distinguish the "depth" or the "dimension" of the objects. In general, we call generic array of depth-N a "N-tensor" or "rank-N tensor". A scalar is "0-tensor"; a simple array/vector is "1-tensor", matrix (array of arrays) is "2-tensor", and so on. // A generic function that operates over tensor is built from an atomic function fn taking two scalar arguments. // Applying a function into depths of tensor is done via distribution, and evaluating a multi-argument function is done via associativity. /** * Sample operation to demonstrate function composition. * * @category composition * @param {*} x An argument. * @param {*} y An argument. * * @example * _.op('a', 'b') * // → 'a*b' * */ // sample operation to demonstrate function composition op: function(x, y) { return x + '*' + y; }, /** * Distributes a unary function over every scalar in tensor Y. * * @category composition * @param {Function} fn A unary function. * @param {tensor} Y A non-scalar tensor. * @returns {tensor} A tensor from the function applied element-wise to Y. * * @example * _.distributeSingle(_.square, [1, 2, 3, 4]) * // → [ 1, 4, 9, 16 ] * * _.distributeSingle(_.square, [[1, 2], [3, 4]]) * // → [ [ 1, 4 ], [ 9, 16 ] ] * */ // distribute a unary function over every scalar in tensor Y; distributeSingle: function(fn, Y) { if (!(Y instanceof Array)) return fn(Y); var len = Y.length, res = Array(len); while (len--) res[len] = Y[len] instanceof Array ? lomath.distributeSingle(fn, Y[len]) : fn(Y[len]) return res; }, /** * Distributes a binary function with left tensor X over right scalar y. Preserves the order of arguments. * * @category composition * @param {Function} fn A binary function. * @param {tensor} X A non-scalar tensor. * @param {number} y A scalar. * @returns {tensor} A tensor from the function applied element-wise between X and y. * * @example * _.distributeLeft(_.op([1, 2, 3, 4], 5)) * // where _.op is used to show the order of composition * // → [ '1*5', '2*5', '3*5', '4*5' ] * * _.distributeLeft(_.op, [[1, 2], [3, 4]], 5) * // → [ [ '1*5', '2*5' ], [ '3*5', '4*5' ] ] * */ // Distribute fn with left tensor X over right scalar y. distributeLeft: function(fn, X, y) { var len = X.length, res = Array(len); while (len--) res[len] = X[len] instanceof Array ? lomath.distributeLeft(fn, X[len], y) : fn(X[len], y) return res; }, /** * Distributes a binary function with left scalar x over right tensor Y. Preserves the order of arguments. * * @category composition * @param {Function} fn A binary function. * @param {number} x A scalar. * @param {tensor} Y A non-scalar tensor. * @returns {tensor} A tensor from the function applied element-wise between x and Y. * * @example * _.distributeRight(_.op, 5, [1, 2, 3, 4]) * // where _.op is used to show the order of composition * // → [ '5*1', '5*2', '5*3', '5*4' ] * * _.distributeRight(_.op, 5, [[1, 2], [3, 4]]) * // → [ [ '5*1', '5*2' ], [ '5*3', '5*4' ] ] * */ // Distribute fn with left scalar x over right tensor Y. distributeRight: function(fn, x, Y) { var len = Y.length, res = Array(len); while (len--) res[len] = Y[len] instanceof Array ? lomath.distributeRight(fn, x, Y[len]) : fn(x, Y[len]) return res; }, /** * Distributes a binary function between non-scalar tensors X, Y: pair them up term-wise and calling `_.distribute` recursively. Perserves the order of arguments. * If at any depth X and Y have different lengths, recycle if the mod of lengths is 0. * * @category composition * @param {Function} fn A binary function. * @param {tensor} X A non-scalar tensor. * @param {tensor} Y A non-scalar tensor. * @returns {tensor} A tensor from the function applied element-wise between X and Y. * * @example * _.distributeBoth(_.op, ['a', 'b', 'c'], [1, 2, 3]) * // where _.op is used to show the order of composition * // → [ 'a*1', 'b*2', 'c*3' ] * * _.distributeBoth(_.op, ['a', 'b', 'c'], [1, 2, 3, 4, 5, 6]) * // → [ 'a*1', 'b*2', 'c*3' , 'a*4', 'b*5', 'c*6'] * * _.distributeBoth(_.op, ['a', 'b', 'c'], [[1, 2], [3, 4], [5, 6]]) * // → [ [ 'a*1', 'a*2' ], [ 'b*3', 'b*4' ], [ 'c*5', 'c*6' ] ] * */ // Distribute fn between non-scalar tensors X, Y: pair them up term-wise and calling distribute recursively. // If at any depth X and Y have different lengths, recycle if the mod of lengths is 0. distributeBoth: function(fn, X, Y) { var Xlen = X.length, Ylen = Y.length; if (Xlen % Ylen == 0 || Ylen % Xlen == 0) { var res; if (Xlen > Ylen) { res = Array(Xlen); while (Xlen--) res[Xlen] = lomath.distribute(fn, X[Xlen], Y[Xlen % Ylen]); } else { res = Array(Ylen); while (Ylen--) res[Ylen] = lomath.distribute(fn, X[Ylen % Xlen], Y[Ylen]); } return res; } else throw "Cannot distribute arrays of different dimensions."; }, /** * Generic Distribution: Distribute fn between left tensor X and right tensor Y, while preserving the argument-ordering (vital for non-commutative functions). * Pairs up the tensors term-wise while descending down the depths recursively using `_.distributeBoth`, until finding a scalar to `_.distributeLeft/Right`. * * @category composition * @param {Function} fn A binary function. * @param {tensor} X A tensor. * @param {tensor} Y A tensor. * @returns {tensor} A tensor from the function applied element-wise between X and Y. * * @example * _.distribute(_.op, 'a', [1, 2, 3]) * // where _.op is used to show the order of composition * // → ['a*1', 'a*2', 'a*3'] * * _.distribute(_.op, 'a', [[1, 2], [3, 4]) * // → [ [ 'a*1', 'a*2' ], [ 'a*3', 'a*4' ] ] * * _.distribute(_.op, ['a', 'b', 'c'], [1, 2, 3]) * // → [ 'a*1', 'b*2', 'c*3' ] * * _.distribute(_.op, ['a', 'b', 'c'], [1, 2, 3, 4, 5, 6]) * // → [ 'a*1', 'b*2', 'c*3' , 'a*4', 'b*5', 'c*6'] * * _.distribute(_.op, ['a', 'b', 'c'], [[1, 2], [3, 4], [5, 6]]) * // → [ [ 'a*1', 'a*2' ], [ 'b*3', 'b*4' ], [ 'c*5', 'c*6' ] ] * */ // Generic Distribute: Distribute fn between left tensor X and right tensor Y, while preserving the argument-ordering (vital for non-commutative functions). // lomath pairs up the tensors term-wise while descending down the depths recursively, until finding a scalar to distributeLeft/Right. // Method is at its fastest, and assuming the data depth isn't too deep (otherwise JS will have troubles with it) distribute: function(fn, X, Y) { if (X instanceof Array) return Y instanceof Array ? lomath.distributeBoth(fn, X, Y) : lomath.distributeLeft(fn, X, Y); else return Y instanceof Array ? lomath.distributeRight(fn, X, Y) : fn(X, Y); }, /** * Generic association: take the arguments object or array and apply atomic function (with scalar arguments) from left to right. * * @category composition * @param {Function} fn An atomic binary function (both arguments must be scalars). * @param {...number} [...x] Scalars; can be grouped in a single array. * @returns {number} A scalar from the function applied to all arguments in order. * * @example * _.asso(_.op, 'a', 'b', 'c') * // where _.op is used to show the order of composition * // → 'a*b*c' * * _.asso(_.op, ['a', 'b', 'c']) * // → 'a*b*c' * */ // Generic associate: take the arguments object or array and apply atomic fn (non-tensor) from left to right asso: function(fn, argObj) { var len = argObj.length, i = 0; // optimize arg form based on length or argObj /* istanbul ignore next */ var args = len < 3 ? argObj : _.toArray(argObj), res = fn(args[i++], args[i++]); while (i < len) res = fn(res, args[i++]); return res; }, /** * Generic association with distributivity: Similar to `_.asso` but is for tensor functions; apply atomic fn distributively in order using `_.distribute`. * Usage: for applying fn on tensors element-wise if they have compatible dimensions. * * @category composition * @param {Function} fn An atomic binary function (both arguments must be scalars). * @param {...tensors} [...X] tensors. * @returns {tensor} A tensor from the function applied to all arguments in order. * * @example * _.assodist(_.op, 'a', 'b', 'c') * // where _.op is used to show the order of composition * // → 'a*b*c' * * _.assodist(_.op, 'a', [1, 2, 3], 'b') * // → ['a*1*b', 'a*2*b', 'a*3*b'] * * _.assodist(_.op, 'a', [[1, 2], [3, 4]]) * // → [['a*1', 'a*2'], ['a*3', 'a*4']] * * _.assodist(_.op, ['a', 'b'], [[1, 2], [3, 4]]) * // → [['a*1', 'a*2'], ['b*3', 'b*4']] * */ // Associate with distributivity: Similar to asso but is for tensor functions; apply atomic fn distributively from left to right. // Usage: for applying fn on tensors element-wise if they have matching dimensions. assodist: function(fn, argObj) { var len = argObj.length, i = 0; // optimize arg form based on length or argObj var args = len < 3 ? argObj : _.toArray(argObj), res = lomath.distribute(fn, args[i++], args[i++]); while (i < len) res = lomath.distribute(fn, res, args[i++]); return res; }, // Future: // Future: // Future: // cross and wedge, need index summation too, matrix mult. ///////////////////// // Basic functions // ///////////////////// /** * Concatenates all arguments into single vector by `_.flattenDeep`. * * @category basics * @param {...tensors} [...X] tensors. * @returns {vector} A vector with the scalars from all tensors. * * @example * _.c('a', 'b', 'c') * // → ['a', 'b', 'c'] * * _.c(1, ['a', 'b', 'c'], 2) * // → [1, 'a', 'b', 'c', 2] * * _.c([[1, 2], [3, 4]) * // → [1, 2, 3, 4] * */ // Concat all arguments into single vector by _.flattenDeep c: function() { return _.flattenDeep(_.toArray(arguments)); }, // atomic sum: takes in a tensor (any rank) and sum all values a_sum: function(T) { // actual function call; recurse if need to var total = 0, len = T.length; while (len--) total += (T[len] instanceof Array ? lomath.a_sum(T[len], 0) : T[len]) return total; }, /** * Sums all scalars in all argument tensors. * * @category basics * @param {...tensors} [...X] tensors. * @returns {scalar} A scalar summed from all scalars in the tensors. * * @example * _.sum('a', 'b', 'c') * // → 'abc' * * _.sum(0, [1, 2, 3], [[1, 2], [3, 4]) * // → 16 * */ // sum all values in all arguments sum: function() { var res = 0; var len = arguments.length; while (len--) res += (arguments[len] instanceof Array ? lomath.a_sum(arguments[len]) : arguments[len]) return res; }, /** * Functional sum, Basically Sigma_i fn(T[i]) with fn(T, i), where T is a tensor, i is first level index. * * @category basics * @param {tensor} T A tensor. * @param {function} fn A function fn(T, i) applied to the i-th term of T for the sum. Note the function can access the whole T for any term i for greater generality. * @returns {scalar} A scalar summed from all the terms from the mapped T fn(T, i). * * @example * // sum of the elements multiplied by indices in a sequence, i.e. Sigma_i i*(x_i) * _.fsum([1,1,1], function(T, i){ * return T[i] * i; * }) * // → 0+1+2 * */ fsum: function(T, fn) { var sum = 0; for (var i = 0; i < T.length; i++) sum += fn(T, i); return sum; }, // atomic prod, analogue to a_sum. Multiply all values in a tensor a_prod: function(T) { // actual function call; recurse if need to var total = 1, len = T.length; while (len--) total *= (T[len] instanceof Array ? lomath.a_prod(T[len], 1) : T[len]) return total; }, /** * Multiplies together all scalars in all argument tensors. * * @category basics * @param {...tensors} [...X] tensors. * @returns {scalar} A product scalar from all scalars in the tensors. * * @example * _.prod(1, 2, 3) * // → 6 * * _.prod([1, 2, 3]) * // → 6 * * _.prod(1, [1, 2, 3], [[1, 2], [3, 4]]) * // → 144 * */ // product of all values in all arguments prod: function() { var res = 1, len = arguments.length; while (len--) res *= (arguments[len] instanceof Array ? lomath.a_prod(arguments[len]) : arguments[len]) return res; }, // atomic add: add two scalars x, y. a_add: function(x, y) { return x + y; }, /** * Adds tensors using `_.assodist`. * * @category basics * @param {...tensors} [...X] tensors. * @returns {tensor} A tensor. * * @example * _.add(1, 2, 3) * // → 6 * * _.add(1, [1, 2, 3]) * // → [2, 3, 4] * * _.add(1, [[1, 2], [3, 4]]) * // → [[2, 3], [4, 5]] * * _.add([10, 20], [[1, 2], [3, 4]]) * // → [[11, 12], [23, 24]] * */ // add all tensor arguments element-wise/distributively and associatively add: function() { // sample call pattern: pass whole args return lomath.assodist(lomath.a_add, arguments); }, // atomic subtract a_subtract: function(x, y) { return x - y; }, /** * Subtracts tensors using `_.assodist`. * * @category basics * @param {...tensors} [...X] tensors. * @returns {tensor} A tensor. * * @example * _.subtract(1, 2, 3) * // → -5 * * _.subtract(1, [1, 2, 3]) * // → [0, -1, -2] * * _.subtract(1, [[1, 2], [3, 4]]) * // → [[0, -1], [-2, -3]] * * _.subtract([10, 20], [[1, 2], [3, 4]]) * // → [[9, 8], [17, 16]] * */ // subtract all tensor arguments element-wise/distributively and associatively subtract: function() { return lomath.assodist(lomath.a_subtract, arguments); }, // atomic multiply a_multiply: function(x, y) { return x * y; }, /** * Multiplies tensors using `_.assodist`. * * @category basics * @param {...tensors} [...X] tensors. * @returns {tensor} A tensor. * * @example * _.multiply(1, 2, 3) * // → 6 * * _.multiply(1, [1, 2, 3]) * // → [1, 2, 3] * * _.multiply(1, [[1, 2], [3, 4]]) * // → [[1, 2], [3, 4]] * * _.multiply([10, 20], [[1, 2], [3, 4]]) * // → [[10, 20], [60, 80]] * */ // multiply all tensor arguments element-wise/distributively and associatively // Note: lomath is generic; is different from matrix multiplication multiply: function() { return lomath.assodist(lomath.a_multiply, arguments); }, // atomic divide a_divide: function(x, y) { return x / y; }, /** * Divides tensors using `_.assodist`. * * @category basics * @param {...tensors} [...X] tensors. * @returns {tensor} A tensor. * * @example * _.divide(3, 2, 1) * // → 1.5 * * _.divide([1, 2, 3], 2) * // → [0.5, 1, 1.5] * * _.divide([[1, 2], [3, 4]], 2) * // → [[0.5, 1], [1.5, 2]] * * _.divide([[1, 2], [3, 4]], [1, 2]) * // → [[1, 2], [1.5, 2]] * */ // divide all tensor arguments element-wise/distributively and associatively divide: function() { return lomath.assodist(lomath.a_divide, arguments); }, // atomic log. Use base e by default a_log: function(x, base) { return base == undefined ? Math.log(x) : Math.log(x) / Math.log(base); }, /** * Takes the log of tensor T to base n (defaulted to e) element-wise using `_.distribute`. * * @category basics * @param {tensor} T A tensor. * @param {number} [n=e] The optional base; defaulted to e. * @returns {tensor} A tensor. * * @example * _.log([1, Math.E]) * // → [0, 1] * */ // take the log of tensor T to the n element-wise log: function(T, base) { return lomath.distribute(lomath.a_log, T, base); }, // atomic square a_square: function(x) { return x * x; }, /** * Squares a tensor element-wise using `_.distributeSingle`. * * @category basics * @param {tensor} T A tensor. * @returns {tensor} A tensor. * * @example * _.square([1, 2]) * // → [1, 4] * */ square: function(T) { return lomath.distributeSingle(lomath.a_square, T); }, // atomic root a_root: function(x, base) { var n = base == undefined ? 2 : base; return n % 2 ? // if odd power Math.sign(x) * Math.pow(Math.abs(x), 1 / n) : Math.pow(x, 1 / n); }, /** * Takes the n-th root (defaulted to 2) of tensor T element-wise using `_.distribute`. * * @category basics * @param {tensor} T A tensor. * @param {number} [n=2] The optional base; defaulted to 2 for squareroot. * @returns {tensor} A tensor. * * @example * _.root([1, 4]) * // → [1, 2] * * _.root([-1, -8], 3) * // → [-1, -2] * */ // take the n-th root of tensor T element-wise root: function(T, n) { return lomath.distribute(lomath.a_root, T, n); }, // atomic logistic a_logistic: function(z) { return 1 / (1 + Math.exp(-z)) }, /** * Applies the logistic (sigmoid) function to tensor T element-wise. * * @category basics * @param {tensor} T A tensor. * @returns {tensor} A tensor. * * @example * _.logistic([-10, 0, 10]) * // → [ 0.00004539786870243441, 0.5, 0.9999546021312976 ] * */ logistic: function(T) { return lomath.distributeSingle(lomath.a_logistic, T); }, //////////////////// // Basic checkers // //////////////////// /** * Checks if `x` is in range, i.e. `left ≤ x ≤ right`. * * @category signature * @param {number} left The lower bound. * @param {number} right The upper bound. * @param {number} x The value to check. * @returns {boolean} true If `x` is in range. * * @example * _.inRange(0, 3, 3) * // → true * * _.inRange.bind(null, 0, 3)(3) * // → true * */ // check if x is in range set by left, right inRange: function(left, right, x) { return left - 1 < x && x < right + 1; }, /** * Checks if `x` is an integer. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x is an integer isInteger: function(x) { return x == Math.floor(x); }, /** * Checks if `x` is a double-precision number/non-Integer. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x is a double isDouble: function(x) { return x != Math.floor(x); }, /** * Checks if `x > 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x is positive isPositive: function(x) { return x > 0; }, /** * Checks if `x ≤ 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x less than or eq to 0 nonPositive: function(x) { return !(x > 0); }, /** * Checks if `x < 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x is negative isNegative: function(x) { return x < 0; }, /** * Checks if `x ≥ 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ // check if x greater than or eq to 0 nonNegative: function(x) { return !(x < 0); }, /** * Checks if `x == 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ isZero: function(x) { return x == 0; }, /** * Checks if `x != 0`. * * @category signature * @param {number} x The value to check. * @returns {boolean} true If so. */ nonZero: function(x) { return x != 0; }, /** * Checks the parity (even/odd) of x. Useful for when doing the sum with alternating sign. * * @category signature * @param {number} x The value to check. * @returns {number} n -1 if odd, +1 if even. */ parity: function(x) { return x % 2 ? -1 : 1; }, /** * Checks if signature function is true for all scalars of a tensor. * * @category signature * @param {tensor} T The tensor whose values to check. * @param {Function} sigFn The signature function. * @returns {boolean} true If all scalars of the tensor return true. * * @example * _.sameSig([1, 2, 3], _.isPositive) * // → true * */ // check if all tensor entries are of the same sign, with the specified sign function sameSig: function(T, sigFn) { return Boolean(lomath.prod(lomath.distributeSingle(sigFn, T))); }, /** * Checks if two tensors have identifcal entries. * * @category signature * @param {tensor} T A tensor * @param {tensor} S Another tensor * @returns {boolean} match True if tensors are identical, false otherwise. * * @example * _.deepEqual([[1,2],[3,4]], [[1,2],[3,4]]) * // → true * _.deepEqual([1,2,3], [1,2,3]) * // → true * _.deepEqual([1,2,3], [1,2,3,4]) * // → false * _.deepEqual([1,2,3], [1,2,0]) * // → false * */ // check if all tensor entries are of the same sign, with the specified sign function deepEqual: function(T, S) { if (T.length != S.length) return false; var Left = T, Right = S; if (!lomath.isFlat(T)) { Left = _.flattenDeep(T); Right = _.flattenDeep(S); }; var Llen = Left.length, Rlen = Right.length; while (Llen--) { if (Left[Llen] != Right[Llen]) return false; } return true; }, ////////////////////////////////////////// // Unary functions from JS Math object, // ////////////////////////////////////////// // wrapped to function with generic tensor /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. * @example * _.abs([-1, -2, -3]) * // → [1, 2, 3] */ abs: function(T) { return lomath.distributeSingle(Math.abs, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ acos: function(T) { return lomath.distributeSingle(Math.acos, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ acosh: function(T) { return lomath.distributeSingle(Math.acosh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ asin: function(T) { return lomath.distributeSingle(Math.asin, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ asinh: function(T) { return lomath.distributeSingle(Math.asinh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ atan: function(T) { return lomath.distributeSingle(Math.atan, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ atanh: function(T) { return lomath.distributeSingle(Math.atanh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ ceil: function(T) { return lomath.distributeSingle(Math.ceil, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ cos: function(T) { return lomath.distributeSingle(Math.cos, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ cosh: function(T) { return lomath.distributeSingle(Math.cosh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ exp: function(T) { return lomath.distributeSingle(Math.exp, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ floor: function(T) { return lomath.distributeSingle(Math.floor, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ log10: function(T) { return lomath.distributeSingle(Math.log10, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ log1p: function(T) { return lomath.distributeSingle(Math.log1p, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ log2: function(T) { return lomath.distributeSingle(Math.log2, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ round: function(T) { return lomath.distributeSingle(Math.round, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ pow: function(T, n) { return lomath.distribute(Math.pow, T, n); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ sign: function(T) { return lomath.distributeSingle(Math.sign, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ sin: function(T) { return lomath.distributeSingle(Math.sin, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ sinh: function(T) { return lomath.distributeSingle(Math.sinh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ sqrt: function(T) { return lomath.distributeSingle(Math.sqrt, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ tan: function(T) { return lomath.distributeSingle(Math.tan, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ tanh: function(T) { return lomath.distributeSingle(Math.tanh, T); }, /** * Generalized JS Math applicable to tensor using function composition. * @category native-Math * @param {tensor} T A tensor. * @returns {tensor} T A tensor. */ trunc: function(T) { return lomath.distributeSingle(Math.trunc, T); }, ///////////////////// // Regex functions // ///////////////////// /** * Returns a boolean function that matches the regex. * @category regexp * @param {RegExp} regex A RegExp. * @returns {Function} fn A boolean function used for matching the regex. * * @example * var matcher1 = _.reMatch('foo') // using a string * matcher1('foobarbaz') * // → true * * var matcher2 = _.reMatch(/\d+/) // using a regexp * matcher2('May 1995') * // → true * */ // return a function that matches regex, // e.g. matchRegex(/red/)('red Apple') returns true reMatch: function(regex) { return function(str) { /* istanbul ignore next */ if (str != undefined) return str.search(regex) != -1; } }, /** * Returns a boolean function that dis-matches the regex. * @category regexp * @param {RegExp} regex A RegExp to NOT match. * @returns {Function} fn A boolean function used for dis-matching the regex. * * @example * var matcher1 = _.reNotMatch('foo') // using a string * matcher1('barbaz') * // → true * * var matcher2 = _.reNotMatch(/\d+/) // using a regexp * matcher2('foobar') * // → true * */ // negation of reMatch reNotMatch: function(regex) { return function(str) { /* istanbul ignore next */ if (str != undefined) return str.search(regex) == -1; } }, /** * Returns a function that returns the first string portion matching the regex. * @category regexp * @param {RegExp} regex A RegExp to match. * @returns {Function} fn A function that returns the string matching the regex. * * @example * var getBar = _.reGet('bar') // using a string * getBar('foobarbaz') * // → 'bar' * * var getNum = _.reGet(/\d+/) // using a regex * getNum('May 1995') * // → '1995' * getNum('May') * // → null * */ // return the string matched by regex reGet: function(regex) { return function(str) { /* istanbul ignore next */ if (str != undefined) { var matched = str.match(regex); return matched == null ? null : matched[0]; } } }, /** * Wraps a regex into string for regex set operation. * @category regexp * @param {RegExp} regex A RegExp to wrap. * @returns {string} wrapped The regex wrapped into the form `(?:regex)` * * @example * _.reWrap('foo') * // → '(?:foo)' * */ // wrap a regex into string for regex set operation reWrap: function(reg) { return '(?:' + String(reg).replace(/\//g, '') + ')' }, /** * Returns a single regex as the "AND" conjunction of all input regexs. This picks up as MANY adjacent substrings that satisfy all the regexs in order. * @category regexp * @param {...RegExp} regexs All the regexs to conjunct together. * @returns {RegExp} regex The conjuncted regex of the form `(?:re1)...(?:reN)` * * @example * var reg1 = _.reAnd('foo', /\d+/) * // → /(?:foo)(?:\d+)/ * _.reGet(reg1)('Mayfoo1995') * // → 'foo1995' * * var reg2 = _.reAnd(/\d+/, 'foo') // order matters here * // → /(?:\d+)(?:foo)/ * _.reGet(reg2)('Mayfoo1995') * // → null * */ // return a single regex as the "AND" of all arg regex's reAnd: function() { return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join('')); }, /** * Returns a boolean function that matches all the regexs conjuncted in the specified order. * * @category regexp * @param {...RegExp} regexs All the regexs to conjunct together. * @returns {Function} fn A boolean function used for matching the conjuncted regexs. * * @example * _.reAndMatch('foo', /\d+/)('Mayfoo1995') * // → true * * _.reAndMatch(/\d+/, 'foo')('Mayfoo1995') // order matters * // → false * */ // return a function that matches all(AND) of the regexs reAndMatch: function() { return lomath.reMatch(lomath.reAnd.apply(null, arguments)); }, /** * Returns a single regex as the "OR" union of all input regexs. This picks up the FIRST substring that satisfies any of the regexs in any order. * @category regexp * @param {...RegExp} regexs All the regexs to union together. * @returns {RegExp} regex The unioned regex of the form `(?:re1)|...|(?:reN)` * * @example * var reg1 = _.reOr('foo', /\d+/) * // → /(?:foo)|(?:\d+)/ * _.reGet(reg1)('Mayfoo1995') * // → 'foo' * * var reg2 = _.reOr(/\d+/, 'foo') // order doesn't matter here * // → /(?:\d+)|(?:foo)/ * _.reGet(reg2)('Mayfoo1995') * // → 'foo' * */ // return a single regex as the "OR" of all arg regex's reOr: function() { return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join('|')); }, /** * Returns a boolean function that matches any of the regexs in any order. * * @category regexp * @param {...RegExp} regexs All the regexs to try to match. * @returns {Function} fn A boolean function used for matching the regexs. * * @example * _.reOrMatch('foo', /\d+/)('Mayfoo1995') * // → true * * _.reOrMatch(\d+/, 'foo')('Mayfoo1995') // order doesn't matter * // → true * */ // return a function that matches at least one(OR) of the regexs reOrMatch: function() { return lomath.reMatch(lomath.reOr.apply(null, arguments)); }, /** * Converts a regexp into a string that can be used to reconstruct the same regex from 'new RegExp()'. This is a way to store a regexp as string. Note that the flags are excluded, and thus must be provided during regexp reconstruction. * * @category regexp * @param {RegExp} regexp To be stored as string. * @returns {string} reStr The regex as string, to be used in the RegExp() constructor with a supplied flag. * * @example * var reStr = _.reString(/\d+|\s+/ig) * // → '\d+|\s+' * * new RegExp(reStr, "gi") * // → /\d+|\s+/gi * */ reString: function(regexp) { return regexp.toString().replace(/^\/|\/.*$/g, '') }, //////////////////// // Array creation // //////////////////// // union, intersection, difference, xor /** * Returns a sequence of numbers from start to end, with interval. Similar to lodash's `_.range` but the default starts from 1; this is for `R` users who are familiar with `seq()`. * * @category initialization * @param {number} [start=0] The start value. * @param {number} end The end value. * @param {number} [step=1] The interval step. * @returns {Array} seq An array initialized to the sequence. * * @example * _.seq(3) * // → [1, 2, 3] * * _.seq(2, 4) * // → [2, 3, 4] * * _.seq(1, 9, 2) * [ 1, 3, 5, 7, 9 ] * */ // seq from R: like _.range, but starts with 1 by default seq: function(start, stop, step) { if (stop == null) { /* istanbul ignore next */ stop = start || 1; start = 1; } step = step || 1; var length = Math.max(Math.ceil((stop - start) / step), 0) + 1; var range = Array(length); for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start; } return range; }, /** * Returns an initialized array of length N filled with the value (defaulted to 0). Reminiscent of `numeric()` of `R`. * * @category initialization * @param {number} N The length of array. * @param {*} [val=0] The value to fill array with. * @returns {Array} filled An array initialized to the value. * * @example * _.numeric(3) * // → [0, 0, 0] * * _.numeric(3, 'a') * // → ['a', 'a', 'a'] * */ // return an array of length N initialized to val (default to 0) numeric: function(N, val) { return val == undefined ? _.fill(Array(N), 0) : _.fill(Array(N), val); }, /////////////////////// // Tensor properties // /////////////////////// // Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited. /** * Returns the depth of an (nested) array, i.e. the rank of a tensor. * Scalar = rank-0, vector = rank-1, matrix = rank-2, ... so on. * Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited. * * @category properties * @param {tensor} T The tensor. * @returns {number} depth The depth of the array. * * @example * _.depth(0) * // → 0 * * _.depth([1, 2, 3]) * // → 1 * * _.depth([[1, 2], [3, 4]]) * // → 2 * */ // return the depth (rank) of tensor, assuming homogeneity depth: function(T) { var t = T, d = 0; while (t.length) { d++; t = t[0]; } return d; }, /** * Returns the "volume" of a tensor, i.e. the totaly number of scalars in it. * * @category properties * @param {tensor} T The tensor. * @returns {number} volume The number of scalar entries in the tensor. * * @example * _.volume(0) * // → 0 * * _.volume([1, 2, 3]) * // → 3 * * _.volume([[1, 2], [3, 4]]) * // → 4 * */ // return the size of a tensor (total number of scalar entries) // return 0 for scalar volume: function(T) { return _.flattenDeep(T).length; }, /** * Returns the "dimension" of a tensor. * Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited. * * @category properties * @param {tensor} T The tensor. * @returns {Array} dim The dimension the tensor. * * @example * _.dim(0) * // → [] * * _.dim([1, 2, 3]) * // → [3] * * _.dim([[1, 2, 3], [4, 5, 6]]) * // → [2, 3] * * _.dim([ [[1,1,1,1],[2,2,2,2],[3,3,3,3]], [[4,4,4,4],[5,5,5,5],[6,6,6,6]] ]) * // → [2, 3, 4] * */ // Get the dimension of a (non-scalar) tensor by _.flattenDeep, assume rectangular dim: function(T) { var dim = [], ptr = T; while (ptr.length) { dim.push(ptr.length); ptr = ptr[0]; } return dim; }, /** * Checks if a tensor is "flat", i.e. all entries are scalars. * * @category properties * @param {tensor} T The tensor. * @returns {boolean} true If tensor is flat. * * @example * _.isFlat(0) * // → true * * _.isFlat([1, 2, 3]) * // → true * * _.isFlat([[1, 2], [3, 4]]) * // → false * */ // check if a tensor is rank-1 isFlat: function(T) { var flat = true, len = T.length; while (len--) { flat *= !(T[len] instanceof Array); if (!flat) break; } return Boolean(flat); }, /** * Returns the maximum length of the deepest array in (non-scalar) tensor T. * Useful for probing the data structure and ensuring tensor is rectangular. * * @category properties * @param {tensor} T The tensor. * @returns {number} length The maximum length of the deepest array in T. * * @example * _.maxDeepestLength(0) * // → 0 * * _.maxDeepestLength([1, 2, 3]) * // → 3 * * _.maxDeepestLength([[1, 2], [3, 4]]) * // → 2 * */ // get the maximum length of the deepest array in (non-scalar) tensor T. maxDeepestLength: function(T) { if (!(T instanceof Array)) return 0; var stack = [], sizes = []; stack.push(T); while (stack.length) { var curr = stack.pop(), len = curr.length; if (lomath.isFlat(curr)) sizes.push(len); else while (len--) stack.push(curr[len]); } return _.max(sizes); }, /////////////////////////// // Tensor transformation // /////////////////////////// // lodash methods // _.chunk // _.flatten, _.flattenDeep /** * Swaps entries at indices `i,j`. * Mutates the array. * * @category transformation * @param {Array} T The array. * @param {number} i The swap-index. * @param {number} j The swap-index. * @returns {Array} T The mutated array after swapping. * * @example * _.swap([1, 2, 3], 0, 2) * // → [3, 2, 1] * */ // swap at index i, j // Mutates the array swap: function(arr, i, j) { arr[i] = arr.splice(j, 1, arr[i])[0]; return arr; }, /** * Returns a copy of the array reversed, optionally from index `i` to `j` inclusive. * * @category transformation * @param {Array} T The array. * @param {number} [i=0] The from-index. * @param {number} [j=T.length-1] The to-index. * @returns {Array} R The reversed copy of the array. * * @example * _.reverse([0, 1, 2, 3, 4, 5]) * // → [5, 4, 3, 2, 1, 0] * * _.reverse([0, 1, 2, 3, 4, 5], 2) // reverse from index 2 * // → [0, 1, 5, 4, 3, 2] * * _.reverse([0, 1, 2, 3, 4, 5], null, 2) // reverse to index 2 * // → [2, 1, 0, 3, 4, 5] * * _.reverse([0, 1, 2, 3, 4, 5], 2, 4) // reverse from index 2 to 4 * // → [0, 1, 4, 3, 2, 5] * */ // return a copy of reversed arr from index i to j inclusive reverse: function(arr, i, j) { var vec = arr.slice(0); var k = i == undefined ? 0 : i; var l = j == undefined ? arr.length - 1 : j; var mid = Math.ceil((k + l) / 2); while (k < mid) lomath.swap(vec, k++, l--); return vec; }, /** * Extends an array till `toLen` by prepending with `val`. * Mutates the array. * * @category transformation * @param {Array} T The array. * @param {number} toLen The new length of the array. Must be longer than T.length. * @param {number} [val=0] The value to prepend with. * @returns {Array} T The mutated array after extending. * * @example * _.extend([1, 2, 3], 6) * // → [1, 2, 3, 0, 0, 0] * * _.extend([1, 2, 3], 6, 'a') * // → [1, 2, 3, 'a', 'a', 'a'] * */ // return a copy: extend an array till toLen, filled with val defaulted to 0. // Mutates the array extend: function(arr, toLen, val) { var lendiff = toLen - arr.length, rePal = (val == undefined ? 0 : val); if (lendiff < 0) throw new Error("Array longer than the length to extend to") while (lendiff--) arr.push(rePal); return arr; }, /** * Searches the array in batch by applying `_.indexOf` in batch; returns the indices of the results in order. * Useful for grabbing the headers in a data set. * * @category transformation * @param {Array} T The array. * @param {Array} fields The array of fields to search for in T. * @returns {Array} inds The indices returned by applying `_.indexOf` to fields. * * @example * _.batchIndexOf(['a','b','c','d','e','f'], [1, 'b', 'a', 'a']) * // → [-1, 1, 0, 0] * */ // applying _.indexOf in batch; returns -1 for field if not found batchIndexOf: function(arr, fieldArr) { return _.map(fieldArr, function(t) { return _.indexOf(arr, t) }); }, /** * Filters out the invalid indices (negatives) in an array of indices. Basically keeps `x` where `0 ≤ x ≤ maxLen`. * Used with `_.batchIndexOf`. * * @category transformation * @param {Array} T The array of indices, can be from `_.batchIndexOf`. * @param {number} maxLen The max value the indices can have. * @returns {Array} inds A copy of the array with only valid indices. * * @example * _.validInds([-2, 4, 0, 2, -1], 2) * // → [0, 2] * */ // return valid indices from indArr, i.e. in range 0 to maxLen validInds: function(indArr, maxLen) { return _.filter(indArr, lomath.inRange.bind(null, 0, maxLen)); }, /** * Returns a new matrix with the selected rows from a matrix. Same as `rbind()` from `R`. * Useful for picking certain rows from a matrix/tensor. * * @category transformation * @param {tensor} M The original matrix. * @param {Array} indArr The array of indices specifying the rows of M to pick. * @returns {tensor} M' The matrix with the selected rows from the indices. * * @example * _.rbind([[1,2,3],[4,5,6],[7,8,9]], [1, 1, 2]) * // → [[4, 5, 6], [4, 5, 6], [7, 8, 9]] * */ // return a copy with sub rows from matrix M rbind: function(M, indArr) { indArr = lomath.validInds(indArr, M.length); if (indArr.length == 0) return []; return _.map(indArr, function(i) { return _.cloneDeep(M[i]); }); }, /** * Returns a new matrix with the selected columns from a matrix. Same as `cbind()` from `R`. * Useful for picking columns from a data matrix/tensor with specified header indices. * * @category transformation * @param {tensor} M The original matrix. * @param {Array} indArr The array of indices specifying the columns of M to pick. * @returns {tensor} M' The matrix with the selected columns from the indices. * * @example * _.cbind([['a','b','c'],[1,2,3],[-1,-2,-3]], [1, 1, 2]) * // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ] * * var M = [['a','b','c'],[1,2,3],[-1,-2,-3]]; // using on a dataset * var titles = M[0]; * _.cbind(M, _.batchIndexOf(titles, ['b', 'b', 'c'])) * // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ] * */ // return a copy with sub rows from matrix M cbind: function(M, indArr) { indArr = lomath.validInds(indArr, M[0].length) if (indArr.length == 0) return []; return _.map(M, function(row) { return _.map(indArr, function(i) { return row[i]; }); }); }, /** * Returns a new matrix with the selected columns from a matrix. Short for `_.cbind(M, _.batchIndexOf())` * Useful for picking columns from a data matrix by directly specifying the header titles. * * @category transformation * @param {tensor} M The original matrix. * @param {Array} fields The array of fields of the columns of M to pick. * @returns {tensor} M' The matrix with the selected columns from the fields. * * @example * var M = [['a','b','c'],[1,2,3],[-1,-2,-3]]; // using on a dataset * _.cbindByField(M, ['b', 'b', 'c']) * // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ] * */ // Assuming matrix has header, rbind by header fields instead of indices cbindByField: function(M, fieldArr) { // assuming header is first row of matrix var header = M[0], fieldInds = lomath.batchIndexOf(header, fieldArr); return lomath.cbind(M, fieldInds); }, /** * Makes a tensor rectangular by filling with val (defaulted to 0). * Mutates the tensor. * * @category transformation * @param {tensor} T The original tensor. * @returns {tensor} T The mutated tensor that is now rectangular. * * @example * _.rectangularize([ [1, 2, 3], [4] ]) * // → [[ 1, 2, 3 ], [ 4, 0, 0 ]] * * _.rectangularize([ [1, 2, 3], [4] ], 'a') * // → [[ 1, 2, 3 ], [ 4, 'a', 'a' ]] * */ // make a tensor rectangular by filling with val, defaulted to 0. // mutates the tensor rectangularize: function(T, val) { var toLen = lomath.maxDeepestLength(T), stack = []; stack.push(T); while (stack.length) { var curr = stack.pop(); if (lomath.isFlat(curr)) lomath.extend(curr, toLen, val); else _.each(curr, function(c) { stack.push(c); }) } return T; }, /** * Reshapes an array into a multi-dimensional tensor. Applies `_.chunk` using a dimension array in sequence. * * @category transformation * @param {Array} A The original flat array. * @param {Array} dimArr The array specifying the dimensions. * @returns {tensor} T The tensor reshaped from the copied array. * * @example * _.reshape([1, 2, 3, 4, 5, 6], [2, 3]) * // → [[ 1, 2, 3 ], [ 4, 5, 6 ]] * * _.reshape([1, 2, 3, 4], [2, 3]) * // → [[ 1, 2, 3 ], [ 4 ]] * */ // use chunk from inside to outside: reshape: function(arr, dimArr) { var tensor = arr; var len = dimArr.length; while (--len) tensor = _.chunk(tensor, dimArr[len]); return tensor; }, /** * Flattens a JSON object into depth 1, using an optional delimiter. * * @category transformation * @param {JSON} obj The original JSON object. * @returns {JSON} flat_obj The flattened (unnested) object. * * @example * formData = { * 'level1': { * 'level2': { * 'level3': 0, * 'level3b': 1 * }, * 'level2b': { * 'level3': [2,3,4] * } * } * } * * _.flattenJSON(formData) * // → { 'level1.level2.level3': 0, * // 'level1.level2.level3b': 1, * // 'level1.level2b.level3': [ 2, 3, 4 ] } * // The deepest values are not flattened (not stringified) * * _.flattenJSON(formData, '_') * // → { 'level1_level2_level3': 0, * // 'level1_level2_level3b': 1, * // 'level1_level2b_level3': [ 2, 3, 4 ] } * // The deepest values are not flattened (not stringified) * */ flattenJSON: function(obj, delimiter) { var delim = delimiter || '.' var nobj = {} _.each(obj, function(val, key) { if (_.isPlainObject(val) && !_.isEmpty(val)) { var strip = lomath.flattenJSON(val, delim) _.each(strip, function(v, k) { nobj[key + delim + k] = v }) } else if (_.isArray(val) && !_.isEmpty(val)) { _.each(val, fu