src/simulation/Simulation.js

import CPM from "../models/CPM.js"
import CPMEvol from "../models/CPMEvol.js"
import Canvas from "../Canvas.js"
import GridManipulator from "../grid/GridManipulator.js"
import CentroidsWithTorusCorrection from "../stats/CentroidsWithTorusCorrection.js"
import Centroids from "../stats/Centroids.js"

/** 
This class provides some boilerplate code for creating simulations easily.
It comes with defaults for seeding cells, drawing, logging of statistics, saving output
images, and running the simulation. Each of these default methods can be overwritten
by the user while keeping the other default methods intact. See the {@link Simulation#constructor}
for details on how to configure a simulation.
@see ../examples
*/
class Simulation {
	/** The constructor of class Simulation takes two arguments.
		@param {object} config - overall configuration settings. This is an object
		with multiple entries, see below.
		@param {GridSize} config.field_size - size of the CPM to build.
	 	@param {Constraint[]} config.constraints - array of additional
	 		constraints to add to the CPM model.
		@param {object} config.conf - configuration settings for the CPM;
		see its {@link CPM#constructor} for details.
		@param {object} simsettings - configuration settings for the simulation 
		itself and for controlling the outputs. See the parameters below for details.
		@param {number[]} simsettings.NRCELLS - array with number of cells to seed for
			every non-background {@link CellKind}.
		@param {number} simsettings.BURNIN - number of MCS to run before the actual
			simulation starts (let cells get their target volume before starting).
		@param {number} simsettings.RUNTIME - number of MCS the simulation should run.
			Only necessary if you plan to use the {@link run} method.
		@param {number} [ simsettings.IMGFRAMERATE = 1 ]- draw the grid every [x] MCS.
		@param {number} [ simsettings.LOGRATE = 1 ] - log stats every [x] MCS.
		@param {object} [ simsettings.LOGSTATS = {browser:false,node:true} ] - 
			whether stats should be logged in browser and node.
		@param {boolean} [ simsettings.SAVEIMG = false ] - should images be saved? (node only).
		@param {string} [ simsettings.SAVEPATH ] - where should images be saved? You only have
			to give this argument when SAVEIMG = true. 
		@param {string} [ simsettings.EXPNAME = "myexp" ] - string used to construct the
			filename of any saved image. 
		@param {HexColor} [ simsettings.CANVASCOLOR = "FFFFFF" ] - color to draw the background in; defaults to white.
		@param {HexColor[]} [ simsettings.CELLCOLOR ] - color to draw each non-background 
			{@link CellKind} in. If left unspecified, the {@link Canvas} will use black.
		@param {boolean[]} [simsettings.ACTCOLOR ] - should activities of the {@link ActivityConstraint}
			be drawn for each {@link CellKind}? If left unspecified, these are not drawn.
		@param {boolean[]} [simsettings.SHOWBORDERS = false] - should borders of each {@link CellKind}
			be drawn? Defaults to false.
		@param {HexColor[]} [simsettings.BORDERCOL = "000000"] - color to draw cellborders of
			each {@link CellKind} in. Defaults to black. 
		*/
	constructor( config, custommethods ){

		/** To check from outside if an object is a Simulation; doing this with
		 * instanceof doesn't work in some cases. Any other object will
		 * not have this variable and return 'undefined', which in an
		 * if-statement equates to a 'false'.
		 * @type{boolean}*/
		this.isSimulation = true

		// ========= configuration and custom methods

		/** Custom methods added to / overwriting the default Simulation class.
		 * These are stored so that the ArtistooImport can check them.
		@type {object}*/
		this.custommethods = custommethods || {}
	
		// overwrite default method if methods are supplied in custommethods
		// these can be initializeGrid(), drawCanvas(), logStats(),
		// postMCSListener().
		for( let m of Object.keys( this.custommethods ) ){
		
			/** Any function suplied in the custommethods argument to
			the {@link constructor} is bound to the object. */
			this[m] = this.custommethods[m]
		}
		
		/** Configuration of the simulation environment 
		@type {object}*/
		this.conf = config.simsettings
		
		// ========= controlling outputs
		
		/** Draw the canvas every [rate] MCS.
		@type {number}*/
		this.imgrate = this.conf["IMGFRAMERATE"] || 1
		
		/** Log stats every [rate] MCS.
		@type {number}*/
		this.lograte = this.conf["LOGRATE"] || 1
		
		/** See if code is run in browser or via node, which will be used
			below to determine what the output should be.
			@type {string}*/
		this.mode = "node"
		if( typeof window !== "undefined" && typeof window.document !== "undefined" ){
			
			this.mode = "browser"
		} 
		
		/** Log stats or not.
		@type {boolean}*/
		this.logstats = false
		
		/** Log stats or not, specified for both browser and node mode.
		@type {object} */
		this.logstats2 = this.conf["STATSOUT"] || { browser: false, node: true }
		
		
		this.logstats = this.logstats2[this.mode]
		
		/** Saving images or not.
		@type {boolean}*/
		this.saveimg = this.conf["SAVEIMG"] || false
		/** Where to save images.
		@type {string}*/
		this.savepath = this.conf["SAVEPATH"] || "undefined"
		
		if( this.saveimg && this.savepath === "undefined" ){
			throw( "You need to specify the SAVEPATH option in the configuration object of your simulation!")
		}
		
		// ========= tracking simulation progress
		
		/** Track the time of the simulation. 
		@type {number}*/
		this.time = 0
		
		/** Should the simulation be running? Change this to pause;
		see the {@link toggleRunning} method.
		@private
		@type {boolean}*/
		this.running = true
		
		// ========= Attached objects
		
		/** Make CPM object based on configuration settings and attach it.
		@type {CPM} */
		if (((config || {}).conf || {})["CELLS"] !== undefined){
			this.C = new CPMEvol( config.field_size, config.conf )
		} else {
			this.C = new CPM( config.field_size, config.conf )
		}
				
		/** See if objects of class {@link Canvas} and {@link GridManipulator} already 
		exist. These are added automatically when required. This will set
		their values in helpClasses to 'true', so they don't have to be added again.
		@type {object}*/ 
		this.helpClasses = { gm: false, canvas: false }

		/** Add additional constraints.
		 * @type {Constraint[]}
		 * */
		this.constraints = config.constraints || []
		this.addConstraints()

		// ========= Begin.
		// Initialize the grid and run the burnin.
		this.initializeGrid()
		this.runBurnin()
		
	}

	/** Adds a {@link GridManipulator} object when required. */
	addGridManipulator(){
		/** Attached {@link GridManipulator} object.
		@type {GridManipulator}*/
		this.gm = new GridManipulator( this.C )
		this.helpClasses[ "gm" ] = true
	}
	
	/** Adds a {@link Canvas} object when required. */
	addCanvas(){
		//let zoom = this.conf.zoom || 2
		/** Attached {@link Canvas} object.
		@type {Canvas}*/
		this.Cim = new Canvas( this.C, this.conf )
		this.helpClasses[ "canvas" ] = true
	}

	/** Add additional constraints to the model before running; this
	 * method is automatically called and adds constraints given in
	 * the config object. */
	addConstraints(){
		for( let constraint of this.constraints ){
			this.C.add( constraint )
		}
	}
	
	/** Method to initialize the Grid should be implemented in each simulation. 
	The default method checks in the simsettings.NRCELLS array how many cells to
	seed for each {@CellKind}, and does this (at random positions). 
	
	Often you'll want to do other things here. In that case you can use the 
	custommethods argument of the {@link constructor} to overwrite this with your
	own initializeGrid method.
	*/
	initializeGrid(){
	
		// add the initializer if not already there
		if( !this.helpClasses["gm"] ){ this.addGridManipulator() }

		// reset C and clear cache (important if this method is called
		// again later in the sim).
		this.C.reset()

		let nrcells = this.conf["NRCELLS"], cellkind, i
		
		// Seed the right number of cells for each cellkind
		for( cellkind = 0; cellkind < nrcells.length; cellkind ++ ){
			
			for( i = 0; i < nrcells[cellkind]; i++ ){
				// first cell always at the midpoint. Any other cells
				// randomly.				
				
				this.gm.seedCell( cellkind+1 )
				
			}
		}


	}
	
	/** Run the brunin period as defined by simsettings.BURNIN : run this number
	of MCS before the {@link time} of this simulation object starts ticking, and 
	before we start drawing etc. 
	*/	
	runBurnin(){
		// Simulate the burnin phase
		let burnin = this.conf["BURNIN"] || 0
		for( let i = 0; i < burnin; i++ ){
			this.C.monteCarloStep()
		}
	}

	
	/** Method to draw the canvas.
	The default method draws the canvas, cells, cellborders, and activityvalues
	as specified in the simsettings object (see the {@link constructor} for details).
	
	This will be enough for most scenarios, but if you want to draw more complicated stuff,
	you can use the custommethods argument of the {@link constructor} to overwrite 
	this with your own drawCanvas method.
	*/
	drawCanvas(){
	
		// Add the canvas if required
		if( !this.helpClasses["canvas"] ){ this.addCanvas() }
	
		// Clear canvas and draw stroma border
		this.Cim.clear( this.conf["CANVASCOLOR"] || "FFFFFF" )


		// Call the drawBelow method for if it is defined. 
		this.drawBelow()

		// Draw each cellkind appropriately
		let cellcolor=( this.conf["CELLCOLOR"] || [] ), actcolor=this.conf["ACTCOLOR"], 
			nrcells=this.conf["NRCELLS"], cellkind, cellborders = this.conf["SHOWBORDERS"]
		for( cellkind = 0; cellkind < nrcells.length; cellkind ++ ){
		
			// draw the cells of each kind in the right color
			if( cellcolor[ cellkind ] !== -1 ){
				this.Cim.drawCells( cellkind+1, cellcolor[cellkind] )
			}
			
			// Draw borders if required
			if(  this.conf.hasOwnProperty("SHOWBORDERS") && cellborders[ cellkind  ] ){
				let bordercol = "000000"
				if( this.conf.hasOwnProperty("BORDERCOL") ){
					bordercol = this.conf["BORDERCOL"][cellkind] || "000000"
				}
				this.Cim.drawCellBorders( cellkind+1, bordercol )
			}
			
			// if there is an activity constraint, draw activity values depending on color.
			if( this.C.conf["LAMBDA_ACT"] !== undefined && this.C.conf["LAMBDA_ACT"][ cellkind + 1 ] > 0 ){ //this.constraints.hasOwnProperty( "ActivityConstraint" ) ){
				let colorAct
				if( typeof actcolor !== "undefined" ){
					colorAct = actcolor[ cellkind ] || false
				} else {
					colorAct = false
				}
				if( ( colorAct ) ){
					this.Cim.drawActivityValues( cellkind + 1 )//, this.constraints["ActivityConstraint"] )
				}			
			}

		}
		
		// Call the drawOnTop() method for if it is defined. 
		this.drawOnTop()
		
	}
	
	/** Methods drawBelow and {@link drawOnTop} allow you to draw extra stuff below and
	on top of the output from {@link drawCanvas}, respectively. You can use them if you
	wish to visualize additional properties but don't want to remove the standard visualization.
	They are called at the beginning and end of {@link drawCanvas}, so they do not work
	if you overwrite this method. 
	*/
	drawBelow(){
	
	}
	
	/** Methods drawBelow and {@link drawOnTop} allow you to draw extra stuff below and
	on top of the output from {@link drawCanvas}, respectively. You can use them if you
	wish to visualize additional properties but don't want to remove the standard visualization.
	They are called at the beginning and end of {@link drawCanvas}, so they do not work
	if you overwrite this method. 
	*/
	drawOnTop(){
	
	}
	
	
	/** Method to log statistics.
	The default method logs time, {@link CellId}, {@link CellKind}, and the 
	{@ArrayCoordinate} of the cell's centroid to the console.
	
	If you want to compute other stats (see subclasses of {@link Stat} for options)
	you can use the custommethods argument of the {@link constructor} to overwrite 
	this with your own drawCanvas method.
	*/
	logStats(){
		
		// compute centroids for all cells
		let allcentroids 
		let torus = false
		for( let d = 0; d < this.C.grid.ndim; d++ ){
			if( this.C.grid.torus[d] ){
				torus = true
			}
		}
		if( torus ){
			allcentroids = this.C.getStat( CentroidsWithTorusCorrection )
		} else {
			allcentroids = this.C.getStat( Centroids )
		} 
		
		for( let cid of this.C.cellIDs() ){
		
			let thecentroid = allcentroids[cid]
			
			// eslint-disable-next-line no-console
			console.log( this.time + "\t" + cid + "\t" + 
				this.C.cellKind(cid) + "\t" + thecentroid.join("\t") )
			
		}

	}
	
	/** Listener for something that needs to be done after every monte carlo step.
	This method is empty but can be overwritten via the custommethods 
	argument of the {@link constructor}.*/
	postMCSListener(){
	
	}
	
	/** This automatically creates all outputs (images and logged stats) at the correct
	rates. See the {@link constructor} documentation for options on how to control these
	outputs. */
	createOutputs(){
		// Draw the canvas every IMGFRAMERATE steps
		if( this.imgrate > 0 && this.time % this.imgrate == 0 ){
			
			if( this.mode == "browser" ){
				this.drawCanvas()
			}
			
			// Save the image if required and if we're in node (not possible in browser)
			if( this.mode == "node" && this.saveimg ){
				this.drawCanvas()
				let outpath = this.conf["SAVEPATH"], expname = this.conf["EXPNAME"] || "mysim"
				this.Cim.writePNG( outpath +"/" + expname + "-t"+this.time+".png" )
			}
		}
		
		// Log stats every LOGRATE steps
		if( this.logstats && this.time % this.lograte == 0 ){
			this.logStats()
		}
	}
	
	/** Run a montecarlostep, produce outputs if required, run any {@link postMCSListener},
	and update the time. */
	step(){
		if( this.running ){
			this.C.monteCarloStep()
			this.postMCSListener()
			this.createOutputs()
			this.time++
		}
	}
	
	/** Use this to pause or restart the simulation from an HTML page. */
	toggleRunning(){
		this.running = !this.running
	}
	
	/** Run the entire simulation. This function is meant for nodejs, as you'll
	want to perform individual {@link step}s in a requestAnimationFrame for an 
	animation in a HTML page. */
	run(){
		while( this.time < this.conf["RUNTIME"] ){
		
			this.step()
			
		}
	}
	
}


export default Simulation