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
anddrawOnTop
, use this if you do want to use the default visualisation methods fromdrawCanvas
, 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 thesimsettings
object inside theconfig
object contains a propertyNRCELLS
. 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 theinitializeGrid()
function of the Simulation class. If you overwrite this function and don't use theNRCELLS
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 theNRCELLS
inside yoursimsettings
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 theartistoo/examples/html/ManyCellsPrefDir.html
example in your favourite text editor, and try if you can do the same by overwritingdrawOnTop
rather thandrawCanvas
.
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()
.