src/stats/CentroidsWithTorusCorrection.js


import Stat from "./Stat.js"
import PixelsByCell from "./PixelsByCell.js"

/**	This Stat computes the centroid of a cell when grid has a torus. 

	!!! Assumption: cell pixels never extend for more than half the size of the grid.
	If this assumption does not hold, centroids may be computed wrongly.
	
	See also {@link Centroids} for a version without torus correction.
		
	@example
	* let CPM = require( "path/to/build" )
	*
	* // Make a CPM, seed two cells, run a little, and get their centroids
	* let C = new CPM.CPM( [100,100], { 
	* 	T:20,
	* 	torus:[true,true],
	* 	J:[[0,20],[20,10]],
	* 	V:[0,200],
	* 	LAMBDA_V:[0,2]
	* } )
	* let gm = new CPM.GridManipulator( C )
	* gm.seedCell(1)
	* gm.seedCell(1)
	* for( let t = 0; t < 100; t++ ){ C.timeStep() }
	*
	* C.getStat( CPM.CentroidsWithTorusCorrection ) 
*/

class CentroidsWithTorusCorrection extends Stat {

	/** The set model method of class CentroidsWithTorusCorrection.
	@param {GridBasedModel} M - the model to compute centroids on. */
	set model( M ){
	
		/** The model to compute centroids on. 
		@type {GridBasedModel}*/
		this.M = M
		// Half the grid dimensions; if pixels with the same cellid are further apart,
		// we assume they are on the border of the grid and that we need to correct
		// their positions to compute the centroid.
		/** @ignore */
		this.halfsize = new Array( this.M.ndim).fill(0)
		for( let i = 0 ; i < this.M.ndim ; i ++ ){
			this.halfsize[i] = this.M.extents[i]/2
		}
	}
	
	/** @ignore */
	constructor( conf ){
		super(conf)
	}
		
	/** This method computes the centroid of a specific cell with id = <cellid>. 
	The cellpixels object is given as an argument so that it only has to be requested
	once for all cells together.
	@param {CellId} cellid ID number of the cell to get centroid of. 
	@param {CellArrayObject} cellpixels object produced by {@link PixelsByCell}, 
	where keys are the cellids
	of all non-background cells on the grid, and the corresponding value is an array
	of the pixels belonging to that cell specified by their {@link ArrayCoordinate}.
	@return {ArrayCoordinate} the centroid of the current cell.
	*/
	computeCentroidOfCell( cellid, cellpixels  ){
	
		//let cellpixels = this.M.getStat( PixelsByCell ) 
	
		const pixels = cellpixels[ cellid ]
		
		// cvec will contain the x, y, (z) coordinate of the centroid.
		// Loop over the dimensions to compute each element separately.
		let cvec = new Array(this.M.ndim).fill(0)
		for( let dim = 0 ; dim < this.M.ndim ; dim ++ ){
			
			let mi = 0.
			const hsi = this.halfsize[dim], si = this.M.extents[dim]
			
			// Loop over the pixels;
			// compute mean position per dimension with online algorithm
			for( let j = 0 ; j < pixels.length ; j ++ ){
				
				// Check distance of current pixel to the accumulated mean in this dim.
				// Check if this distance is greater than half the grid size in this
				// dimension; if so, this indicates that the cell has moved to the
				// other end of the grid because of the torus. Note that this only
				// holds AFTER the first pixel (so for j > 0), when we actually have
				// an idea of where the cell is.
				let dx = pixels[j][dim] - mi
				if( this.M.grid.torus[dim] && j > 0 ){
					// If distance is greater than half the grid size, correct the
					// coordinate.
					if( dx > hsi ){
						dx -= si
					} else if( dx < -hsi ){
						dx += si
					}
				}
				// Update the mean with the appropriate weight. 
				mi += dx/(j+1)
			}
			
			// Correct the final position so that it falls in the current grid.
			// (Because of the torus, it can happen to get a centroid at eg x = -1. )
			if( mi < 0 ){
				mi += si
			} else if( mi > si ){
				mi -= si
			}
			
			// Set the mean position in the cvec vector.
			cvec[dim] = mi
		}
		return cvec
		
	}
		
	/** This method computes the centroids of all cells on the grid. 
	@return {CellObject} with an {@link ArrayCoordinate} of the centroid for each cell
	 on the grid (see {@link computeCentroidOfCell}). */
	compute(){
		// Get object with arrays of pixels for each cell on the grid, and get
		// the array for the current cell.
		let cellpixels = this.M.getStat( PixelsByCell ) 
		
		// Create an object for the centroids. Add the centroid array for each cell.
		let centroids = {}
		for( let cid of this.M.cellIDs() ){
			centroids[cid] = this.computeCentroidOfCell( cid, cellpixels )
		}
		
		return centroids
		
	}
}

export default CentroidsWithTorusCorrection