diff --git a/README.md b/README.md index 51ca10c..03fef73 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,13 @@ array([ 1, 2, 3], dtype=uint8) __Note__: possible types are int8, uint8, int16, uint16, int32, uint32, float32, float64 and array (the default) -To create arrays with a given shape, you can use `zeros`, `ones` or `random` functions: +To create arrays with a given shape, you can use `zeros`, `ones`, `random`, and `full` functions: ```js > nj.zeros([2,3]); array([[ 0, 0, 0], [ 0, 0, 0]]) + > nj.ones([2,3,4], 'int32') // dtype can also be specified array([[[ 1, 1, 1, 1], [ 1, 1, 1, 1], @@ -81,6 +82,38 @@ array([[ 0.9182 , 0.85176, 0.22587], [ 0.50088, 0.74376, 0.84024], [ 0.74045, 0.23345, 0.20289], [ 0.00612, 0.37732, 0.06932]]) + +> nj.full([3,3], -3.14159) +array([[ -3.14159, -3.14159, -3.14159], + [ -3.14159, -3.14159, -3.14159], + [ -3.14159, -3.14159, -3.14159]]) + +> nj.full([3, 2], [1, 2]) +array([[ 1, 2], + [ 1, 2], + [ 1, 2]]) +``` + +You can also infer the shape from another array with methods like `zerosLike`, `onesLike`, `randomLike`, and `fullLike`: + +Given `const arr = nj.array([[1,2], [3,4]]);` which has shape of (2,2) + +```js +> nj.zerosLike(arr); +array([[ 0, 0], + [ 0, 0]]) + +> nj.onesLike(arr, 'int32') // dtype can also be specified +array([[ 1, 1], + [ 1, 1]], dtype=int32) + +> nj.randomLike(arr) +array([[ 0.82251, 0.76331], + [ 0.22786, 0.73417]]) + +> nj.fullLike(arr, -3.14159) +array([[ -3.14159, -3.14159], + [ -3.14159, -3.14159]]) ``` To create sequences of numbers, __NumJs__ provides a function called `arange`: @@ -96,6 +129,25 @@ array([ 10, 15, 20, 25]) array([ 1, 2, 3, 4], dtype=uint8) ``` +To generate matrices with nice properties like the identity `eye` or a triangular `tri`: + +```js +> nj.eye(3, 3) +array([[ 1, 0, 0], + [ 0, 1, 0], + [ 0, 0, 1]]) + +> nj.tri(3, 3) // fills diagonal under k=0 +array([[ 0, 0, 0], + [ 1, 0, 0], + [ 1, 1, 0]]) + +> nj.tri(3, 3, 1) // fills diagonal under k=1 +array([[ 1, 0, 0], + [ 1, 1, 0], + [ 1, 1, 1]]) +``` + ### More info about the array __NumJs__’s array class is called `NdArray`. It is also known by the alias `array`. The more important properties of an `NdArray` object are: @@ -369,9 +421,15 @@ array([[0.62755, 0.8278,0.21384], > a.min() 0.2138431086204946 > +> a.argmin() // index of a.min() +[[0, 2]] +> > a.max() 0.8278025290928781 > +> a.argmax() // index of a.max() +[[0, 1]] +> > a.mean() 0.5187748112172509 > @@ -379,6 +437,51 @@ array([[0.62755, 0.8278,0.21384], 0.22216977543691244 ``` +### Applying Functions over an Axis + +Commonly, you might want to apply a vector function across an axis of a matrix. You can do this with the `applyOverAxis` function. For example suppose I have a matrix a + +```js +> a = nj.array([[0, 1, 2], + [3, 4, 5]]) +``` + +You can sum across the ith axis. For example the last axis is the rows, so I can specify the last axis -1 + +```js +> nj.applyOverAxis(a, nj.sum, { axis: -1 }) +array([ 3, 12]) +``` + +Or find the max down the columns (first axis is 0) +```js +> nj.applyOverAxis(a, nj.max, { axis: 0 }) +array([ 3, 4, 5]) +``` + +If you don't want to reduce the shape/dimensions of the output, you can put `keepdims: true`. For example when we summed across the rows I can maintain those row dimensions like + +```js +> nj.applyOverAxis(a, nj.sum, { axis: -1, keepdims: true }) +array([[ 3], + [ 12]]) +``` + +This also works for tensors for example an array with (2,2,2) shape I can find the mean across the last axis: + +```js +> b = array([[[ 0, 1], + [ 2, 3]], + [[ 4, 5], + [ 6, 7]]]) +> +> nj.applyOverAxis(b, nj.mean, { axis: -1, keepdims: true }) +array([[[ 0.5], + [ 2.5]], + [[ 4.5], + [ 6.5]]]) +``` + ### Universal Functions __NumJs__ provides familiar mathematical functions such as `sin`, `cos`, and `exp`. These functions operate element-wise on an array, producing an `NdArray` as output: diff --git a/src/index.js b/src/index.js index 39c93ab..8f5d28a 100644 --- a/src/index.js +++ b/src/index.js @@ -195,6 +195,26 @@ function max (x) { return NdArray.new(x).max(); } +/** + * Return the index of the minimum value of the array + * + * @param {(Array|NdArray|number)} x + * @returns {Number} + */ +function argmin (x) { + return NdArray.new(x).argmin(); +} + +/** + * Return the index of the maximum value of the array + * + * @param {(Array|NdArray|number)} x + * @returns {Number} + */ +function argmax (x) { + return NdArray.new(x).argmax(); +} + /** * Return element-wise remainder of division. * Computes the remainder complementary to the `floor` function. It is equivalent to the Javascript modulus operator``x1 % x2`` and has the same sign as the divisor x2. @@ -316,6 +336,148 @@ function ones (shape, dtype) { return arr; } +/** + * Return a new array of given shape and type, filled with the specified value. + * The fillValue + * + * @param {(Array|number)} shape - Shape of the new array, e.g., [2, 3] or 2. + * @param {(number|Array)} fillValue - number to fill the entire array with + * @param {(string|object)} dtype - The type of the output array. + * + * @return {NdArray} Array of ones with the given shape and dtype + */ +function full (shape, fillValue, dtype) { + if (_.isNumber(shape) && shape >= 0) { + shape = [shape]; + } + var s = _.shapeSize(shape); + var T = _.getType(dtype); + var ndarrayMemory = new T(s); + var arr = new NdArray(ndarrayMemory, shape); + + if(_.isNumber(fillValue)) { + ndarrayMemory.fill(fillValue); + } else { + // if array provided, fill out the array by repeating the fillValue + for(var i = 0; i < ndarrayMemory.length; i++) { + ndarrayMemory[i] = fillValue[i % fillValue.length]; + } + } + return arr; +} + +/** + * Return a new array filled with zeros shaped like another array + * + * @param {(NdArray)} array - the shape we want to use for a new array + * @param {(string|object)} dtype The type of the output array. + * + * @return {NdArray} Array of zeros shaped like the array argument + */ +function zerosLike (array, dtype) { + return zeros(array.shape, dtype); +} + +/** + * Return a new array filled with zeros shaped like another array + * + * @param {(NdArray)} array - the shape we want to use for a new array + * @param {(string|object)} dtype The type of the output array. + * + * @return {NdArray} Array of ones shaped like the array argument + */ +function onesLike (array, dtype) { + return ones(array.shape, dtype); +} + +/** + * Return a new array filled with fillValue shaped like another array + * + * @param {(NdArray)} array - the shape we want to use for a new array + * @param {(number|Array)} fillValue - number to fill the entire array with + * @param {(string|object)} dtype - The type of the output array. + * + * @return {NdArray} Array of fillValue shaped like the array argument + */ +function fullLike (array, fillValue, dtype) { + return full(array.shape, fillValue, dtype); +} + +/** + * Return a new array filled with random numbers + * + * @param {(NdArray)} array - the shape we want to use for a new array + * + * @return {NdArray} Array of random numbers shaped like the array argument + */ +function randomLike (array) { + return random(array.shape); +} + +/** + * Return a new array of given shape and type, with 1s in the diagonal as the identity matrix + * + * @param {number} M - the number of rows + * @param {number?} N - the number of columns, defaults to N + * @param {(string|object)} dtype - The type of the output array. + * + * @return {NdArray} Array of ones with the given shape and dtype + */ +function eye (N, M, dtype) { + // in the case where eye(N, dtype) + if (_.isString(M)) { + dtype = M + M = undefined; + } + if(M === undefined) M = N; + + var T = _.getType(dtype); + var flatData = new T(N*M).fill(0); + var arr = new NdArray(flatData, [N, M]); + + // then when i=j fill with 1s + for(var i = 0; i < N; i++) + for(var j = 0; j < M; j++) + if(i === j) arr.set(i, j, 1); + + return arr; +} + +/** + * Return a new array of given shape and type, with 1s in the lower triange + * + * @param {number} M - the number of rows + * @param {number?} N - the number of columns, defaults to N + * @param {number?} k - the diagonal to fill under, defaults to 0 + * @param {(string|object)} dtype - The type of the output array. + * + * @return {NdArray} Array of ones with the given shape and dtype + */ +function tri (N, M, k = 0, dtype) { + // in the case where tri(N, dtype) + if (_.isString(M)) { + dtype = M + M = undefined; + } + // in the case where tri(N, M, dtype) + else if (_.isString(k)) { + dtype = k; + k = 0; + }; + if(M === undefined) M = N; + + var T = _.getType(dtype); + var flatData = new T(N*M).fill(0); + var arr = new NdArray(flatData, [N, M]); + + // then when i>j fill with 1s (lower triangle) increase k to increase the diagonl to fill + for(var i = 0; i < N; i++) + for(var j = 0; j < M; j++) + if((i + k) > j) arr.set(i, j, 1); + + return arr; +} + /** * Return a new array of given shape and type, filled with `undefined` values. * @@ -792,6 +954,97 @@ function rot90 (m, k, axes) { } } +/** + * Apply an aggregate operation across an axis for all of the array + * + * @param {Array|NdArray} arr array to apply to + * @param {(vectorInput: NdArray) => number} vectorFunc a function that takes in a vector and returns a number + * @param {{axis: number|undefined, keepdims: boolean}?} optional takes in which axis you are applyOverAxising over + * and keepdims=true will put 1 for the axis dimension instead of removing it + * + * axis refers to the dimension you applyOverAxis over. So axis:1 will apply the vectorFunc to the vector rows in a matrix + * if axis=-1 this just means to applyOverAxis over the last axis, -2 second to last and so on + * + * @example here I apply nj.sum across the rows of a matrix + * ```js + * > var A = nj.array([[1,2], + * [3,4]]); + * > nj.applyOverAxis(A, nj.sum, {axis: 1}) + * array([3, 7]) + * > + * > nj.applyOverAxis(A, nj.sum, {axis: 1, keepdims: true}) + * array([[3], + * [7]]); + * ``` + * + * @throws error if axis is too large for the given arr + * @return {NdArray} An array of the results from the vectorFunc batched over the axis + */ +function applyOverAxis (arr, vectorFunc, { axis=undefined, keepdims=false } = {}) { + // by default, compute across the flat array + if(axis === undefined) return vectorFunc(arr); + // ie when the axis is negative refer to end axes + if(axis < 0) { + axis = arr.shape.length + axis; // axis is - so will be < arr.shape.length + } + // if the axis is negative then wrap to end + if(axis > arr.shape.length || axis < 0) throw new errors.ValueError('the axis exceeds max number of axes (shape.length)'); + + // + // Now iterate over all vectors around the given axis and apply the vectorFunc to it + // + var results = []; + var iterShape = arr.shape.filter((d, i) => i !== axis); + // all possible indices we need to iterate over around the axis + var p = nestedIteration(iterShape); + for(var i = 0; i < p.length; i++) { + // put the null back where the axis is + var sliceLocation = p[i]; + sliceLocation.splice(axis, 0, null); + + // select the vector given location and apply the vectorOperation + var vector = arr.pick(...sliceLocation); // column at axis + var apply = vectorFunc(vector); + + // accumulate results + results.push(apply); + } + + // reshape back to original array, but with the reduced axis dimension + var resultShape = [...arr.shape]; + if(keepdims) { + resultShape[axis] = 1; + } else { + resultShape.splice(axis, 1); + } + return NdArray.new(results).reshape(resultShape); +} + +/** + * Helper method to essentially dynamically generated nestex for loops + * + * for(var i = 0; i < shape[0]; i++) { + * for(var j = 0; j < shape[1]; j++) { + * ... and so on + * } + * } + * + * @param {Array} shape + * @returns i,j,k... indices for a nested for loop based on shape + */ +function nestedIteration (shape) { + var result = []; + function _iterate(shapeIndex, temp=[]) { + if(temp.length === shape.length) return temp; + for(var i = 0; i < shape[shapeIndex]; i++) { + var nested = _iterate(shapeIndex+1, [...temp, i]); + if (nested) result.push(nested); + } + } + _iterate(0); + return result; +} + module.exports = { config: CONF, dtypes: DTYPES, @@ -802,6 +1055,13 @@ module.exports = { reshape: reshape, zeros: zeros, ones: ones, + full: full, + eye: eye, + tri: tri, + zerosLike: zerosLike, + onesLike: onesLike, + fullLike: fullLike, + randomLike: randomLike, empty: empty, flatten: flatten, flip: flip, @@ -834,6 +1094,8 @@ module.exports = { equal: equal, max: max, min: min, + argmax: argmax, + argmin: argmin, mod: mod, remainder: mod, concatenate: concatenate, @@ -857,5 +1119,6 @@ module.exports = { uint32: function (array) { return NdArray.new(array, 'uint32'); }, float32: function (array) { return NdArray.new(array, 'float32'); }, float64: function (array) { return NdArray.new(array, 'float64'); }, + applyOverAxis: applyOverAxis, images: require('./images') }; diff --git a/src/ndarray.js b/src/ndarray.js index 89e9422..ec78fba 100644 --- a/src/ndarray.js +++ b/src/ndarray.js @@ -555,6 +555,30 @@ NdArray.prototype.min = function () { return ops.inf(this.selection); }; +/** +* Return the index of the maximum value of the array +* +* @returns {Array} +*/ +NdArray.prototype.argmax = function () { + if (this.selection.size === 0) { + return null; + } + return ops.argmax(this.selection); +}; + +/** +* Return the index of the minimum value of the array +* +* @returns {Array} +*/ +NdArray.prototype.argmin = function () { + if (this.selection.size === 0) { + return null; + } + return ops.argmin(this.selection); +}; + /** * Sum of array elements. * @@ -675,6 +699,13 @@ NdArray.prototype.toString = function () { } }; +/** +* Calls console.log(this.toString()) +*/ +NdArray.prototype.print = function () { + console.log(this.toString()); +}; + /** * Stringify the array to make it readable in the console, by a human. * diff --git a/test/mocha/applyOverAxis.spec.js b/test/mocha/applyOverAxis.spec.js new file mode 100644 index 0000000..b4f6b58 --- /dev/null +++ b/test/mocha/applyOverAxis.spec.js @@ -0,0 +1,26 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('nestedIteration', function () { + var A = nj.arange(4).reshape(2, 2); + var B = nj.arange(8).reshape(2, 2 ,2); + + it('Works across no axis (flat)', function () { + expect(nj.applyOverAxis(A, nj.sum)).to.eql(6); + expect(nj.applyOverAxis(B, nj.sum)).to.eql(28); + }); + + it('Works across last axis', function () { + expect(nj.applyOverAxis(A, nj.sum, {axis: -1}).tolist()).to.eql([ 1, 5]); + expect(nj.applyOverAxis(B, nj.sum, {axis: -1}).tolist()).to.eql([[ 1, 5], [ 9, 13]]); + }); + + it('keepdims works', function () { + expect(nj.applyOverAxis(A, nj.sum, {axis: -1, keepdims: true}).tolist()).to.eql([ [1], [5]]); + expect(nj.applyOverAxis(B, nj.sum, {axis: -1, keepdims: true}).tolist()).to.eql([[[ 1], [ 5]], [[ 9], [ 13]]]); + }); +}); diff --git a/test/mocha/argmax.spec.js b/test/mocha/argmax.spec.js new file mode 100644 index 0000000..83e2658 --- /dev/null +++ b/test/mocha/argmax.spec.js @@ -0,0 +1,23 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('argmax', function () { + it('should be null for an empty array', function () { + var arr = nj.array([]); + expect(arr.argmax()).to.equal(null); + }); + it('should return the max element in array', function () { + var arr = nj.arange(10); + expect(arr.argmax()[0]).to.equal(9); + }); + it('should return the max element in matrix', function () { + var arr = nj.arange(10).reshape(2, 5); + const a = arr.argmax(); + expect(a[0]).to.equal(1); + expect(a[1]).to.equal(4); + }); +}); diff --git a/test/mocha/argmin.spec.js b/test/mocha/argmin.spec.js new file mode 100644 index 0000000..45b0923 --- /dev/null +++ b/test/mocha/argmin.spec.js @@ -0,0 +1,23 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('argmin', function () { + it('should be null for an empty array', function () { + var arr = nj.array([]); + expect(arr.argmin()).to.equal(null); + }); + it('should return the min element in array', function () { + var arr = nj.arange(10); + expect(arr.argmin()[0]).to.equal(0); + }); + it('should return the min element in matrix', function () { + var arr = nj.arange(10).reshape(2, 5); + const a = arr.argmin(); + expect(a[0]).to.equal(0); + expect(a[1]).to.equal(0); + }); +}); diff --git a/test/mocha/eye.spec.js b/test/mocha/eye.spec.js new file mode 100644 index 0000000..a44cf76 --- /dev/null +++ b/test/mocha/eye.spec.js @@ -0,0 +1,20 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('eye', function () { + it('can generate square matrix ', function () { + expect(nj.eye(2).tolist()).to.eql([[1, 0], [0, 1]]); + }); + + it('can generate matrix with different dimensions', function () { + expect(nj.eye(1, 2).tolist()).to.eql([[1, 0]]); + }); + + it('should accept a dtype', function () { + expect(nj.eye(0, 'uint8').dtype).to.equal('uint8'); + }); +}); diff --git a/test/mocha/full.spec.js b/test/mocha/full.spec.js new file mode 100644 index 0000000..bab0d12 --- /dev/null +++ b/test/mocha/full.spec.js @@ -0,0 +1,32 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('full', function () { + it('can generate from scalar fillValue', function () { + expect(nj.full(0, 5).tolist()).to.eql([]); + expect(nj.full(2, 5).tolist()).to.eql([5, 5]); + expect(nj.full([2], 5).tolist()).to.eql([5, 5]); + }); + + it('can generate matrix from scalar fillValue', function () { + expect(nj.full([2, 2], 5).tolist()) + .to.eql([ + [5, 5], + [5, 5]]); + }); + + it('can generate matrix from array fillValue', function () { + expect(nj.full([2, 2], [1, 2]).tolist()) + .to.eql([ + [1, 2], + [1, 2]]); + }); + + it('should accept a dtype', function () { + expect(nj.full(0, 5, 'uint8').dtype).to.equal('uint8'); + }); +}); diff --git a/test/mocha/fullLike.spec.js b/test/mocha/fullLike.spec.js new file mode 100644 index 0000000..ba8ecae --- /dev/null +++ b/test/mocha/fullLike.spec.js @@ -0,0 +1,23 @@ +/* eslint-env mocha */ +"use strict"; + +var expect = require("expect.js"); + +var nj = require("../../src"); + +describe("fullLike", function () { + var A = nj.array([ + [1, 2], + [3, 4], + ]); + it("can generate fillValue like shaped like a given matrix", function () { + expect(nj.fullLike(A, 5).tolist()).to.eql([ + [5, 5], + [5, 5], + ]); + }); + + it("should accept a dtype", function () { + expect(nj.fullLike(A, 5, "uint8").dtype).to.equal("uint8"); + }); +}); diff --git a/test/mocha/onesLike.spec.js b/test/mocha/onesLike.spec.js new file mode 100644 index 0000000..6e8f8ac --- /dev/null +++ b/test/mocha/onesLike.spec.js @@ -0,0 +1,23 @@ +/* eslint-env mocha */ +"use strict"; + +var expect = require("expect.js"); + +var nj = require("../../src"); + +describe("zerosLike", function () { + var A = nj.array([ + [1, 2], + [3, 4], + ]); + it("can generate zeros like shaped like a given matrix", function () { + expect(nj.onesLike(A).tolist()).to.eql([ + [1, 1], + [1, 1], + ]); + }); + + it("should accept a dtype", function () { + expect(nj.onesLike(A, "uint8").dtype).to.equal("uint8"); + }); +}); diff --git a/test/mocha/randomLike.spec.js b/test/mocha/randomLike.spec.js new file mode 100644 index 0000000..0a01b56 --- /dev/null +++ b/test/mocha/randomLike.spec.js @@ -0,0 +1,16 @@ +/* eslint-env mocha */ +"use strict"; + +var expect = require("expect.js"); + +var nj = require("../../src"); + +describe("randomLike", function () { + var A = nj.array([ + [1, 2], + [3, 4], + ]); + it("can generate zeros like shaped like a given matrix", function () { + expect(nj.randomLike(A).shape).to.eql([2, 2]); + }); +}); diff --git a/test/mocha/tri.spec.js b/test/mocha/tri.spec.js new file mode 100644 index 0000000..f660af6 --- /dev/null +++ b/test/mocha/tri.spec.js @@ -0,0 +1,20 @@ +/* eslint-env mocha */ +'use strict'; + +var expect = require('expect.js'); + +var nj = require('../../src'); + +describe('tri', function () { + it('can generate square matrix ', function () { + expect(nj.tri(2).tolist()).to.eql([[0, 0], [1, 0]]); + }); + + it('can generate matrix with different dimensions', function () { + expect(nj.tri(3, 2).tolist()).to.eql([[0, 0], [1, 0], [1, 1]]); + }); + + it('should accept a dtype', function () { + expect(nj.tri(0, 'uint8').dtype).to.equal('uint8'); + }); +}); diff --git a/test/mocha/zerosLike.spec.js b/test/mocha/zerosLike.spec.js new file mode 100644 index 0000000..8a5270e --- /dev/null +++ b/test/mocha/zerosLike.spec.js @@ -0,0 +1,23 @@ +/* eslint-env mocha */ +"use strict"; + +var expect = require("expect.js"); + +var nj = require("../../src"); + +describe("zerosLike", function () { + var A = nj.array([ + [1, 2], + [3, 4], + ]); + it("can generate zeros like shaped like a given matrix", function () { + expect(nj.zerosLike(A).tolist()).to.eql([ + [0, 0], + [0, 0], + ]); + }); + + it("should accept a dtype", function () { + expect(nj.zerosLike(A, "uint8").dtype).to.equal("uint8"); + }); +});