Configuring Simulations (2)

In the previous tutorial on configuring simulations, we have discussed how you can customize some parts of the simulation directly by modifying the config object. However, you may wish to have more extensive customization options that are not possible through configuration only. The Simulation class therefore contains the option of overwriting some of its methods. This allows you to customize some of them without requiring you to implement everything from scratch.

In this tutorial, we will discuss how to use this option to customize different aspects of your simulation — including grid initialisation, drawing of the grid, computing statistics, or adding additional biological processes such as production and diffusion of soluble molecules, cell proliferation, or cell death.

Overwriting methods

To see how you can overwrite methods, consider the code from an earlier tutorial:

let config = {
    ...
}
let sim
function initialize(){
    sim = new CPM.Simulation( config )
    step()
}
function step(){
    sim.step()
    requestAnimationFrame( step )
}

(Here, note that the ... inside the config object should be replaced by its normal properties, but its contents are left out here for brevity. Have a look at this previous tutorial)

Now suppose that we want to overwrite the initializeGrid method. Normally, this method seeds the first cell at the midpoint of the grid, and then seeds any other cells at random locations. If we want more control over the location of cells on our grid, we can overwrite this method. We can do this by adapting the code as follows:

let config = {
    ...
}
let sim
function initialize(){
	let custommethods = {
	 	initializeGrid : initializeGrid
	 }
    sim = new CPM.Simulation( config, custommethods )
    step()
}
function step(){
    sim.step()
    requestAnimationFrame( step )
}
function initializeGrid(){
	// own initialization code here
}

There are two changes here. In the initialze() function, we now define an object custommethods which contains a key (before the colon) for the methods we want to overwrite, and a value (after the colon) specifying which function we want to overwrite them with. We then pass this object along when we initialize the simulation ( sim = new CPM.Simulation(...)). Since we have said here that we are going to replace the initializeGrid function of the Simulation class, we must also define its replacement somewhere in the code. That happens at the bottom of the above code fragment, where we define a new function initializeGrid. (Currently, this method is empty, so if you would run this code you would just see an empty grid. We'll discuss below what you might want to put inside this replacement function).

The Simulation class contains the following methods that you may wish to overwrite for your custom simulation:

  • initializeGrid, overwrite this if you want more control over where cells are placed, or if you want to add other objects/obstacles to the grid. See below for examples;
  • drawCanvas, overwrite this if you want more control over the visualisation of your grid;
  • drawBelow and drawOnTop, use this if you do want to use the default visualisation methods from drawCanvas, but also want to draw something else either below or on top;
  • logStats, use if you want to log something other than cell centroids to the console during your simulation;
  • postMCSListener, use to add other processes (cell death, proliferation, production/diffusion of chemokines, ...) to your simulation.

We will now discuss use cases for each of these methods below.

Custom initialisation

As mentioned in the example above, we can overwrite the initializeGrid() method to have more control over how the grid is initialized. For example, the following example uses this method to seed cells at regularly spaced positions instead of randomly:

This example overwrites the initializeGrid() method with the following function:

function initializeGrid(){

	// add the initializer if not already there
	if( !this.helpClasses["gm"] ){ this.addGridManipulator() }

	// Seed epidermal cell layer
	let step = 12
	
	for( let i = 1 ; i < this.C.extents[0] ; i += step ){
		for( let j = 1 ; j < this.C.extents[1] ; j += step ){
			this.gm.seedCellAt( 1, [i,j] )
		}
	}
}

Note that although we are writing this function in the HTML file, it will be called from inside the Simulation object. That means we have to refer to the simulation object as this rather than sim (e.g. this.gm.seedCellAt). If you attach a method to the Simulation object by adding it to the custommethods, use this. If you write a function inside your HTML file but don't attach it to the Simulation object, use sim (the variable name under which you stored your simulation).

For full code, have a look at this page (once you are on the page and it has loaded, right click anywhere outside the grid, then select 'view page source').

Another example is the following, where we overwrite the initializeGrid() to place microchannel walls:

The custom functions look like this:

function initializeGrid(){
	
		// add the initializer if not already there
		if( !this.helpClasses["gm"] ){ this.addGridManipulator() }
	
		let nrcells = this.conf["NRCELLS"], cellkind, i
		this.buildChannel()
		
		// 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.				
				if( i == 0 ){
					this.gm.seedCellAt( cellkind+1, this.C.midpoint )
				} else {
					this.gm.seedCell( cellkind+1 )
				}
			}
		}
}
	
function buildChannel(){
		
	
		this.channelvoxels = this.gm.makePlane( [], 1, 0 )
		let gridheight = this.C.extents[1]
		this.channelvoxels = this.gm.makePlane( this.channelvoxels, 1, gridheight-1 )
		
		this.C.add( new CPM.BorderConstraint({
			BARRIER_VOXELS : this.channelvoxels
		}) )
		
}

This is pretty much the default initializeGrid code, except that it now calls a new function this.buildChannel first. That means that also the buildChannel method has to be defined, and passed along to the simulation object when you define the custommethods inside the initialize() function:

let custommethods = {
	 	initializeGrid : initializeGrid,
	 	buildChannel : buildChannel
	 }
! Important: Note that the simsettings object inside the config object contains a property NRCELLS. This is an array that specifies how many cells of each type of cell have to be seeded on the grid. Normally, this is used by the initializeGrid() function of the Simulation class. If you overwrite this function and don't use the NRCELLS inside your replacement code, the values you enter there will no longer influence the number of cells you get on the grid. However, you must still have the NRCELLS inside your simsettings object!. This property is used by other methods to know how many cells are on the grid, and these methods will crash if you don't have this array. So while its values may not matter, you must still ensure that an array of the right length (one element per non-background kind of cell on the grid) is present.

Custom visualisation

Your simulation might also contain custom visualisation methods. For example, the following example uses custom visualisation to draw a line indicating in which direction each cell is going:

It does that by overwriting the drawCanvas() method:

function drawCanvas(){
	
	// This part is the normal drawing function
	
		// Add the canvas if required
		if( !this.helpClasses["canvas"] ){ this.addCanvas() }
	
		// Clear canvas and draw stroma border
		this.Cim.clear( this.conf["CANVASCOLOR"] )
		
		// 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(  cellborders[ cellkind  ]  ){
				this.Cim.drawCellBorders( cellkind+1, "000000" )
			}
		}
		
	// This part is for drawing the preferred directions 
	let pdc = this.C.getConstraint( "PersistenceConstraint" )
		let ctx = this.Cim.context(), zoom = this.conf["zoom"]
		let prefdir = ( pdc.conf["LAMBDA_DIR"][ cellkind+1 ] > 0  ) || false
		ctx.beginPath()
		ctx.lineWidth = 2*zoom

		for( let i of this.C.cellIDs() ){
		
			// Only draw for cells that have a preferred direction.
			//if( i == 0 ) continue
			prefdir = ( pdc.conf["LAMBDA_DIR"][ this.C.cellKind( i ) ] > 0  ) || false
			if( !prefdir ) continue
			
			ctx.moveTo( 
				pdc.cellcentroidlists[i][0][0]*zoom,
				pdc.cellcentroidlists[i][0][1]*zoom,
		       	)
			ctx.lineTo( (pdc.cellcentroidlists[i][0][0]+.1*pdc.celldirections[i][0])*zoom,
				(pdc.cellcentroidlists[i][0][1]+.1*pdc.celldirections[i][1])*zoom,
		       	)
		}
		ctx.stroke()		
}

Since only the second part of this method is actually new, we could also have just replaced the drawOnTop method (which is empty by default).

Exercise: Open the artistoo/examples/html/ManyCellsPrefDir.html example in your favourite text editor, and try if you can do the same by overwriting drawOnTop rather than drawCanvas.

Another example is the microchannel, where we use the drawBelow() method to draw the microchannel walls before drawing the other cells:

function drawBelow(){
	this.Cim.drawPixelSet( this.channelvoxels, "AAAAAA" ) 
}

That yields the following:

Have a look at the Canvas class for an overview of drawing methods you can use in your custom visualisations. This class uses the canvas framework, with a syntax you can also use to define your own methods (for example, we did that to draw the lines in the first example of this section; have a look at this tutorial).

Custom logging

Artistoo's default is to log every cell's ID, cell kind, and centroid (along with the system time in MCS). Sometimes, you may wish to log other statistics. Consider the following example simulation:

Suppose we just want to log the total number of cells on the grid every time. We can do this by overwriting the logStats() method:

function logStats(){
	
	// count the cell IDs currently on the grid:
	let nrcells = 0
	for( let i of this.C.cellIDs() ){
		nrcells++
	}
	
	console.log( this.time + "\t" + nrcells )
	
}

Adding biological processes

Finally, we may wish to do other stuff in between the CPM steps. For example, the cell division example above allows cells to proliferate between steps. For this, the Simulation class contains an (empty) function called the postMCSListener(), which can be overwritten. This function is called after every step. By default, it does nothing, but you can overwrite it with processes you wish to perform. For example, the cell division example uses it to let cells proliferate:

function postMCSListener(){
	
		// methods for cell seeding and division are in the GridManipulator class, which
		// is added to the simulation like this:
		if( !this.helpClasses["gm"] ){ this.addGridManipulator() }
	
		// Loop over all the cells and let them proliferate with some probability, 
		// but only if their volume is at least 95% of their target volume
		for( let i of this.C.cellIDs() ){
			if( this.C.getVolume(i) > this.C.conf.V[1]*0.95 && this.C.random() < 0.01 ){
				this.gm.divideCell(i)
			}
		}
}

Also have a look at the cancer invasion and chemotaxis examples to see what you can do with the postMCSListener().