spec/grid/GridSpec.js

/** @test {Grid}*/
describe("Grid", function () {
	let CPM = require("../../build/artistoo-cjs.js")
	//eslint-disable-next-line no-unused-vars
	let grid2d, grid3d, grid

	beforeEach(function () {
		grid2d = new CPM.Grid2D([100, 100])
		grid3d = new CPM.Grid3D([100, 100, 100])
	})


	describe( " [ unit tests ] ", function (){
		/** @test {Grid#constructor} */
		describe( " constructor ", function(){
			/* Checking errors thrown by the constructor*/
			it("should throw an error when torus is specified for an incorrect number of dimensions", function () {
				expect(function () {
					grid = new CPM.Grid2D([100, 100], [true])
				}).toThrow("Torus should be specified for each dimension, or not at all!")
				expect(function () {
					//noinspection JSCheckFunctionSignatures
					grid = new CPM.Grid2D([100, 100], true)
				}).toThrow("Torus should be specified for each dimension, or not at all!")
				expect(function () {
					grid = new CPM.Grid2D([100, 100], [true, true, true])
				}).toThrow("Torus should be specified for each dimension, or not at all!")
				expect(function () {
					grid = new CPM.Grid3D([100, 100, 100], [true, true])
				}).toThrow("Torus should be specified for each dimension, or not at all!")
			})

			/* Checking properties set by the constructor*/
			it("should set a size for each dimension", function () {
				expect(grid2d.ndim).toEqual(grid2d.extents.length)
				expect(grid3d.ndim).toEqual(grid3d.extents.length)
			})

			it("should set a torus property in each dimension", function () {
				expect(grid2d.ndim).toEqual(grid2d.torus.length)
				expect(grid3d.ndim).toEqual(grid3d.torus.length)
			})

			it("should by default set torus = true in each dimension", function () {
				for (let i = 0; i < grid2d.ndim; i++) {
					expect(grid2d.torus[i] = true)
				}
				for (let i = 0; i < grid3d.ndim; i++) {
					expect(grid3d.torus[i] = true)
				}
			})

			it("should be able to handle different torus settings for each dimension", function () {
				let grid = new CPM.Grid2D([100, 100], [true, false])
				expect(grid.torus[0]).toBe(true)
				expect(grid.torus[1]).toBe(false)
			})

			it("should be able to handle a different size in each dimension", function () {
				let grid = new CPM.Grid2D([100, 300])
				expect(grid.extents[0]).not.toEqual(grid.extents[1])
			})

			it("should compute a midpoint at the correct position", function () {
				expect(grid2d.midpoint.length).toEqual(2)
				expect(grid3d.midpoint.length).toEqual(3)

				grid2d = new CPM.Grid2D([101, 101])
				expect((grid2d.midpoint[0] - grid2d.extents[0]/2) <= 1).toBeTruthy()
				expect((grid2d.midpoint[1] - grid2d.extents[1]/2) <= 1).toBeTruthy()
				expect((grid3d.midpoint[2] - grid3d.extents[2]/2) <= 1).toBeTruthy()
			})
		})

		/** @test {Grid#neigh} */
		describe( " neigh method ", function(){
			let g2D
			beforeEach( function() {
				// Create a grid 2D object with mock functions except neigh to test the
				// functionality of the Grid class independently of the
				// Grid2D and Grid3D subclasses.
				g2D = new CPM.Grid2D( [100,100] )
				//g2D = jasmine.createSpyObj("g2D", [ "neighi","p2i","i2p" ])
				// A mock neighi method for the unit test
				spyOn( g2D, "neighi" ).and.callFake( function ( i, torus ){
					let arr = []
					if( (i-1) >= 0 ){
						arr.push( i-1 )
					} else {
						for( let d = 0; d < torus.length; d++ ){
							if( torus[d] ) { arr.push( -(1+d )) }
						}
					}
					arr.push( i + 1 )

					return arr
				})
				spyOn( g2D, "i2p" ).and.callFake( function(i) {
					return( [0,i] )
				})
				spyOn( g2D, "p2i" ).and.callFake( function(p) {
					return p[1]
				})
			})

			it( " should return an array coordinate", function(){
				let nbh = g2D.neigh( [0,0] )
				for( let i = 0; i < nbh.length; i++ ){
					let n = nbh[i]
					expect( n.length ).toEqual( 2 )
				}
			})

			it( " should listen to torus for each dimension",
				function() {
					// the mock function adds a neighbor for each dim with a torus.
					// the mock function should return only one neighbor for [0,0]
					// when there is no torus.
					let nNeighbors = g2D.neigh( [0,0], [false,false] ).length
					expect( nNeighbors ).toEqual(1)
					nNeighbors = g2D.neigh( [0,0], [false,true] ).length
					expect( nNeighbors ).toEqual( 2 )
					nNeighbors = g2D.neigh( [0,0], [true,false] ).length
					expect( nNeighbors ).toEqual( 2 )
					nNeighbors = g2D.neigh( [0,0], [true,true] ).length
					expect( nNeighbors ).toEqual( 3 )

					// torus doesn't matter when not at 'border'
					nNeighbors =  g2D.neigh( [1,1], [false,false] ).length
					expect( nNeighbors ).toEqual( 2 )
					nNeighbors = g2D.neigh( [1,1], [true,true] ).length
					expect( nNeighbors ).toEqual( 2 )
				})
		})

		/** @test {Grid#setpix}
		 * @test {Grid#setpixi} */
		describe( " setpix(i) methods ", function() {
			let grid2D, grid2Db

			beforeEach( function(){
				grid2D = new CPM.Grid2D( [50,50] )
				grid2Db = new CPM.Grid2D( [50,50], [false,false],"Float32" )
				// mock functions of the p2i and i2p implemented in the subclass.
				spyOn( grid2D, "p2i" ).and.returnValue( 0 )
				spyOn( grid2D, "i2p" ).and.returnValue( [0,0] )
				spyOn( grid2Db, "p2i" ).and.returnValue( 0 )
				spyOn( grid2Db, "i2p" ).and.returnValue( [0,0] )
			})

			/**
			 * @test {Grid#setpix}
			 * @test {Grid#setpixi}
			 * */
			it( "can be called", function(){

				expect( grid2D.pixti( 0 ) ).toEqual( 0 )
				expect( function(){ grid2D.setpixi( 0, 1 ) } ).not.toThrow()
				expect( function(){ grid2D.setpix( [0,0], 2 ) } ).not.toThrow()
				expect( function(){ grid2Db.setpix( [0,0], -1 ) } ).not.toThrow()
			})

			/**
			 * @test {Grid#setpix}
			 * @test {Grid#setpixi}
			 * @test {Grid#_isValidValue}
			 * */
			it( " should prohibit setting an invalid type on the grid to avoid bugs", function() {
				expect( function(){ grid2D.setpix( [0,0], -1 ) } ).toThrow()
				expect( function(){ grid2D.setpix( [0,0], 2.5 ) } ).toThrow()
				expect( function(){ grid2D.setpixi( 0, -1 ) } ).toThrow()
				expect( function(){ grid2D.setpixi( 0, 2.5 ) } ).toThrow()

				// but small numeric differences are tolerated
				expect( function(){ grid2D.setpixi( 0, 1.00000001 )}).not.toThrow()
				expect( function(){ grid2D.setpix( [0,0], 1.00000001 )}).not.toThrow()
			})

			/**
			 * @test {Grid#setpix}
			 * @test {Grid#setpixi}
			 * */
			it( "store values in the Grid correctly", function(){

				// ---- Case 1 : Uint16 grid
				// Before setpix, values are zero:
				expect( grid2D.pixti( 0 ) ).toEqual( 0 )
				let randomInt1 = Math.round( Math.random()*100 )
				grid2D.setpixi( 0, randomInt1 )
				expect( grid2D.pixti( 0 ) ).toEqual( randomInt1 )

				let randomInt2 = Math.round( Math.random()*100 )
				grid2D.setpix( [0,0], randomInt2 )
				expect( grid2D.pixti( 0 ) ).toEqual( randomInt2 )

				// --- Case 2 : Float32 grid
				// It should be possible to set a negative value.
				expect( function(){ grid2Db.setpix( [0,0], -1 ) } ).not.toThrow()
				expect( grid2Db.pixti( 0 ) ).toEqual( -1 )

				// ... Or a floating point number
				let value = 2.345
				expect( function(){ grid2Db.setpix( [0,0], value ) } ).not.toThrow()
				expect( grid2Db.pixti( 0 ) ).toBeCloseTo( value, 6 )
			})



		})

		/** @test {Grid#pixt}
		 * @test {Grid#pixti} */
		describe( " pixt(i) methods ", function() {
			let grid2D

			beforeEach( function(){
				grid2D = new CPM.Grid2D( [50,50] )
				// mock functions of the p2i and i2p implemented in the subclass.
				spyOn( grid2D, "p2i" ).and.returnValue( 0 )
				spyOn( grid2D, "i2p" ).and.returnValue( [0,0] )
			})

			/**
			 * @test {Grid#pixt}
			 * @test {Grid#pixti}
			 * */
			it( "pixt(i) can show types on the grid.", function(){
				// before change, types are always zero.
				expect( grid2D.pixti( 0 ) ).toEqual( 0 )
				// pixt uses internally the p2i method from the grid subclass
				// (but not i2p)
				expect( grid2D.p2i ).not.toHaveBeenCalled()
				expect( grid2D.pixt( [0,0] ) ).toEqual( 0 )
				expect( grid2D.p2i ).toHaveBeenCalledWith( [0,0] )
				expect( grid2D.i2p ).not.toHaveBeenCalled()
			})
		})

		/** @test {Grid#pixelsBuffer} */
		describe( " pixelsBuffer method ", function() {
			let grid2D, grid2Db

			beforeEach( function(){
				grid2D = new CPM.Grid2D( [50,50] )
				grid2Db = new CPM.Grid2D( [100,100], [false,false], "Float32" )
			})

			it( "should work on Float32 and Uint16 grids", function(){
				// before change, there is no buffer yet
				expect( grid2D._pixelsbuffer === undefined ).toBeTruthy()
				expect( grid2Db._pixelsbuffer === undefined ).toBeTruthy()

				// after calling the method, there is.
				expect( function(){ grid2D.pixelsBuffer() } ).not.toThrow()
				expect( function(){ grid2Db.pixelsBuffer() } ).not.toThrow()
				expect( grid2D._pixelsbuffer === undefined ).toBeFalse()
				expect( grid2Db._pixelsbuffer === undefined ).toBeFalse()

			})
		})

		/** @test {Grid#laplaciani} */
		describe( " laplacian(i) method ", function() {
			let grid2Db, grid2D

			beforeEach( function(){
				grid2D =  new CPM.Grid2D( [100,100] )
				grid2Db = new CPM.Grid2D( [100,100], [false,false], "Float32" )
			})

			it( "should work on Float32 grids", function(){
				expect( function(){ grid2Db.laplaciani(1) } ).not.toThrow()
				expect( function(){ grid2Db.laplacian([1,1] ) } ).not.toThrow()
			})

			it( "should throw error when you try to call it on Uint16 grid", function(){
				expect( function(){ grid2D.laplaciani(1) } ).toThrow()
				expect( function(){ grid2D.laplacian([1,1] ) } ).toThrow()
			})

			describe( " should compute laplacian correctly ", function() {

				// spy on the neighNeumanni method by returning always pixels 1,2,3,4
				beforeEach( function (){
					grid2Db.neighNeumanni = function * (){
						for( let i = 0; i < 4; i++ ){
							yield i+1
						}
					}
				})


				it( " case 1 : everything zero ", function(){
					// spy on pixti method to let it always return 0
					spyOn( grid2Db, "pixti" ).and.returnValue(0)

					// check that the spying works, this value of 1000 should not
					// be detected.
					grid2Db.setpixi( 1, 1000 )

					// check that laplacian returns zero
					expect( grid2Db.laplaciani(0) ).toEqual(0)

				})

				it( " case 2 : everything a positive value ", function(){

					spyOn( grid2Db, "pixti" ).and.callFake( function( i ){
						return i*1000
					})
					expect( grid2Db.laplaciani(0) ).toEqual( 10000 )

				})

				it( " case 3 : everything a negative value ", function(){
					spyOn( grid2Db, "pixti" ).and.callFake( function( i ){
						return -i*1000
					})
					expect( grid2Db.laplaciani(0) ).toEqual( -10000 )
				})

				it( " case 4 : floating point values ", function(){
					spyOn( grid2Db, "pixti" ).and.callFake( function( i ){
						return i*1000 + 0.1* Math.random()
					})
					expect( grid2Db.laplaciani(0) ).toBeCloseTo( 10000, 0 )
				})

				it( " case 4 : positive and negative floating point values ", function(){
					spyOn( grid2Db, "pixti" ).and.callFake( function( i ){
						if( i >= 1 && i <= 4 ){
							let num = 1000 + 0.1* Math.random()
							if( i % 2 === 0 ){ return -num }
							return num
						}
						return 0
					})
					expect( grid2Db.laplaciani(0) ).toBeCloseTo( 0, 0 )
				})

			})

		})

		/** @test {Grid#diffusion} */
		describe( " diffusion method ", function() {
			let grid2Db, grid2D

			beforeEach(function () {
				grid2D = new CPM.Grid2D([200, 200])
				grid2Db = new CPM.Grid2D([200, 200], [false, false], "Float32")
			})

			it("can be called on Float32 grids", function () {
				expect(function () {
					grid2Db.diffusion(0.01)
				}).not.toThrow()
			})

			it("should throw error when you try to call it on Uint16 grid", function () {
				let message = "Diffusion/laplacian methods do not work on a Uint16 grid! " +
					"Consider setting datatype='Float32'."
				expect(function () {
					grid2D.diffusion(1)
				}).toThrow(message)
			})

		})


	})

	describe( " [ class extension ] ", function (){
		let g

		class MyGrid extends CPM.Grid {
			myMethod(){
				return 1
			}
		}
		beforeEach( function(){
			g = new MyGrid( [50,50] )
		})

		it( "should be possible to extend with a method", function() {
			expect( g.myMethod() ).toEqual(1)
		})

		it( "should be possible to build a custom Grid subclass" , function(){
			//eslint-disable-next-line no-unused-vars
			expect( function(){ new MyGrid( [ 50,50 ] ) } ).not.toThrow()
			let g = new MyGrid( [ 50,50 ] )
			expect( g.extents[0] ).toEqual(50)
		})

		/** @test {Grid#_pixels} */
		it( "should throw an error when _pixelArray is not set in subclass", function(){
			expect( function(){ g._pixels }).toThrow()
		})

		/**
		 * @test {Grid#p2i}
		 * @test {Grid#i2p}
		 */
		it( "superclass should throw error when p2i/i2p not implemented", function() {
			expect( function(){ g.p2i( [0,0] ) }).toThrow()
			expect( function(){ g.i2p( 0 )}).toThrow()
		})

		/** @test{Grid#neighi}
		 * @test {Grid#neigh}
		 * */
		it( "should throw error when neighi not implemented", function(){
			expect( function(){g.neigh([0,0])}).toThrow()
			expect( function(){g.neighi(0)}).toThrow()

			// but it should work as soon as p2i, i2p, and neighi are defined.
			spyOn( g, "p2i" ).and.returnValue( 0 )
			spyOn( g, "i2p" ).and.returnValue( [0,0] )
			spyOn( g, "neighi" ).and.returnValue( [0] )
			expect( function(){ g.neigh([0,0])} ).not.toThrow()
		})

		/** @test{Grid#pixelsi}
		 * @test {Grid#pixels}
		 * */
		it( "should throw error when pixels/pixelsi not implemented", function(){

			let pixels = []
			expect( function(){ for( let p of g.pixels() ){ pixels.push(p) } }).toThrow()
			expect( function(){ for( let p of g.pixelsi() ){ pixels.push(p) } }).toThrow()
			expect( pixels.length ).toEqual(0)
		})

		/** @test{Grid#gradienti}
		 * @test {Grid#gradient}
		 * */
		it( "should throw error when gradienti not implemented", function(){
			expect( function(){g.gradient([0,0])}).toThrow()
			expect( function(){g.gradienti(0)}).toThrow()
		})

		/** @test{Grid#gradienti}
		 * @test {Grid#gradient}
		 * */
		it( "gradient should work as soon as gradienti and p2i are implemented.", function(){
			spyOn( g, "p2i" ).and.returnValue( 0 )
			spyOn( g, "gradienti" ).and.callFake( function(i){ return i + 10 } )
			expect( function(){ g.gradient([0,0] ) } ).not.toThrow()
			expect( g.gradient( [0,0] ) ).toEqual(10)
		})

		/** @test{Grid#neighNeumanni}
		 * @test {Grid#laplacian}
		 * @test {Grid#laplaciani}
		 * */
		it( "laplaciani should throw error when neighNeumanni not implemented", function(){
			let message = "Trying to call the method neighNeumanni, but you haven't " +
				"implemented this method in the Grid subclass you are using!"
			expect( function(){g.laplaciani(0)}).toThrow(message)
		})

		/** @test{Grid#neighNeumanni}
		 * @test {Grid#laplacian}
		 * @test {Grid#laplaciani}
		 * */
		it( "laplacian should throw error when p2i/neighNeumanni not implemented", function(){
			let message = "A p2i method should be implemented in every Grid subclass!"
			expect( function(){g.laplacian([0,0] ) } ).toThrow(message)

			// should still throw error when p2i is implemented but neighNeumanni is not
			spyOn( g, "p2i" ).and.returnValue(0)
			message = "Trying to call the method neighNeumanni, but you haven't " +
				"implemented this method in the Grid subclass you are using!"
			expect( function(){g.laplacian([0,0] ) } ).toThrow(message)
		})

		/** @test{Grid#neighNeumanni}
		 * @test {Grid#laplacian}
		 * @test {Grid#laplaciani}
		 * */
		it( "laplacian(i) should work if p2i, neighNeumanni, and _pixelArray exist", function(){

			spyOn( g, "p2i" ).and.returnValue(0)
			g.neighNeumanni = function* (){
				for( let k = 0; k < 4; k++ ){ yield k + 1 }
			}
			g._pixelArray = new Float32Array(1000)
			expect( function(){g.laplacian([0,0] ) } ).not.toThrow()

		})

	})


})