var _ = require('lodash').runInContext();////////// 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
  op: function(x, y) {
    return x + '*' + y;
  },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;
  },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;
  },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;
  },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 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 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
    var args = len < 3 ? argObj : _.toArray(argObj),
      res = fn(args[i++], args[i++]);
    while (i < len) res = fn(res, args[i++]);
    return res;
  },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 // /////////////////// 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;
  },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;
  },
  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;
  },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;
  },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;
  },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;
  },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;
  },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);
  },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;
  },
  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);
  },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))
  },
  logistic: function(T) {
    return lomath.distributeSingle(lomath.a_logistic, T);
  },////////////////// Basic checkers // ////////////////// check if x is in range set by left, right
  inRange: function(left, right, x) {
    return left - 1 < x && x < right + 1;
  },check if x is an integer
  isInteger: function(x) {
    return x == Math.floor(x);
  },check if x is a double
  isDouble: function(x) {
    return x != Math.floor(x);
  },check if x is positive
  isPositive: function(x) {
    return x > 0;
  },check if x less than or eq to 0
  nonPositive: function(x) {
    return !(x > 0);
  },check if x is negative
  isNegative: function(x) {
    return x < 0;
  },check if x greater than or eq to 0
  nonNegative: function(x) {
    return !(x < 0);
  },
  isZero: function(x) {
    return x == 0;
  },
  nonZero: function(x) {
    return x != 0;
  },
  parity: function(x) {
    return x % 2 ? -1 : 1;
  },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)));
  },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
  abs: function(T) {
    return lomath.distributeSingle(Math.abs, T);
  },
  acos: function(T) {
    return lomath.distributeSingle(Math.acos, T);
  },
  acosh: function(T) {
    return lomath.distributeSingle(Math.acosh, T);
  },
  asin: function(T) {
    return lomath.distributeSingle(Math.asin, T);
  },
  asinh: function(T) {
    return lomath.distributeSingle(Math.asinh, T);
  },
  atan: function(T) {
    return lomath.distributeSingle(Math.atan, T);
  },
  atanh: function(T) {
    return lomath.distributeSingle(Math.atanh, T);
  },
  ceil: function(T) {
    return lomath.distributeSingle(Math.ceil, T);
  },
  cos: function(T) {
    return lomath.distributeSingle(Math.cos, T);
  },
  cosh: function(T) {
    return lomath.distributeSingle(Math.cosh, T);
  },
  exp: function(T) {
    return lomath.distributeSingle(Math.exp, T);
  },
  floor: function(T) {
    return lomath.distributeSingle(Math.floor, T);
  },
  log10: function(T) {
    return lomath.distributeSingle(Math.log10, T);
  },
  log1p: function(T) {
    return lomath.distributeSingle(Math.log1p, T);
  },
  log2: function(T) {
    return lomath.distributeSingle(Math.log2, T);
  },
  round: function(T) {
    return lomath.distributeSingle(Math.round, T);
  },
  pow: function(T, n) {
    return lomath.distribute(Math.pow, T, n);
  },
  sign: function(T) {
    return lomath.distributeSingle(Math.sign, T);
  },
  sin: function(T) {
    return lomath.distributeSingle(Math.sin, T);
  },
  sinh: function(T) {
    return lomath.distributeSingle(Math.sinh, T);
  },
  sqrt: function(T) {
    return lomath.distributeSingle(Math.sqrt, T);
  },
  tan: function(T) {
    return lomath.distributeSingle(Math.tan, T);
  },
  tanh: function(T) {
    return lomath.distributeSingle(Math.tanh, T);
  },
  trunc: function(T) {
    return lomath.distributeSingle(Math.trunc, T);
  },/////////////////// Regex functions // /////////////////// return a function that matches regex, e.g. matchRegex(/red/)(‘red Apple’) returns true
  reMatch: function(regex) {
    return function(str) {
      if (str != undefined)
        return str.search(regex) != -1;
    }
  },negation of reMatch
  reNotMatch: function(regex) {
    return function(str) {
      if (str != undefined)
        return str.search(regex) == -1;
    }
  },return the string matched by regex
  reGet: function(regex) {
    return function(str) {
      if (str != undefined) {
        var matched = str.match(regex);
        return matched == null ? null : matched[0];
      }
    }
  },wrap a regex into string for regex set operation
  reWrap: function(reg) {
    return '(?:' + String(reg).replace(/\//g, '') + ')'
  },return a single regex as the “AND” of all arg regex’s
  reAnd: function() {
    return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join(''));
  },return a function that matches all(AND) of the regexs
  reAndMatch: function() {
    return lomath.reMatch(lomath.reAnd.apply(null, arguments));
  },return a single regex as the “OR” of all arg regex’s
  reOr: function() {
    return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join('|'));
  },return a function that matches at least one(OR) of the regexs
  reOrMatch: function() {
    return lomath.reMatch(lomath.reOr.apply(null, arguments));
  },
  reString: function(regexp) {
    return regexp.toString().replace(/^\/|\/.*$/g, '')
  },////////////////// Array creation // //////////////////
union, intersection, difference, xor seq from R: like _.range, but starts with 1 by default
  seq: function(start, stop, step) {
    if (stop == null) {
      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;
  },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. 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;
  },return the size of a tensor (total number of scalar entries) return 0 for scalar
  volume: function(T) {
    return _.flattenDeep(T).length;
  },
     [[1,1,1,1],[2,2,2,2],[3,3,3,3]],
     [[4,4,4,4],[5,5,5,5],[6,6,6,6]]
   ])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;
  },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);
  },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 swap at index i, j Mutates the array
  swap: function(arr, i, j) {
    arr[i] = arr.splice(j, 1, arr[i])[0];
    return arr;
  },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;
  },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;
  },applying _.indexOf in batch; returns -1 for field if not found
  batchIndexOf: function(arr, fieldArr) {
    return _.map(fieldArr, function(t) {
      return _.indexOf(arr, t)
    });
  },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));
  },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]);
    });
  },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];
      });
    });
  },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);
  },
     [1, 2, 3],
     [4]
   ])
     [1, 2, 3],
     [4]
   ], '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;
  },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;
  },
  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, function(v, index) {
          nobj[key + delim + index] = v
          if (_.isObject(v)) {
            nobj = lomath.flattenJSON(nobj, delim)
          };
        })
      } else {
        nobj[key] = val
      }
    })
    return nobj
  },
  unflattenJSON: function(obj, delimiter) {
    return lomath.unobjectifyArray(lomath.unflattenJSON_objectifyArray(obj, delimiter))
  },
  unflattenJSON_objectifyArray: function(obj, delimiter) {
    var delim = delimiter || '.';cache deep keys for deletion later
    var deepKeys = [];iterate over each key-val pair breath first
    _.each(obj, function(val, key) {if can be delimiter-splitted
      if (_.includes(key, delim)) {
        var ind = key.lastIndexOf(delim)new key
        var v0 = key.slice(0, ind);the tail as new key now
        var v1 = key.slice(ind + delim.length);guard, if sibling didn’t already create it
        if (!obj[v0]) {
          obj[v0] = {}
        };
        obj[v0][v1] = val;push for deletion
        deepKeys.push(key)
      }
    });delete the old deepkeys
    _.map(deepKeys, function(dkey) {
      delete obj[dkey]
    });check if should go down further if still has delim
    var goDown = false;
    _.each(_.keys(obj), function(k) {
      if (k.lastIndexOf(delim) != -1) {
        goDown = true;
        return false
      };
    })
    if (goDown) {
      return lomath.unflattenJSON_objectifyArray(obj, delim)
    } else {
      return obj
    }
  },
  unobjectifyArray: function(obj) {
    var index = 0;
    var arr = [];
    var isIndex = true;
    if (_.isEmpty(obj)) {
      isIndex = false;
    };iterate through and see if all keys indeed form a numeric sequence sort keys. Sort since iteration order is not guaranteed.
    var sortedKeys = _.keys(obj).sort()
    _.each(sortedKeys, function(k) {add the value while keys still behaving like numeric
      if (parseInt(k) == index++) {
        arr.push(obj[k])
      } else {
        isIndex = false
        return false
      }
    })
    var res = isIndex ? arr : objrecurse down the val if it’s not primitive type
    _.each(res, function(v, k) {
      if (_.isObject(v)) {
        res[k] = lomath.unobjectifyArray(v)
      }
    })
    return res
  },//////////// Matrices // //////////// transpose a matrix
  transpose: function(M) {
    return _.zip.apply(null, M);
  },trace a matrix
  trace: function(M) {
    var len = M.length,
      sum = 0;
    while (len--)
      sum += M[len][len]
    return sum;
  },multiply two matrices
  matMultiply: function(A, B) {
    var T = lomath.transpose(B);
    return _.map(A, function(a) {
      return _.map(T, lomath.dot.bind(null, a))
    })
  },
  coSubMatrix: function(M, r, c) {
    r = r || 0;
    c = c || 0;
    var coSubMat = [];
    for (var i = 0; i < M.length; i++) {
      if (i != r) {
        var row = M[i],
          coRow = [];
        for (var j = 0; j < M.length; j++) {
          if (j != c) coRow.push(row[j]);
        }
        coSubMat.push(coRow);
      }
    }
    return coSubMat;
  },
  coMatrix: function(M) {
    var coMat = [];
    for (var i = 0; i < M.length; i++) {
      var coRow = [];
      for (var j = 0; j < M.length; j++) {
        var subMat = lomath.coSubMatrix(M, i, j);
        coRow.push(lomath.parity(i + j) * lomath.det(subMat));
      }
      coMat.push(coRow);
    }
    return coMat;
  },
  adj: function(M) {
    var adjMat = [];
    for (var i = 0; i < M.length; i++) {
      var adjRow = [];
      for (var j = 0; j < M.length; j++) {
        var subMat = lomath.coSubMatrix(M, j, i);
        adjRow.push(lomath.parity(i + j) * lomath.det(subMat));
      }
      adjMat.push(adjRow);
    }
    return adjMat;
  },
  detSum: function(M, i) {
    var sign = lomath.parity(i),
      cofactor = M[0][i],
      coSubMat = lomath.coSubMatrix(M, 0, i);
    return sign * cofactor * lomath.det(coSubMat);
  },
  det: function(M) {
    var n = M.length;
    var det = 0;
    if (!n) {
      det = M
    } else if (n == 1) {
      det = M[0][0] || M[0]
    } else if (n == 2) {
      det = M[0][0] * M[1][1] - M[1][0] * M[0][1]
    } else {
      var head = M[0];
      det += lomath.fsum(M, lomath.detSum)
    }
    return det;
  },
  inv: function(M) {
    var det = lomath.det(M);
    if (det == 0) return null;
    else
      return lomath.multiply(1 / det, lomath.adj(M))
  },///////////////////////////// Subsets and combinatorics // ///////////////////////////// generate n-nary number of length
  genAry: function(length, n) {
    var range = _.map(_.range(n), String);
    var tmp = range,
      it = length;
    while (--it) {
      tmp = _.flattenDeep(_.map(range, function(x) {
        return lomath.distributeRight(lomath.a_add, x, tmp)
      }));
    }
    return tmp;
  },convert array of strings to array of array of numbers
  toNumArr: function(sarr) {
    return _.map(sarr, function(str) {
      return _.map(str.split(''), function(x) {
        return parseInt(x);
      })
    })
  },generate all permutation subset indices of n items
  pSubset: function(n) {
    var range = _.map(_.range(n), String),
      res = [],
      count = n;
    res.push(range); //init
    if (count == 0) return res;
    while (--count) {the last batch to expand on
      var last = _.last(res);
      var batch = [];
      _.each(last, function(k) {
        for (var i = 0; i < n; i++)
          if (!_.includes(k.split(''), String(i)))
            batch.push(k + i);
      })
      res.push(batch);
    }
    return res;
  },generate all subset indices of n items
  subset: function(n) {
    var range = _.map(_.range(n), String),
      res = [],
      count = n;
    res.push(range); //init
    if (count == 0) return res;
    while (--count) {the last batch to expand on
      var last = _.last(res);
      var batch = [];
      _.each(last, function(k) {
        for (var i = Number(_.last(k)) + 1; i < n; i++)
          batch.push(k + i);
      })
      res.push(batch);
    }
    return res;
  },generate the indices of n-perm-r
  permList: function(n, r) {
    return lomath.toNumArr(lomath.pSubset(n)[r - 1]);
  },generate the indices of n-choose-r
  combList: function(n, r) {
    return lomath.toNumArr(lomath.subset(n)[r - 1]);
  },generate all permutations of n items
  permute: function(n) {
    var range = _.range(n),
      res = [],
      diffs, k = 0;
    while (k != -1) {
      res.push(range.slice(0));
      diffs = lomath.stairs(range),
        k = _.findLastIndex(diffs, lomath.isPositive);
      var l = _.findLastIndex(range, function(t) {
        return t > range[k];
      });
      lomath.swap(range, k, l);
      range = lomath.reverse(range, k + 1, null);
    }
    return res;
  },return factorial(n) alias: fact
  factorial: function(n) {
    if (n == 0) return 1;
    if (n < 0) throw "Negative factorial not defined"
    var count = n,
      res = n;
    while (--count)
      res *= count;
    return res;
  },return n-permute-r alias: perm
  permutation: function(n, r) {
    if (r == 0) return 1;
    if (n < 0 || r < 0) throw "Negative permutation not defined"
    var count = r,
      term = n;
    res = n;
    while (--count)
      res *= --term;
    return res;
  },return n-choose-r alias: comb
  combination: function(n, r) {
    var l = (r > n / 2) ? n - r : r;
    if (n < 0 || l < 0) throw "Negative combination not defined"
    return lomath.permutation(n, l) / lomath.factorial(l);
  },/////////////////// Handy vectorial // /////////////////// return the dot product of two vectors recyle if lengths mismatch
  dot: function(X, Y) {
    return _.sum(lomath.multiply(X, Y));
  },return the sum of n-powers of a tensor, default to n = 2
  powSum: function(T, n) {
    var L = n == undefined ? 2 : n;
    return _.sum(lomath.pow(T, L));
  },return the L-n norm of a vector, default to L-2
  norm: function(v, n) {
    var L = n == undefined ? 2 : n;
    return lomath.a_root(lomath.powSum(v, L), L);
  },normalize a vector(tensor) by L-n norm, default to n=2
  normalize: function(v, n) {
    return lomath.divide(v, lomath.norm(v, n));
  },rescale a vector to unit length
  rescale: function(v) {
    return lomath.normalize(v, 1);
  },//////////////// handy Matrix // ////////////////
Matrix ops Matrix ops Matrix ops
/////////////// Handy trend // /////////////// return the stairs: adjacent difference in a vector
  stairs: function(v) {
    var dlen = v.length - 1,
      st = Array(dlen);
    while (dlen--)
      st[dlen] = v[dlen + 1] - v[dlen];
    return st;
  },check the trend of vector v using sign-function
  stairsTrend: function(v, sigFn) {
    return lomath.sameSig(lomath.stairs(v), sigFn);
  },check if vector v is increasing
  increasing: function(v) {
    return lomath.stairsTrend(v, lomath.isPositive);
  },check is vector v is non-decreasing
  nonDecreasing: function(v) {
    return lomath.stairsTrend(v, lomath.nonNegative);
  },check is vector v is decreasing
  decreasing: function(v) {
    return lomath.stairsTrend(v, lomath.isNegative);
  },check is vector v is non-increasing
  nonIncreasing: function(v) {
    return lomath.stairsTrend(v, lomath.nonPositive);
  },///////////////////// Handy statistical // ///////////////////// return the average of a vector
  mean: function(v) {
    return _.sum(v) / v.length;
  },return the expectation value E(fn(x)), given probability and value vectors, and an optional atomic fn, defaulted to identity E(x). Note: fn must be atomic alias E
  expVal: function(X, P, fn) {
    var val, prob, func;if only X is specified
    if (P == undefined && fn == undefined) {
      var hist = lomath.histogram(X);
      val = hist.value,
        prob = hist.prob;
    }if X, P specified (maybe fn too)
    else if (typeof P === 'object') {
      val = X;
      prob = P;
      func = fn;
    }if X, fn specified
    else if (typeof P === 'function') {
      var hist = lomath.histogram(X);
      val = hist.value,
        prob = hist.prob;
      func = P;
    }
    if (func != undefined)
      return lomath.dot(lomath.distributeSingle(func, val), prob);
    return lomath.dot(val, prob);
  },return the variance, given probability and value vectors alias Var
  variance: function(X, P, fn) {if only X is specified
    if (P == undefined && fn == undefined) {
      return lomath.expVal(X, lomath.a_square) - lomath.a_square(lomath.expVal(X))
    }if X, P specified (maybe fn too)
    else if (typeof P === 'object') {
      return fn == undefined ?
        lomath.expVal(X, P, lomath.a_square) - lomath.a_square(lomath.expVal(X, P)) :
        lomath.expVal(X, P, _.flow(fn, lomath.a_square)) - lomath.a_square(lomath.expVal(X, P, fn));
    }if X, fn specified
    else if (typeof P === 'function') {
      return lomath.expVal(X, _.flow(P, lomath.a_square)) - lomath.a_square(lomath.expVal(X, P));
    }
  },return the variance, given probability and value vectors
  stdev: function(X, P, fn) {
    return Math.sqrt(lomath.variance(X, P, fn));
  },
  histogram: function(data, fn, pair) {
    if (fn == true) // called with data, pair
      return _.toPairs(_.countBy(data));
    var bin = _.countBy(data, fn);
    if (pair == true) // called with data, fn, pair
      return _.toPairs(bin);
    var freq = _.values(bin);
    return { //called with data, [fn]
      value: _.keys(bin),
      freq: freq,
      prob: lomath.rescale(freq)
    }
  },Calculate the rate of return r in % of an exp growth, given final value m_f, initial value m_i, and time interval t
  expGRate: function(m_f, m_i, t) {
    return 100 * (Math.exp(Math.log(m_f / m_i) / t) - 1);
  },Calculate the trailing exp rate of return in the last t years given a vector v
  trailExpGRate: function(v, t) {
    var len = v.length;
    return lomath.expGRate(v[len - 1], v[len - 1 - t], t);
  },//////////////////////////////////////// Plotting modules: normal and dynamic // ////////////////////////////////////////
       [{
           name: "linear",
           data: v
       }, {
           name: "square",
           data: vv
       }],
       "Title 1"
       )
       [{
           name: "log",
           data: _.log(v)
       }],
       "Title 2"
       )hc: require(__dirname+’/chart/plot.js’).hc
  hc: function() {
    var p = require(__dirname + '/chart/plot.js').p;
    return new p();
  },
       [{
           name: "linear",
           data: [1, 2, 3, 4, 5, 6]
       }, {
           name: "square",
           data: [1, 4, 9, 16, 25, 36]
       }],
       "Title 1"
       )
       [{
           name: "square",
           data: [[3, 9], [4, 16], [5, 25], [6, 36]]
       }],
       "Title 2"
       )
  plot: function(seriesArr, title, yLabel, xLabel) {
    console.log("Please call this method by _.hc.plot");
    return 0;
  },
   chart: {
        type: 'column'
    },
    title: {
        text: 'Monthly Average Rainfall'
    },
    subtitle: {
        text: 'Source: WorldClimate.com'
    },
    xAxis: {
        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        crosshair: true
    },
    yAxis: {
        min: 0,
        title: {
            text: 'Rainfall (mm)'
        }
    },
    plotOptions: {
        column: {
            pointPadding: 0.2,
            borderWidth: 0
        }
    },
    series: [{
        name: 'Tokyo',
        data: [49.9, 71.5, 106.4, 129.2, 144.0, 176.0]
    }, {
        name: 'New York',
        data: [83.6, 78.8, 98.5, 93.4, 106.0, 84.5]
    }, {
        name: 'London',
        data: [48.9, 38.8, 39.3, 41.4, 47.0, 48.3]
    }, {
        name: 'Berlin',
        data: [42.4, 33.2, 34.5, 39.7, 52.6, 75.5]
    }]
       })
  advPlot: function(options) {
    console.log("Please call this method by _.hc.advPlot");
    return 0;
  },
  render: function(autosave) {
    console.log("Please call this method by _.hc.advPlot");
    return 0;
  },The time variables for tick tock
  t_start: 0,
  t_end: 0,
  tick: function() {
    lomath.t_start = _.now();
    return lomath.t_start;
  },
  tock: function() {
    lomath.t_end = _.now();
    var diff = lomath.t_end - lomath.t_start;
    console.log('Elapsed ms:', diff);
    return diff;
  },
  p: function() {
    console.log.apply(null, arguments);
  }
})Export lomath as _
module.exports = lomath;