From f164662541f0e47ec07106eac4ffc5c0988117de Mon Sep 17 00:00:00 2001 From: Nikhil Thorat Date: Mon, 9 Oct 2017 14:26:33 -0400 Subject: [PATCH] iOS (#109) * ios * merge * Merge remote-tracking branch 'origin/master' into ios * ios * its working...? haha, not. * improve precision * fix accuracy on iOS * fix the faster / slightly less precise version * slight speedup * fix numerical issues on ios. (use resultUV instead of gl_FragCoord, and highp int) * merge master * actually merge * flag guard byte textures * Merge remote-tracking branch 'origin' into ios * Merge remote-tracking branch 'origin' into ios * more changes * merge * start pulling tests apart * tests * merge * get remaining tests to pass * Merge remote-tracking branch 'origin' into ios * remove comments, remove imagenet util change * imagenet * test_util commits * ndarray tests * test_util blank space * softmax underflow on mac, copy gpu test revert * revert _gpu_tests * remove console.log * fix lint errors * respond to comments --- demos/mnist/mnist.ts | 12 +- demos/one_plus_one/one_plus_one.ts | 40 +- src/environment.ts | 41 +- src/graph/ops/argmax_test.ts | 5 +- src/index.ts | 4 +- src/math/batchnorm_test.ts | 191 +++ src/math/clone_test.ts | 49 + src/math/concat_test.ts | 251 ++++ src/math/conv2d_der_test.ts | 84 ++ src/math/conv2d_test.ts | 157 ++ src/math/conv2d_transpose_test.ts | 115 ++ src/math/copy2d_test.ts | 82 + src/math/element_wise_arithmetic_test.ts | 447 ++++++ src/math/lstm_test.ts | 125 ++ src/math/math_cpu_test.ts | 1748 ---------------------- src/math/math_test.ts | 205 +++ src/math/matmul_test.ts | 393 +++++ src/math/max_pool_backprop_test.ts | 235 +++ src/math/ndarray_test.ts | 669 +++++---- src/math/pool_test.ts | 249 +++ src/math/reduction_ops_test.ts | 235 +++ src/math/resize_bilinear_test.ts | 101 ++ src/math/slice_test.ts | 230 +++ src/math/softmax_test.ts | 67 + src/math/transpose_test.ts | 75 + src/math/unaryop_test.ts | 649 ++++++++ src/math/webgl/gpgpu_context_test.ts | 66 +- src/math/webgl/gpgpu_math.ts | 17 + src/math/webgl/gpgpu_util.ts | 89 +- src/math/webgl/shader_compiler.ts | 119 +- src/math/webgl/tex_util.ts | 58 +- src/math/webgl/tex_util_test.ts | 12 + src/math/webgl/webgl_util.ts | 5 + src/test_util.ts | 133 +- 34 files changed, 4777 insertions(+), 2181 deletions(-) create mode 100644 src/math/batchnorm_test.ts create mode 100644 src/math/clone_test.ts create mode 100644 src/math/concat_test.ts create mode 100644 src/math/conv2d_der_test.ts create mode 100644 src/math/conv2d_test.ts create mode 100644 src/math/conv2d_transpose_test.ts create mode 100644 src/math/copy2d_test.ts create mode 100644 src/math/element_wise_arithmetic_test.ts create mode 100644 src/math/lstm_test.ts delete mode 100644 src/math/math_cpu_test.ts create mode 100644 src/math/math_test.ts create mode 100644 src/math/matmul_test.ts create mode 100644 src/math/max_pool_backprop_test.ts create mode 100644 src/math/pool_test.ts create mode 100644 src/math/reduction_ops_test.ts create mode 100644 src/math/resize_bilinear_test.ts create mode 100644 src/math/slice_test.ts create mode 100644 src/math/softmax_test.ts create mode 100644 src/math/transpose_test.ts create mode 100644 src/math/unaryop_test.ts diff --git a/demos/mnist/mnist.ts b/demos/mnist/mnist.ts index 552fdfc962..a5a47852a0 100644 --- a/demos/mnist/mnist.ts +++ b/demos/mnist/mnist.ts @@ -39,7 +39,7 @@ reader.getAllVariables().then(vars => { const probsVal = sess.eval(probs, [{tensor: input, data: inputData}]); console.log(`Item ${i}, probsVal ${probsVal.get()}.`); const label = data.labels[i]; - const predictedLabel = probsVal.get(); + const predictedLabel = Math.round(probsVal.get()); if (label === predictedLabel) { numCorrect++; } @@ -79,12 +79,10 @@ export function buildModelMathAPI( return (x: Array1D): Scalar => { return math.scope(() => { - const hidden1 = - math.relu(math.add(math.vectorTimesMatrix(x, hidden1W), hidden1B)) as - Array1D; - const hidden2 = - math.relu(math.add( - math.vectorTimesMatrix(hidden1, hidden2W), hidden2B)) as Array1D; + const hidden1 = math.relu( + math.add(math.vectorTimesMatrix(x, hidden1W), hidden1B)) as Array1D; + const hidden2 = math.relu(math.add( + math.vectorTimesMatrix(hidden1, hidden2W), hidden2B)) as Array1D; const logits = math.add(math.vectorTimesMatrix(hidden2, softmaxW), softmaxB); return math.argMax(logits); diff --git a/demos/one_plus_one/one_plus_one.ts b/demos/one_plus_one/one_plus_one.ts index 8fd7936d5a..a925045523 100644 --- a/demos/one_plus_one/one_plus_one.ts +++ b/demos/one_plus_one/one_plus_one.ts @@ -16,40 +16,12 @@ */ // tslint:disable-next-line:max-line-length -import {Graph, NDArrayMath, NDArrayMathGPU, Scalar, Session, Tensor} from '../deeplearn'; +import {NDArrayMathGPU, Scalar} from '../deeplearn'; -class Adder { - inputTensorA: Tensor; - inputTensorB: Tensor; - sum: Tensor; - session: Session; - math: NDArrayMath = new NDArrayMathGPU(); - setupSession(): void { - const graph = new Graph(); +const math = new NDArrayMathGPU(); +const a = Scalar.new(1); +const b = Scalar.new(1); - this.inputTensorA = graph.placeholder('A', []); - this.inputTensorB = graph.placeholder('B', []); - this.sum = graph.add(this.inputTensorA, this.inputTensorB); - this.session = new Session(graph, this.math); - } +const result = math.add(a, b).get(); - computeSum(a: number, b: number): number { - const feeds = [ - {tensor: this.inputTensorA, data: Scalar.new(a)}, - {tensor: this.inputTensorB, data: Scalar.new(b)} - ]; - let result; - this.math.scope(() => { - result = this.session.eval(this.sum, feeds).get(); - }); - return result; - } -} - -const adder = new Adder(); -adder.setupSession(); -const result = adder.computeSum(1, 1); - -const outputEl = document.getElementById('output'); -if (!outputEl) throw new Error('output element not found'); -outputEl.innerText = String(result); +document.getElementById('output').innerText = '' + result; diff --git a/src/environment.ts b/src/environment.ts index 1c1157506f..dd2fca97e3 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -31,12 +31,16 @@ export interface Features { 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_RELIABLE'?: boolean; // 0: No WebGL, 1: WebGL 1.0, 2: WebGL 2.0. 'WEBGL_VERSION'?: number; + // Whether writing & reading floating point textures is enabled. When + // false, fall back to using unsigned byte textures. + 'WEBGL_FLOAT_TEXTURE_ENABLED'?: boolean; } export const URL_PROPERTIES: URLProperty[] = [ {name: 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED', type: Type.BOOLEAN}, {name: 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_RELIABLE', type: Type.BOOLEAN}, - {name: 'WEBGL_VERSION', type: Type.NUMBER} + {name: 'WEBGL_VERSION', type: Type.NUMBER}, + {name: 'WEBGL_FLOAT_TEXTURE_ENABLED', type: Type.BOOLEAN} ]; export interface URLProperty { @@ -91,6 +95,37 @@ function isWebGLDisjointQueryTimerEnabled(webGLVersion: number) { return isExtEnabled; } +function isFloatTextureReadPixelsEnabled(webGLVersion: number): boolean { + if (webGLVersion === 0) { + return false; + } + + if (webGLVersion === 2) { + // WebGL 2 has floating point textures enabled by default. + return true; + } + + const gl = getWebGLRenderingContext(webGLVersion); + gl.getExtension('OES_texture_float'); + gl.getExtension('WEBGL_color_buffer_float'); + + const frameBuffer = gl.createFramebuffer(); + const texture = gl.createTexture(); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.FLOAT, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + + const frameBufferComplete = + (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE); + + loseContext(gl); + + return frameBufferComplete; +} + export class Environment { private features: Features = {}; @@ -129,6 +164,8 @@ export class Environment { return 1; } return 0; + } else if (feature === 'WEBGL_FLOAT_TEXTURE_ENABLED') { + return isFloatTextureReadPixelsEnabled(this.get('WEBGL_VERSION')); } throw new Error(`Unknown feature ${feature}.`); } @@ -139,7 +176,7 @@ const DEEPLEARNJS_FLAGS_PREFIX = 'dljsflags'; function getFeaturesFromURL(): Features { const features: Features = {}; - if(typeof window === 'undefined') { + if (typeof window === 'undefined') { return features; } diff --git a/src/graph/ops/argmax_test.ts b/src/graph/ops/argmax_test.ts index 2b74b5c83d..f8f07a741e 100644 --- a/src/graph/ops/argmax_test.ts +++ b/src/graph/ops/argmax_test.ts @@ -17,6 +17,7 @@ import {NDArrayMathCPU} from '../../math/math_cpu'; import {Array1D, Array2D} from '../../math/ndarray'; +import * as test_util from '../../test_util'; import {Tensor} from '../graph'; import {TensorArrayMap} from '../tensor_array_map'; @@ -50,7 +51,7 @@ describe('Argmax oper', () => { const yVal = tensorArrayMap.get(y); expect(yVal.shape).toEqual([]); - expect(yVal.get()).toEqual(1); + test_util.expectNumbersClose(yVal.get(), 1); }); it('argmax of Array2D', () => { @@ -64,6 +65,6 @@ describe('Argmax oper', () => { const yVal = tensorArrayMap.get(y); expect(yVal.shape).toEqual([]); - expect(yVal.get()).toEqual(4); + test_util.expectNumbersClose(yVal.get(), 4); }); }); diff --git a/src/index.ts b/src/index.ts index 98a4ce6c5d..b98ce7ca48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ */ import * as xhr_dataset from './data/xhr-dataset'; +import * as environment from './environment'; import * as conv_util from './math/conv_util'; import * as gpgpu_util from './math/webgl/gpgpu_util'; import * as render_ndarray_gpu_util from './math/webgl/render_ndarray_gpu_util'; @@ -28,7 +29,7 @@ export {DataStats, InMemoryDataset} from './data/dataset'; // tslint:disable-next-line:max-line-length export {InCPUMemoryShuffledInputProviderBuilder, InGPUMemoryShuffledInputProviderBuilder, InputProvider} from './data/input_provider'; export {XhrDataset, XhrDatasetConfig, XhrModelConfig} from './data/xhr-dataset'; -export {ENV, Features} from './environment'; +export {ENV, Environment, Features} from './environment'; export {Graph, Tensor} from './graph/graph'; export {AdadeltaOptimizer} from './graph/optimizers/adadelta_optimizer'; export {AdagradOptimizer} from './graph/optimizers/adagrad_optimizer'; @@ -51,6 +52,7 @@ export {GPGPUContext} from './math/webgl/gpgpu_context'; // Second level exports. export { conv_util, + environment, gpgpu_util, render_ndarray_gpu_util, test_util, diff --git a/src/math/batchnorm_test.ts b/src/math/batchnorm_test.ts new file mode 100644 index 0000000000..43663eb2b6 --- /dev/null +++ b/src/math/batchnorm_test.ts @@ -0,0 +1,191 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array3D} from './ndarray'; + +// math.batchNormalization3D +{ + // TODO(nsthorat): Fix the precision for byte-packed batchnorm. + const epsilon = 1e-1; + const tests: MathTests = it => { + it('simple batchnorm, no offset or scale, 2x1x2', math => { + const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); + const mean = Array1D.new([1, 2]); + const variance = Array1D.new([2, 3]); + const varianceEpsilon = .001; + + const result = math.batchNormalization3D( + x, mean, variance, varianceEpsilon, undefined, undefined); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([ + (x.get(0, 0, 0) - mean.get(0)) * 1 / + Math.sqrt(variance.get(0) + varianceEpsilon), + (x.get(0, 0, 1) - mean.get(1)) * 1 / + Math.sqrt(variance.get(1) + varianceEpsilon), + (x.get(1, 0, 0) - mean.get(0)) * 1 / + Math.sqrt(variance.get(0) + varianceEpsilon), + (x.get(1, 0, 1) - mean.get(1)) * 1 / + Math.sqrt(variance.get(1) + varianceEpsilon) + ]), + epsilon); + + x.dispose(); + mean.dispose(); + variance.dispose(); + }); + + it('simple batchnorm, no offset, 2x1x2', math => { + const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); + const mean = Array1D.new([1, 2]); + const variance = Array1D.new([2, 3]); + const scale = Array1D.new([4, 5]); + const varianceEpsilon = .001; + + const result = math.batchNormalization3D( + x, mean, variance, varianceEpsilon, scale, undefined); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([ + (x.get(0, 0, 0) - mean.get(0)) * scale.get(0) / + Math.sqrt(variance.get(0) + varianceEpsilon), + (x.get(0, 0, 1) - mean.get(1)) * scale.get(1) / + Math.sqrt(variance.get(1) + varianceEpsilon), + (x.get(1, 0, 0) - mean.get(0)) * scale.get(0) / + Math.sqrt(variance.get(0) + varianceEpsilon), + (x.get(1, 0, 1) - mean.get(1)) * scale.get(1) / + Math.sqrt(variance.get(1) + varianceEpsilon) + ]), + epsilon); + + x.dispose(); + mean.dispose(); + variance.dispose(); + scale.dispose(); + }); + + it('simple batchnorm, no scale, 2x1x2', math => { + const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); + const mean = Array1D.new([1, 2]); + const variance = Array1D.new([2, 3]); + const offset = Array1D.new([4, 5]); + + const varianceEpsilon = .001; + + const result = math.batchNormalization3D( + x, mean, variance, varianceEpsilon, undefined, offset); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([ + offset.get(0) + + (x.get(0, 0, 0) - mean.get(0)) * 1 / + Math.sqrt(variance.get(0) + varianceEpsilon), + offset.get(1) + + (x.get(0, 0, 1) - mean.get(1)) * 1 / + Math.sqrt(variance.get(1) + varianceEpsilon), + offset.get(0) + + (x.get(1, 0, 0) - mean.get(0)) * 1 / + Math.sqrt(variance.get(0) + varianceEpsilon), + offset.get(1) + + (x.get(1, 0, 1) - mean.get(1)) * 1 / + Math.sqrt(variance.get(1) + varianceEpsilon) + ]), + epsilon); + x.dispose(); + mean.dispose(); + variance.dispose(); + offset.dispose(); + }); + + it('simple batchnorm, 2x1x2', math => { + const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); + const mean = Array1D.new([1, 2]); + const variance = Array1D.new([2, 3]); + const offset = Array1D.new([3, 4]); + const scale = Array1D.new([4, 5]); + + const varianceEpsilon = .001; + + const result = math.batchNormalization3D( + x, mean, variance, varianceEpsilon, scale, offset); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([ + offset.get(0) + + (x.get(0, 0, 0) - mean.get(0)) * scale.get(0) / + Math.sqrt(variance.get(0) + varianceEpsilon), + offset.get(1) + + (x.get(0, 0, 1) - mean.get(1)) * scale.get(1) / + Math.sqrt(variance.get(1) + varianceEpsilon), + offset.get(0) + + (x.get(1, 0, 0) - mean.get(0)) * scale.get(0) / + Math.sqrt(variance.get(0) + varianceEpsilon), + offset.get(1) + + (x.get(1, 0, 1) - mean.get(1)) * scale.get(1) / + Math.sqrt(variance.get(1) + varianceEpsilon) + ]), + epsilon); + x.dispose(); + mean.dispose(); + variance.dispose(); + scale.dispose(); + offset.dispose(); + }); + + it('batchnorm matches tensorflow, 2x3x3', math => { + const x = Array3D.new( + [2, 3, 3], new Float32Array([ + 0.49955603, 0.04158615, -1.09440524, 2.03854165, -0.61578344, + 2.87533573, 1.18105987, 0.807462, 1.87888837, 2.26563962, + -0.37040935, 1.35848753, -0.75347094, 0.15683117, 0.91925946, + 0.34121279, 0.92717143, 1.89683965 + ])); + const mean = Array1D.new([0.39745062, -0.48062894, 0.4847822]); + const variance = Array1D.new([0.32375343, 0.67117643, 1.08334653]); + const offset = Array1D.new([0.69398749, -1.29056387, 0.9429723]); + const scale = Array1D.new([-0.5607271, 0.9878457, 0.25181573]); + const varianceEpsilon = .001; + + const result = math.batchNormalization3D( + x, mean, variance, varianceEpsilon, scale, offset); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([ + 0.59352049, -0.66135202, 0.5610874, -0.92077015, -1.45341019, + 1.52106473, -0.07704776, 0.26144429, 1.28010017, -1.14422404, + -1.15776136, 1.15425493, 1.82644104, -0.52249442, 1.04803919, + 0.74932291, 0.40568101, 1.2844412 + ])); + + x.dispose(); + mean.dispose(); + variance.dispose(); + scale.dispose(); + offset.dispose(); + }); + }; + + test_util.describeMathCPU('batchNormalization3D', [tests]); + test_util.describeMathGPU('batchNormalization3D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/clone_test.ts b/src/math/clone_test.ts new file mode 100644 index 0000000000..4bdf6acb75 --- /dev/null +++ b/src/math/clone_test.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array2D} from './ndarray'; + +const commonTests: MathTests = it => { + it('returns a ndarray with the same shape and value', math => { + const a = Array2D.new([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]); + const aPrime = math.clone(a); + expect(aPrime.shape).toEqual(a.shape); + test_util.expectArraysClose(aPrime.getValues(), a.getValues()); + a.dispose(); + }); +}; + +const gpuTests: MathTests = it => { + it('returns a ndarray with a different texture handle', math => { + const a = Array2D.new([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]); + const aPrime = math.clone(a); + expect(a.inGPU()).toEqual(true); + expect(aPrime.inGPU()).toEqual(true); + expect(aPrime.getTexture()).not.toBe(a.getTexture()); + a.dispose(); + }); +}; + +test_util.describeMathCPU('clone', [commonTests]); +test_util.describeMathGPU('clone', [commonTests, gpuTests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} +]); diff --git a/src/math/concat_test.ts b/src/math/concat_test.ts new file mode 100644 index 0000000000..e7bc79a191 --- /dev/null +++ b/src/math/concat_test.ts @@ -0,0 +1,251 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D, Array3D} from './ndarray'; + +// math.concat1D +{ + const tests: MathTests = it => { + it('3 + 5', math => { + const a = Array1D.new([3]); + const b = Array1D.new([5]); + + const result = math.concat1D(a, b); + const expected = new Float32Array([3, 5]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('3 + [5,7]', math => { + const a = Array1D.new([3]); + const b = Array1D.new([5, 7]); + + const result = math.concat1D(a, b); + const expected = new Float32Array([3, 5, 7]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('[3,5] + 7', math => { + const a = Array1D.new([3, 5]); + const b = Array1D.new([7]); + + const result = math.concat1D(a, b); + const expected = new Float32Array([3, 5, 7]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + }; + + test_util.describeMathCPU('concat1D', [tests]); + test_util.describeMathGPU('concat1D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.concat2D +{ + const tests: MathTests = it => { + it('[[3]] + [[5]], axis=0', math => { + const axis = 0; + const a = Array2D.new([1, 1], [3]); + const b = Array2D.new([1, 1], [5]); + + const result = math.concat2D(a, b, axis); + const expected = new Float32Array([3, 5]); + + expect(result.shape).toEqual([2, 1]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('[[3]] + [[5]], axis=1', math => { + const axis = 1; + const a = Array2D.new([1, 1], [3]); + const b = Array2D.new([1, 1], [5]); + + const result = math.concat2D(a, b, axis); + const expected = new Float32Array([3, 5]); + + expect(result.shape).toEqual([1, 2]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('[[1, 2], [3, 4]] + [[5, 6]], axis=0', math => { + const axis = 0; + const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); + const b = Array2D.new([1, 2], [[5, 6]]); + + const result = math.concat2D(a, b, axis); + const expected = new Float32Array([1, 2, 3, 4, 5, 6]); + + expect(result.shape).toEqual([3, 2]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('[[1, 2], [3, 4]] + [[5, 6]], axis=1 throws error', math => { + const axis = 1; + const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); + const b = Array2D.new([1, 2], [[5, 6]]); + + expect(() => math.concat2D(a, b, axis)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('[[1, 2], [3, 4]] + [[5, 6], [7, 8]], axis=1', math => { + const axis = 1; + const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); + const b = Array2D.new([2, 2], [[5, 6], [7, 8]]); + + const result = math.concat2D(a, b, axis); + const expected = new Float32Array([1, 2, 5, 6, 3, 4, 7, 8]); + + expect(result.shape).toEqual([2, 4]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + }; + + test_util.describeMathCPU('concat2D', [tests]); + test_util.describeMathGPU('concat2D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.concat3D +{ + const tests: MathTests = it => { + it('shapes correct concat axis=0', math => { + const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); + const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); + const values = math.concat3D(ndarray1, ndarray2, 0); + expect(values.shape).toEqual([2, 1, 3]); + test_util.expectArraysClose( + values.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + }); + + it('concat axis=0', math => { + const ndarray1 = Array3D.new([1, 2, 3], [1, 11, 111, 2, 22, 222]); + const ndarray2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + const values = math.concat3D(ndarray1, ndarray2, 0); + expect(values.shape).toEqual([3, 2, 3]); + test_util.expectArraysClose(values.getValues(), new Float32Array([ + 1, 11, 111, 2, 22, 222, 5, 55, 555, 6, 66, + 666, 7, 77, 777, 8, 88, 888 + ])); + }); + + it('shapes correct concat axis=1', math => { + const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); + const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); + const values = math.concat3D(ndarray1, ndarray2, 1); + expect(values.shape).toEqual([1, 2, 3]); + test_util.expectArraysClose( + values.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + }); + + it('concat axis=1', math => { + const ndarray1 = Array3D.new([2, 1, 3], [1, 11, 111, 3, 33, 333]); + const ndarray2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + const values = math.concat3D(ndarray1, ndarray2, 1); + expect(values.shape).toEqual([2, 3, 3]); + test_util.expectArraysClose(values.getValues(), new Float32Array([ + 1, 11, 111, 5, 55, 555, 6, 66, 666, 3, 33, + 333, 7, 77, 777, 8, 88, 888 + ])); + }); + + it('shapes correct concat axis=2', math => { + const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); + const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); + const values = math.concat3D(ndarray1, ndarray2, 2); + expect(values.shape).toEqual([1, 1, 6]); + test_util.expectArraysClose( + values.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + }); + + it('concat axis=2', math => { + const ndarray1 = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); + const ndarray2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + const values = math.concat3D(ndarray1, ndarray2, 2); + expect(values.shape).toEqual([2, 2, 5]); + test_util.expectArraysClose(values.getValues(), new Float32Array([ + 1, 11, 5, 55, 555, 2, 22, 6, 66, 666, + 3, 33, 7, 77, 777, 4, 44, 8, 88, 888 + ])); + }); + + it('concat throws when invalid non-axis shapes, axis=0', math => { + const axis = 0; + const x1 = Array3D.new([1, 1, 3], [1, 11, 111]); + const x2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + expect(() => math.concat3D(x1, x2, axis)).toThrowError(); + }); + + it('concat throws when invalid non-axis shapes, axis=1', math => { + const axis = 1; + const x1 = Array3D.new([1, 1, 3], [1, 11, 111]); + const x2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + expect(() => math.concat3D(x1, x2, axis)).toThrowError(); + }); + + it('concat throws when invalid non-axis shapes, axis=2', math => { + const axis = 2; + const x1 = Array3D.new([1, 2, 2], [1, 11, 2, 22]); + const x2 = Array3D.new( + [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); + expect(() => math.concat3D(x1, x2, axis)).toThrowError(); + }); + }; + + test_util.describeMathCPU('concat3D', [tests]); + test_util.describeMathGPU('concat3D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/conv2d_der_test.ts b/src/math/conv2d_der_test.ts new file mode 100644 index 0000000000..31a0b7a6f2 --- /dev/null +++ b/src/math/conv2d_der_test.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array3D} from './ndarray'; + +// math.conv2dDerWeights +{ + const tests: MathTests = it => { + it('input=3x3x1,d2=1,f=2,s=1,p=0', math => { + const inputDepth = 1; + const outputDepth = 1; + const inputShape: [number, number, number] = [3, 3, inputDepth]; + const fSize = 2; + const stride = 1; + const pad = 0; + + const weightsShape: [number, number, number, number] = + [fSize, fSize, inputDepth, outputDepth]; + + const x = Array3D.new(inputShape, [1, 2, 3, 4, 5, 6, 7, 8, 9]); + const dy = Array3D.new([2, 2, 1], [3, 1, 2, 0]); + + const result = math.conv2dDerFilter(x, dy, weightsShape, stride, pad); + const expected = new Float32Array([13, 19, 31, 37]); + + expect(result.shape).toEqual(weightsShape); + // TODO(nsthorat): Fix the precision for byte textures. + test_util.expectArraysClose(result.getValues(), expected, 1e-1); + + x.dispose(); + dy.dispose(); + }); + }; + + test_util.describeMathCPU('conv2dDerWeights', [tests]); + test_util.describeMathGPU('conv2dDerWeights', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.conv2dDerBias +{ + const tests: MathTests = it => { + it(' dy=2x2x2', math => { + const outputDepth = 2; + const dyShape: [number, number, number] = [2, 2, outputDepth]; + const dy = Array3D.new(dyShape, [1, 2, 3, 4, 5, 6, 7, 8]); + + const result = math.conv2dDerBias(dy); + const expected = new Float32Array([16, 20]); + + expect(result.shape).toEqual([outputDepth]); + test_util.expectArraysClose(result.getValues(), expected); + + dy.dispose(); + }); + }; + + test_util.describeMathCPU('conv2dDerBias', [tests]); + test_util.describeMathGPU('conv2dDerBias', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/conv2d_test.ts b/src/math/conv2d_test.ts new file mode 100644 index 0000000000..0227cda75b --- /dev/null +++ b/src/math/conv2d_test.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D, Array3D, Array4D} from './ndarray'; + +// math.conv2d +{ + const tests: MathTests = it => { + it('input=2x2x1,d2=1,f=1,s=1,p=0', math => { + const inputDepth = 1; + const inputShape: [number, number, number] = [2, 2, inputDepth]; + const outputDepth = 1; + const fSize = 1; + const pad = 0; + const stride = 1; + + const x = Array3D.new(inputShape, [1, 2, 3, 4]); + const w = Array4D.new([fSize, fSize, inputDepth, outputDepth], [2]); + const bias = Array1D.new([-1]); + + const result = math.conv2d(x, w, bias, stride, pad); + const expected = new Float32Array([1, 3, 5, 7]); + + test_util.expectArraysClose(result.getValues(), expected); + x.dispose(); + w.dispose(); + bias.dispose(); + }); + + it('input=2x2x1,d2=1,f=2,s=1,p=0', math => { + const inputDepth = 1; + const inputShape: [number, number, number] = [2, 2, inputDepth]; + const outputDepth = 1; + const fSize = 2; + const pad = 0; + const stride = 1; + + const x = Array3D.new(inputShape, [1, 2, 3, 4]); + const w = + Array4D.new([fSize, fSize, inputDepth, outputDepth], [3, 1, 5, 0]); + const bias = Array1D.new([-1]); + + const result = math.conv2d(x, w, bias, stride, pad); + const expected = new Float32Array([19]); + + test_util.expectArraysClose(result.getValues(), expected); + + x.dispose(); + w.dispose(); + bias.dispose(); + }); + + it('throws when x is not rank 3', math => { + const inputDepth = 1; + const outputDepth = 1; + const fSize = 2; + const pad = 0; + const stride = 1; + + // tslint:disable-next-line:no-any + const x: any = Array2D.new([2, 2], [1, 2, 3, 4]); + const w = + Array4D.new([fSize, fSize, inputDepth, outputDepth], [3, 1, 5, 0]); + const bias = Array1D.new([-1]); + + expect(() => math.conv2d(x, w, bias, stride, pad)).toThrowError(); + + x.dispose(); + w.dispose(); + bias.dispose(); + }); + + it('throws when weights is not rank 4', math => { + const inputDepth = 1; + const inputShape: [number, number, number] = [2, 2, inputDepth]; + const pad = 0; + const stride = 1; + + const x = Array3D.new(inputShape, [1, 2, 3, 4]); + // tslint:disable-next-line:no-any + const w: any = Array3D.new([2, 2, 1], [3, 1, 5, 0]); + const bias = Array1D.new([-1]); + + expect(() => math.conv2d(x, w, bias, stride, pad)).toThrowError(); + + x.dispose(); + w.dispose(); + bias.dispose(); + }); + + it('throws when biases is not rank 1', math => { + const inputDepth = 1; + const inputShape: [number, number, number] = [2, 2, inputDepth]; + const outputDepth = 1; + const fSize = 2; + const pad = 0; + const stride = 1; + + const x = Array3D.new(inputShape, [1, 2, 3, 4]); + const w = + Array4D.new([fSize, fSize, inputDepth, outputDepth], [3, 1, 5, 0]); + // tslint:disable-next-line:no-any + const bias: any = Array2D.new([2, 2], [2, 2, 2, 2]); + + expect(() => math.conv2d(x, w, bias, stride, pad)).toThrowError(); + + x.dispose(); + w.dispose(); + bias.dispose(); + }); + + it('throws when x depth does not match weight depth', math => { + const inputDepth = 1; + const wrongInputDepth = 5; + const inputShape: [number, number, number] = [2, 2, inputDepth]; + const outputDepth = 1; + const fSize = 2; + const pad = 0; + const stride = 1; + + const x = Array3D.new(inputShape, [1, 2, 3, 4]); + const w = + Array4D.randNormal([fSize, fSize, wrongInputDepth, outputDepth]); + const bias = Array1D.new([-1]); + + expect(() => math.conv2d(x, w, bias, stride, pad)).toThrowError(); + + x.dispose(); + w.dispose(); + bias.dispose(); + }); + }; + + test_util.describeMathCPU('conv2d', [tests]); + test_util.describeMathGPU('conv2d', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/conv2d_transpose_test.ts b/src/math/conv2d_transpose_test.ts new file mode 100644 index 0000000000..031fe8b87a --- /dev/null +++ b/src/math/conv2d_transpose_test.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array2D, Array3D, Array4D} from './ndarray'; + +// math.conv2dTranspose +{ + const tests: MathTests = it => { + it('input=2x2x1,d2=1,f=2,s=1,p=0', math => { + const origInputDepth = 1; + const origOutputDepth = 1; + const inputShape: [number, number, number] = [1, 1, origOutputDepth]; + const fSize = 2; + const origPad = 0; + const origStride = 1; + + const x = Array3D.new(inputShape, [2]); + const w = Array4D.new( + [fSize, fSize, origInputDepth, origOutputDepth], [3, 1, 5, 0]); + + const result = math.conv2dTranspose(x, w, [2, 2, 1], origStride, origPad); + const expected = new Float32Array([6, 2, 10, 0]); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose(result.getValues(), expected); + + x.dispose(); + w.dispose(); + }); + + it('throws when x is not rank 3', math => { + const origInputDepth = 1; + const origOutputDepth = 1; + const fSize = 2; + const origPad = 0; + const origStride = 1; + + // tslint:disable-next-line:no-any + const x: any = Array2D.new([2, 1], [2, 2]); + const w = Array4D.new( + [fSize, fSize, origInputDepth, origOutputDepth], [3, 1, 5, 0]); + + expect(() => math.conv2dTranspose(x, w, [2, 2, 1], origStride, origPad)) + .toThrowError(); + + x.dispose(); + w.dispose(); + }); + + it('throws when weights is not rank 4', math => { + const origInputDepth = 1; + const origOutputDepth = 1; + const inputShape: [number, number, number] = [1, 1, origOutputDepth]; + const fSize = 2; + const origPad = 0; + const origStride = 1; + + const x = Array3D.new(inputShape, [2]); + // tslint:disable-next-line:no-any + const w: any = Array3D.new([fSize, fSize, origInputDepth], [3, 1, 5, 0]); + + expect(() => math.conv2dTranspose(x, w, [2, 2, 1], origStride, origPad)) + .toThrowError(); + + x.dispose(); + w.dispose(); + }); + + it('throws when x depth does not match weights original output depth', + math => { + const origInputDepth = 1; + const origOutputDepth = 2; + const wrongOrigOutputDepth = 3; + const inputShape: [number, number, number] = [1, 1, origOutputDepth]; + const fSize = 2; + const origPad = 0; + const origStride = 1; + + const x = Array3D.new(inputShape, [2, 2]); + const w = Array4D.randNormal( + [fSize, fSize, origInputDepth, wrongOrigOutputDepth]); + + expect( + () => math.conv2dTranspose(x, w, [2, 2, 2], origStride, origPad)) + .toThrowError(); + + x.dispose(); + w.dispose(); + }); + }; + + test_util.describeMathCPU('conv2dTranspose', [tests]); + test_util.describeMathGPU('conv2dTranspose', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/copy2d_test.ts b/src/math/copy2d_test.ts new file mode 100644 index 0000000000..602f9b7514 --- /dev/null +++ b/src/math/copy2d_test.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array2D} from './ndarray'; + +const tests: MathTests = it => { + it('throws an error if source and dest shapes have different areas', math => { + const source = Array2D.zeros([100, 100]); + const dest = Array2D.zeros([100, 100]); + const sourceSize: [number, number] = [20, 20]; + const destSize: [number, number] = [5, 5]; + + expect( + () => math.copy2D(source, [0, 0], sourceSize, dest, [0, 0], destSize)) + .toThrowError(); + + source.dispose(); + dest.dispose(); + }); + + it('copies a src shape into a dst shape', math => { + const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const dest = Array2D.zeros([6, 2]); + + math.copy2D(source, [1, 1], [2, 3], dest, [2, 0], [3, 2]); + + test_util.expectArraysClose( + dest.getValues(), + new Float32Array([0, 0, 0, 0, 6, 7, 8, 10, 11, 12, 0, 0])); + + source.dispose(); + dest.dispose(); + }); + + it('throws when requesting out of bounds source copy', math => { + const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const dest = Array2D.zeros([6, 2]); + + expect(() => math.copy2D(source, [1, 1], [10, 10], dest, [2, 0], [ + 3, 2 + ])).toThrowError(); + + source.dispose(); + dest.dispose(); + }); + + it('throws when requesting out of bounds dest copy', math => { + const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const dest = Array2D.zeros([6, 2]); + + expect(() => math.copy2D(source, [1, 1], [2, 3], dest, [2, 0], [ + 3, 10 + ])).toThrowError(); + + source.dispose(); + dest.dispose(); + }); +}; + +test_util.describeMathCPU('copy2D', [tests]); +test_util.describeMathGPU('copy2D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} +]); diff --git a/src/math/element_wise_arithmetic_test.ts b/src/math/element_wise_arithmetic_test.ts new file mode 100644 index 0000000000..8feb271ec8 --- /dev/null +++ b/src/math/element_wise_arithmetic_test.ts @@ -0,0 +1,447 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D, Scalar} from './ndarray'; + +// element-wise mul / div +{ + const tests: MathTests = it => { + it('multiplies same-shaped ndarrays', math => { + const a = Array2D.new([2, 2], [1, 2, -3, -4]); + const b = Array2D.new([2, 2], [5, 3, 4, -7]); + const expected = new Float32Array([5, 6, -12, 28]); + const result = math.elementWiseMul(a, b); + + expect(result.shape).toEqual([2, 2]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array2D.new([2, 2], [1, 3, 4, 0]); + const b = Array2D.new([2, 2], [NaN, 3, NaN, 3]); + + const result = math.elementWiseMul(a, b).getValues(); + test_util.expectArraysClose(result, new Float32Array([NaN, 9, NaN, 0])); + + a.dispose(); + b.dispose(); + }); + + it('mul throws when passed ndarrays of different shapes', math => { + const a = Array2D.new([2, 3], [1, 2, -3, -4, 5, 6]); + const b = Array2D.new([2, 2], [5, 3, 4, -7]); + + expect(() => math.elementWiseMul(a, b)).toThrowError(); + expect(() => math.elementWiseMul(b, a)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('divide', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const c = Array2D.new([2, 3], [1, 2, 3, 4, 2, 5]); + + const r = math.divide(a, c); + + test_util.expectArraysClose( + r.getValues(), new Float32Array([1, 1, 1, 1, 2.5, 6 / 5])); + + a.dispose(); + c.dispose(); + }); + + it('divide propagates NaNs', math => { + const a = Array2D.new([2, 1], [1, 2]); + const c = Array2D.new([2, 1], [3, NaN]); + + const r = math.divide(a, c).getValues(); + + test_util.expectArraysClose(r, new Float32Array([1 / 3, NaN])); + + a.dispose(); + c.dispose(); + }); + + it('div throws when passed ndarrays of different shapes', math => { + const a = Array2D.new([2, 3], [1, 2, -3, -4, 5, 6]); + const b = Array2D.new([2, 2], [5, 3, 4, -7]); + + expect(() => math.divide(a, b)).toThrowError(); + expect(() => math.divide(b, a)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('scalar divided by array', math => { + const c = Scalar.new(2); + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + + const r = math.scalarDividedByArray(c, a); + + test_util.expectArraysClose( + r.getValues(), + new Float32Array([2 / 1, 2 / 2, 2 / 3, 2 / 4, 2 / 5, 2 / 6])); + + a.dispose(); + c.dispose(); + }); + + it('scalar divided by array propagates NaNs', math => { + const c = Scalar.new(NaN); + const a = Array2D.new([1, 3], [1, 2, 3]); + + const r = math.scalarDividedByArray(c, a).getValues(); + + expect(r).toEqual(new Float32Array([NaN, NaN, NaN])); + + a.dispose(); + c.dispose(); + }); + + it('scalar divided by array throws when passed non scalar', math => { + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3]); + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + + expect(() => math.scalarDividedByArray(c, a)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + + it('array divided by scalar', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const c = Scalar.new(2); + + const r = math.arrayDividedByScalar(a, c); + + test_util.expectArraysClose( + r.getValues(), + new Float32Array([1 / 2, 2 / 2, 3 / 2, 4 / 2, 5 / 2, 6 / 2])); + + a.dispose(); + c.dispose(); + }); + + it('array divided by scalar propagates NaNs', math => { + const a = Array2D.new([1, 3], [1, 2, NaN]); + const c = Scalar.new(2); + + const r = math.arrayDividedByScalar(a, c).getValues(); + test_util.expectArraysClose(r, new Float32Array([1 / 2, 2 / 2, NaN])); + + a.dispose(); + c.dispose(); + }); + + it('array divided by scalar throws when passed non scalar', math => { + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3]); + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + + expect(() => math.arrayDividedByScalar(a, c)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + + it('scalar times ndarray', math => { + const a = Array2D.new([3, 2], [2, -5, 1, 1, 4, 0]); + const c = Scalar.new(2); + + const expected = new Float32Array([4, -10, 2, 2, 8, 0]); + const result = math.scalarTimesArray(c, a); + + expect(result.shape).toEqual([3, 2]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + c.dispose(); + }); + + it('scalar times ndarray throws when passed non-scalar', math => { + const a = Array2D.new([3, 2], [2, -5, 1, 1, 4, 0]); + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3, 4]); + + expect(() => math.scalarTimesArray(c, a)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + }; + + test_util.describeMathCPU('element-wise mul/div', [tests]); + test_util.describeMathGPU('element-wise mul/div', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// element-wise add / sub +{ + const tests: MathTests = it => { + it('c + A', math => { + const c = Scalar.new(5); + const a = Array1D.new([1, 2, 3]); + + const result = math.scalarPlusArray(c, a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([6, 7, 8])); + + a.dispose(); + c.dispose(); + }); + + it('c + A propagates NaNs', math => { + const c = Scalar.new(NaN); + const a = Array1D.new([1, 2, 3]); + + const res = math.scalarPlusArray(c, a).getValues(); + + expect(res).toEqual(new Float32Array([NaN, NaN, NaN])); + + a.dispose(); + c.dispose(); + }); + + it('c + A throws when passed non scalar', math => { + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3]); + const a = Array1D.new([1, 2, 3]); + + expect(() => math.scalarPlusArray(c, a)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + + it('c - A', math => { + const c = Scalar.new(5); + const a = Array1D.new([7, 2, 3]); + + const result = math.scalarMinusArray(c, a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([-2, 3, 2])); + + a.dispose(); + c.dispose(); + }); + + it('c - A throws when passed non scalar', math => { + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3]); + const a = Array1D.new([1, 2, 3]); + + expect(() => math.scalarMinusArray(c, a)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + + it('A - c', math => { + const a = Array1D.new([1, 2, -3]); + const c = Scalar.new(5); + + const result = math.arrayMinusScalar(a, c); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([-4, -3, -8])); + + a.dispose(); + c.dispose(); + result.dispose(); + }); + + it('A - c propagates NaNs', math => { + const a = Array1D.new([1, NaN, 3]); + const c = Scalar.new(5); + + const res = math.arrayMinusScalar(a, c).getValues(); + + test_util.expectArraysClose(res, new Float32Array([-4, NaN, -2])); + + a.dispose(); + c.dispose(); + }); + + it('A - c throws when passed non scalar', math => { + // tslint:disable-next-line:no-any + const c: any = Array1D.new([1, 2, 3]); + const a = Array1D.new([1, 2, 3]); + + expect(() => math.arrayMinusScalar(a, c)).toThrowError(); + + a.dispose(); + c.dispose(); + }); + + it('A - B', math => { + const a = Array1D.new([2, 5, 1]); + const b = Array1D.new([4, 2, -1]); + + const result = math.sub(a, b); + + const expected = new Float32Array([-2, 3, 2]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('A - B propagates NaNs', math => { + const a = Array1D.new([2, 5, 1]); + const b = Array1D.new([4, NaN, -1]); + + const res = math.sub(a, b).getValues(); + + test_util.expectArraysClose(res, new Float32Array([-2, NaN, 2])); + + a.dispose(); + b.dispose(); + }); + + it('A - B throws when passed ndarrays with different shape', math => { + const a = Array1D.new([2, 5, 1, 5]); + const b = Array1D.new([4, 2, -1]); + + expect(() => math.sub(a, b)).toThrowError(); + expect(() => math.sub(b, a)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('A + B', math => { + const a = Array1D.new([2, 5, 1]); + const b = Array1D.new([4, 2, -1]); + + const result = math.add(a, b); + + const expected = new Float32Array([6, 7, 0]); + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('A + B propagates NaNs', math => { + const a = Array1D.new([2, 5, NaN]); + const b = Array1D.new([4, 2, -1]); + + const res = math.add(a, b).getValues(); + test_util.expectArraysClose(res, new Float32Array([6, 7, NaN])); + + a.dispose(); + b.dispose(); + }); + + it('A + B throws when passed ndarrays with different shape', math => { + const a = Array1D.new([2, 5, 1, 5]); + const b = Array1D.new([4, 2, -1]); + + expect(() => math.add(a, b)).toThrowError(); + expect(() => math.add(b, a)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + }; + + test_util.describeMathCPU('element-wise add/sub', [tests]); + test_util.describeMathGPU('element-wise add/sub', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.scaledArrayAdd +{ + const tests: MathTests = it => { + it('Scaled ndarray add', math => { + const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); + const b = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const c1 = Scalar.new(3); + const c2 = Scalar.new(2); + + const result = math.scaledArrayAdd(c1, a, c2, b); + + expect(result.shape).toEqual([2, 3]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([8, 16, 24, 32, 40, 48])); + + // Different sizes throws an error. + const wrongSizeMat = Array2D.new([2, 2], [1, 2, 3, 4]); + expect(() => math.scaledArrayAdd(c1, wrongSizeMat, c2, b)) + .toThrowError(); + + a.dispose(); + b.dispose(); + c1.dispose(); + c2.dispose(); + }); + + it('throws when passed non-scalars', math => { + const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); + const b = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + // tslint:disable-next-line:no-any + const c1: any = Array1D.randNormal([10]); + const c2 = Scalar.new(2); + + expect(() => math.scaledArrayAdd(c1 as Scalar, a, c2, b)).toThrowError(); + expect(() => math.scaledArrayAdd(c2, a, c1 as Scalar, b)).toThrowError(); + + a.dispose(); + b.dispose(); + c1.dispose(); + c2.dispose(); + }); + + it('throws when NDArrays are different shape', math => { + const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); + const b = Array2D.new([2, 4], [1, 2, 3, 4, 5, 6, 7, 8]); + const c1 = Scalar.new(3); + const c2 = Scalar.new(2); + + expect(() => math.scaledArrayAdd(c1, a, c2, b)).toThrowError(); + + a.dispose(); + b.dispose(); + c1.dispose(); + c2.dispose(); + }); + }; + + test_util.describeMathCPU('scaledArrayAdd', [tests]); + test_util.describeMathGPU('scaledArrayAdd', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/lstm_test.ts b/src/math/lstm_test.ts new file mode 100644 index 0000000000..722ff9a37b --- /dev/null +++ b/src/math/lstm_test.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D, Scalar} from './ndarray'; + +// math.basicLSTMCell +{ + const tests: MathTests = it => { + it('Batch size must be 1 for MultiRNNCell', math => { + const lstmKernel1 = Array2D.zeros([3, 4]); + const lstmBias1 = Array1D.zeros([4]); + const lstmKernel2 = Array2D.zeros([2, 4]); + const lstmBias2 = Array1D.zeros([4]); + + const forgetBias = Scalar.new(1.0); + const lstm1 = + math.basicLSTMCell.bind(math, forgetBias, lstmKernel1, lstmBias1); + const lstm2 = + math.basicLSTMCell.bind(math, forgetBias, lstmKernel2, lstmBias2); + + const c = [ + Array2D.zeros([1, lstmBias1.shape[0] / 4]), + Array2D.zeros([1, lstmBias2.shape[0] / 4]) + ]; + const h = [ + Array2D.zeros([1, lstmBias1.shape[0] / 4]), + Array2D.zeros([1, lstmBias2.shape[0] / 4]) + ]; + + const onehot = Array2D.zeros([2, 2]); + onehot.set(1.0, 1, 0); + const output = () => math.multiRNNCell([lstm1, lstm2], onehot, c, h); + expect(output).toThrowError(); + }); + + it('Batch size must be 1 for basicLSTMCell', math => { + const lstmKernel = Array2D.zeros([3, 4]); + const lstmBias = Array1D.zeros([4]); + + const forgetBias = Scalar.new(1.0); + + const c = Array2D.zeros([1, lstmBias.shape[0] / 4]); + const h = Array2D.zeros([1, lstmBias.shape[0] / 4]); + + const onehot = Array2D.zeros([2, 2]); + onehot.set(1.0, 1, 0); + const output = () => + math.basicLSTMCell(forgetBias, lstmKernel, lstmBias, onehot, c, h); + expect(output).toThrowError(); + }); + + it('MultiRNNCell with 2 BasicLSTMCells', math => { + const lstmKernel1 = Array2D.new( + [3, 4], new Float32Array([ + 0.26242125034332275, -0.8787832260131836, 0.781475305557251, + 1.337337851524353, 0.6180247068405151, -0.2760246992111206, + -0.11299663782119751, -0.46332040429115295, -0.1765323281288147, + 0.6807947158813477, -0.8326982855796814, 0.6732975244522095 + ])); + const lstmBias1 = Array1D.new(new Float32Array( + [1.090713620185852, -0.8282332420349121, 0, 1.0889357328414917])); + const lstmKernel2 = Array2D.new( + [2, 4], new Float32Array([ + -1.893059492111206, -1.0185645818710327, -0.6270437240600586, + -2.1829540729522705, -0.4583775997161865, -0.5454602241516113, + -0.3114445209503174, 0.8450229167938232 + ])); + const lstmBias2 = Array1D.new(new Float32Array( + [0.9906240105628967, 0.6248329877853394, 0, 1.0224634408950806])); + + const forgetBias = Scalar.new(1.0); + const lstm1 = + math.basicLSTMCell.bind(math, forgetBias, lstmKernel1, lstmBias1); + const lstm2 = + math.basicLSTMCell.bind(math, forgetBias, lstmKernel2, lstmBias2); + + const c = [ + Array2D.zeros([1, lstmBias1.shape[0] / 4]), + Array2D.zeros([1, lstmBias2.shape[0] / 4]) + ]; + const h = [ + Array2D.zeros([1, lstmBias1.shape[0] / 4]), + Array2D.zeros([1, lstmBias2.shape[0] / 4]) + ]; + + const onehot = Array2D.zeros([1, 2]); + onehot.set(1.0, 0, 0); + + const output = math.multiRNNCell([lstm1, lstm2], onehot, c, h); + + test_util.expectArraysClose( + output[0][0].getValues(), new Float32Array([-0.7440074682235718])); + test_util.expectArraysClose( + output[0][1].getValues(), new Float32Array([0.7460772395133972])); + test_util.expectArraysClose( + output[1][0].getValues(), new Float32Array([-0.5802832245826721])); + test_util.expectArraysClose( + output[1][1].getValues(), new Float32Array([0.5745711922645569])); + }); + }; + + test_util.describeMathCPU('basicLSTMCell', [tests]); + test_util.describeMathGPU('basicLSTMCell', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/math_cpu_test.ts b/src/math/math_cpu_test.ts deleted file mode 100644 index 8d884ed3eb..0000000000 --- a/src/math/math_cpu_test.ts +++ /dev/null @@ -1,1748 +0,0 @@ -/** - * @license - * Copyright 2017 Google Inc. All Rights Reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================================= - */ - -import * as test_util from '../test_util'; -import * as util from '../util'; - -import {MatrixOrientation} from './math'; -import {NDArrayMathCPU} from './math_cpu'; -import {Array1D, Array2D, Array3D, Array4D, Scalar} from './ndarray'; - -describe('NDArrayMathCPU clone', () => { - it('returns a ndarray with the same shape and data', () => { - const math = new NDArrayMathCPU(); - const a = Array2D.new([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]); - const aPrime = math.clone(a); - expect(aPrime.shape).toEqual(a.shape); - expect(aPrime.getValues()).toEqual(a.getValues()); - }); -}); - -describe('NDArrayMathCPU slice1D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('slices 1x1 into 1x1 (effectively a copy)', () => { - const a = Array1D.new([5]); - const result = math.slice1D(a, 0, 1); - expect(result.shape).toEqual([1]); - expect(result.get(0)).toBe(5); - }); - - it('slices 5x1 into shape 2x1 starting at 3', () => { - const a = Array1D.new([1, 2, 3, 4, 5]); - const result = math.slice1D(a, 3, 2); - expect(result.shape).toEqual([2]); - expect(result.getValues()).toEqual(new Float32Array([4, 5])); - }); - - it('slices 5x1 into shape 3x1 starting at 1', () => { - const a = Array1D.new([1, 2, 3, 4, 5]); - const result = math.slice1D(a, 1, 3); - expect(result.shape).toEqual([3]); - expect(result.getValues()).toEqual(new Float32Array([2, 3, 4])); - }); -}); - -describe('NDArrayMathCPU slice2D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('slicing a 1x1 from a 1x1 returns a 1x1', () => { - const a = Array2D.new([1, 1], [0]); - const b = math.slice2D(a, [0, 0], [1, 1]); - expect(b.shape).toEqual([1, 1]); - }); - - it('returns a ndarray of slice size', () => { - const a = Array2D.zeros([100, 100]); - const b = math.slice2D(a, [0, 0], [12, 34]); - expect(b.shape).toEqual([12, 34]); - }); - - it('returns the upper-left submatrix when begin is [0, 0]', () => { - const a = Array2D.randUniform([10, 10], -1, 1); - const b = math.slice2D(a, [0, 0], [2, 2]); - const aValues = a.getValues(); - const expected = - new Float32Array([aValues[0], aValues[1], aValues[10], aValues[11]]); - test_util.expectArraysClose(b.getValues(), expected); - }); - - it('returns the rectangle specified', () => { - const a = Array2D.new([4, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - const b = math.slice2D(a, [1, 1], [3, 2]); - const expected = new Float32Array([5, 6, 8, 9, 11, 12]); - expect(b.getValues()).toEqual(expected); - }); - - it('throws when requesting out of bounds slice', () => { - const a = Array2D.new([4, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - expect(() => math.slice2D(a, [1, 1], [10, 10])).toThrowError(); - }); -}); - -describe('NDArrayMathCPU slice3D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('slices 1x1x1 into shape 1x1x1 (effectively a copy)', () => { - const a = Array3D.new([1, 1, 1], [[[5]]]); - const result = math.slice3D(a, [0, 0, 0], [1, 1, 1]); - expect(result.shape).toEqual([1, 1, 1]); - expect(result.get(0, 0, 0)).toBe(5); - }); - - it('slices 2x2x2 array into 1x2x2 starting at [1, 0, 0]', () => { - const a = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); - const result = math.slice3D(a, [1, 0, 0], [1, 2, 2]); - expect(result.shape).toEqual([1, 2, 2]); - expect(result.getValues()).toEqual(new Float32Array([5, 6, 7, 8])); - }); - - it('slices 2x2x2 array into 2x1x1 starting at [0, 1, 1]', () => { - const a = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); - const result = math.slice3D(a, [0, 1, 1], [2, 1, 1]); - expect(result.shape).toEqual([2, 1, 1]); - expect(result.getValues()).toEqual(new Float32Array([4, 8])); - }); -}); - -describe('NDArrayMathCPU slice4D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('slices 1x1x1x1 into shape 1x1x1x1 (effectively a copy)', () => { - const a = Array4D.new([1, 1, 1, 1], [[[[5]]]]); - const result = math.slice4D(a, [0, 0, 0, 0], [1, 1, 1, 1]); - expect(result.shape).toEqual([1, 1, 1, 1]); - expect(result.get(0, 0, 0, 0)).toBe(5); - }); - - it('slices 2x2x2x2 array into 1x2x2x2 starting at [1, 0, 0, 0]', () => { - const a = Array4D.new( - [2, 2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88]); - const result = math.slice4D(a, [1, 0, 0, 0], [1, 2, 2, 2]); - expect(result.shape).toEqual([1, 2, 2, 2]); - expect(result.getValues()).toEqual(new Float32Array([ - 11, 22, 33, 44, 55, 66, 77, 88 - ])); - }); - - it('slices 2x2x2x2 array into 2x1x1x1 starting at [0, 1, 1, 1]', () => { - const a = Array4D.new( - [2, 2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88]); - const result = math.slice4D(a, [0, 1, 1, 1], [2, 1, 1, 1]); - expect(result.shape).toEqual([2, 1, 1, 1]); - expect(result.getValues()).toEqual(new Float32Array([8, 88])); - }); -}); - -describe('NDArrayMathCPU copy2D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('throws an error if source and dest shapes have different areas', () => { - const source = Array2D.zeros([100, 100]); - const dest = Array2D.zeros([100, 100]); - const sourceSize: [number, number] = [20, 20]; - const destSize: [number, number] = [5, 5]; - expect( - () => math.copy2D(source, [0, 0], sourceSize, dest, [0, 0], destSize)) - .toThrowError(); - }); - - it('copies a src shape into a dst shape', () => { - const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - const dest = Array2D.zeros([6, 2]); - math.copy2D(source, [1, 1], [2, 3], dest, [2, 0], [3, 2]); - expect(dest.getValues()).toEqual(new Float32Array([ - 0, 0, 0, 0, 6, 7, 8, 10, 11, 12, 0, 0 - ])); - }); - - it('throws when requesting out of bounds source copy', () => { - const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - const dest = Array2D.zeros([6, 2]); - - expect(() => math.copy2D(source, [1, 1], [10, 10], dest, [2, 0], [ - 3, 2 - ])).toThrowError(); - }); - - it('throws when requesting out of bounds dest copy', () => { - const source = Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - const dest = Array2D.zeros([6, 2]); - - expect(() => math.copy2D(source, [1, 1], [2, 3], dest, [2, 0], [ - 3, 10 - ])).toThrowError(); - }); -}); - -describe('NDArrayMathCPU concat3D', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('shapes correct concat axis=0', () => { - const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); - const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); - const values = math.concat3D(ndarray1, ndarray2, 0); - expect(values.shape).toEqual([2, 1, 3]); - expect(values.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - }); - - it('concat axis=0', () => { - const ndarray1 = Array3D.new([1, 2, 3], [1, 11, 111, 2, 22, 222]); - const ndarray2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - const values = math.concat3D(ndarray1, ndarray2, 0); - expect(values.shape).toEqual([3, 2, 3]); - expect(values.getValues()).toEqual(new Float32Array([ - 1, 11, 111, 2, 22, 222, 5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888 - ])); - }); - - it('shapes correct concat axis=1', () => { - const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); - const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); - const values = math.concat3D(ndarray1, ndarray2, 1); - expect(values.shape).toEqual([1, 2, 3]); - expect(values.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - }); - - it('concat axis=1', () => { - const ndarray1 = Array3D.new([2, 1, 3], [1, 11, 111, 3, 33, 333]); - const ndarray2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - const values = math.concat3D(ndarray1, ndarray2, 1); - expect(values.shape).toEqual([2, 3, 3]); - expect(values.getValues()).toEqual(new Float32Array([ - 1, 11, 111, 5, 55, 555, 6, 66, 666, 3, 33, 333, 7, 77, 777, 8, 88, 888 - ])); - }); - - it('shapes correct concat axis=2', () => { - const ndarray1 = Array3D.new([1, 1, 3], [1, 2, 3]); - const ndarray2 = Array3D.new([1, 1, 3], [4, 5, 6]); - const values = math.concat3D(ndarray1, ndarray2, 2); - expect(values.shape).toEqual([1, 1, 6]); - expect(values.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - }); - - it('concat axis=2', () => { - const ndarray1 = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); - const ndarray2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - const values = math.concat3D(ndarray1, ndarray2, 2); - expect(values.shape).toEqual([2, 2, 5]); - expect(values.getValues()).toEqual(new Float32Array([ - 1, 11, 5, 55, 555, 2, 22, 6, 66, 666, - 3, 33, 7, 77, 777, 4, 44, 8, 88, 888 - ])); - }); - - it('concat throws when invalid non-axis shapes, axis=0', () => { - const axis = 0; - const x1 = Array3D.new([1, 1, 3], [1, 11, 111]); - const x2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - expect(() => math.concat3D(x1, x2, axis)).toThrowError(); - }); - - it('concat throws when invalid non-axis shapes, axis=1', () => { - const axis = 1; - const x1 = Array3D.new([1, 1, 3], [1, 11, 111]); - const x2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - expect(() => math.concat3D(x1, x2, axis)).toThrowError(); - }); - - it('concat throws when invalid non-axis shapes, axis=2', () => { - const axis = 2; - const x1 = Array3D.new([1, 2, 2], [1, 11, 2, 22]); - const x2 = Array3D.new( - [2, 2, 3], [5, 55, 555, 6, 66, 666, 7, 77, 777, 8, 88, 888]); - expect(() => math.concat3D(x1, x2, axis)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU concat1D', () => { - let math: NDArrayMathCPU; - - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('3 + 5', () => { - const a = Array1D.new([3]); - const b = Array1D.new([5]); - - const result = math.concat1D(a, b); - const expected = new Float32Array([3, 5]); - test_util.expectArraysClose(result.getValues(), expected); - }); - - it('3 + [5,7]', () => { - const a = Array1D.new([3]); - const b = Array1D.new([5, 7]); - - const result = math.concat1D(a, b); - const expected = new Float32Array([3, 5, 7]); - test_util.expectArraysClose(result.getValues(), expected); - }); - - it('[3,5] + 7', () => { - const a = Array1D.new([3, 5]); - const b = Array1D.new([7]); - - const result = math.concat1D(a, b); - const expected = new Float32Array([3, 5, 7]); - test_util.expectArraysClose(result.getValues(), expected); - }); -}); - -describe('NDArrayMathCPU concat2D', () => { - let math: NDArrayMathCPU; - - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('[[3]] + [[5]], axis=0', () => { - const axis = 0; - const a = Array2D.new([1, 1], [3]); - const b = Array2D.new([1, 1], [5]); - - const result = math.concat2D(a, b, axis); - const expected = new Float32Array([3, 5]); - - expect(result.shape).toEqual([2, 1]); - test_util.expectArraysClose(result.getValues(), expected); - }); - - it('[[3]] + [[5]], axis=1', () => { - const axis = 1; - const a = Array2D.new([1, 1], [3]); - const b = Array2D.new([1, 1], [5]); - - const result = math.concat2D(a, b, axis); - const expected = new Float32Array([3, 5]); - - expect(result.shape).toEqual([1, 2]); - test_util.expectArraysClose(result.getValues(), expected); - }); - - it('[[1, 2], [3, 4]] + [[5, 6]], axis=0', () => { - const axis = 0; - const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); - const b = Array2D.new([1, 2], [[5, 6]]); - - const result = math.concat2D(a, b, axis); - const expected = new Float32Array([1, 2, 3, 4, 5, 6]); - - expect(result.shape).toEqual([3, 2]); - test_util.expectArraysClose(result.getValues(), expected); - }); - - it('[[1, 2], [3, 4]] + [[5, 6]], axis=1 throws error', () => { - const axis = 1; - const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); - const b = Array2D.new([1, 2], [[5, 6]]); - - expect(() => math.concat2D(a, b, axis)).toThrowError(); - }); - - it('[[1, 2], [3, 4]] + [[5, 6], [7, 8]], axis=1', () => { - const axis = 1; - const a = Array2D.new([2, 2], [[1, 2], [3, 4]]); - const b = Array2D.new([2, 2], [[5, 6], [7, 8]]); - - const result = math.concat2D(a, b, axis); - const expected = new Float32Array([1, 2, 5, 6, 3, 4, 7, 8]); - - expect(result.shape).toEqual([2, 4]); - test_util.expectArraysClose(result.getValues(), expected); - }); -}); - -describe('NDArrayMathCPU matMul', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('A x B', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const b = Array2D.new([3, 2], [0, 1, -3, 2, 2, 1]); - const c = math.matMul(a, b); - expect(c.shape).toEqual([2, 2]); - expect(c.getValues()).toEqual(new Float32Array([0, 8, -3, 20])); - }); - - it('A x B^t', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); - const c = math.matMul( - a, b, MatrixOrientation.REGULAR, MatrixOrientation.TRANSPOSED); - const expected = new Float32Array([7, 10, 16, 31]); - expect(c.getValues()).toEqual(expected); - }); - - it('A^t x B', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); - const c = math.matMul( - a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.REGULAR); - const expected = new Float32Array([17, 12, 2, 22, 15, 4, 27, 18, 6]); - expect(c.getValues()).toEqual(expected); - }); - - it('A^t x B^t', () => { - const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); - const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); - const c = math.matMul( - a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.TRANSPOSED); - const expected = new Float32Array([11, 13, 14, 20]); - expect(c.getValues()).toEqual(expected); - }); - - it('A x B^t shapes do not match', () => { - const a = Array2D.zeros([2, 3]); - const b = Array2D.zeros([3, 2]); - const f = () => { - math.matMul( - a, b, MatrixOrientation.REGULAR, MatrixOrientation.TRANSPOSED); - }; - expect(f).toThrowError(); - }); - - it('A^t x B shapes do not match', () => { - const a = Array2D.zeros([2, 3]); - const b = Array2D.zeros([3, 2]); - const f = () => { - math.matMul( - a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.REGULAR); - }; - expect(f).toThrowError(); - }); - - it('A^t x B^t shapes do not match', () => { - const a = Array2D.zeros([3, 2]); - const b = Array2D.zeros([3, 2]); - const f = () => { - math.matMul( - a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.TRANSPOSED); - }; - expect(f).toThrowError(); - }); - - it('matmul throws when inner dimensions dont match', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const b = Array2D.new([4, 2], [0, 1, -3, 2, 2, 1, 2, 2]); - expect(() => math.matMul(a, b)).toThrowError(); - }); - - it('matmul throws when passed non matrices', () => { - // tslint:disable-next-line:no-any - const a: any = - Array3D.new([2, 3, 2], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - const b = Array2D.new([4, 2], [0, 1, -3, 2, 2, 1, 2, 2]); - expect(() => math.matMul(a, b)).toThrowError(); - expect(() => math.matMul(b, a)).toThrowError(); - }); - - it('Vector times matrix', () => { - const v = Array1D.new([2, 3]); - const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); - const result = math.vectorTimesMatrix(v, matrix); - - const expected = new Float32Array([11, 16]); - expect(result.getValues()).toEqual(expected); - }); - - it('Vector times matrix throws when not passed a vector', () => { - // tslint:disable-next-line:no-any - const v: any = Array2D.new([2, 2], [1, 2, 3, 4]); - const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); - expect(() => math.vectorTimesMatrix(v, matrix)).toThrowError(); - }); - - it('Vector times matrix throws when not passed a matrix', () => { - const v = Array1D.new([2, 3]); - // tslint:disable-next-line:no-any - const matrix: any = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); - expect(() => math.vectorTimesMatrix(v, matrix)).toThrowError(); - }); - - it('Matrix times vector', () => { - const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); - const v = Array1D.new([2, 3]); - const result = math.matrixTimesVector(matrix, v); - - const expected = new Float32Array([8, 18]); - expect(result.getValues()).toEqual(expected); - }); - - it('matrix times vector throws when not passed a vector', () => { - // tslint:disable-next-line:no-any - const v: any = Array2D.new([2, 2], [1, 2, 3, 4]); - const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); - expect(() => math.matrixTimesVector(matrix, v)).toThrowError(); - }); - - it('matrix times vector throws when not passed a matrix', () => { - const v = Array1D.new([2, 3]); - // tslint:disable-next-line:no-any - const matrix: any = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); - expect(() => math.matrixTimesVector(matrix, v)).toThrowError(); - }); - - it('Dot product', () => { - const v1 = Array1D.new([2, 3]); - const v2 = Array1D.new([2, 1]); - const result = math.dotProduct(v1, v2); - - expect(result.get()).toEqual(7); - }); - - it('Dot product throws when vectors are different size', () => { - const v1 = Array1D.new([2, 3, 3]); - const v2 = Array1D.new([2, 1]); - expect(() => math.dotProduct(v1, v2)).toThrowError(); - expect(() => math.dotProduct(v2, v1)).toThrowError(); - }); - - it('Dot product throws when passed non vectors', () => { - // tslint:disable-next-line:no-any - const v1: any = Array2D.new([2, 2], [1, 2, 3, 3]); - const v2 = Array1D.new([2, 1]); - expect(() => math.dotProduct(v1, v2)).toThrowError(); - expect(() => math.dotProduct(v2, v1)).toThrowError(); - }); - - it('Outer product', () => { - const v1 = Array1D.new([2, 3]); - const v2 = Array1D.new([2, 1]); - const result = math.outerProduct(v1, v2); - - const expected = new Float32Array([4, 2, 6, 3]); - expect(result.shape).toEqual([2, 2]); - expect(result.getValues()).toEqual(expected); - }); - - it('Dot product propagates NaNs', () => { - const v1 = Array1D.new([2, NaN]); - const v2 = Array1D.new([2, 1]); - const result = math.dotProduct(v1, v2); - expect(result.get()).toEqual(NaN); - }); - - it('Matrix * vector propagates NaNs', () => { - const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); - const v = Array1D.new([2, NaN]); - const result = math.matrixTimesVector(matrix, v); - - const expected = new Float32Array([NaN, NaN]); - expect(result.getValues()).toEqual(expected); - }); -}); - -describe('NDArrayMathCPU element-wise mul/div', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('multiplication with broadcasting.', () => { - // Same shapes, no broadcasting. - let a = Array2D.new([2, 2], [1, 2, 3, 4]); - let b = Array2D.new([2, 2], [5, 4, 3, 2]); - let expected = Array2D.new([2, 2], [5, 8, 9, 8]); - expect(expected.equals(math.multiply(a, b))).toBe(true); - - // Broadcast a over b. - a = Array2D.new([1, 2], [1, 2]); - b = Array2D.new([4, 2], [2, 3, 4, 5, 6, 7, 8, 9]); - expected = Array2D.new([4, 2], [2, 6, 4, 10, 6, 14, 8, 18]); - expect(expected.equals(math.multiply(a, b))).toBe(true); - }); - - it('multiplication, no broadcasting', () => { - const a = Array2D.new([2, 2], [1, 2, 3, 4]); - const b = Array2D.new([2, 2], [5, 4, 3, 2]); - const expected = Array2D.new([2, 2], [5, 8, 9, 8]); - expect(expected.equals(math.elementWiseMul(a, b))).toBe(true); - }); - - it('multiplication propagates NaNs', () => { - const a = Array2D.new([2, 2], [1, 3, 4, 0]); - const b = Array2D.new([2, 2], [NaN, 3, NaN, 3]); - const result = math.elementWiseMul(a, b).getValues(); - expect(result).toEqual(new Float32Array([NaN, 9, NaN, 0])); - }); - - it('mul throws when passed ndarrays of different shapes', () => { - const a = Array2D.new([2, 3], [1, 2, -3, -4, 5, 6]); - const b = Array2D.new([2, 2], [5, 3, 4, -7]); - expect(() => math.elementWiseMul(a, b)).toThrowError(); - expect(() => math.elementWiseMul(b, a)).toThrowError(); - }); - - it('divide', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const c = Array2D.new([2, 3], [1, 2, 3, 4, 2, 5]); - const r = math.divide(a, c); - - expect(r.get(0, 0)).toBeCloseTo(1); - expect(r.get(0, 1)).toBeCloseTo(1); - expect(r.get(0, 2)).toBeCloseTo(1); - expect(r.get(1, 0)).toBeCloseTo(1); - expect(r.get(1, 1)).toBeCloseTo(2.5); - expect(r.get(1, 2)).toBeCloseTo(6 / 5); - }); - - it('divide propagates NaNs', () => { - const a = Array2D.new([2, 1], [1, 2]); - const c = Array2D.new([2, 1], [3, NaN]); - const r = math.divide(a, c).getValues(); - expect(r[0]).toBeCloseTo(1 / 3); - expect(r[1]).toEqual(NaN); - }); - - it('divide throws when passed ndarrays of different shapes', () => { - const a = Array2D.new([2, 3], [1, 2, -3, -4, 5, 6]); - const b = Array2D.new([2, 2], [5, 3, 4, -7]); - expect(() => math.divide(a, b)).toThrowError(); - expect(() => math.divide(b, a)).toThrowError(); - }); - - it('scalar divided by array', () => { - const c = Scalar.new(2); - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const r = math.scalarDividedByArray(c, a); - - expect(r.get(0, 0)).toBeCloseTo(2 / 1); - expect(r.get(0, 1)).toBeCloseTo(2 / 2); - expect(r.get(0, 2)).toBeCloseTo(2 / 3); - expect(r.get(1, 0)).toBeCloseTo(2 / 4); - expect(r.get(1, 1)).toBeCloseTo(2 / 5); - expect(r.get(1, 2)).toBeCloseTo(2 / 6); - }); - - it('scalar divided by array propagates NaNs', () => { - const c = Scalar.new(NaN); - const a = Array2D.new([1, 3], [1, 2, 3]); - const r = math.scalarDividedByArray(c, a).getValues(); - expect(r).toEqual(new Float32Array([NaN, NaN, NaN])); - }); - - it('scalar divided by array throws when passed non scalar', () => { - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3]); - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - - expect(() => math.scalarDividedByArray(c, a)).toThrowError(); - }); - - it('array divided by scalar', () => { - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const c = Scalar.new(2); - const r = math.arrayDividedByScalar(a, c); - - expect(r.get(0, 0)).toBeCloseTo(1 / 2); - expect(r.get(0, 1)).toBeCloseTo(2 / 2); - expect(r.get(0, 2)).toBeCloseTo(3 / 2); - expect(r.get(1, 0)).toBeCloseTo(4 / 2); - expect(r.get(1, 1)).toBeCloseTo(5 / 2); - expect(r.get(1, 2)).toBeCloseTo(6 / 2); - }); - - it('array divided by scalar propagates NaNs', () => { - const a = Array2D.new([1, 3], [1, 2, NaN]); - const c = Scalar.new(2); - const r = math.arrayDividedByScalar(a, c).getValues(); - expect(r[0]).toBeCloseTo(1 / 2); - expect(r[1]).toBeCloseTo(2 / 2); - expect(r[2]).toEqual(NaN); - }); - - it('array divided by scalar throws when passed non scalar', () => { - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3]); - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - - expect(() => math.arrayDividedByScalar(a, c)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU add/sub', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('add', () => { - const a = Array1D.new([2, 5, 1]); - const b = Array1D.new([4, 2, -1]); - const expected = Array1D.new([6, 7, 0]); - expect(expected.getValues()).toEqual(math.add(a, b).getValues()); - }); - - it('add propagates NaNs', () => { - const a = Array1D.new([2, 5, NaN]); - const b = Array1D.new([4, 2, -1]); - const res = math.add(a, b).getValues(); - expect(res).toEqual(new Float32Array([6, 7, NaN])); - }); - - it('add throws when passed ndarrays with different shape', () => { - const a = Array1D.new([2, 5, 1, 5]); - const b = Array1D.new([4, 2, -1]); - expect(() => math.add(a, b)).toThrowError(); - expect(() => math.add(b, a)).toThrowError(); - }); - - it('sub', () => { - const a = Array1D.new([2, 5, 1]); - const b = Array1D.new([4, 2, -1]); - const expected = Array1D.new([-2, 3, 2]); - expect(expected.getValues()).toEqual(math.sub(a, b).getValues()); - }); - - it('sub propagates NaNs', () => { - const a = Array1D.new([2, 5, 1]); - const b = Array1D.new([4, NaN, -1]); - const res = math.sub(a, b).getValues(); - expect(res).toEqual(new Float32Array([-2, NaN, 2])); - }); - - it('sub throws when passed ndarrays with different shape', () => { - const a = Array1D.new([2, 5, 1, 5]); - const b = Array1D.new([4, 2, -1]); - expect(() => math.sub(a, b)).toThrowError(); - expect(() => math.sub(b, a)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU scalarTimesNDArray', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('scalar times ndarray', () => { - const a = Array2D.new([3, 2], [2, -5, 1, 1, 4, 0]); - const c = Scalar.new(2); - const expected = Array2D.new([3, 2], [4, -10, 2, 2, 8, 0]); - expect(expected.getValues()) - .toEqual(math.scalarTimesArray(c, a).getValues()); - }); - - it('scalar times ndarray throws when passed non-scalar', () => { - const a = Array2D.new([3, 2], [2, -5, 1, 1, 4, 0]); - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3, 4]); - expect(() => math.scalarTimesArray(c, a)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU scaledNDArrayAdd', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('Scaled ndarray add', () => { - const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); - const b = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const c1 = Scalar.new(3); - const c2 = Scalar.new(2); - - const expected = Array2D.new([2, 3], [8, 16, 24, 32, 40, 48]); - expect(math.scaledArrayAdd(c1, a, c2, b).equals(expected)) - .toBe(true); - - // Different sizes throws an error. - const wrongSizeMat = Array2D.new([2, 2], [1, 2, 3, 4]); - expect(() => math.scaledArrayAdd(c1, wrongSizeMat, c2, b)) - .toThrowError(); - }); - - it('throws when passed non-scalars', () => { - const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); - const b = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - // tslint:disable-next-line:no-any - const c1: any = Array1D.randNormal([10]); - const c2 = Scalar.new(2); - - expect(() => math.scaledArrayAdd(c1 as Scalar, a, c2, b)).toThrowError(); - expect(() => math.scaledArrayAdd(c2, a, c1 as Scalar, b)).toThrowError(); - }); - - it('throws when NDArrays are different shape', () => { - const a = Array2D.new([2, 3], [2, 4, 6, 8, 10, 12]); - const b = Array2D.new([2, 4], [1, 2, 3, 4, 5, 6, 7, 8]); - const c1 = Scalar.new(3); - const c2 = Scalar.new(2); - - expect(() => math.scaledArrayAdd(c1, a, c2, b)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU argmin/max, argmaxequals, min/max', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('Arg max', () => { - expect(math.argMax(Array1D.new([5, 0, 3, 7, 3])).get()).toBe(3); - expect(math.argMax(Array1D.new([-100.3, .3, 11.1, 9.9, 7.33])).get()) - .toBe(2); - expect(math.argMax(Array1D.new([-100.3, -20.0, -10.0, -5])).get()).toBe(3); - }); - - it('Arg max propagates NaNs', () => { - expect(math.argMax(Array1D.new([5, 0, 3, NaN, 3])).get()).toEqual(NaN); - }); - - it('Argmaxequals equals', () => { - const a = Array1D.new([5, 0, 3, 7]); - const b = Array1D.new([-100.3, -20.0, -10.0, -5]); - const result = math.argMaxEquals(a, b); - expect(result.get()).toBe(1); - }); - - it('Argmaxequals not equals', () => { - const a = Array1D.new([5, 0, 3, 1]); - const b = Array1D.new([-100.3, -20.0, -10.0, -5]); - const result = math.argMaxEquals(a, b); - expect(result.get()).toBe(0); - }); - - it('Argmaxequals propagates NaNs', () => { - const a = Array1D.new([5, 3, 1, 3]); - const b = Array1D.new([NaN, -20.0, -10.0, -5]); - const result = math.argMaxEquals(a, b); - expect(result.get()).toEqual(NaN); - }); - - it('throws when given arrays of different shape', () => { - const a = Array1D.new([5, 0, 3, 7, 3, 10]); - const b = Array1D.new([-100.3, -20.0, -10.0, -5, -100]); - expect(() => math.argMaxEquals(a, b)).toThrowError(); - }); - - it('topk', () => { - const topk = math.topK(Array1D.new([1, -1, 100, -5, -10.6, 3.3, 5]), 3); - test_util.expectArraysClose( - topk.values.getValues(), new Float32Array([100, 5, 3.3])); - test_util.expectArraysClose( - topk.indices.getValues(), new Float32Array([2, 6, 5])); - }); - - it('Arg min', () => { - expect(math.argMin(Array1D.new([5, 0, 3, 7, 3])).get()).toBe(1); - expect(math.argMin(Array1D.new([-100.3, .3, 11.1, 9.9, 7.33])).get()) - .toBe(0); - }); - - it('Arg min propagates NaNs', () => { - expect(math.argMin(Array1D.new([5, 0, NaN, 7, 3])).get()).toEqual(NaN); - }); - - it('min', () => { - expect(math.min(Array1D.new([3, -1, 0, 100, -7, 2])).get()).toBe(-7); - }); - - it('min propagates NaNs', () => { - expect(math.min(Array1D.new([3, NaN, 2])).get()).toEqual(NaN); - }); - - it('max', () => { - expect(math.max(Array1D.new([3, -1, 0, 100, -7, 2])).get()).toBe(100); - }); - - it('max propagates NaNs', () => { - expect(math.max(Array1D.new([3, NaN, 2])).get()).toEqual(NaN); - }); -}); - -describe('NDArrayMathCPU log/exp', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('exp', () => { - const r = math.exp(Array1D.new([1, 2, 0])); - - expect(r.get(0)).toBeCloseTo(Math.exp(1)); - expect(r.get(1)).toBeCloseTo(Math.exp(2)); - expect(r.get(2)).toBeCloseTo(1); - }); - - it('exp propagates NaNs', () => { - const a = Array1D.new([1, NaN, 0]); - const r = math.exp(a).getValues(); - expect(r).toEqual(new Float32Array([Math.exp(1), NaN, 1])); - }); - - it('log', () => { - const r = math.log(Array1D.new([1, 2])); - - expect(r.get(0)).toBeCloseTo(Math.log(1)); - expect(r.get(1)).toBeCloseTo(Math.log(2)); - }); - - it('log propagates NaNs', () => { - const r = math.log(Array1D.new([1, NaN])).getValues(); - expect(r).toEqual(new Float32Array([Math.log(1), NaN])); - }); - - it('logSumExp', () => { - const a = Array1D.new([1, 2, -3]); - const result = math.logSumExp(a); - expect(result.get()) - .toBeCloseTo(Math.log(Math.exp(1) + Math.exp(2) + Math.exp(-3))); - }); - - it('logSumExp propagates NaNs', () => { - const a = Array1D.new([1, 2, NaN]); - const result = math.logSumExp(a); - expect(result.get()).toEqual(NaN); - }); -}); - -describe('NDArrayMathCPU sqrt', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('sqrt', () => { - const r = math.sqrt(Array1D.new([2, 4])); - - expect(r.get(0)).toBeCloseTo(Math.sqrt(2)); - expect(r.get(1)).toBeCloseTo(Math.sqrt(4)); - }); - - it('sqrt propagates NaNs', () => { - const r = math.sqrt(Array1D.new([1, NaN])).getValues(); - expect(r).toEqual(new Float32Array([Math.sqrt(1), NaN])); - }); -}); - -describe('softmax', () => { - let math: NDArrayMathCPU; - - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('regular test', () => { - const y = math.softmax(Array1D.new([2, 1, 3])); - expect(y.get(0)).toBeCloseTo(0.24472847, 6); - expect(y.get(1)).toBeCloseTo(0.09003057, 6); - expect(y.get(2)).toBeCloseTo(0.66524095, 6); - expect(y.get(0) + y.get(1) + y.get(2)).toBeCloseTo(1, 6); - }); - - it('Overflow', () => { - const y = math.softmax(Array1D.new([10000, 10000])); - expect(y.get(0)).toBeCloseTo(0.5, 3); - expect(y.get(1)).toBeCloseTo(0.5, 3); - }); - - it('Underflow', () => { - const y = math.softmax(Array1D.new([-10000, -10000])); - expect(y.get(0)).toBeCloseTo(0.5, 3); - expect(y.get(1)).toBeCloseTo(0.5, 3); - }); - - it('Huge difference between probabilities', () => { - const y = math.softmax(Array1D.new([-10000, +10000])); - expect(y.get(0)).toBeCloseTo(0.0, 6); - expect(y.get(1)).toBeCloseTo(1, 6); - }); - - it('Propagates NaNs', () => { - const y = math.softmax(Array1D.new([2, 1, NaN])); - expect(y.getValues()).toEqual(new Float32Array([NaN, NaN, NaN])); - }); -}); - -describe('NDArrayMathCPU sum', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('sums values in ndarray', () => { - const a = Array2D.new([3, 2], [1, 2, 3, 0, 0, 1]); - expect(math.sum(a).get()).toBe(7); - }); - - it('propagates NaNs', () => { - const a = Array2D.new([3, 2], [1, 2, 3, NaN, 0, 1]); - expect(math.sum(a).get()).toEqual(NaN); - }); -}); - -describe('NDArrayMathCPU unary ops', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('relu', () => { - const a = Array1D.new([1, -2, 0, 3, -0.1]); - const result = math.relu(a); - expect(result.getValues()).toEqual(new Float32Array([1, 0, 0, 3, 0])); - }); - - it('relu propagates NaNs', () => { - const a = Array1D.new([1, -2, 0, 3, -0.1, NaN]); - const result = math.relu(a); - expect(result.getValues()).toEqual(new Float32Array([1, 0, 0, 3, 0, NaN])); - }); - - it('step', () => { - const a = Array1D.new([1, -2, 0, 3, -0.1]); - const result = math.step(a); - expect(result.getValues()).toEqual(new Float32Array([1, 0, 0, 1, 0])); - }); - - it('step propagates NaNs', () => { - const a = Array1D.new([1, -2, 0, 3, NaN]); - const result = math.step(a); - expect(result.getValues()).toEqual(new Float32Array([1, 0, 0, 1, NaN])); - }); - - it('neg', () => { - const a = Array1D.new([1, -3, 2, 7, -4]); - expect(math.neg(a).getValues()).toEqual(new Float32Array([ - -1, 3, -2, -7, 4 - ])); - }); - - it('neg propagate NaNs', () => { - const a = Array1D.new([1, -3, 2, 7, NaN]); - expect(math.neg(a).getValues()).toEqual(new Float32Array([ - -1, 3, -2, -7, NaN - ])); - }); - - it('sigmoid', () => { - const a = Array1D.new([3, 5]); - const res = math.sigmoid(a).getValues(); - const expected = [3, 5].map(x => 1 / (1 + Math.exp(-x))); - expect(res).toEqual(new Float32Array(expected)); - }); - - it('sigmoid propagates NaNs', () => { - const a = Array1D.new([3, NaN]); - const res = math.sigmoid(a).getValues(); - expect(res).toEqual(new Float32Array([1 / (1 + Math.exp(-3)), NaN])); - }); - - it('tanh', () => { - const a = Array1D.new([4, -3, 0]); - const res = math.tanh(a).getValues(); - const expected = [util.tanh(4), util.tanh(-3), util.tanh(0)]; - expect(res).toEqual(new Float32Array(expected)); - }); - - it('tanh propagates NaNs', () => { - const a = Array1D.new([4, NaN, 0]); - const res = math.tanh(a).getValues(); - const expected = [util.tanh(4), NaN, util.tanh(0)]; - expect(res).toEqual(new Float32Array(expected)); - }); - - it('sin', () => { - const a = Array1D.new([4, -3, 0]); - const res = math.sin(a).getValues(); - const expected = [Math.sin(4), Math.sin(-3), Math.sin(0)]; - expect(res).toEqual(new Float32Array(expected)); - }); - - it('sin propagates NaNs', () => { - const a = Array1D.new([4, NaN, 0]); - const res = math.sin(a).getValues(); - const expected = [Math.sin(4), NaN, Math.sin(0)]; - expect(res).toEqual(new Float32Array(expected)); - }); -}); - -describe('NDArrayMathCPU scalar OP ndarray', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('c + A', () => { - const c = Scalar.new(5); - const a = Array1D.new([1, 2, 3]); - expect(math.scalarPlusArray(c, a).getValues()).toEqual(new Float32Array([ - 6, 7, 8 - ])); - }); - - it('c + A propagates NaNs', () => { - const c = Scalar.new(NaN); - const a = Array1D.new([1, 2, 3]); - const res = math.scalarPlusArray(c, a).getValues(); - expect(res).toEqual(new Float32Array([NaN, NaN, NaN])); - }); - - it('c + A throws when passed non scalar', () => { - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3]); - const a = Array1D.new([1, 2, 3]); - expect(() => math.scalarPlusArray(c, a)).toThrowError(); - }); - - it('c - A', () => { - const c = Scalar.new(5); - const a = Array1D.new([1, 2, 3]); - expect(math.scalarMinusArray(c, a).getValues()).toEqual(new Float32Array([ - 4, 3, 2 - ])); - }); - - it('c - A throws when passed non scalar', () => { - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3]); - const a = Array1D.new([1, 2, 3]); - expect(() => math.scalarMinusArray(c, a)).toThrowError(); - }); - - it('A - c', () => { - const a = Array1D.new([1, 2, 3]); - const c = Scalar.new(5); - expect(math.arrayMinusScalar(a, c).getValues()).toEqual(new Float32Array([ - -4, -3, -2 - ])); - }); - - it('A - c propagates NaNs', () => { - const a = Array1D.new([1, NaN, 3]); - const c = Scalar.new(5); - const res = math.arrayMinusScalar(a, c).getValues(); - expect(res).toEqual(new Float32Array([-4, NaN, -2])); - }); - - it('A - c throws when passed non scalar', () => { - // tslint:disable-next-line:no-any - const c: any = Array1D.new([1, 2, 3]); - const a = Array1D.new([1, 2, 3]); - expect(() => math.arrayMinusScalar(a, c)).toThrowError(); - }); -}); - -describe('NDArrayMathCPU switchDim', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('Switch dim 2D (no change)', () => { - const t = Array2D.new([2, 4], [1, 11, 2, 22, 3, 33, 4, 44]); - const t2 = math.switchDim(t, [0, 1]); - expect(t2.shape).toEqual(t.shape); - expect(t2.getValues()).toEqual(t.getValues()); - }); - - it('Switch dim 2D (transpose)', () => { - const t = Array2D.new([2, 4], [1, 11, 2, 22, 3, 33, 4, 44]); - const t2 = math.switchDim(t, [1, 0]); - expect(t2.shape).toEqual([4, 2]); - const expected = new Float32Array([1, 3, 11, 33, 2, 4, 22, 44]); - expect(t2.getValues()).toEqual(expected); - }); - - it('Switch dim 3D [r, c, d] => [d, r, c]', () => { - const t = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); - const t2 = math.switchDim(t, [2, 0, 1]); - expect(t2.shape).toEqual([2, 2, 2]); - const expected = new Float32Array([1, 2, 3, 4, 11, 22, 33, 44]); - expect(t2.getValues()).toEqual(expected); - }); - - it('Switch dim 3D [r, c, d] => [d, c, r]', () => { - const t = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); - const t2 = math.switchDim(t, [2, 1, 0]); - expect(t2.shape).toEqual([2, 2, 2]); - const expected = new Float32Array([1, 3, 2, 4, 11, 33, 22, 44]); - expect(t2.getValues()).toEqual(expected); - }); -}); - -describe('NDArrayMathCPU maxPool', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', () => { - const a = Array3D.new([1, 1, 1], [0]); - const result = math.maxPool(a, 1, 1, 0); - expect(result.getValues()).toBeCloseTo(0); - }); - - it('3x3x1 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); - const result = math.maxPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([5, 6, 9, 9])); - }); - - it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', () => { - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 9]); - const result = math.maxPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([5, 6, NaN, NaN])); - }); - - it('3x3x2 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new( - [3, 3, 2], - [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); - const result = math.maxPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 2]); - expect(result.getValues()).toEqual(new Float32Array([ - 5, 99, 6, 88, 9, 66, 9, 55 - ])); - }); - - it('4x4x1 in, 2x2 filter, 2 stride', () => { - // Feed forward. - const a = Array3D.new( - [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - const result = math.maxPool(a, 2, 2, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([5, 7, 13, 15])); - }); - - it('2x2x1 in, 2x2 filter, 2 stride, pad=1', () => { - // Feed forward. - const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const result = math.maxPool(a, 2, 2, 1); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([1, 2, 3, 4])); - }); -}); - -describe('NDArrayMathCPU minPool', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', () => { - const a = Array3D.new([1, 1, 1], [0]); - const result = math.minPool(a, 1, 1, 0); - expect(result.getValues()).toBeCloseTo(0); - }); - - it('3x3x1 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); - const result = math.minPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([1, 2, 4, 5])); - }); - - it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', () => { - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 8]); - const result = math.minPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([1, 2, NaN, NaN])); - }); - - it('3x3x2 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new( - [3, 3, 2], - [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); - const result = math.minPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 2]); - expect(result.getValues()).toEqual(new Float32Array([ - 1, 55, 2, 44, 4, 22, 5, 11 - ])); - }); - - it('4x4x1 in, 2x2 filter, 2 stride', () => { - // Feed forward. - const a = Array3D.new( - [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - const result = math.minPool(a, 2, 2, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([0, 2, 8, 10])); - }); - - it('2x2x1 in, 2x2 filter, 2 stride, pad=1', () => { - // Feed forward. - const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const result = math.minPool(a, 2, 2, 1); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([1, 2, 3, 4])); - }); -}); - -describe('NDArrayMathCPU avgPool', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', () => { - const a = Array3D.new([1, 1, 1], [0]); - const result = math.avgPool(a, 1, 1, 0); - expect(result.getValues()).toBeCloseTo(0); - }); - - it('3x3x1 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); - const result = math.avgPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([3, 4, 6.25, 7])); - }); - - it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', () => { - // Feed forward. - const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 8]); - const result = math.avgPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([3, 4, NaN, NaN])); - }); - - it('3x3x2 in, 2x2 filter, 1 stride', () => { - // Feed forward. - const a = Array3D.new( - [3, 3, 2], - [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); - const result = math.avgPool(a, 2, 1, 0); - - expect(result.shape).toEqual([2, 2, 2]); - expect(result.getValues()).toEqual(new Float32Array([ - 3, 77, 4, 66, 6.25, 44, 7, 33 - ])); - }); - - it('4x4x1 in, 2x2 filter, 2 stride', () => { - // Feed forward. - const a = Array3D.new( - [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - const result = math.avgPool(a, 2, 2, 0); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([ - 2.5, 4.5, 10.5, 12.5 - ])); - }); - - it('2x2x1 in, 2x2 filter, 2 stride, pad=1', () => { - // Feed forward. - const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const result = math.avgPool(a, 2, 2, 1); - - expect(result.shape).toEqual([2, 2, 1]); - expect(result.getValues()).toEqual(new Float32Array([0.25, 0.5, 0.75, 1])); - }); -}); - -describe('NDArrayMathCPU maxPoolBackprop', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('x=3x3x1, f=2, s=1, no duplicate max value, test #1', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 8, 9]); - const expected = new Float32Array([0, 0, 0, 0, 1, 2, 0, 3, 4]); - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=3x3x1, f=2, s=1, no duplicate max value, test #2', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([3, 3, 1], [9, 5, 6, 6, 8, 4, 9, 5, 10]); - const expected = new Float32Array([1, 0, 0, 0, 2, 0, 3, 0, 4]); - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=3x3x1, f=2, s=1 duplicate max value, test 1', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([3, 3, 1], [0, 0, 0, 0, 5, 0, 0, 0, 0]); - const expected = new Float32Array([0, 0, 0, 0, 10, 0, 0, 0, 0]); - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=3x3x1, f=2, s=1 duplicate max value, test 2', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([3, 3, 1], [1, 3, 2, 1, 2, 1, 1, 1, 5]); - const expected = new Float32Array([0, 3, 0, 0, 3, 0, 0, 0, 4]); - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=4x4x1, f=2, s=2, test #1', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new( - [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - const expected = - new Float32Array([0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 3, 0, 4]); - const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=4x4x1, f=2, s=2, test #2', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new( - [4, 4, 1], [1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]); - const expected = - new Float32Array([0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0]); - const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=5x5x1, f=3, s=2 no duplicate max value', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([5, 5, 1], [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, - 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 - ]); - const expected = new Float32Array([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 4 - ]); - const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=5x5x1, f=3, s=2 duplicate max value', () => { - const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); - const x = Array3D.new([5, 5, 1], [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24, - 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12 - ]); - const expected = new Float32Array([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]); - const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); - - // Max pool backprop depth > 1. - it('x=3x3x2, f=2, s=1, no duplicate max value', () => { - // This test combines the first two 3x3x1 tests with no duplicates to - // make depth=2, - // dy is slightly modified to show the difference. - const dy = Array3D.new([2, 2, 2], [1, 44, 2, 33, 3, 22, 4, 11]); - const x = Array3D.new( - [3, 3, 2], - [1, 99, 2, 55, 3, 66, 4, 66, 5, 88, 6, 44, 7, 99, 8, 55, 9, 100]); - const expected = new Float32Array( - [0, 44, 0, 0, 0, 0, 0, 0, 1, 33, 2, 0, 0, 22, 3, 0, 4, 11]); - - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=3x3x2, f=2, s=1, duplicate max value', () => { - // This test combines the first two 3x3x1 tests with duplicates to - // make depth=2, - // dy is slightly modified to show the difference. - const dy = Array3D.new([2, 2, 2], [1, 44, 2, 33, 3, 22, 4, 11]); - const x = Array3D.new( - [3, 3, 2], [0, 1, 0, 3, 0, 2, 0, 1, 5, 2, 0, 1, 0, 1, 0, 1, 0, 5]); - const expected = new Float32Array( - [0, 0, 0, 77, 0, 0, 0, 0, 10, 22, 0, 0, 0, 0, 0, 0, 0, 11]); - - const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=4x4x2, f=2, s=1', () => { - // This test combines the first two 4x4x1 tests with duplicates to make - // depth=2, - // dy is slightly modified to show the difference. - const dy = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); - const x = Array3D.new([4, 4, 2], [ - 0, 1, 1, 2, 2, 2, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, - 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 2, 14, 2, 15, 1 - ]); - const expected = new Float32Array([ - 0, 0, 0, 11, 0, 22, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 33, 0, 44, 4, 0 - ]); - const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); - - it('x=5x5x2, f=3, s=2 no duplicate max value', () => { - // This test combines the first two 5x5x1 tests with duplicates to make - // depth=2, - // dy is slightly modified to show the difference. - const dy = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); - const x = Array3D.new([5, 5, 2], [ - 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, - 8, 9, 9, 10, 10, 11, 11, 12, 24, 13, 13, 14, 14, 15, 15, 16, 16, - 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 12 - ]); - const expected = new Float32Array([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 110, 0, 0, 2, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 4, 0 - ]); - const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); - expect(dx.getValues()).toEqual(expected); - }); -}); - -describe('NDArrayMathCPU resizeBilinear', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('simple alignCorners=false', () => { - const input = Array3D.new([2, 2, 1], [2, 2, 4, 4]); - const output = math.resizeBilinear3D(input, [3, 3], false); - - test_util.expectArraysClose( - output.getValues(), - new Float32Array([2, 2, 2, 10 / 3, 10 / 3, 10 / 3, 4, 4, 4])); - }); - - it('simple alignCorners=true', () => { - const input = Array3D.new([2, 2, 1], [2, 2, 4, 4]); - const output = math.resizeBilinear3D(input, [3, 3], true); - - test_util.expectArraysClose( - output.getValues(), new Float32Array([2, 2, 2, 3, 3, 3, 4, 4, 4])); - }); - - it('matches tensorflow w/ random numbers alignCorners=false', () => { - const input = Array3D.new([2, 3, 2], [ - 1.19074044, 0.91373104, 2.01611669, -0.52270832, 0.38725395, 1.30809779, - 0.61835143, 3.49600659, 2.09230986, 0.56473997, 0.03823943, 1.19864896 - ]); - const output = math.resizeBilinear3D(input, [4, 5], false); - - test_util.expectArraysClose( - output.getValues(), new Float32Array([ - 1.19074047, 0.91373104, 1.68596613, 0.05186744, 1.69034398, - -0.15654698, 0.7130264, 0.94193673, 0.38725394, 1.30809784, - 0.9045459, 2.20486879, 1.59434628, 0.89455694, 1.68591988, - 0.26748738, 0.58103991, 1.00690198, 0.21274668, 1.25337338, - 0.6183514, 3.49600649, 1.50272655, 1.73724651, 1.68149579, - 0.69152176, 0.44905344, 1.07186723, 0.03823943, 1.19864893, - 0.6183514, 3.49600649, 1.50272655, 1.73724651, 1.68149579, - 0.69152176, 0.44905344, 1.07186723, 0.03823943, 1.19864893 - ])); - }); - - it('matches tensorflow w/ random numbers alignCorners=true', () => { - const input = Array3D.new([2, 3, 2], [ - 1.56324531, 2.13817752, 1.44398421, 1.07632684, 0.59306785, -0.36970865, - 1.62451879, 1.8367334, 1.13944798, 2.01993218, 2.01919952, 2.67524054 - ]); - const output = math.resizeBilinear3D(input, [4, 5], true); - - test_util.expectArraysClose( - output.getValues(), new Float32Array([ - 1.5632453, 2.13817763, 1.50361478, 1.60725224, 1.44398427, - 1.07632685, 1.01852608, 0.35330909, 0.59306782, -0.36970866, - 1.58366978, 2.03769612, 1.46307099, 1.71427906, 1.3424722, - 1.39086199, 1.20545864, 1.01806819, 1.06844509, 0.6452744, - 1.60409427, 1.93721485, 1.42252707, 1.82130599, 1.24096, - 1.70539713, 1.3923912, 1.68282723, 1.54382229, 1.66025746, - 1.62451875, 1.83673346, 1.38198328, 1.92833281, 1.13944793, - 2.01993227, 1.57932377, 2.34758639, 2.01919961, 2.67524052 - ])); - }); -}); - -describe('NDArrayMathCPU batchNorm', () => { - let math: NDArrayMathCPU; - beforeEach(() => { - math = new NDArrayMathCPU(); - }); - - it('simple batchnorm, no offset or scale, 2x1x2', () => { - const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); - const mean = Array1D.new([1, 2]); - const variance = Array1D.new([2, 3]); - const varianceEpsilon = .001; - - const result = math.batchNormalization3D( - x, mean, variance, varianceEpsilon, undefined, undefined); - - test_util.expectArraysClose( - result.getValues(), new Float32Array([ - (x.get(0, 0, 0) - mean.get(0)) * 1 / - Math.sqrt(variance.get(0) + varianceEpsilon), - (x.get(0, 0, 1) - mean.get(1)) * 1 / - Math.sqrt(variance.get(1) + varianceEpsilon), - (x.get(1, 0, 0) - mean.get(0)) * 1 / - Math.sqrt(variance.get(0) + varianceEpsilon), - (x.get(1, 0, 1) - mean.get(1)) * 1 / - Math.sqrt(variance.get(1) + varianceEpsilon) - ])); - }); - - it('simple batchnorm, no offset, 2x1x2', () => { - const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); - const mean = Array1D.new([1, 2]); - const variance = Array1D.new([2, 3]); - const scale = Array1D.new([4, 5]); - const varianceEpsilon = .001; - - const result = math.batchNormalization3D( - x, mean, variance, varianceEpsilon, scale, undefined); - - test_util.expectArraysClose( - result.getValues(), new Float32Array([ - (x.get(0, 0, 0) - mean.get(0)) * scale.get(0) / - Math.sqrt(variance.get(0) + varianceEpsilon), - (x.get(0, 0, 1) - mean.get(1)) * scale.get(1) / - Math.sqrt(variance.get(1) + varianceEpsilon), - (x.get(1, 0, 0) - mean.get(0)) * scale.get(0) / - Math.sqrt(variance.get(0) + varianceEpsilon), - (x.get(1, 0, 1) - mean.get(1)) * scale.get(1) / - Math.sqrt(variance.get(1) + varianceEpsilon) - ])); - }); - - it('simple batchnorm, no scale, 2x1x2', () => { - const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); - const mean = Array1D.new([1, 2]); - const variance = Array1D.new([2, 3]); - const offset = Array1D.new([4, 5]); - - const varianceEpsilon = .001; - - const result = math.batchNormalization3D( - x, mean, variance, varianceEpsilon, undefined, offset); - - test_util.expectArraysClose( - result.getValues(), new Float32Array([ - offset.get(0) + - (x.get(0, 0, 0) - mean.get(0)) * 1 / - Math.sqrt(variance.get(0) + varianceEpsilon), - offset.get(1) + - (x.get(0, 0, 1) - mean.get(1)) * 1 / - Math.sqrt(variance.get(1) + varianceEpsilon), - offset.get(0) + - (x.get(1, 0, 0) - mean.get(0)) * 1 / - Math.sqrt(variance.get(0) + varianceEpsilon), - offset.get(1) + - (x.get(1, 0, 1) - mean.get(1)) * 1 / - Math.sqrt(variance.get(1) + varianceEpsilon) - ])); - }); - - it('simple batchnorm, 2x1x2', () => { - const x = Array3D.new([2, 1, 2], new Float32Array([2, 100, 4, 400])); - const mean = Array1D.new([1, 2]); - const variance = Array1D.new([2, 3]); - const offset = Array1D.new([3, 4]); - const scale = Array1D.new([4, 5]); - - const varianceEpsilon = .001; - - const result = math.batchNormalization3D( - x, mean, variance, varianceEpsilon, scale, offset); - - test_util.expectArraysClose( - result.getValues(), new Float32Array([ - offset.get(0) + - (x.get(0, 0, 0) - mean.get(0)) * scale.get(0) / - Math.sqrt(variance.get(0) + varianceEpsilon), - offset.get(1) + - (x.get(0, 0, 1) - mean.get(1)) * scale.get(1) / - Math.sqrt(variance.get(1) + varianceEpsilon), - offset.get(0) + - (x.get(1, 0, 0) - mean.get(0)) * scale.get(0) / - Math.sqrt(variance.get(0) + varianceEpsilon), - offset.get(1) + - (x.get(1, 0, 1) - mean.get(1)) * scale.get(1) / - Math.sqrt(variance.get(1) + varianceEpsilon) - ])); - }); - - it('batchnorm matches tensorflow, 2x3x3', () => { - const x = - Array3D.new([2, 3, 3], new Float32Array([ - 0.49955603, 0.04158615, -1.09440524, 2.03854165, - -0.61578344, 2.87533573, 1.18105987, 0.807462, 1.87888837, - 2.26563962, -0.37040935, 1.35848753, -0.75347094, - 0.15683117, 0.91925946, 0.34121279, 0.92717143, 1.89683965 - ])); - const mean = Array1D.new([0.39745062, -0.48062894, 0.4847822]); - const variance = Array1D.new([0.32375343, 0.67117643, 1.08334653]); - const offset = Array1D.new([0.69398749, -1.29056387, 0.9429723]); - const scale = Array1D.new([-0.5607271, 0.9878457, 0.25181573]); - const varianceEpsilon = .001; - - const result = math.batchNormalization3D( - x, mean, variance, varianceEpsilon, scale, offset); - - test_util.expectArraysClose( - result.getValues(), new Float32Array([ - 0.59352049, -0.66135202, 0.5610874, -0.92077015, -1.45341019, - 1.52106473, -0.07704776, 0.26144429, 1.28010017, -1.14422404, - -1.15776136, 1.15425493, 1.82644104, -0.52249442, 1.04803919, - 0.74932291, 0.40568101, 1.2844412 - ])); - }); -}); diff --git a/src/math/math_test.ts b/src/math/math_test.ts new file mode 100644 index 0000000000..9158c539ad --- /dev/null +++ b/src/math/math_test.ts @@ -0,0 +1,205 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; +import {NDArrayMathGPU} from './math_gpu'; + +import {Array1D} from './ndarray'; + +// math.scope +{ + const gpuTests: MathTests = it => { + it('scope returns NDArray', (math: NDArrayMathGPU) => { + const a = Array1D.new([1, 2, 3]); + let b = Array1D.new([0, 0, 0]); + + const numUsedTexturesBefore = + math.getTextureManager().getNumUsedTextures(); + + math.scope(() => { + const result = math.scope(() => { + b = math.add(a, b) as Array1D; + b = math.add(a, b) as Array1D; + b = math.add(a, b) as Array1D; + return math.add(a, b); + }); + + // a, b, and result are new textures. All intermediates should be + // disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 3); + test_util.expectArraysClose( + result.getValues(), new Float32Array([4, 8, 12])); + }); + + // a, b are new textures, result should be disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 2); + a.dispose(); + b.dispose(); + }); + + it('scope returns NDArray[]', (math: NDArrayMathGPU) => { + const a = Array1D.new([1, 2, 3]); + const b = Array1D.new([0, -1, 1]); + + const numUsedTexturesBefore = + math.getTextureManager().getNumUsedTextures(); + + math.scope(() => { + const result = math.scope(() => { + math.add(a, b); + return [math.add(a, b), math.sub(a, b)]; + }); + + // a, b, and 2 results are new textures. All intermediates should be + // disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 4); + test_util.expectArraysClose( + result[0].getValues(), new Float32Array([1, 1, 4])); + test_util.expectArraysClose( + result[1].getValues(), new Float32Array([1, 3, 2])); + }); + + // a, b are new textures, result should be disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 2); + a.dispose(); + b.dispose(); + }); + + it('basic scope usage without return', (math: NDArrayMathGPU) => { + const a = Array1D.new([1, 2, 3]); + let b = Array1D.new([0, 0, 0]); + + const numUsedTexturesBefore = + math.getTextureManager().getNumUsedTextures(); + + math.scope(() => { + b = math.add(a, b) as Array1D; + b = math.add(a, b) as Array1D; + b = math.add(a, b) as Array1D; + math.add(a, b); + }); + + const numUsedTexturesAfter = + math.getTextureManager().getNumUsedTextures(); + + // original a and b, all intermediates should be disposed. + expect(numUsedTexturesAfter).toEqual(numUsedTexturesBefore + 2); + }); + + it('nested scope usage', (math: NDArrayMathGPU) => { + const a = Array1D.new([1, 2, 3]); + let b = Array1D.new([0, 0, 0]); + + const numUsedTexturesBefore = + math.getTextureManager().getNumUsedTextures(); + + math.scope(() => { + const result = math.scope(() => { + b = math.add(a, b) as Array1D; + b = math.scope(() => { + b = math.scope(() => { + return math.add(a, b) as Array1D; + }); + // a, original b, and two intermediate textures should be the only + // textures. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 4); + + math.scope(() => { + math.add(a, b); + }); + // All the intermediates should be cleaned up. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 4); + + return math.add(a, b) as Array1D; + }); + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 4); + + return math.add(a, b) as Array1D; + }); + + // a, b, and result are new textures. All intermediates should be + // disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 3); + test_util.expectArraysClose( + result.getValues(), new Float32Array([4, 8, 12])); + }); + // a, b, are new textures, result should be disposed. + expect(math.getTextureManager().getNumUsedTextures()) + .toEqual(numUsedTexturesBefore + 2); + }); + }; + + test_util.describeMathGPU('scope', [gpuTests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// debug mode +{ + const gpuTests: MathTests = it => { + it('debug mode does not error when no nans', math => { + math.enableDebugMode(); + const a = Array1D.new([2, -1, 0, 3]); + + const res = math.relu(a); + + test_util.expectArraysClose( + res.getValues(), new Float32Array([2, 0, 0, 3])); + + a.dispose(); + }); + + it('debug mode errors when there are nans', math => { + math.enableDebugMode(); + const a = Array1D.new([2, NaN]); + + const f = () => math.relu(a); + + expect(f).toThrowError(); + + a.dispose(); + }); + + it('no errors where there are nans, and debug mode is disabled', math => { + const a = Array1D.new([2, NaN]); + + const res = math.relu(a); + + test_util.expectArraysClose(res.getValues(), new Float32Array([2, NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('debug mode', [gpuTests]); + test_util.describeMathGPU('debug mode', [gpuTests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/matmul_test.ts b/src/math/matmul_test.ts new file mode 100644 index 0000000000..f84ddcb7aa --- /dev/null +++ b/src/math/matmul_test.ts @@ -0,0 +1,393 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {MatrixOrientation} from './math'; +import {NDArrayMathGPU} from './math_gpu'; +import {Array1D, Array2D, Array3D} from './ndarray'; +import * as webgl_util from './webgl/webgl_util'; + +const commonTests: MathTests = it => { + it('A x B', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const b = Array2D.new([3, 2], [0, 1, -3, 2, 2, 1]); + + const c = math.matMul(a, b); + + expect(c.shape).toEqual([2, 2]); + test_util.expectArraysClose( + c.getValues(), new Float32Array([0, 8, -3, 20])); + + a.dispose(); + b.dispose(); + }); + + it('A x B^t', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); + + const c = math.matMul( + a, b, MatrixOrientation.REGULAR, MatrixOrientation.TRANSPOSED); + + const expected = new Float32Array([7, 10, 16, 31]); + test_util.expectArraysClose(c.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('A^t x B', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); + + const c = math.matMul( + a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.REGULAR); + + const expected = new Float32Array([17, 12, 2, 22, 15, 4, 27, 18, 6]); + test_util.expectArraysClose(c.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('A^t x B^t', math => { + const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); + const b = Array2D.new([2, 3], [1, 0, 2, 4, 3, 0]); + + const c = math.matMul( + a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.TRANSPOSED); + + const expected = new Float32Array([11, 13, 14, 20]); + test_util.expectArraysClose(c.getValues(), expected); + + a.dispose(); + b.dispose(); + }); + + it('A x B^t shapes do not match', math => { + const a = Array2D.zeros([2, 3]); + const b = Array2D.zeros([3, 2]); + + const f = () => { + math.matMul( + a, b, MatrixOrientation.REGULAR, MatrixOrientation.TRANSPOSED); + }; + expect(f).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('A^t x B shapes do not match', math => { + const a = Array2D.zeros([2, 3]); + const b = Array2D.zeros([3, 2]); + + const f = () => { + math.matMul( + a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.REGULAR); + }; + expect(f).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('A^t x B^t shapes do not match', math => { + const a = Array2D.zeros([3, 2]); + const b = Array2D.zeros([3, 2]); + + const f = () => { + math.matMul( + a, b, MatrixOrientation.TRANSPOSED, MatrixOrientation.TRANSPOSED); + }; + expect(f).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('matmul throws when inner dimensions dont match', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const b = Array2D.new([4, 2], [0, 1, -3, 2, 2, 1, 2, 2]); + + expect(() => math.matMul(a, b)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('matmul throws when passed non matrices', math => { + // tslint:disable-next-line:no-any + const a: any = + Array3D.new([2, 3, 2], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const b = Array2D.new([4, 2], [0, 1, -3, 2, 2, 1, 2, 2]); + + expect(() => math.matMul(a, b)).toThrowError(); + expect(() => math.matMul(b, a)).toThrowError(); + + a.dispose(); + b.dispose(); + }); + + it('Vector times matrix', math => { + const v = Array1D.new([2, 3]); + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + const result = math.vectorTimesMatrix(v, matrix); + + const expected = new Float32Array([11, 16]); + test_util.expectArraysClose(result.getValues(), expected); + + v.dispose(); + matrix.dispose(); + result.dispose(); + }); + + it('Vector times matrix with implicit reshape', math => { + const v = Array1D.new([2, 3]); + + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + const result = math.vectorTimesMatrix(v, matrix); + + const expected = new Float32Array([11, 16]); + test_util.expectArraysClose(result.getValues(), expected); + + v.dispose(); + matrix.dispose(); + }); + + it('Vector times matrix throws when not passed a vector', math => { + // tslint:disable-next-line:no-any + const v: any = Array2D.new([2, 2], [1, 2, 3, 4]); + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + + expect(() => math.vectorTimesMatrix(v, matrix)).toThrowError(); + + v.dispose(); + matrix.dispose(); + }); + + it('Vector times matrix throws when not passed a matrix', math => { + const v = Array1D.new([2, 3]); + // tslint:disable-next-line:no-any + const matrix: any = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); + + expect(() => math.vectorTimesMatrix(v, matrix)).toThrowError(); + + v.dispose(); + matrix.dispose(); + }); + + it('Matrix times vector', math => { + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + const v = Array1D.new([2, 3]); + const result = math.matrixTimesVector(matrix, v); + + const expected = new Float32Array([8, 18]); + test_util.expectArraysClose(result.getValues(), expected); + + matrix.dispose(); + v.dispose(); + }); + + it('Matrix * vector propagates NaNs', math => { + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + const v = Array1D.new([2, NaN]); + const result = math.matrixTimesVector(matrix, v); + + const expected = new Float32Array([NaN, NaN]); + test_util.expectArraysClose(result.getValues(), expected); + + matrix.dispose(); + v.dispose(); + }); + + it('matrix times vector throws when not passed a vector', math => { + // tslint:disable-next-line:no-any + const v: any = Array2D.new([2, 2], [1, 2, 3, 4]); + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + + expect(() => math.matrixTimesVector(matrix, v)).toThrowError(); + + v.dispose(); + matrix.dispose(); + }); + + it('matrix times vector throws when not passed a matrix', math => { + const v = Array1D.new([2, 3]); + + // tslint:disable-next-line:no-any + const matrix: any = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); + + expect(() => math.matrixTimesVector(matrix, v)).toThrowError(); + + v.dispose(); + }); + + it('Dot product', math => { + const v1 = Array1D.new([2, 3]); + const v2 = Array1D.new([2, 1]); + const result = math.dotProduct(v1, v2); + + test_util.expectNumbersClose(result.get(), 7); + + v1.dispose(); + v2.dispose(); + result.dispose(); + }); + + it('Dot product propagates NaNs', math => { + const v1 = Array1D.new([2, NaN]); + const v2 = Array1D.new([2, 1]); + const result = math.dotProduct(v1, v2); + expect(result.get()).toEqual(NaN); + + v1.dispose(); + v2.dispose(); + }); + + it('Dot product throws when vectors are different size', math => { + const v1 = Array1D.new([2, 3, 3]); + const v2 = Array1D.new([2, 1]); + + expect(() => math.dotProduct(v1, v2)).toThrowError(); + expect(() => math.dotProduct(v2, v1)).toThrowError(); + + v1.dispose(); + v2.dispose(); + }); + + it('Dot product throws when passed non vectors', math => { + // tslint:disable-next-line:no-any + const v1: any = Array2D.new([2, 2], [1, 2, 3, 3]); + const v2 = Array1D.new([2, 1]); + + expect(() => math.dotProduct(v1, v2)).toThrowError(); + expect(() => math.dotProduct(v2, v1)).toThrowError(); + + v1.dispose(); + v2.dispose(); + }); + + it('Outer product', math => { + const v1 = Array1D.new([2, 3]); + const v2 = Array1D.new([2, 1]); + const result = math.outerProduct(v1, v2); + + const expected = new Float32Array([4, 2, 6, 3]); + expect(result.shape).toEqual([2, 2]); + test_util.expectArraysClose(result.getValues(), expected); + v1.dispose(); + v2.dispose(); + }); +}; + +const gpuTests: MathTests = it => { + it('with implicit texture reshaping on GPU', math => { + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + // Make the texture shape different than the logical shape on purpose. + expect(a.getTextureShapeRC([6, 1])).toEqual([6, 1]); + + const b = Array2D.new([3, 2], [1, 3, 0, 1, 2, 0]); + expect(b.getTextureShapeRC()).toEqual([3, 2]); + + // Matmul should do implicit texture reshape on ndarray A in order to + // do the right logical multiplication. + const result = math.matMul(a, b); + expect(result.shape).toEqual([2, 2]); + expect(result.getTextureShapeRC()).toEqual([2, 2]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([7, 5, 16, 17])); + a.dispose(); + b.dispose(); + }); + + it('Matrix times vector, larger than max texture size', math => { + const maxTexSize = webgl_util.queryMaxTextureSize( + (math as NDArrayMathGPU).getGPGPUContext().gl); + + const sharedDim = maxTexSize + 4; + + const matrix = Array2D.zeros([1, sharedDim]); + matrix.set(1, 0, sharedDim - 3); + matrix.set(1, 0, sharedDim - 2); + + const v = Array1D.zeros([sharedDim]); + v.set(1, sharedDim - 3); + v.set(1, sharedDim - 2); + + const result = math.matrixTimesVector(matrix, v); + const expected = new Float32Array([2]); + test_util.expectArraysClose(result.getValues(), expected); + + matrix.dispose(); + v.dispose(); + }); + + it('Matrix times vector with implicit reshape', math => { + const matrix = Array2D.new([2, 2], [1, 2, 3, 4]); + const v = Array1D.new([2, 3]); + // Make the texture shape be row on purpose. + expect(v.getTextureShapeRC([1, 2])).toEqual([1, 2]); + const result = math.matrixTimesVector(matrix, v); + + const expected = new Float32Array([8, 18]); + test_util.expectArraysClose(result.getValues(), expected); + matrix.dispose(); + v.dispose(); + }); + + it('Dot product with implicit reshaping', math => { + const v1 = Array1D.new([2, 3]); + // Make the texture shape be column on purpose. + expect(v1.getTextureShapeRC([2, 1])).toEqual([2, 1]); + + const v2 = Array1D.new([2, 1]); + // Make the texture shape be row on purpose. + expect(v2.getTextureShapeRC([1, 2])).toEqual([1, 2]); + + const result = math.dotProduct(v1, v2); + expect(result.get()).toBeCloseTo(7); + v1.dispose(); + v2.dispose(); + }); + + it('Outer product with implicit reshape', math => { + const v1 = Array1D.new([2, 3]); + // Make the texture shape be row on purpose. + expect(v1.getTextureShapeRC([1, 2])).toEqual([1, 2]); + + const v2 = Array1D.new([2, 1]); + // Make the texture shape be column on purpose. + expect(v2.getTextureShapeRC([2, 1])).toEqual([2, 1]); + + const result = math.outerProduct(v1, v2); + const expected = new Float32Array([4, 2, 6, 3]); + expect(result.shape).toEqual([2, 2]); + test_util.expectArraysClose(result.getValues(), expected); + v1.dispose(); + v2.dispose(); + }); +}; + +test_util.describeMathCPU('matMul', [commonTests]); +test_util.describeMathGPU('matMul', [commonTests, gpuTests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} +]); diff --git a/src/math/max_pool_backprop_test.ts b/src/math/max_pool_backprop_test.ts new file mode 100644 index 0000000000..5a5481636b --- /dev/null +++ b/src/math/max_pool_backprop_test.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array3D} from './ndarray'; + +// math.maxPoolBackprop +{ + const tests: MathTests = it => { + it('x=3x3x1, f=2, s=1, no duplicate max value, test #1', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 8, 9]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array([0, 0, 0, 0, 1, 2, 0, 3, 4]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=3x3x1, f=2, s=1, no duplicate max value, test #2', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([3, 3, 1], [9, 5, 6, 6, 8, 4, 9, 5, 10]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array([1, 0, 0, 0, 2, 0, 3, 0, 4]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=3x3x1, f=2, s=1 duplicate max value, test 1', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([3, 3, 1], [0, 0, 0, 0, 5, 0, 0, 0, 0]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array([0, 0, 0, 0, 10, 0, 0, 0, 0]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=3x3x1, f=2, s=1 duplicate max value, test 2', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([3, 3, 1], [1, 3, 2, 1, 2, 1, 1, 1, 5]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array([0, 3, 0, 0, 3, 0, 0, 0, 4]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=4x4x1, f=2, s=2, test #1', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new( + [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + + const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); + + const expected = + new Float32Array([0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 3, 0, 4]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=4x4x1, f=2, s=2, test #2', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new( + [4, 4, 1], [1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]); + const expected = + new Float32Array([0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0]); + const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=5x5x1, f=3, s=2 no duplicate max value', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([5, 5, 1], [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 + ]); + + const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); + + const expected = new Float32Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 4 + ]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=5x5x1, f=3, s=2 duplicate max value', math => { + const dy = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const x = Array3D.new([5, 5, 1], [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 12 + ]); + + const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); + + const expected = new Float32Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + // Max pool backprop depth > 1. + it('x=3x3x2, f=2, s=1, no duplicate max value', math => { + // This test combines the first two 3x3x1 tests with no duplicates to + // make depth=2, + // dy is slightly modified to show the difference. + const dy = Array3D.new([2, 2, 2], [1, 44, 2, 33, 3, 22, 4, 11]); + const x = Array3D.new( + [3, 3, 2], + [1, 99, 2, 55, 3, 66, 4, 66, 5, 88, 6, 44, 7, 99, 8, 55, 9, 100]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array( + [0, 44, 0, 0, 0, 0, 0, 0, 1, 33, 2, 0, 0, 22, 3, 0, 4, 11]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=3x3x2, f=2, s=1, duplicate max value', math => { + // This test combines the first two 3x3x1 tests with duplicates to + // make depth=2, + // dy is slightly modified to show the difference. + const dy = Array3D.new([2, 2, 2], [1, 44, 2, 33, 3, 22, 4, 11]); + const x = Array3D.new( + [3, 3, 2], [0, 1, 0, 3, 0, 2, 0, 1, 5, 2, 0, 1, 0, 1, 0, 1, 0, 5]); + + const dx = math.maxPoolBackprop(dy, x, 2, 1, 0); + + const expected = new Float32Array( + [0, 0, 0, 77, 0, 0, 0, 0, 10, 22, 0, 0, 0, 0, 0, 0, 0, 11]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=4x4x2, f=2, s=1', math => { + // This test combines the first two 4x4x1 tests with duplicates to make + // depth=2, + // dy is slightly modified to show the difference. + const dy = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); + const x = Array3D.new([4, 4, 2], [ + 0, 1, 1, 2, 2, 2, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, + 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 2, 14, 2, 15, 1 + ]); + + const dx = math.maxPoolBackprop(dy, x, 2, 2, 0); + + const expected = new Float32Array([ + 0, 0, 0, 11, 0, 22, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 33, 0, 44, 4, 0 + ]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + + it('x=5x5x2, f=3, s=2 no duplicate max value', math => { + // This test combines the first two 5x5x1 tests with duplicates to make + // depth=2, + // dy is slightly modified to show the difference. + const dy = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); + const x = Array3D.new([5, 5, 2], [ + 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, + 8, 9, 9, 10, 10, 11, 11, 12, 24, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 12 + ]); + + const dx = math.maxPoolBackprop(dy, x, 3, 2, 0); + + const expected = new Float32Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 110, 0, 0, 2, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 4, 0 + ]); + test_util.expectArraysClose(dx.getValues(), expected); + + dy.dispose(); + x.dispose(); + }); + }; + + test_util.describeMathCPU('maxPoolBackprop', [tests]); + test_util.describeMathGPU('maxPoolBackprop', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/ndarray_test.ts b/src/math/ndarray_test.ts index ff65602dca..4ce556c47c 100644 --- a/src/math/ndarray_test.ts +++ b/src/math/ndarray_test.ts @@ -15,360 +15,389 @@ * ============================================================================= */ +import * as test_util from '../test_util'; +import {Tests} from '../test_util'; + import * as ndarray from './ndarray'; import {Array1D, Array2D, Array3D, Array4D, NDArray, Scalar} from './ndarray'; import {GPGPUContext} from './webgl/gpgpu_context'; import * as gpgpu_util from './webgl/gpgpu_util'; import {TextureManager} from './webgl/texture_manager'; -describe('NDArray', () => { +{ let gl: WebGLRenderingContext; let gpgpu: GPGPUContext; let textureManager: TextureManager; - beforeEach(() => { - gl = gpgpu_util.createWebGLContext(); - gpgpu = new GPGPUContext(gl); - textureManager = new TextureManager(gpgpu); - ndarray.initializeGPU(gpgpu, textureManager); - }); + const tests: Tests = () => { + it('NDArrays of arbitrary size', () => { + // [1, 2, 3] + let t: NDArray = Array1D.new([1, 2, 3]); + expect(t instanceof Array1D).toBe(true); + expect(t.rank).toBe(1); + expect(t.size).toBe(3); + test_util.expectArraysClose(t.getValues(), new Float32Array([1, 2, 3])); + // Out of bounds indexing. + expect(t.get(4)).toBeUndefined(); + + // [[1, 2, 3]] + t = Array2D.new([1, 3], [1, 2, 3]); + expect(t instanceof Array2D).toBe(true); + expect(t.rank).toBe(2); + expect(t.size).toBe(3); + test_util.expectArraysClose(t.getValues(), new Float32Array([1, 2, 3])); + // Out of bounds indexing. + expect(t.get(4)).toBeUndefined(); + + // [[1, 2, 3], + // [4, 5, 6]] + t = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + expect(t instanceof Array2D).toBe(true); + expect(t.rank).toBe(2); + expect(t.size).toBe(6); + + test_util.expectArraysClose( + t.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + + // Out of bounds indexing. + expect(t.get(5, 3)).toBeUndefined(); + + // Shape mismatch with the values. + expect(() => Array2D.new([1, 2], [1])).toThrowError(); + }); - afterEach(() => { - textureManager.dispose(); - gpgpu.dispose(); - }); - - it('NDArrays of arbitrary size', () => { - // [1, 2, 3] - let t: NDArray = Array1D.new([1, 2, 3]); - expect(t instanceof Array1D).toBe(true); - expect(t.rank).toBe(1); - expect(t.size).toBe(3); - expect(t.getValues()).toEqual(new Float32Array([1, 2, 3])); - expect(t.get(0)).toBe(1); - expect(t.get(1)).toBe(2); - expect(t.get(2)).toBe(3); - // Out of bounds indexing. - expect(t.get(4)).toBeUndefined(); - - // [[1, 2, 3]] - t = Array2D.new([1, 3], [1, 2, 3]); - expect(t instanceof Array2D).toBe(true); - expect(t.rank).toBe(2); - expect(t.size).toBe(3); - expect(t.get(0, 0)).toBe(1); - expect(t.get(0, 1)).toBe(2); - expect(t.get(0, 2)).toBe(3); - // Out of bounds indexing. - expect(t.get(4)).toBeUndefined(); - - // [[1, 2, 3], - // [4, 5, 6]] - t = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - expect(t instanceof Array2D).toBe(true); - expect(t.rank).toBe(2); - expect(t.size).toBe(6); - expect(t.get(0, 0)).toBe(1); - expect(t.get(0, 1)).toBe(2); - expect(t.get(0, 2)).toBe(3); - expect(t.get(1, 0)).toBe(4); - expect(t.get(1, 1)).toBe(5); - expect(t.get(1, 2)).toBe(6); - // Out of bounds indexing. - expect(t.get(5, 3)).toBeUndefined(); - - // Shape mismatch with the values. - expect(() => Array2D.new([1, 2], [1])).toThrowError(); - }); - - it('NDArrays of explicit size', () => { - const t = Array1D.new([5, 3, 2]); - expect(t.rank).toBe(1); - expect(t.shape).toEqual([3]); - expect(t.get(1)).toBe(3); - - expect(() => Array3D.new([1, 2, 3, 5], [ - 1, 2 - ])).toThrowError('Shape should be of length 3'); - - const t4 = Array4D.new([1, 2, 1, 2], [1, 2, 3, 4]); - expect(t4.get(0, 0, 0, 0)).toBe(1); - expect(t4.get(0, 0, 0, 1)).toBe(2); - expect(t4.get(0, 1, 0, 0)).toBe(3); - expect(t4.get(0, 1, 0, 1)).toBe(4); - - const t4Like = NDArray.like(t4); - // Change t4. - t4.set(10, 0, 0, 0, 1); - expect(t4.get(0, 0, 0, 1)).toBe(10); - // Make suree t4_like hasn't changed. - expect(t4Like.get(0, 0, 0, 1)).toBe(2); - - // NDArray of zeros. - const z = NDArray.zeros([3, 4, 2]) as Array3D; - expect(z.rank).toBe(3); - expect(z.size).toBe(24); - for (let i = 0; i < 3; i++) { - for (let j = 0; j < 4; j++) { - for (let k = 0; k < 2; k++) { - expect(z.get(i, j, k)).toBe(0); + it('NDArrays of explicit size', () => { + const t = Array1D.new([5, 3, 2]); + expect(t.rank).toBe(1); + expect(t.shape).toEqual([3]); + expect(t.get(1)).toBe(3); + + expect(() => Array3D.new([1, 2, 3, 5], [ + 1, 2 + ])).toThrowError('Shape should be of length 3'); + + const t4 = Array4D.new([1, 2, 1, 2], [1, 2, 3, 4]); + expect(t4.get(0, 0, 0, 0)).toBe(1); + expect(t4.get(0, 0, 0, 1)).toBe(2); + expect(t4.get(0, 1, 0, 0)).toBe(3); + expect(t4.get(0, 1, 0, 1)).toBe(4); + + const t4Like = NDArray.like(t4); + // Change t4. + t4.set(10, 0, 0, 0, 1); + expect(t4.get(0, 0, 0, 1)).toBe(10); + // Make suree t4_like hasn't changed. + expect(t4Like.get(0, 0, 0, 1)).toBe(2); + + // NDArray of zeros. + const z = NDArray.zeros([3, 4, 2]) as Array3D; + expect(z.rank).toBe(3); + expect(z.size).toBe(24); + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 2; k++) { + expect(z.get(i, j, k)).toBe(0); + } } } - } - // Reshaping ndarrays. - const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); - const b = a.reshape([3, 2, 1]); - expect(a.get(1, 2)).toBe(6); + // Reshaping ndarrays. + const a = Array2D.new([2, 3], [1, 2, 3, 4, 5, 6]); + const b = a.reshape([3, 2, 1]); + expect(a.get(1, 2)).toBe(6); - // Modify the reshaped ndarray. - b.set(10, 2, 1, 0); - // Make sure the original ndarray is also modified. - expect(a.get(1, 2)).toBe(10); - }); + // Modify the reshaped ndarray. + b.set(10, 2, 1, 0); + // Make sure the original ndarray is also modified. + expect(a.get(1, 2)).toBe(10); + }); - it('NDArray getValues CPU --> GPU', () => { - const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); + it('NDArray getValues CPU --> GPU', () => { + const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); - expect(a.inGPU()).toBe(false); + expect(a.inGPU()).toBe(false); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); + test_util.expectArraysClose( + a.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); - expect(a.inGPU()).toBe(false); + expect(a.inGPU()).toBe(false); - // Upload to GPU. - expect(a.getTexture() != null).toBe(true); + // Upload to GPU. + expect(a.getTexture() != null).toBe(true); - expect(a.inGPU()).toBe(true); - a.dispose(); - }); + expect(a.inGPU()).toBe(true); + a.dispose(); + }); - it('NDArray getValues GPU --> CPU', () => { - const texture = textureManager.acquireTexture([3, 2]); - gpgpu.uploadMatrixToTexture( - texture, 3, 2, new Float32Array([1, 2, 3, 4, 5, 6])); + it('NDArray getValues GPU --> CPU', () => { + const texture = textureManager.acquireTexture([3, 2]); + gpgpu.uploadMatrixToTexture( + texture, 3, 2, new Float32Array([1, 2, 3, 4, 5, 6])); - const a = new Array2D([3, 2], {texture, textureShapeRC: [3, 2]}); - expect(a.inGPU()).toBe(true); + const a = new Array2D([3, 2], {texture, textureShapeRC: [3, 2]}); + expect(a.inGPU()).toBe(true); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - expect(a.inGPU()).toBe(false); - }); + test_util.expectArraysClose( + a.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + expect(a.inGPU()).toBe(false); + }); - it('NDArray getValuesAsync CPU --> GPU', (doneFn) => { - const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); + it('NDArray getValuesAsync CPU --> GPU', (doneFn) => { + const a = Array2D.new([3, 2], [1, 2, 3, 4, 5, 6]); - expect(a.inGPU()).toBe(false); + expect(a.inGPU()).toBe(false); - a.getValuesAsync().then(values => { - expect(values).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); + a.getValuesAsync().then(values => { + test_util.expectArraysClose( + values, new Float32Array([1, 2, 3, 4, 5, 6])); - expect(a.inGPU()).toBe(false); + expect(a.inGPU()).toBe(false); - // Upload to GPU. - expect(a.getTexture() != null).toBe(true); + // Upload to GPU. + expect(a.getTexture() != null).toBe(true); + expect(a.inGPU()).toBe(true); + a.dispose(); + doneFn(); + }); + }); + + it('NDArray getValuesAsync GPU --> CPU', (doneFn) => { + const texture = textureManager.acquireTexture([3, 2]); + gpgpu.uploadMatrixToTexture( + texture, 3, 2, new Float32Array([1, 2, 3, 4, 5, 6])); + + const a = new Array2D([3, 2], {texture, textureShapeRC: [3, 2]}); expect(a.inGPU()).toBe(true); - a.dispose(); - doneFn(); + + a.getValuesAsync().then(values => { + test_util.expectArraysClose( + values, new Float32Array([1, 2, 3, 4, 5, 6])); + expect(a.inGPU()).toBe(false); + doneFn(); + }); + }); + + it('Scalar basic methods', () => { + const a = Scalar.new(5); + expect(a.get()).toBe(5); + test_util.expectArraysClose(a.getValues(), new Float32Array([5])); + expect(a.rank).toBe(0); + expect(a.size).toBe(1); + expect(a.shape).toEqual([]); }); - }); - it('NDArray getValuesAsync GPU --> CPU', (doneFn) => { - const texture = textureManager.acquireTexture([3, 2]); - gpgpu.uploadMatrixToTexture( - texture, 3, 2, new Float32Array([1, 2, 3, 4, 5, 6])); + it('Scalar in GPU', () => { + const texture = textureManager.acquireTexture([1, 1]); + gpgpu.uploadMatrixToTexture(texture, 1, 1, new Float32Array([10])); - const a = new Array2D([3, 2], {texture, textureShapeRC: [3, 2]}); - expect(a.inGPU()).toBe(true); + const a = new Scalar({texture}); + expect(a.inGPU()).toBe(true); + test_util.expectArraysClose(a.getValues(), new Float32Array([10])); + expect(a.inGPU()).toBe(false); + }); - a.getValuesAsync().then(values => { - expect(values).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); + it('Array1D in GPU', () => { + const texture = textureManager.acquireTexture([1, 3]); + gpgpu.uploadMatrixToTexture(texture, 1, 3, new Float32Array([10, 7, 3])); + + const a = new Array1D({texture, textureShapeRC: [1, 3]}); + expect(a.inGPU()).toBe(true); + test_util.expectArraysClose(a.getValues(), new Float32Array([10, 7, 3])); expect(a.inGPU()).toBe(false); - doneFn(); }); - }); - - it('Scalar basic methods', () => { - const a = Scalar.new(5); - expect(a.get()).toBe(5); - expect(a.getValues()).toEqual(new Float32Array([5])); - expect(a.rank).toBe(0); - expect(a.size).toBe(1); - expect(a.shape).toEqual([]); - }); - - it('Scalar in GPU', () => { - const texture = textureManager.acquireTexture([1, 1]); - gpgpu.uploadMatrixToTexture(texture, 1, 1, new Float32Array([10])); - - const a = new Scalar({texture}); - expect(a.inGPU()).toBe(true); - expect(a.getValues()).toEqual(new Float32Array([10])); - expect(a.inGPU()).toBe(false); - }); - - it('Array1D in GPU', () => { - const texture = textureManager.acquireTexture([1, 3]); - gpgpu.uploadMatrixToTexture(texture, 1, 3, new Float32Array([10, 7, 3])); - - const a = new Array1D({texture, textureShapeRC: [1, 3]}); - expect(a.inGPU()).toBe(true); - expect(a.getValues()).toEqual(new Float32Array([10, 7, 3])); - expect(a.inGPU()).toBe(false); - }); - - it('Array1D in GPU, but incorrect c-tor (missing textureShape)', () => { - const texture = textureManager.acquireTexture([1, 3]); - gpgpu.uploadMatrixToTexture(texture, 1, 3, new Float32Array([10, 7, 3])); - - const f = () => { - return new Array1D({texture}); - }; - - expect(f).toThrowError(); - textureManager.releaseTexture(texture, [1, 3]); - }); - - it('NDArray.make() constructs a Scalar', () => { - const a = NDArray.make([], {values: new Float32Array([3])}); - expect(a instanceof Scalar).toBe(true); - }); - - it('Array2D in GPU, reshaped to Array1D', () => { - const texture = textureManager.acquireTexture([2, 2]); - gpgpu.uploadMatrixToTexture(texture, 2, 2, new Float32Array([10, 7, 3, 5])); - - const a = new Array2D([2, 2], {texture, textureShapeRC: [2, 2]}); - const a1d = a.as1D(); - - expect(a1d.getValues()).toEqual(new Float32Array([10, 7, 3, 5])); - }); - - it('Array1D in GPU, reshaped to Array2D', () => { - const texture = textureManager.acquireTexture([1, 4]); - gpgpu.uploadMatrixToTexture(texture, 1, 4, new Float32Array([10, 7, 3, 5])); - - const a = new Array1D({texture, textureShapeRC: [1, 4]}); - const a2d = a.as2D(2, 2); - - expect(a2d.getValues()).toEqual(new Float32Array([10, 7, 3, 5])); - }); - - it('Array2D in GPU with custom texture shape', () => { - const texture = textureManager.acquireTexture([4, 1]); - gpgpu.uploadMatrixToTexture(texture, 4, 1, new Float32Array([10, 7, 3, 5])); - - const a = new Array2D([2, 2], {texture, textureShapeRC: [4, 1]}); - - expect(a.getValues()).toEqual(new Float32Array([10, 7, 3, 5])); - }); - - it('index2Loc Array1D', () => { - const t = Array1D.zeros([3]); - expect(t.indexToLoc(0)).toEqual([0]); - expect(t.indexToLoc(1)).toEqual([1]); - expect(t.indexToLoc(2)).toEqual([2]); - }); - - it('index2Loc Array2D', () => { - const t = Array2D.zeros([3, 2]); - expect(t.indexToLoc(0)).toEqual([0, 0]); - expect(t.indexToLoc(1)).toEqual([0, 1]); - expect(t.indexToLoc(2)).toEqual([1, 0]); - expect(t.indexToLoc(3)).toEqual([1, 1]); - expect(t.indexToLoc(4)).toEqual([2, 0]); - expect(t.indexToLoc(5)).toEqual([2, 1]); - }); - - it('index2Loc Array3D', () => { - const t = Array2D.zeros([3, 2, 2]); - expect(t.indexToLoc(0)).toEqual([0, 0, 0]); - expect(t.indexToLoc(1)).toEqual([0, 0, 1]); - expect(t.indexToLoc(2)).toEqual([0, 1, 0]); - expect(t.indexToLoc(3)).toEqual([0, 1, 1]); - expect(t.indexToLoc(4)).toEqual([1, 0, 0]); - expect(t.indexToLoc(5)).toEqual([1, 0, 1]); - expect(t.indexToLoc(11)).toEqual([2, 1, 1]); - }); - - it('index2Loc NDArray 5D', () => { - const values = new Float32Array([1, 2, 3, 4]); - const t = NDArray.make([2, 1, 1, 1, 2], {values}); - expect(t.indexToLoc(0)).toEqual([0, 0, 0, 0, 0]); - expect(t.indexToLoc(1)).toEqual([0, 0, 0, 0, 1]); - expect(t.indexToLoc(2)).toEqual([1, 0, 0, 0, 0]); - expect(t.indexToLoc(3)).toEqual([1, 0, 0, 0, 1]); - }); - - it('preferred texture shape, Scalar', () => { - const t = Scalar.new(1); - expect(t.getTextureShapeRC()).toEqual([1, 1]); - }); - - it('preferred texture shape, Array1D column vector', () => { - const t = Array1D.zeros([4]); - expect(t.getTextureShapeRC()).toEqual([4, 1]); - }); - - it('preferred texture shape, Array2D same shape', () => { - const t = Array2D.zeros([5, 2]); - expect(t.getTextureShapeRC()).toEqual([5, 2]); - }); - - it('preferred texture shape, Array3D depth strided along columns', () => { - const t = Array3D.zeros([2, 2, 2]); - expect(t.getTextureShapeRC()).toEqual([2, 4]); - }); - - it('preferred texture shape, Array4D d1 and d2 strided along columns', () => { - const t = Array4D.zeros([8, 2, 4, 4]); - expect(t.getTextureShapeRC()).toEqual([8, 2 * 4 * 4]); - }); -}); // Close describe. - -describe('NDArray.new method', () => { - it('Array1D.new() from number[]', () => { - const a = Array1D.new([1, 2, 3]); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 3])); - }); - - it('Array1D.new() from number[][], shape mismatch', () => { - // tslint:disable-next-line:no-any - expect(() => Array1D.new([[1], [2], [3]] as any)).toThrowError(); - }); - - it('Array2D.new() from number[][]', () => { - const a = Array2D.new([2, 3], [[1, 2, 3], [4, 5, 6]]); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - }); - - it('Array2D.new() from number[][], but shape does not match', () => { - // Actual shape is [2, 3]. - expect(() => Array2D.new([3, 2], [[1, 2, 3], [4, 5, 6]])).toThrowError(); - }); - - it('Array3D.new() from number[][][]', () => { - const a = Array3D.new([2, 3, 1], [[[1], [2], [3]], [[4], [5], [6]]]); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 3, 4, 5, 6])); - }); - - it('Array3D.new() from number[][][], but shape does not match', () => { - const values = [[[1], [2], [3]], [[4], [5], [6]]]; - // Actual shape is [2, 3, 1]. - expect(() => Array3D.new([3, 2, 1], values)).toThrowError(); - }); - - it('Array4D.new() from number[][][][]', () => { - const a = Array4D.new([2, 2, 1, 1], [[[[1]], [[2]]], [[[4]], [[5]]]]); - expect(a.getValues()).toEqual(new Float32Array([1, 2, 4, 5])); - }); - - it('Array4D.new() from number[][][][], but shape does not match', () => { - const f = () => { - // Actual shape is [2, 2, 1, 1]. - Array4D.new([2, 1, 2, 1], [[[[1]], [[2]]], [[[4]], [[5]]]]); - }; - expect(f).toThrowError(); - }); -}); + + it('Array1D in GPU, but incorrect c-tor (missing textureShape)', () => { + const texture = textureManager.acquireTexture([1, 3]); + gpgpu.uploadMatrixToTexture(texture, 1, 3, new Float32Array([10, 7, 3])); + + const f = () => { + return new Array1D({texture}); + }; + + expect(f).toThrowError(); + textureManager.releaseTexture(texture, [1, 3]); + }); + + it('NDArray.make() constructs a Scalar', () => { + const a = NDArray.make([], {values: new Float32Array([3])}); + expect(a instanceof Scalar).toBe(true); + }); + + it('Array2D in GPU, reshaped to Array1D', () => { + const texture = textureManager.acquireTexture([2, 2]); + gpgpu.uploadMatrixToTexture( + texture, 2, 2, new Float32Array([10, 7, 3, 5])); + + const a = new Array2D([2, 2], {texture, textureShapeRC: [2, 2]}); + const a1d = a.as1D(); + + test_util.expectArraysClose( + a1d.getValues(), new Float32Array([10, 7, 3, 5])); + }); + + it('Array1D in GPU, reshaped to Array2D', () => { + const texture = textureManager.acquireTexture([1, 4]); + gpgpu.uploadMatrixToTexture( + texture, 1, 4, new Float32Array([10, 7, 3, 5])); + + const a = new Array1D({texture, textureShapeRC: [1, 4]}); + const a2d = a.as2D(2, 2); + + test_util.expectArraysClose( + a2d.getValues(), new Float32Array([10, 7, 3, 5])); + }); + + it('Array2D in GPU with custom texture shape', () => { + const texture = textureManager.acquireTexture([4, 1]); + gpgpu.uploadMatrixToTexture( + texture, 4, 1, new Float32Array([10, 7, 3, 5])); + + const a = new Array2D([2, 2], {texture, textureShapeRC: [4, 1]}); + + test_util.expectArraysClose( + a.getValues(), new Float32Array([10, 7, 3, 5])); + }); + + it('index2Loc Array1D', () => { + const t = Array1D.zeros([3]); + expect(t.indexToLoc(0)).toEqual([0]); + expect(t.indexToLoc(1)).toEqual([1]); + expect(t.indexToLoc(2)).toEqual([2]); + }); + + it('index2Loc Array2D', () => { + const t = Array2D.zeros([3, 2]); + expect(t.indexToLoc(0)).toEqual([0, 0]); + expect(t.indexToLoc(1)).toEqual([0, 1]); + expect(t.indexToLoc(2)).toEqual([1, 0]); + expect(t.indexToLoc(3)).toEqual([1, 1]); + expect(t.indexToLoc(4)).toEqual([2, 0]); + expect(t.indexToLoc(5)).toEqual([2, 1]); + }); + + it('index2Loc Array3D', () => { + const t = Array2D.zeros([3, 2, 2]); + expect(t.indexToLoc(0)).toEqual([0, 0, 0]); + expect(t.indexToLoc(1)).toEqual([0, 0, 1]); + expect(t.indexToLoc(2)).toEqual([0, 1, 0]); + expect(t.indexToLoc(3)).toEqual([0, 1, 1]); + expect(t.indexToLoc(4)).toEqual([1, 0, 0]); + expect(t.indexToLoc(5)).toEqual([1, 0, 1]); + expect(t.indexToLoc(11)).toEqual([2, 1, 1]); + }); + + it('index2Loc NDArray 5D', () => { + const values = new Float32Array([1, 2, 3, 4]); + const t = NDArray.make([2, 1, 1, 1, 2], {values}); + expect(t.indexToLoc(0)).toEqual([0, 0, 0, 0, 0]); + expect(t.indexToLoc(1)).toEqual([0, 0, 0, 0, 1]); + expect(t.indexToLoc(2)).toEqual([1, 0, 0, 0, 0]); + expect(t.indexToLoc(3)).toEqual([1, 0, 0, 0, 1]); + }); + + it('preferred texture shape, Scalar', () => { + const t = Scalar.new(1); + expect(t.getTextureShapeRC()).toEqual([1, 1]); + }); + + it('preferred texture shape, Array1D column vector', () => { + const t = Array1D.zeros([4]); + expect(t.getTextureShapeRC()).toEqual([4, 1]); + }); + + it('preferred texture shape, Array2D same shape', () => { + const t = Array2D.zeros([5, 2]); + expect(t.getTextureShapeRC()).toEqual([5, 2]); + }); + + it('preferred texture shape, Array3D depth strided along columns', () => { + const t = Array3D.zeros([2, 2, 2]); + expect(t.getTextureShapeRC()).toEqual([2, 4]); + }); + + it('preferred texture shape, Array4D d1 and d2 strided along columns', + () => { + const t = Array4D.zeros([8, 2, 4, 4]); + expect(t.getTextureShapeRC()).toEqual([8, 2 * 4 * 4]); + }); + }; + + const customBeforeEach = () => { + gl = gpgpu_util.createWebGLContext(); + gpgpu = new GPGPUContext(gl); + textureManager = new TextureManager(gpgpu); + ndarray.initializeGPU(gpgpu, textureManager); + }; + + const customAfterEach = () => { + textureManager.dispose(); + gpgpu.dispose(); + }; + + test_util.describeCustom( + 'NDArray', [tests], + [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ], + customBeforeEach, customAfterEach); +} + +{ + const tests: Tests = () => { + it('Array1D.new() from number[]', () => { + const a = Array1D.new([1, 2, 3]); + test_util.expectArraysClose(a.getValues(), new Float32Array([1, 2, 3])); + }); + + it('Array1D.new() from number[][], shape mismatch', () => { + // tslint:disable-next-line:no-any + expect(() => Array1D.new([[1], [2], [3]] as any)).toThrowError(); + }); + + it('Array2D.new() from number[][]', () => { + const a = Array2D.new([2, 3], [[1, 2, 3], [4, 5, 6]]); + test_util.expectArraysClose( + a.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + }); + + it('Array2D.new() from number[][], but shape does not match', () => { + // Actual shape is [2, 3]. + expect(() => Array2D.new([3, 2], [[1, 2, 3], [4, 5, 6]])).toThrowError(); + }); + + it('Array3D.new() from number[][][]', () => { + const a = Array3D.new([2, 3, 1], [[[1], [2], [3]], [[4], [5], [6]]]); + test_util.expectArraysClose( + a.getValues(), new Float32Array([1, 2, 3, 4, 5, 6])); + }); + + it('Array3D.new() from number[][][], but shape does not match', () => { + const values = [[[1], [2], [3]], [[4], [5], [6]]]; + // Actual shape is [2, 3, 1]. + expect(() => Array3D.new([3, 2, 1], values)).toThrowError(); + }); + + it('Array4D.new() from number[][][][]', () => { + const a = Array4D.new([2, 2, 1, 1], [[[[1]], [[2]]], [[[4]], [[5]]]]); + test_util.expectArraysClose( + a.getValues(), new Float32Array([1, 2, 4, 5])); + }); + + it('Array4D.new() from number[][][][], but shape does not match', () => { + const f = () => { + // Actual shape is [2, 2, 1, 1]. + Array4D.new([2, 1, 2, 1], [[[[1]], [[2]]], [[[4]], [[5]]]]); + }; + expect(f).toThrowError(); + }); + }; + + test_util.describeCustom('NDArray.new', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/pool_test.ts b/src/math/pool_test.ts new file mode 100644 index 0000000000..dce8404192 --- /dev/null +++ b/src/math/pool_test.ts @@ -0,0 +1,249 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array2D, Array3D} from './ndarray'; + +// math.maxPool +{ + const tests: MathTests = it => { + it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', math => { + const a = Array3D.new([1, 1, 1], [0]); + + const result = math.maxPool(a, 1, 1, 0); + + test_util.expectArraysClose(result.getValues(), new Float32Array([0])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); + + const result = math.maxPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([5, 6, 9, 9])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', math => { + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 9]); + + const result = math.maxPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([5, 6, NaN, NaN])); + }); + + it('3x3x2 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new( + [3, 3, 2], + [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); + + const result = math.maxPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 2]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([5, 99, 6, 88, 9, 66, 9, 55])); + }); + + it('4x4x1 in, 2x2 filter, 2 stride', math => { + // Feed forward. + const a = Array3D.new( + [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + + const result = math.maxPool(a, 2, 2, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([5, 7, 13, 15])); + }); + + it('2x2x1 in, 2x2 filter, 2 stride, pad=1', math => { + // Feed forward. + const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + + const result = math.maxPool(a, 2, 2, 1); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, 3, 4])); + }); + + it('throws when x is not rank 3', math => { + // tslint:disable-next-line:no-any + const a: any = Array2D.new([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]); + + expect(() => math.maxPool(a, 2, 1, 0)).toThrowError(); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('maxPool', [tests]); + test_util.describeMathGPU('maxPool', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.minPool +{ + const tests: MathTests = it => { + it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', math => { + const a = Array3D.new([1, 1, 1], [0]); + const result = math.minPool(a, 1, 1, 0); + test_util.expectArraysClose(result.getValues(), new Float32Array([0])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); + const result = math.minPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, 4, 5])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', math => { + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 8]); + const result = math.minPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, NaN, NaN])); + }); + + it('3x3x2 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new( + [3, 3, 2], + [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); + const result = math.minPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 2]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 55, 2, 44, 4, 22, 5, 11])); + }); + + it('4x4x1 in, 2x2 filter, 2 stride', math => { + // Feed forward. + const a = Array3D.new( + [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + const result = math.minPool(a, 2, 2, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([0, 2, 8, 10])); + }); + + it('2x2x1 in, 2x2 filter, 2 stride, pad=1', math => { + // Feed forward. + const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const result = math.minPool(a, 2, 2, 1); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, 3, 4])); + }); + }; + + test_util.describeMathCPU('minPool', [tests]); + test_util.describeMathGPU('minPool', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.avgPool +{ + const tests: MathTests = it => { + it('1x1x1 in, 1x1 filter, 1 stride: [0] => [0]', math => { + const a = Array3D.new([1, 1, 1], [0]); + const result = math.avgPool(a, 1, 1, 0); + test_util.expectArraysClose(result.getValues(), new Float32Array([0])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, 9, 8]); + const result = math.avgPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([3, 4, 6.25, 7])); + }); + + it('3x3x1 in, 2x2 filter, 1 stride, propagates NaNs', math => { + // Feed forward. + const a = Array3D.new([3, 3, 1], [1, 2, 3, 4, 5, 6, 7, NaN, 8]); + const result = math.avgPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([3, 4, NaN, NaN])); + }); + + it('3x3x2 in, 2x2 filter, 1 stride', math => { + // Feed forward. + const a = Array3D.new( + [3, 3, 2], + [1, 99, 2, 88, 3, 77, 4, 66, 5, 55, 6, 44, 7, 33, 9, 22, 8, 11]); + const result = math.avgPool(a, 2, 1, 0); + + expect(result.shape).toEqual([2, 2, 2]); + test_util.expectArraysClose( + result.getValues(), + new Float32Array([3, 77, 4, 66, 6.25, 44, 7, 33])); + }); + + it('4x4x1 in, 2x2 filter, 2 stride', math => { + // Feed forward. + const a = Array3D.new( + [4, 4, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + const result = math.avgPool(a, 2, 2, 0); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([2.5, 4.5, 10.5, 12.5])); + }); + + it('2x2x1 in, 2x2 filter, 2 stride, pad=1', math => { + // Feed forward. + const a = Array3D.new([2, 2, 1], [1, 2, 3, 4]); + const result = math.avgPool(a, 2, 2, 1); + + expect(result.shape).toEqual([2, 2, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([0.25, 0.5, 0.75, 1])); + }); + }; + + test_util.describeMathCPU('avgPool', [tests]); + test_util.describeMathGPU('avgPool', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/reduction_ops_test.ts b/src/math/reduction_ops_test.ts new file mode 100644 index 0000000000..4d478d82e7 --- /dev/null +++ b/src/math/reduction_ops_test.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D} from './ndarray'; + +// math.min +{ + const tests: MathTests = it => { + it('Array1D', math => { + const a = Array1D.new([3, -1, 0, 100, -7, 2]); + + expect(math.min(a).get()).toBeCloseTo(-7); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([3, NaN, 2]); + + expect(math.min(a).get()).toEqual(NaN); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('min', [tests]); + test_util.describeMathGPU('min', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.max +{ + const tests: MathTests = it => { + it('with one element dominating', math => { + const a = Array1D.new([3, -1, 0, 100, -7, 2]); + + const r = math.max(a); + + expect(r.get()).toBeCloseTo(100); + + a.dispose(); + }); + + it('with all elements being the same', math => { + const a = Array1D.new([3, 3, 3]); + + const r = math.max(a); + + expect(r.get()).toBeCloseTo(3); + + a.dispose(); + }); + + it('propagates NaNs', math => { + expect(math.max(Array1D.new([3, NaN, 2])).get()).toEqual(NaN); + }); + }; + + test_util.describeMathCPU('max', [tests]); + test_util.describeMathGPU('max', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.argmax +{ + const tests: MathTests = it => { + it('Array1D', math => { + const a = Array1D.new([1, 0, 3, 2]); + const result = math.argMax(a); + expect(result.get()).toBeCloseTo(2); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([5, 0, 3, NaN, 3]); + expect(math.argMax(a).get()).toEqual(NaN); + a.dispose(); + }); + }; + + test_util.describeMathCPU('argmax', [tests]); + test_util.describeMathGPU('argmax', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.argmin +{ + const tests: MathTests = it => { + it('argmin', math => { + const a = Array1D.new([1, 0, 3, 2]); + + const result = math.argMin(a); + + expect(result.get()).toBeCloseTo(1); + + a.dispose(); + }); + + it('Arg min propagates NaNs', math => { + const a = Array1D.new([5, 0, NaN, 7, 3]); + + expect(math.argMin(a).get()).toEqual(NaN); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('argmin', [tests]); + test_util.describeMathGPU('argmin', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.argMaxEquals +{ + const tests: MathTests = it => { + it('equals', math => { + const a = Array1D.new([5, 0, 3, 7, 3]); + const b = Array1D.new([-100.3, -20.0, -10.0, -5, -100]); + const result = math.argMaxEquals(a, b); + expect(result.get()).toBeCloseTo(1); + }); + + it('not equals', math => { + const a = Array1D.new([5, 0, 3, 1, 3]); + const b = Array1D.new([-100.3, -20.0, -10.0, -5, 0]); + const result = math.argMaxEquals(a, b); + expect(result.get()).toBeCloseTo(0); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([0, 3, 1, 3]); + const b = Array1D.new([NaN, -20.0, -10.0, -5]); + const result = math.argMaxEquals(a, b); + expect(result.get()).toEqual(NaN); + }); + + it('throws when given arrays of different shape', math => { + const a = Array1D.new([5, 0, 3, 7, 3, 10]); + const b = Array1D.new([-100.3, -20.0, -10.0, -5, -100]); + expect(() => math.argMaxEquals(a, b)).toThrowError(); + }); + }; + + test_util.describeMathCPU('argMaxEquals', [tests]); + test_util.describeMathGPU('argMaxEquals', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.logSumExp +{ + const tests: MathTests = it => { + it('basic', math => { + const a = Array1D.new([1, 2, -3]); + const result = math.logSumExp(a); + expect(result.get()) + .toBeCloseTo(Math.log(Math.exp(1) + Math.exp(2) + Math.exp(-3))); + + a.dispose(); + result.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([1, 2, NaN]); + const result = math.logSumExp(a); + expect(result.get()).toEqual(NaN); + a.dispose(); + }); + }; + + test_util.describeMathCPU('logSumExp', [tests]); + test_util.describeMathGPU('logSumExp', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.sum +{ + const tests: MathTests = it => { + it('basic', math => { + const a = Array2D.new([3, 2], [1, 2, 3, 0, 0, 1]); + const result = math.sum(a); + expect(result.get()).toBeCloseTo(7); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array2D.new([3, 2], [1, 2, 3, NaN, 0, 1]); + expect(math.sum(a).get()).toEqual(NaN); + a.dispose(); + }); + }; + + test_util.describeMathCPU('sum', [tests]); + test_util.describeMathGPU('sum', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/resize_bilinear_test.ts b/src/math/resize_bilinear_test.ts new file mode 100644 index 0000000000..b5d2bc132b --- /dev/null +++ b/src/math/resize_bilinear_test.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array3D} from './ndarray'; + +// math.resizeBilinear3D +{ + const tests: MathTests = it => { + it('simple alignCorners=false', math => { + const input = Array3D.new([2, 2, 1], [2, 2, 4, 4]); + + const output = math.resizeBilinear3D(input, [3, 3], false); + + test_util.expectArraysClose( + output.getValues(), + new Float32Array([2, 2, 2, 10 / 3, 10 / 3, 10 / 3, 4, 4, 4])); + input.dispose(); + }); + + it('simple alignCorners=true', math => { + const input = Array3D.new([2, 2, 1], [2, 2, 4, 4]); + + const output = math.resizeBilinear3D(input, [3, 3], true); + + test_util.expectArraysClose( + output.getValues(), new Float32Array([2, 2, 2, 3, 3, 3, 4, 4, 4])); + + input.dispose(); + }); + + it('matches tensorflow w/ random numbers alignCorners=false', math => { + const input = Array3D.new([2, 3, 2], [ + 1.19074044, 0.91373104, 2.01611669, -0.52270832, 0.38725395, 1.30809779, + 0.61835143, 3.49600659, 2.09230986, 0.56473997, 0.03823943, 1.19864896 + ]); + + const output = math.resizeBilinear3D(input, [4, 5], false); + + test_util.expectArraysClose( + output.getValues(), new Float32Array([ + 1.19074047, 0.91373104, 1.68596613, 0.05186744, 1.69034398, + -0.15654698, 0.7130264, 0.94193673, 0.38725394, 1.30809784, + 0.9045459, 2.20486879, 1.59434628, 0.89455694, 1.68591988, + 0.26748738, 0.58103991, 1.00690198, 0.21274668, 1.25337338, + 0.6183514, 3.49600649, 1.50272655, 1.73724651, 1.68149579, + 0.69152176, 0.44905344, 1.07186723, 0.03823943, 1.19864893, + 0.6183514, 3.49600649, 1.50272655, 1.73724651, 1.68149579, + 0.69152176, 0.44905344, 1.07186723, 0.03823943, 1.19864893 + ])); + + input.dispose(); + }); + + it('matches tensorflow w/ random numbers alignCorners=true', math => { + const input = Array3D.new([2, 3, 2], [ + 1.56324531, 2.13817752, 1.44398421, 1.07632684, 0.59306785, -0.36970865, + 1.62451879, 1.8367334, 1.13944798, 2.01993218, 2.01919952, 2.67524054 + ]); + + const output = math.resizeBilinear3D(input, [4, 5], true); + + test_util.expectArraysClose( + output.getValues(), new Float32Array([ + 1.5632453, 2.13817763, 1.50361478, 1.60725224, 1.44398427, + 1.07632685, 1.01852608, 0.35330909, 0.59306782, -0.36970866, + 1.58366978, 2.03769612, 1.46307099, 1.71427906, 1.3424722, + 1.39086199, 1.20545864, 1.01806819, 1.06844509, 0.6452744, + 1.60409427, 1.93721485, 1.42252707, 1.82130599, 1.24096, + 1.70539713, 1.3923912, 1.68282723, 1.54382229, 1.66025746, + 1.62451875, 1.83673346, 1.38198328, 1.92833281, 1.13944793, + 2.01993227, 1.57932377, 2.34758639, 2.01919961, 2.67524052 + ])); + + input.dispose(); + }); + }; + + test_util.describeMathCPU('resizeBilinear3D', [tests]); + test_util.describeMathGPU('resizeBilinear3D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/slice_test.ts b/src/math/slice_test.ts new file mode 100644 index 0000000000..96136d11e0 --- /dev/null +++ b/src/math/slice_test.ts @@ -0,0 +1,230 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D, Array2D, Array3D, Array4D} from './ndarray'; + +// math.slice1D +{ + const tests: MathTests = it => { + it('slices 1x1 into 1x1 (effectively a copy)', math => { + const a = Array1D.new([5]); + + const result = math.slice1D(a, 0, 1); + + expect(result.shape).toEqual([1]); + expect(result.get(0)).toBeCloseTo(5); + + a.dispose(); + }); + + it('slices 5x1 into shape 2x1 starting at 3', math => { + const a = Array1D.new([1, 2, 3, 4, 5]); + + const result = math.slice1D(a, 3, 2); + + expect(result.shape).toEqual([2]); + test_util.expectArraysClose(result.getValues(), new Float32Array([4, 5])); + + a.dispose(); + }); + + it('slices 5x1 into shape 3x1 starting at 1', math => { + const a = Array1D.new([1, 2, 3, 4, 5]); + + const result = math.slice1D(a, 1, 3); + + expect(result.shape).toEqual([3]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([2, 3, 4])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('slice1D', [tests]); + test_util.describeMathGPU('slice1D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.slice2D +{ + const tests: MathTests = it => { + it('slicing a 1x1 from a 1x1 returns a 1x1', math => { + const a = Array2D.new([1, 1], [0]); + + const b = math.slice2D(a, [0, 0], [1, 1]); + + expect(b.shape).toEqual([1, 1]); + + a.dispose(); + }); + + it('returns a ndarray of slice size', math => { + const a = Array2D.zeros([100, 100]); + + const b = math.slice2D(a, [0, 0], [12, 34]); + + expect(b.shape).toEqual([12, 34]); + + a.dispose(); + }); + + it('returns the upper-left submatrix when begin is [0, 0]', math => { + const a = Array2D.randUniform([10, 10], -1, 1); + + const b = math.slice2D(a, [0, 0], [2, 2]); + + const aValues = a.getValues(); + + const expected = + new Float32Array([aValues[0], aValues[1], aValues[10], aValues[11]]); + test_util.expectArraysClose(b.getValues(), expected); + + a.dispose(); + }); + + it('returns the rectangle specified', math => { + const a = Array2D.new([4, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + const b = math.slice2D(a, [1, 1], [3, 2]); + + const expected = new Float32Array([5, 6, 8, 9, 11, 12]); + test_util.expectArraysClose(b.getValues(), expected); + + a.dispose(); + }); + + it('throws when requesting out of bounds slice', math => { + const a = Array2D.new([4, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + expect(() => math.slice2D(a, [1, 1], [10, 10])).toThrowError(); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('slice2D', [tests]); + test_util.describeMathGPU('slice2D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.slice3D +{ + const tests: MathTests = it => { + it('slices 1x1x1 into shape 1x1x1 (effectively a copy)', math => { + const a = Array3D.new([1, 1, 1], [[[5]]]); + + const result = math.slice3D(a, [0, 0, 0], [1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1]); + expect(result.get(0, 0, 0)).toBeCloseTo(5); + + a.dispose(); + }); + + it('slices 2x2x2 array into 1x2x2 starting at [1, 0, 0]', math => { + const a = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); + + const result = math.slice3D(a, [1, 0, 0], [1, 2, 2]); + + expect(result.shape).toEqual([1, 2, 2]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([5, 6, 7, 8])); + + a.dispose(); + }); + + it('slices 2x2x2 array into 2x1x1 starting at [0, 1, 1]', math => { + const a = Array3D.new([2, 2, 2], [1, 2, 3, 4, 5, 6, 7, 8]); + + const result = math.slice3D(a, [0, 1, 1], [2, 1, 1]); + + expect(result.shape).toEqual([2, 1, 1]); + test_util.expectArraysClose(result.getValues(), new Float32Array([4, 8])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('slice3D', [tests]); + test_util.describeMathGPU('slice3D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.slice4D +{ + const tests: MathTests = it => { + it('slices 1x1x1x1 into shape 1x1x1x1 (effectively a copy)', math => { + const a = Array4D.new([1, 1, 1, 1], [[[[5]]]]); + + const result = math.slice4D(a, [0, 0, 0, 0], [1, 1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1, 1]); + expect(result.get(0, 0, 0, 0)).toBeCloseTo(5); + + a.dispose(); + }); + + it('slices 2x2x2x2 array into 1x2x2x2 starting at [1, 0, 0, 0]', math => { + const a = Array4D.new( + [2, 2, 2, 2], + [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88]); + + const result = math.slice4D(a, [1, 0, 0, 0], [1, 2, 2, 2]); + + expect(result.shape).toEqual([1, 2, 2, 2]); + test_util.expectArraysClose( + result.getValues(), + new Float32Array([11, 22, 33, 44, 55, 66, 77, 88])); + + a.dispose(); + }); + + it('slices 2x2x2x2 array into 2x1x1x1 starting at [0, 1, 1, 1]', math => { + const a = Array4D.new( + [2, 2, 2, 2], + [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88]); + + const result = math.slice4D(a, [0, 1, 1, 1], [2, 1, 1, 1]); + + expect(result.shape).toEqual([2, 1, 1, 1]); + test_util.expectArraysClose( + result.getValues(), new Float32Array([8, 88])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('slice4D', [tests]); + test_util.describeMathGPU('slice4D', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/softmax_test.ts b/src/math/softmax_test.ts new file mode 100644 index 0000000000..2f0b4eaca9 --- /dev/null +++ b/src/math/softmax_test.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array1D} from './ndarray'; + +const tests: MathTests = it => { + it('regular test', math => { + const y = math.softmax(Array1D.new([2, 1, 3])); + + test_util.expectArraysClose( + y.getValues(), new Float32Array([0.24472847, 0.09003057, 0.66524095])); + test_util.expectNumbersClose(y.get(0) + y.get(1) + y.get(2), 1); + }); + + it('overflow', math => { + const y = math.softmax(Array1D.new([1000, 1000])); + + test_util.expectArraysClose(y.getValues(), new Float32Array([0.5, 0.5])); + }); + + it('underflow', math => { + const y = math.softmax(Array1D.new([-1000, -1000])); + + test_util.expectArraysClose(y.getValues(), new Float32Array([0.5, 0.5])); + }); + + it('Huge difference between probabilities', math => { + const y = math.softmax(Array1D.new([-1000, +1000])); + + test_util.expectArraysClose(y.getValues(), new Float32Array([0.0, 1])); + }); + + it('Propagates NaNs', math => { + const a = Array1D.new([2, 1, NaN]); + + const y = math.softmax(a); + + test_util.expectArraysClose( + y.getValues(), new Float32Array([NaN, NaN, NaN])); + + a.dispose(); + }); +}; + +test_util.describeMathCPU('softmax', [tests]); +test_util.describeMathGPU('softmax', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} +]); diff --git a/src/math/transpose_test.ts b/src/math/transpose_test.ts new file mode 100644 index 0000000000..7f6e2100f1 --- /dev/null +++ b/src/math/transpose_test.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; + +import {Array2D, Array3D} from './ndarray'; + +// math.switchDim +{ + const cpuTests: MathTests = it => { + it('Switch dim 2D (no change)', math => { + const t = Array2D.new([2, 4], [1, 11, 2, 22, 3, 33, 4, 44]); + + const t2 = math.switchDim(t, [0, 1]); + + expect(t2.shape).toEqual(t.shape); + expect(t2.getValues()).toEqual(t.getValues()); + + t.dispose(); + }); + + it('Switch dim 2D (transpose)', math => { + const t = Array2D.new([2, 4], [1, 11, 2, 22, 3, 33, 4, 44]); + + const t2 = math.switchDim(t, [1, 0]); + + expect(t2.shape).toEqual([4, 2]); + const expected = new Float32Array([1, 3, 11, 33, 2, 4, 22, 44]); + expect(t2.getValues()).toEqual(expected); + + t.dispose(); + }); + + it('Switch dim 3D [r, c, d] => [d, r, c]', math => { + const t = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); + + const t2 = math.switchDim(t, [2, 0, 1]); + + expect(t2.shape).toEqual([2, 2, 2]); + const expected = new Float32Array([1, 2, 3, 4, 11, 22, 33, 44]); + expect(t2.getValues()).toEqual(expected); + + t.dispose(); + }); + + it('Switch dim 3D [r, c, d] => [d, c, r]', math => { + const t = Array3D.new([2, 2, 2], [1, 11, 2, 22, 3, 33, 4, 44]); + + const t2 = math.switchDim(t, [2, 1, 0]); + + expect(t2.shape).toEqual([2, 2, 2]); + const expected = new Float32Array([1, 3, 2, 4, 11, 33, 22, 44]); + expect(t2.getValues()).toEqual(expected); + + t.dispose(); + }); + }; + + test_util.describeMathCPU('switchDim', [cpuTests]); +} diff --git a/src/math/unaryop_test.ts b/src/math/unaryop_test.ts new file mode 100644 index 0000000000..186e0ccfe4 --- /dev/null +++ b/src/math/unaryop_test.ts @@ -0,0 +1,649 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as test_util from '../test_util'; +import {MathTests} from '../test_util'; +import * as util from '../util'; + +import {Array1D, Array2D} from './ndarray'; + +// math.relu +{ + const tests: MathTests = it => { + it('basic', math => { + const a = Array1D.new([1, -2, 0, 3, -0.1]); + + const result = math.relu(a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 0, 0, 3, 0])); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([1, -2, 0, 3, -0.1, NaN]); + + const result = math.relu(a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 0, 0, 3, 0, NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('relu', [tests]); + test_util.describeMathGPU('relu', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.abs +{ + const tests: MathTests = it => { + it('basic', math => { + const a = Array1D.new([1, -2, 0, 3, -0.1]); + const result = math.abs(a); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, 0, 3, 0.1])); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([1, -2, 0, 3, -0.1, NaN]); + const result = math.abs(a); + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 2, 0, 3, 0.1, NaN])); + a.dispose(); + }); + }; + + test_util.describeMathCPU('abs', [tests]); + test_util.describeMathGPU('abs', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.step +{ + const tests: MathTests = it => { + it('with 1d ndarray', math => { + const a = Array1D.new([1, -2, -.01, 3, -0.1]); + + const result = math.step(a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 0, 0, 1, 0])); + + a.dispose(); + }); + + it('with 2d ndarray', math => { + const a = Array2D.new([2, 2], [1, -5, -3, 4]); + const result = math.step(a); + + expect(result.shape).toEqual([2, 2]); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 0, 0, 1])); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([1, -2, -.01, 3, NaN]); + + const result = math.step(a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([1, 0, 0, 1, NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('step', [tests]); + test_util.describeMathGPU('step', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.neg +{ + const tests: MathTests = it => { + it('basic', math => { + const a = Array1D.new([1, -3, 2, 7, -4]); + + const result = math.neg(a); + + test_util.expectArraysClose( + result.getValues(), new Float32Array([-1, 3, -2, -7, 4])); + + a.dispose(); + }); + + it('propagate NaNs', math => { + const a = Array1D.new([1, -3, 2, 7, NaN]); + + const result = math.neg(a); + + const expected = [-1, 3, -2, -7, NaN]; + test_util.expectArraysClose( + result.getValues(), new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('neg', [tests]); + test_util.describeMathGPU('neg', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.sigmoid +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.sigmoid(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = 1 / (1 + Math.exp(-values[i])); + } + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([3, NaN]); + + const res = math.sigmoid(a).getValues(); + + test_util.expectArraysClose( + res, new Float32Array([1 / (1 + Math.exp(-3)), NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('sigmoid', [tests]); + test_util.describeMathGPU('sigmoid', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.sqrt +{ + const tests: MathTests = it => { + it('sqrt', math => { + const a = Array1D.new([2, 4]); + + const r = math.sqrt(a); + + expect(r.get(0)).toBeCloseTo(Math.sqrt(2)); + expect(r.get(1)).toBeCloseTo(Math.sqrt(4)); + + a.dispose(); + }); + + it('sqrt propagates NaNs', math => { + const a = Array1D.new([1, NaN]); + + const r = math.sqrt(a).getValues(); + + test_util.expectArraysClose(r, new Float32Array([Math.sqrt(1), NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('sqrt', [tests]); + test_util.describeMathGPU('sqrt', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.log +{ + const tests: MathTests = it => { + it('log', math => { + const a = Array1D.new([1, 2]); + + const r = math.log(a); + + expect(r.get(0)).toBeCloseTo(Math.log(1)); + expect(r.get(1)).toBeCloseTo(Math.log(2)); + + a.dispose(); + }); + + it('log propagates NaNs', math => { + const a = Array1D.new([1, NaN]); + + const r = math.log(a).getValues(); + + test_util.expectArraysClose(r, new Float32Array([Math.log(1), NaN])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('log', [tests]); + test_util.describeMathGPU('log', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.exp +{ + const tests: MathTests = it => { + it('exp', math => { + const a = Array1D.new([1, 2, 0]); + + const r = math.exp(a); + + expect(r.get(0)).toBeCloseTo(Math.exp(1)); + expect(r.get(1)).toBeCloseTo(Math.exp(2)); + expect(r.get(2)).toBeCloseTo(1); + + a.dispose(); + }); + + it('exp propagates NaNs', math => { + const a = Array1D.new([1, NaN, 0]); + + const r = math.exp(a).getValues(); + + test_util.expectArraysClose(r, new Float32Array([Math.exp(1), NaN, 1])); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('exp', [tests]); + test_util.describeMathGPU('exp', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.sin +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.sin(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.sin(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.sin(a).getValues(); + + const expected = [Math.sin(4), NaN, Math.sin(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('sin', [tests]); + test_util.describeMathGPU('sin', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.cos +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.cos(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.cos(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.cos(a).getValues(); + + const expected = [Math.cos(4), NaN, Math.cos(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('cos', [tests]); + test_util.describeMathGPU('cos', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.tan +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.tan(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.tan(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected, 1e-1); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.tan(a).getValues(); + + const expected = [Math.tan(4), NaN, Math.tan(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('tan', [tests]); + test_util.describeMathGPU('tan', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.asin +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [.1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.asin(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.asin(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected, 1e-3); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.asin(a).getValues(); + + const expected = [Math.asin(4), NaN, Math.asin(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('asin', [tests]); + test_util.describeMathGPU('asin', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.acos +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [.1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.acos(a); + + const expected = new Float32Array(a.size); + + for (let i = 0; i < a.size; i++) { + expected[i] = Math.acos(values[i]); + } + // TODO(nsthorat): Fix the precision with byte textures here. + test_util.expectArraysClose(result.getValues(), expected, 1e-1); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + const res = math.acos(a).getValues(); + const expected = [Math.acos(4), NaN, Math.acos(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + a.dispose(); + }); + }; + + test_util.describeMathCPU('acos', [tests]); + test_util.describeMathGPU('acos', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.atan +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.atan(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.atan(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected, 1e-3); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.atan(a).getValues(); + + const expected = [Math.atan(4), NaN, Math.atan(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('atan', [tests]); + test_util.describeMathGPU('atan', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.sinh +{ + // TODO(nsthorat): Fix the precision problem here. + const epsilon = 1e-1; + + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + + const result = math.sinh(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.sinh(values[i]); + } + + test_util.expectArraysClose(result.getValues(), expected, epsilon); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.sinh(a).getValues(); + + const expected = [Math.sinh(4), NaN, Math.sinh(0)]; + test_util.expectArraysClose(res, new Float32Array(expected), epsilon); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('sinh', [tests]); + test_util.describeMathGPU('sinh', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.cosh +{ + // TODO(nsthorat): Fix the precision problem here. + const epsilon = 1e-1; + + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, -1, -4]; + const a = Array1D.new(values); + + const result = math.cosh(a); + + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = Math.cosh(values[i]); + } + + // TODO(nsthorat): Fix the precision problem here. + test_util.expectArraysClose(result.getValues(), expected, epsilon); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + + const res = math.cosh(a).getValues(); + + const expected = [Math.cosh(4), NaN, Math.cosh(0)]; + test_util.expectArraysClose(res, new Float32Array(expected), epsilon); + + a.dispose(); + }); + }; + + test_util.describeMathCPU('cosh', [tests]); + test_util.describeMathGPU('cosh', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} + +// math.tanh +{ + const tests: MathTests = it => { + it('basic', math => { + const values = [1, -3, 2, 7, -4]; + const a = Array1D.new(values); + const result = math.tanh(a); + const expected = new Float32Array(a.size); + for (let i = 0; i < a.size; i++) { + expected[i] = util.tanh(values[i]); + } + test_util.expectArraysClose(result.getValues(), expected); + + a.dispose(); + }); + + it('propagates NaNs', math => { + const a = Array1D.new([4, NaN, 0]); + const res = math.tanh(a).getValues(); + const expected = [util.tanh(4), NaN, util.tanh(0)]; + test_util.expectArraysClose(res, new Float32Array(expected)); + a.dispose(); + }); + }; + + test_util.describeMathCPU('tanh', [tests]); + test_util.describeMathGPU('tanh', [tests], [ + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 1}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': true, 'WEBGL_VERSION': 2}, + {'WEBGL_FLOAT_TEXTURE_ENABLED': false, 'WEBGL_VERSION': 1} + ]); +} diff --git a/src/math/webgl/gpgpu_context_test.ts b/src/math/webgl/gpgpu_context_test.ts index a6b42f1763..1d5ed33e2d 100644 --- a/src/math/webgl/gpgpu_context_test.ts +++ b/src/math/webgl/gpgpu_context_test.ts @@ -35,14 +35,7 @@ describe('GPGPUContext downloadMatrixFromTexture WebGL 2.0', () => { afterEach(() => { gpgpu.deleteMatrixTexture(texture); gpgpu.dispose(); - }); - - it('returns clear color from the output texture', () => { - gpgpu.setOutputMatrixTexture(texture, 1, 1); - gpgpu.gl.clearColor(0.123, 0, 0, 0); - gpgpu.gl.clear(gpgpu.gl.COLOR_BUFFER_BIT); - const result = gpgpu.downloadMatrixFromTexture(texture, 1, 1); - expect(result[0]).toBeCloseTo(0.123); + environment.setEnvironment(new Environment()); }); it('returns 1x1 matrix that was uploaded', () => { @@ -56,7 +49,7 @@ describe('GPGPUContext downloadMatrixFromTexture WebGL 2.0', () => { gpgpu.uploadMatrixToTexture( texture2, 2, 2, new Float32Array([1.234, 2, 3, 4])); const result = gpgpu.downloadMatrixFromTexture(texture2, 2, 2); - expect(result).toEqual(new Float32Array([1.234, 2, 3, 4])); + test_util.expectArraysClose(result, new Float32Array([1.234, 2, 3, 4])); gpgpu.deleteMatrixTexture(texture2); }); @@ -92,14 +85,6 @@ describe('GPGPUContext downloadMatrixFromTexture WebGL 1.0', () => { environment.setEnvironment(new Environment()); }); - it('returns clear color from the output texture', () => { - gpgpu.setOutputMatrixTexture(texture, 1, 1); - gpgpu.gl.clearColor(0.123, 0, 0, 0); - gpgpu.gl.clear(gpgpu.gl.COLOR_BUFFER_BIT); - const result = gpgpu.downloadMatrixFromTexture(texture, 1, 1); - expect(result[0]).toBeCloseTo(0.123); - }); - it('returns 1x1 matrix that was uploaded', () => { gpgpu.uploadMatrixToTexture(texture, 1, 1, new Float32Array([1.234])); const result = gpgpu.downloadMatrixFromTexture(texture, 1, 1); @@ -111,7 +96,7 @@ describe('GPGPUContext downloadMatrixFromTexture WebGL 1.0', () => { gpgpu.uploadMatrixToTexture( texture2, 2, 2, new Float32Array([1.234, 2, 3, 4])); const result = gpgpu.downloadMatrixFromTexture(texture2, 2, 2); - expect(result).toEqual(new Float32Array([1.234, 2, 3, 4])); + test_util.expectArraysClose(result, new Float32Array([1.234, 2, 3, 4])); gpgpu.deleteMatrixTexture(texture2); }); @@ -127,6 +112,51 @@ describe('GPGPUContext downloadMatrixFromTexture WebGL 1.0', () => { }); }); +describe('GPGPUContext clear color texture', () => { + let gpgpu: GPGPUContext; + let texture: WebGLTexture; + + afterEach(() => { + gpgpu.deleteMatrixTexture(texture); + gpgpu.dispose(); + environment.setEnvironment(new Environment()); + }); + + it('webgl 1', () => { + const featureValues: Features = {}; + featureValues['WEBGL_FLOAT_TEXTURE_ENABLED'] = true; + featureValues['WEBGL_VERSION'] = 1; + environment.setEnvironment(new Environment(featureValues)); + + gpgpu = new GPGPUContext(); + gpgpu.enableAutomaticDebugValidation(true); + texture = gpgpu.createMatrixTexture(1, 1); + + gpgpu.setOutputMatrixTexture(texture, 1, 1); + gpgpu.gl.clearColor(0.123, 0, 0, 0); + gpgpu.gl.clear(gpgpu.gl.COLOR_BUFFER_BIT); + const result = gpgpu.downloadMatrixFromTexture(texture, 1, 1); + expect(result[0]).toBeCloseTo(0.123); + }); + + it('webgl 2', () => { + const featureValues: Features = {}; + featureValues['WEBGL_FLOAT_TEXTURE_ENABLED'] = true; + featureValues['WEBGL_VERSION'] = 2; + environment.setEnvironment(new Environment(featureValues)); + + gpgpu = new GPGPUContext(); + gpgpu.enableAutomaticDebugValidation(true); + texture = gpgpu.createMatrixTexture(1, 1); + + gpgpu.setOutputMatrixTexture(texture, 1, 1); + gpgpu.gl.clearColor(0.123, 0, 0, 0); + gpgpu.gl.clear(gpgpu.gl.COLOR_BUFFER_BIT); + const result = gpgpu.downloadMatrixFromTexture(texture, 1, 1); + expect(result[0]).toBeCloseTo(0.123); + }); +}); + describe('GPGPUContext setOutputMatrixTexture WebGL 2.0', () => { let gpgpu: GPGPUContext; let texture: WebGLTexture; diff --git a/src/math/webgl/gpgpu_math.ts b/src/math/webgl/gpgpu_math.ts index 5b0af06c56..67f514268a 100644 --- a/src/math/webgl/gpgpu_math.ts +++ b/src/math/webgl/gpgpu_math.ts @@ -15,6 +15,7 @@ * ============================================================================= */ +import {ENV} from '../../environment'; import * as util from '../../util'; import {NDArray} from '../ndarray'; @@ -43,6 +44,12 @@ export interface GPGPUBinary { outShapeInfo: ShapeInfo; } +const NAN_UNIFORM_NAME = 'NaN'; + +function shouldUploadNaNUniform(): boolean { + return !ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED'); +} + export function compileProgram( gpgpu: GPGPUContext, program: GPGPUProgram, inputs: T[], output: K): GPGPUBinary { @@ -77,6 +84,11 @@ export function compileProgram( gpgpu.getAttributeLocation(webGLProgram, attribute); }); + if (shouldUploadNaNUniform()) { + uniformLocations[NAN_UNIFORM_NAME] = + gpgpu.getUniformLocation(webGLProgram, NAN_UNIFORM_NAME); + } + return { program, source, @@ -133,6 +145,11 @@ export function runProgram( const variableUniformLocation = binary.uniformLocations[variableName]; gpgpu.setInputMatrixTexture(tex, variableUniformLocation, i); }); + + if (shouldUploadNaNUniform()) { + gpgpu.gl.uniform1f(binary.uniformLocations[NAN_UNIFORM_NAME], NaN); + } + if (customSetup != null) { customSetup(gpgpu, binary.webGLProgram); } diff --git a/src/math/webgl/gpgpu_util.ts b/src/math/webgl/gpgpu_util.ts index 66d1baf61c..e17dcf23c0 100644 --- a/src/math/webgl/gpgpu_util.ts +++ b/src/math/webgl/gpgpu_util.ts @@ -81,6 +81,10 @@ export function createIndexBuffer(gl: WebGLRenderingContext): WebGLBuffer { function getTextureInternalFormat( gl: WebGLRenderingContext, numChannels: number): number { + if (!ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) { + return gl.RGBA; + } + if (ENV.get('WEBGL_VERSION') === 2) { if (numChannels === 4) { // tslint:disable-next-line:no-any @@ -94,6 +98,10 @@ function getTextureInternalFormat( function getTextureFormat( gl: WebGLRenderingContext, numChannels: number): number { + if (!ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) { + return gl.RGBA; + } + if (ENV.get('WEBGL_VERSION') === 2) { if (numChannels === 4) { // tslint:disable-next-line:no-any @@ -105,6 +113,14 @@ function getTextureFormat( return gl.RGBA; } +function getTextureType(gl: WebGLRenderingContext) { + if (!ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) { + return gl.UNSIGNED_BYTE; + } + + return gl.FLOAT; +} + function createAndConfigureTexture( gl: WebGLRenderingContext, width: number, height: number, numChannels: number): WebGLTexture { @@ -126,7 +142,8 @@ function createAndConfigureTexture( webgl_util.callAndCheck( gl, () => gl.texImage2D( - tex2d, 0, internalFormat, width, height, 0, format, gl.FLOAT, null)); + tex2d, 0, internalFormat, width, height, 0, format, + getTextureType(gl), null)); webgl_util.callAndCheck(gl, () => gl.bindTexture(gl.TEXTURE_2D, null)); return texture; } @@ -179,13 +196,14 @@ export function uploadPixelDataToTexture( webgl_util.callAndCheck( gl, () => gl.texImage2D( - gl.TEXTURE_2D, 0, internalFormat, gl.RGBA, gl.FLOAT, pixels)); + gl.TEXTURE_2D, 0, internalFormat, gl.RGBA, getTextureType(gl), + pixels)); webgl_util.callAndCheck(gl, () => gl.bindTexture(gl.TEXTURE_2D, null)); } function uploadDataToTexture( gl: WebGLRenderingContext, texture: WebGLTexture, width: number, - height: number, data: Float32Array, numChannels: number) { + height: number, data: Float32Array|Uint8Array, numChannels: number) { const textureFormat = getTextureFormat(gl, numChannels); webgl_util.validateTextureSize(gl, width, height); @@ -193,8 +211,8 @@ function uploadDataToTexture( webgl_util.callAndCheck( gl, () => gl.texSubImage2D( - gl.TEXTURE_2D, 0, 0, 0, width, height, textureFormat, gl.FLOAT, - data)); + gl.TEXTURE_2D, 0, 0, 0, width, height, textureFormat, + getTextureType(gl), data)); webgl_util.callAndCheck(gl, () => gl.bindTexture(gl.TEXTURE_2D, null)); } @@ -204,19 +222,25 @@ export function uploadMatrixToTexture( const [w, h] = tex_util.getUnpackedMatrixTextureShapeWidthHeight(rows, columns); - const channelsPerTexture = - numChannels === 1 ? webgl_util.getChannelsPerTexture() : numChannels; - let unpackedArray: Float32Array; - if (channelsPerTexture === 1) { - // No need to allocate a temporary array. - unpackedArray = matrix; + let unpackedArray: Float32Array|Uint8Array; + + if (ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) { + const channelsPerTexture = + numChannels === 1 ? webgl_util.getChannelsPerTexture() : numChannels; + if (channelsPerTexture === 1) { + // No need to allocate a temporary array. + unpackedArray = matrix; + } else { + unpackedArray = + new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize( + matrix.length, channelsPerTexture)); + tex_util.encodeMatrixToUnpackedArray( + matrix, unpackedArray, channelsPerTexture); + } } else { - unpackedArray = - new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize( - matrix.length, channelsPerTexture)); - tex_util.encodeMatrixToUnpackedArray( - matrix, unpackedArray, channelsPerTexture); + unpackedArray = tex_util.encodeFloatArray(matrix); } + uploadDataToTexture(gl, texture, w, h, unpackedArray, numChannels); } @@ -237,16 +261,30 @@ export function downloadMatrixFromOutputTexture( tex_util.getUnpackedMatrixTextureShapeWidthHeight(rows, columns); const channelsPerTexture = 4; - const unpackedArray = - new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize( - rows * columns, channelsPerTexture)); + const isFloatTexture = ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED'); + + let downloadTarget: Float32Array|Uint8Array; + if (isFloatTexture) { + downloadTarget = + new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize( + rows * columns, channelsPerTexture)); + } else { + downloadTarget = new Uint8Array(rows * columns * channelsPerTexture); + } + webgl_util.callAndCheck( - gl, () => gl.readPixels(0, 0, w, h, gl.RGBA, gl.FLOAT, unpackedArray)); + gl, + () => gl.readPixels( + 0, 0, w, h, gl.RGBA, getTextureType(gl), downloadTarget)); - const matrix = new Float32Array(rows * columns); - tex_util.decodeMatrixFromUnpackedArray( - unpackedArray, matrix, channelsPerTexture); - return matrix; + if (isFloatTexture) { + const matrix = new Float32Array(rows * columns); + tex_util.decodeMatrixFromUnpackedArray( + downloadTarget as Float32Array, matrix, channelsPerTexture); + return matrix; + } else { + return tex_util.decodeToFloatArray(downloadTarget as Uint8Array); + } } export function downloadMatrixFromPackedOutputTexture( @@ -255,7 +293,8 @@ export function downloadMatrixFromPackedOutputTexture( const packedRGBA = new Float32Array( tex_util.getPackedRGBAArraySizeFromMatrixShape(rows, columns)); webgl_util.callAndCheck( - gl, () => gl.readPixels(0, 0, w, h, gl.RGBA, gl.FLOAT, packedRGBA)); + gl, + () => gl.readPixels(0, 0, w, h, gl.RGBA, getTextureType(gl), packedRGBA)); const matrix = new Float32Array(rows * columns); return tex_util.decodeMatrixFromPackedRGBA(packedRGBA, rows, columns, matrix); } diff --git a/src/math/webgl/shader_compiler.ts b/src/math/webgl/shader_compiler.ts index b8c9cdff97..f6a493e207 100644 --- a/src/math/webgl/shader_compiler.ts +++ b/src/math/webgl/shader_compiler.ts @@ -15,8 +15,11 @@ * ============================================================================= */ +import {ENV} from '../../environment'; import * as util from '../../util'; +import * as tex_util from './tex_util'; + export type ShapeInfo = { logicalShape: number[], texShape: [number, number] @@ -30,6 +33,8 @@ export type InputInfo = { export function makeShader( inputsInfo: InputInfo[], outputShape: ShapeInfo, userCode: string, broadcast: boolean): string { + const sampleSnippet = getSampleSnippet(); + const setOutputSnippet = getSetOutputSnippet(); const inputPrefixSnippet = inputsInfo.map(x => `uniform sampler2D ${x.name};`).join('\n'); const inputSamplingSnippet = @@ -39,12 +44,24 @@ export function makeShader( const outputSamplingSnippet = getOutputSamplingSnippet(outputShape.logicalShape, outTexShape); const source = [ - SHADER_PREFIX, inputPrefixSnippet, inputSamplingSnippet, - outputSamplingSnippet, userCode + SHADER_PREFIX, sampleSnippet, setOutputSnippet, inputPrefixSnippet, + inputSamplingSnippet, outputSamplingSnippet, userCode ].join('\n'); return source; } +function getSampleSnippet() { + return ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED') ? + FLOAT_TEXTURE_SAMPLE_SNIPPET : + UNSIGNED_BYTE_TEXTURE_SAMPLE_SNIPPET; +} + +function getSetOutputSnippet() { + return ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED') ? + FLOAT_TEXTURE_SETOUTPUT_SNIPPET : + UNSIGNED_BYTE_TEXTURE_SETOUTPUT_SNIPPET; +} + function getInputSamplingSnippet( inInfo: InputInfo, outShapeInfo: ShapeInfo, broadcast: boolean) { const shape = inInfo.shapeInfo.logicalShape; @@ -148,18 +165,81 @@ vec2 UVfrom4D(int texNumR, int texNumC, int stride0, } `; -const SHADER_PREFIX = ` - precision highp float; - varying vec2 resultUV; - const vec2 halfCR = vec2(0.5, 0.5); +const UNSIGNED_BYTE_TEXTURE_SAMPLE_SNIPPET = ` + uniform float NaN; + + const vec4 floatDeltas = vec4( + 1.0, + 1.0 / 255.0, + 1.0 / (255.0 * 255.0), + 1.0 / (255.0 * 255.0 * 255.0) + ); + const float minValue = ${tex_util.FLOAT_MIN}.0; + const float maxValue = ${tex_util.FLOAT_MAX}.0; + const float range = (maxValue - minValue) / 255.0; + const vec2 dotRange = vec2(1.0, range); + float sample(sampler2D texture, vec2 uv) { + vec4 sampleValue = texture2D(texture, uv); + if (all(equal(sampleValue, vec4(${tex_util.BYTE_NAN_VALUE})))) { + return NaN; + } + + vec4 encValue = floor(sampleValue * 255.0 + 0.5); + float decodedValue = dot(encValue, floatDeltas); + return dot(vec2(minValue, decodedValue), dotRange); + } +`; + +const UNSIGNED_BYTE_TEXTURE_SETOUTPUT_SNIPPET = ` + const vec4 floatPowers = vec4( + 1.0, + 255.0, + 255.0 * 255.0, + 255.0 * 255.0 * 255.0 + ); + const vec2 recipRange = vec2(1.0/range); + const vec2 recipRange255 = vec2(1.0/(maxValue - minValue)); + + void setOutput(float decodedValue) { + if (isNaN(decodedValue)) { + gl_FragColor = vec4(${tex_util.BYTE_NAN_VALUE}); + return; + } + + float a = dot(vec2(decodedValue, -minValue), recipRange); + float b = fract(a) * 255.0; + float c = fract(b) * 255.0; + float d = fract(c) * 255.0; + gl_FragColor = floor(vec4(a, b, c, d)) / 255.0; + + // TODO(dsmilkov): Version above gets better accuracy but probably slower + // than the version below. Benchmark to determine if the accuracy is worth + // the cost. + + // float normValue = dot(vec2(decodedValue, -minValue), recipRange255); + // vec4 f = normValue * floatPowers; + // gl_FragColor = floor(fract(f) * 255.0) / 255.0; + } +`; + +const FLOAT_TEXTURE_SAMPLE_SNIPPET = ` float sample(sampler2D texture, vec2 uv) { return texture2D(texture, uv).r; } +`; +const FLOAT_TEXTURE_SETOUTPUT_SNIPPET = ` void setOutput(float val) { gl_FragColor = vec4(val, 0, 0, 0); } +`; + +const SHADER_PREFIX = ` + precision highp float; + precision highp int; + varying vec2 resultUV; + const vec2 halfCR = vec2(0.5, 0.5); bool isNaN(float val) { return val == val ? false : true; @@ -197,20 +277,21 @@ function getOutput1DCoords( if (texShape[0] === 1) { return ` int getOutputCoords() { - return int(gl_FragCoord.x); + return int(resultUV.x * ${texShape[1]}.0); } `; } if (texShape[1] === 1) { return ` int getOutputCoords() { - return int(gl_FragCoord.y); + return int(resultUV.y * ${texShape[0]}.0); } `; } return ` int getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); return resTexRC.x * ${texShape[1]} + resTexRC.y; } `; @@ -222,7 +303,8 @@ function getOutput3DCoords( const stride1 = shape[2]; return ` ivec3 getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); int index = resTexRC.x * ${texShape[1]} + resTexRC.y; int r = index / ${stride0}; index -= r * ${stride0}; @@ -241,7 +323,8 @@ function getOutput4DCoords( const stride0 = shape[1] * stride1; return ` ivec4 getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); int index = resTexRC.x * ${texShape[1]} + resTexRC.y; int r = index / ${stride0}; @@ -263,14 +346,15 @@ function getOutput2DCoords( if (util.arraysEqual(shape, texShape)) { return ` ivec2 getOutputCoords() { - return ivec2(gl_FragCoord.yx); + return ivec2(resultUV.yx * vec2(${texShape[0]}, ${texShape[1]})); } `; } if (shape[1] === 1) { return ` ivec2 getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); int index = resTexRC.x * ${texShape[1]} + resTexRC.y; return ivec2(index, 0); } @@ -279,7 +363,8 @@ function getOutput2DCoords( if (shape[0] === 1) { return ` ivec2 getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); int index = resTexRC.x * ${texShape[1]} + resTexRC.y; return ivec2(0, index); } @@ -287,7 +372,8 @@ function getOutput2DCoords( } return ` ivec2 getOutputCoords() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${texShape[0]}, ${texShape[1]})); int index = resTexRC.x * ${texShape[1]} + resTexRC.y; int r = index / ${shape[1]}; int c = index - r * ${shape[1]}; @@ -511,7 +597,8 @@ function getSamplerAtOutputCoords( } return ` float ${funcName}() { - ivec2 resTexRC = ivec2(gl_FragCoord.yx); + ivec2 resTexRC = ivec2(resultUV.yx * + vec2(${outTexShape[0]}, ${outTexShape[1]})); int index = resTexRC.x * ${outTexShape[1]} + resTexRC.y; ${broadcastSnippet} int texR = index / ${inTexShape[1]}; diff --git a/src/math/webgl/tex_util.ts b/src/math/webgl/tex_util.ts index 39766284db..c96be6f1c7 100644 --- a/src/math/webgl/tex_util.ts +++ b/src/math/webgl/tex_util.ts @@ -40,9 +40,10 @@ export function getMatrixSizeFromUnpackedArraySize( return unpackedSize / channelsPerTexture; } +export type TypedArray = Float32Array|Uint8Array; + export function encodeMatrixToUnpackedArray( - matrix: Float32Array, unpackedArray: Float32Array, - channelsPerTexture: number) { + matrix: TypedArray, unpackedArray: TypedArray, channelsPerTexture: number) { const requiredSize = getUnpackedArraySizeFromMatrixSize(matrix.length, channelsPerTexture); if (unpackedArray.length < requiredSize) { @@ -57,6 +58,59 @@ export function encodeMatrixToUnpackedArray( } } +export const FLOAT_MAX = 20000; +export const FLOAT_MIN = -FLOAT_MAX; +const FLOAT_RANGE = (FLOAT_MAX - FLOAT_MIN) / 255; + +const FLOAT_DELTAS = [1, 1 / 255, 1 / (255 * 255), 1 / (255 * 255 * 255)]; +const FLOAT_POWERS = [1, 255, 255 * 255]; + +export const BYTE_NAN_VALUE = 0; +export function encodeFloatArray(floatArray: Float32Array): Uint8Array { + const uintArray = new Uint8Array(floatArray.length * 4); + for (let i = 0; i < uintArray.length; i += 4) { + const value = floatArray[i / 4]; + if (isNaN(value)) { + uintArray[i] = BYTE_NAN_VALUE; + uintArray[i + 1] = BYTE_NAN_VALUE; + uintArray[i + 2] = BYTE_NAN_VALUE; + uintArray[i + 3] = BYTE_NAN_VALUE; + continue; + } + + const normalizedValue = (value - FLOAT_MIN) / FLOAT_RANGE; + const enc = FLOAT_POWERS.map(pow => pow * normalizedValue); + const buckets = enc.map(value => Math.floor((value % 1) * 255)); + + uintArray[i] = Math.floor(normalizedValue); + uintArray[i + 1] = buckets[0]; + uintArray[i + 2] = buckets[1]; + uintArray[i + 3] = buckets[2]; + } + return uintArray; +} + +export function decodeToFloatArray(uintArray: Uint8Array): Float32Array { + const floatArray = new Float32Array(uintArray.length / 4); + for (let i = 0; i < uintArray.length; i += 4) { + if (uintArray[i] === BYTE_NAN_VALUE && + uintArray[i + 1] === BYTE_NAN_VALUE && + uintArray[i + 2] === BYTE_NAN_VALUE && + uintArray[i + 3] === BYTE_NAN_VALUE) { + floatArray[i / 4] = NaN; + continue; + } + + let dot = 0; + FLOAT_DELTAS.forEach((delta, j) => { + dot += delta * uintArray[i + j]; + }); + const value = dot * FLOAT_RANGE + FLOAT_MIN; + floatArray[i / 4] = value; + } + return floatArray; +} + export function decodeMatrixFromUnpackedArray( unpackedArray: Float32Array, matrix: Float32Array, channelsPerTexture: number) { diff --git a/src/math/webgl/tex_util_test.ts b/src/math/webgl/tex_util_test.ts index 0408493e6a..f6de536d41 100644 --- a/src/math/webgl/tex_util_test.ts +++ b/src/math/webgl/tex_util_test.ts @@ -296,3 +296,15 @@ describe('tex_util decodeMatrixFromPackedRGBA', () => { matrix, new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9])); }); }); + +describe('tex_util_float_packing', () => { + it('packs a float32array as a uint8 array', () => { + const elements = test_util.randomArrayInRange( + 1000, tex_util.FLOAT_MIN, tex_util.FLOAT_MAX); + + const matrix = new Float32Array(elements); + const uintArray = tex_util.encodeFloatArray(matrix); + const floatArray = tex_util.decodeToFloatArray(uintArray); + test_util.expectArraysClose(matrix, floatArray); + }); +}); diff --git a/src/math/webgl/webgl_util.ts b/src/math/webgl/webgl_util.ts index 59e6b3c9a1..8bf2cccedf 100644 --- a/src/math/webgl/webgl_util.ts +++ b/src/math/webgl/webgl_util.ts @@ -44,6 +44,7 @@ export function createWebGLRenderingContextFromCanvas( canvas: HTMLCanvasElement, attributes: WebGLContextAttributes): WebGLRenderingContext { let gl: WebGLRenderingContext; + const webglVersion = ENV.get('WEBGL_VERSION'); if (webglVersion === 2) { gl = canvas.getContext('webgl2', attributes) as WebGLRenderingContext; @@ -222,6 +223,10 @@ export function queryMaxTextureSize(gl: WebGLRenderingContext): number { } export function getChannelsPerTexture(): number { + if (!ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) { + return 4; + } + if (ENV.get('WEBGL_VERSION') === 2) { return 1; } diff --git a/src/test_util.ts b/src/test_util.ts index d308b2255e..796dcb0ab9 100644 --- a/src/test_util.ts +++ b/src/test_util.ts @@ -15,11 +15,18 @@ * ============================================================================= */ +import * as environment from './environment'; +import {Environment, Features} from './environment'; +import {NDArrayMath} from './math/math'; +import {NDArrayMathCPU} from './math/math_cpu'; +import {NDArrayMathGPU} from './math/math_gpu'; + /** Accuracy for tests. */ -const EPSILON = 1e-4; +// TODO(nsthorat || smilkov): Fix this low precision for byte-backed textures. +export const TEST_EPSILON = 1e-2; export function expectArraysClose( - actual: Float32Array, expected: Float32Array, epsilon = EPSILON) { + actual: Float32Array, expected: Float32Array, epsilon = TEST_EPSILON) { if (actual.length !== expected.length) { throw new Error( 'Matrices have different lengths (' + actual.length + ' vs ' + @@ -28,10 +35,8 @@ export function expectArraysClose( for (let i = 0; i < expected.length; ++i) { const a = actual[i]; const e = expected[i]; - if (isNaN(a) && isNaN(e)) { - continue; - } - if (isNaN(a) || isNaN(e) || Math.abs(a - e) > epsilon) { + + if (!areClose(a, e, epsilon)) { const actualStr = 'actual[' + i + '] === ' + a; const expectedStr = 'expected[' + i + '] === ' + e; throw new Error('Arrays differ: ' + actualStr + ', ' + expectedStr); @@ -39,6 +44,23 @@ export function expectArraysClose( } } +export function expectNumbersClose( + a: number, e: number, epsilon = TEST_EPSILON) { + if (!areClose(a, e, epsilon)) { + throw new Error('Numbers differ: actual === ' + a + ', expected === ' + e); + } +} + +function areClose(a: number, e: number, epsilon: number): boolean { + if (isNaN(a) && isNaN(e)) { + return true; + } + if (isNaN(a) || isNaN(e) || Math.abs(a - e) > epsilon) { + return false; + } + return true; +} + export function randomArrayInRange( n: number, minValue: number, maxValue: number): Float32Array { const v = new Float32Array(n); @@ -97,3 +119,102 @@ export function cpuDotProduct(a: Float32Array, b: Float32Array): number { } return d; } + +export type MathTests = + (it: (name: string, testFn: (math: NDArrayMath) => void) => void) => void; +export type Tests = (it: (name: string, testFn: () => void) => void) => void; + +export function describeMathCPU( + name: string, tests: MathTests[], featuresList?: Features[]) { + const testNameBase = 'math_cpu.' + name; + describeWithFeaturesAndExecutor( + testNameBase, tests as Tests[], + (testName, tests, features) => executeMathTests( + testName, tests, () => new NDArrayMathCPU(), features), + featuresList); +} + +export function describeMathGPU( + name: string, tests: MathTests[], featuresList?: Features[]) { + const testNameBase = 'math_gpu.' + name; + describeWithFeaturesAndExecutor( + testNameBase, tests as Tests[], + (testName, tests, features) => executeMathTests( + testName, tests, () => new NDArrayMathGPU(), features), + featuresList); +} + +export function describeCustom( + name: string, tests: Tests[], featuresList?: Features[], + customBeforeEach?: () => void, customAfterEach?: () => void) { + describeWithFeaturesAndExecutor( + name, tests as Tests[], + (testName, tests, features) => executeTests( + testName, tests, features, customBeforeEach, customAfterEach), + featuresList); +} + +type TestExecutor = (testName: string, tests: Tests[], features?: Features) => + void; +function describeWithFeaturesAndExecutor( + testNameBase: string, tests: Tests[], executor: TestExecutor, + featuresList?: Features[]) { + if (featuresList != null) { + featuresList.forEach(features => { + const testName = testNameBase + ' ' + JSON.stringify(features); + executor(testName, tests, features); + }); + } else { + executor(testNameBase, tests); + } +} + +export function executeMathTests( + testName: string, tests: MathTests[], mathFactory: () => NDArrayMath, + features?: Features) { + let math: NDArrayMath; + const customBeforeEach = () => { + math = mathFactory(); + math.startScope(); + }; + const customAfterEach = () => { + math.endScope(null); + math.dispose(); + }; + const customIt = (name: string, testFunc: (math: NDArrayMath) => void) => { + it(name, () => testFunc(math)); + }; + + executeTests( + testName, tests as Tests[], features, customBeforeEach, customAfterEach, + customIt); +} + +export function executeTests( + testName: string, tests: Tests[], features?: Features, + customBeforeEach?: () => void, customAfterEach?: () => void, + customIt: (expectation: string, testFunc: () => void) => void = it) { + describe(testName, () => { + beforeEach(() => { + if (features != null) { + environment.setEnvironment(new Environment(features)); + } + + if (customBeforeEach != null) { + customBeforeEach(); + } + }); + + afterEach(() => { + if (customAfterEach != null) { + customAfterEach(); + } + + if (features != null) { + environment.setEnvironment(new Environment()); + } + }); + + tests.forEach(test => test(customIt)); + }); +}