Artistoo (Artificial Tissue Toolbox)

This manual is under construction. It currently contains some basic instructions on how to get started, but stay tuned for the most recent version! In the meantime, see the examples (with provided code) and the full method documentation to get an idea of what you can do with Artistoo.

Getting Started

Set up Artistoo in 3 easy steps

Step 1: download the repository.

Via the Github website

Visit the Github repository and click on the green "clone or download" button, then select "Download ZIP":

Save the zipped folder somewhere on your computer and unzip.

Via the command line

Alternatively, in the console, go to the folder where you want to save Artistoo, and clone the repository:

cd folder/to/save-into/
git clone https://github.com/ingewortel/artistoo.git

Step 2: install node dependencies.

Most users will run Artistoo in browser simulations only and will not need to extend the software with custom modules. If that's you, you can skip this step. But if you plan to use Artistoo from the command line using node, or if you plan to write your own modules (such as hamiltonian terms), you will need to install some node dependencies.

To do this, go to the artistoo/ folder from the command line and install the required packages automatically using npm:

cd folder/containing/artistoo
npm install

If you do not have nodejs and its package manager npm, see this page to install them first.

Step 3: link the build in your code.

You can now use Artistoo! See this tutorial to build your first simulation, or start from one of the simulations in the artistoo/examples/ folder. In these examples, you will see that the package can be loaded as follows:

<script src="path/to/artistoo/build/artistoo.js"></script>

for html, and

let CPM = require("path/to/artistoo/build/artistoo-cjs.js")

for node scripts.

When you include these lines in your own scripts, just make sure they contain the correct path from your simulation file to the artistoo/build/ folder.

Additional notes

If you wish to build simulations in a different directory than the artistoo folder, it may be convenient to create a symbolic link to the build folder there:

cd path/to/my/simulations
ln -s path/to/artistoo/build build

You can then access the code using:

<script src="path/to/my/simulations/build/artistoo.js"></script>

or

let CPM = require("path/to/my/simulations/build/artistoo-cjs.js")

Finally, if you want to run Artistoo simulations with nodejs from this folder, you will need to create a link to the installed node_modules from step 2:

cd path/to/my/simulations
ln -s path/to/artistoo/node_modules node_modules

Your First Simulation

This tutorial will show you how to build a simple simulation in the web browser or in a nodejs script. Choose either Set up a simulation in the web browser or Set up a simulation in nodejs to get the required template code, and then see Writing your simulation to start using Artistoo in the environment of your choice.

The simulation we will build is a simple CPM cell: your favourite web browser (as long as that favourite web browser is not Internet Explorer). The advantage of this method is that it allows you to visualize the simulation immediately, and that you can easily explore the effect of changing parameters in this manner. However, if you wish to run a simulation and store output to your computer, a simulation using nodejs may be more appropriate – see Set up a simulation in nodejs for details.

An HTML template page

Unfortunately, writing an HTML page requires quite some boilerplate code. You can mostly just copy-paste this for every simulation you build. For now, we will just copy-paste the following template so you can continue with building your simulation. If you are unfamiliar with HTML, you may want to check out this tutorial later -- it will guide you through the template step by step so you know which parts you may have to adapt.

<!-- Page setup and title -->
<!DOCTYPE html>
<html lang="en">
<head><meta http-equiv="Content-Type" content="text/html;
charset=UTF-8">
<title>PageTitle</title>
<style type="text/css"> 
body{
    font-family: "HelveticaNeue-Light", sans-serif; padding : 15px;
}
</style>

<!-- Sourcing the cpm build -->
<script src="../../build/artistoo.js"></script>
<script>
"use strict"

            // Simulation code here.


</script>
</head>
<body onload="initialize()">
<h1>Your Page Title</h1>
<p>
Description of your page.
</p>
</body>
</html>

Copy the above code into a file called MyFirstSimulation.html, which you can save in the artistoo/examples/html/ folder for now.

! Important: If you wish to save the file elsewhere, please read these instructions first, and ensure that you include the correct path to the cpm build in the part <script src="../../build/artistoo.js"></script>.

You can now proceed with adding your simulation to this file.

Set up a simulation in nodejs

Another way to use Artistoo – besides using HTML – is to use nodejs from the console. This method of running Artistoo allows you to print statistics to the console and store them in external files, as well as to save images of the simulation to create a movie later. To set up a more interactive version of your simulation with a live animation, an HTML version may be more appropriate – see Set up a simulation in the web browser

In contrast to a browser simulation, a node simulation requires almost no boilerplate code.

To set up your first node simulation, just create a file MyFirstSimulation.js in the folder artistoo/examples/node/ (or see these instructions to create it elsewhere). Then add the following line of code to the (still empty) script to source the package:

/* Source the CPM module (cpm-cjs version because this is a node script).*/
let CPM = require("../../build/artistoo-cjs.js")

Make sure that the path supplied to require() is the correct path from the location of MyFirstSimulation.js to artistoo/build/artistoo-cjs.js.

You can now proceed with adding your simulation.

Writing your simulation

We are now ready to add some simulation code. The following code goes either in between the <script></script> tags of your HTML page (see the comment // Simulation code here), or at the bottom of your node script.

The easiest way to build a simulation in Artistoo is to use the Simulation class. This class provides some default methods for running the simulation and producing outputs, so we won't have to worry about this yet.

Step 1 : Configure the CPM & Simulation

The first thing we need to do is supply a config object with all the required parameters and settings for the simulation. A configuration object for a simulation should look something like this:

let config = {

    ndim : 2,
    field_size : [50,50],
    conf : {

    },
    simsettings : {

    }
}

Here, ndim is the number of dimensions of the grid, field_size is the number of pixels in each dimension (in this case: 50 x 50 pixels), conf is the configuration object parsed to the CPM class, and simsettings contains configuration options used directly by the simulation class.

First, we configure the CPM by setting values in the conf object:

conf : {
        T : 20,                        // CPM temperature

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],            // VolumeConstraint importance per cellkind
        V : [0,500]                    // Target volume of each cellkind

    }

The T parameter is the CPM temperature, which determines how strongly the model "listens" to the energy constraints given in the CPM. We then add an adhesion and volume constraint by supplying their parameters. In this case, we will have only one type of cell and the background, so parameters are arrays of length 2 (or a 2 by 2 matrix for the adhesion parameters).

Finally, we need to supply some settings for the simulation class itself in simsettings:

simsettings : {
    NRCELLS : [1],
    RUNTIME : 500,
    CANVASCOLOR : "eaecef",
    zoom : 4
}

This ensures that one cell is seeded on the grid before the simulation, the simulation runs for 500 MCS (in node; in the browser it will just keep running), the background of the grid is colored gray, and the grid is drawn at 4x zoom.

The full config object becomes:

let config = {

    // Grid settings
    ndim : 2,
    field_size : [100,100],

    // CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

    // Simulation setup and configuration
    simsettings : {
        // Cells on the grid
        NRCELLS : [1],                    // Number of cells to seed for all
                                        // non-background cellkinds.

        RUNTIME : 500,                  // Only used in node

        CANVASCOLOR : "eaecef",
        zoom : 4                        // zoom in on canvas with this factor.
    }
}

Step 2: Create a simulation object

Once we have the configuration object, we can use it to construct a simulation.

In nodejs

In nodejs, simply construct the simulation as follows:

let sim = new CPM.Simulation( config )

In HTML

If you are writing an HTML page, you have to define an initialize() function - as this is the function that will be run when the page is loaded (see this section):

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

Step 3 : Tell the simulation to run

We are now almost ready; the only thing still missing is a command in the script that tells the simulation to start running. This works slightly differently in the browser- and nodejs versions.

In nodejs

In nodejs, getting the simulation to run is easy: just call the run() method of the simulation class after creating the simulation object. We get:

let config = {
    ...
}
let sim = new CPM.Simulation( config )
sim.run()

You are now ready to run your simulation. From your console, run the script with node:

node path/to/MyFirstSimulation.js

In HTML

In HTML, we create a function that runs a single step, and then make sure that this function is called from the initialize() function:

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

To see your simulation, open your file MyFirstSimulation.html in the web browser (any except Internet Explorer; but we recommend Chrome because it is fast).

An HTML Template

This tutorial will take you through the HTML simulation template step by step so you know which parts you may have to adapt when you are building your own simulations.

An HTML template

We will build the following template:

<!-- Page setup and title -->
<!DOCTYPE html>
<html lang="en">
<head><meta http-equiv="Content-Type" content="text/html;
charset=UTF-8">
<title>PageTitle</title>
<style type="text/css"> 
body{
    font-family: "HelveticaNeue-Light", sans-serif; padding : 15px;
}
</style>

<!-- Sourcing the cpm build -->
<script src="../../build/artistoo.js"></script>
<script>
"use strict"

            // Simulation code here.


</script>
</head>
<body onload="initialize()">
<h1>Your Page Title</h1>
<p>
Description of your page.
</p>
</body>
</html>

We will now go through this step by step.

Step 1 : Create a basic HTML page

A very simple HTML page looks like this:

<!DOCTYPE html>
<html>
<head> </head>
<body> </body>
</html>

The <html> tag shows where the page starts, and </html> shows where it ends. The page consists of a header, which starts at <head> and ends at </head>, and a body, starting at <body> and ending at </body>. (In general, anything you place in your HTML file starts with <something> and ends with </something>).

Step 2 : Configure the header

The header of the HTML page is the place that contains some meta-information about that page, and will also contain the simulation code.

First, we will expand the header code above (between the <head></head> tags):

<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>PageTitle</title>
</head>

The additional code in the first line just includes some document settings into the header that you will rarely need to change. The only thing you may want to change is the second line, where you set the title that will be displayed in the open tab in your web browser when you open the page.

Step 3 : Add JavaScript

We will now add some JavaScript code to the header part of the page (again, between the <head></head> tags):

<head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>PageTitle</title>
<script src="path/to/artistoo/build/artistoo.js"></script>
<script>
"use strict"
// Simulation code will go here:

</script>
</head>

The first script just loads the Artistoo package for HTML, which is stored in artistoo/build/artistoo.js. Please ensure that the path supplied here is the correct path from the folder where you stored MyFirstSimulation.html to the file artistoo/build/artistoo.js. If you have stored your simulation in artistoo/examples/html, you can use the path ../../build/artistoo.js

The second script is where your actual simulation code will go when you are Writing your simulation. For now, we'll leave it empty.

Step 4: Write the body

Finally, we make some changes to the body of the page (between the <body></body> tags):

<body onload="initialize()">
<h1>Your Page Title</h1>
<p>
Description of your page.
</p>
</body>

In the first line, we tell the HTML page to run the JavaScript function intitialize(), which we will define later in Writing your simulation (between the <script></script> tags of the page header we just set up).

The rest of the code just adds a title and a description to the web page. The simulation will then be placed below (as in the example shown at the top of this page).

Step 5 (optional): Add CSS

The code we have now added is sufficient to make the page work once we have added a simulation, but to make it look better we may want to add some CSS styling code to the header of the page. The header now becomes:


<head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>PageTitle</title>

<style type="text/css"> 
body{
font-family: "HelveticaNeue-Light", sans-serif; padding : 15px;
}
</style>

<script src="path/to/artistoo/build/artistoo.js"></script>
<script>
"use strict"
// Simulation code will go here:

</script>
</head>

To see the final result, have a look again at the complete template. You can now proceed with adding your simulation to this file.

Configuring Simulations (1)

Once you have written a fist simulation, you can adapt it by modifying parameters, or you can increase the number of cells in your simulation and examine interactions between different cell types.

The Simulation class allows you to make many of those changes by simply modifying the its configuration object, which will be the topic of this tutorial. The next tutorial will show you how you can make even more advanced simulations by writing custom functions - but for now we will focus on changes you can make through configuration only.

We will start from this very simple simulation from the previous tutorial:

This tutorial assumes you have completed the previous tutorial, and we will examine the config object of the simulation you generated there in more detail:

let config = {

    // Grid settings
    field_size : [100,100],

    // CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

    // Simulation setup and configuration
    simsettings : {
        // Cells on the grid
        NRCELLS : [1],                    // Number of cells to seed for all
                                        // non-background cellkinds.

        RUNTIME : 500,                  // Only used in node

        CANVASCOLOR : "eaecef",
        zoom : 4                        // zoom in on canvas with this factor.
    }
}

We will now see how changes to this object can alter the simulation.

Configuring the CPM itself

First, let's see how we can control the CPM model class itself. For this lesson, we recommend you start from the HTML version of the simulation in the previous tutorial. This HTML version will allow you to see the effect of your changes more quickly than running a node script would.

Changing CPM parameters

As we've seen before, the config object has three parts: the field_size, the conf, and the simsettings. We will now look at the conf part, which controls the CPM model itself.

// CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

The CPM shown here has only two constraints: the Adhesion and Volume constraints. The T parameter belongs to the CPM itself and controls to what extent the model "listens" to the energy rules it gets. The higher T, the more likely the model is to misbehave. But if T is too low, the cell might get stuck in a state it can't get out of.

Exercise :

Try some different values for T here, and see what happens with your simulation.

The other parameters here belong to the constraints. The J values form a matrix controlling the adhesion between cell and background. The $J_{00}$ value, which is here 0, is never used (the adhesion constraint only counts adhesion between pixels belonging to different cells, and since there is only one background "cell", there can be no background-background adhesion). The $J_{01}$ and $J_{10}$ values (here 20) are the adhesion between cell and background, and are important for this simulation. The $J_{11}$ would be the adhesion between two different cells and is also not relevant yet, since there is only one cell.

The V and LAMBDA_V parameters control the volume constraint, see its documentation.

Exercises :

  1. Change the values in J, and try to make the cell fall apart. Do you understand why this happens?
  2. How would you make the cell twice as big? Try it.

Finally, note that we can also add more different constraints to the model via the conf object. We'll do that in a later tutorial.

Setting a random seed

If you look at the simulation on the top of this page and then refresh a few times, you should see that every simulation "run" goes a little differently. This happens because the CPM is stochastic. If you don't want this, because you want someone else to be able to reproduce exactly your results, you can try setting a random seed:

// CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature
        seed : 1,

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

Exercise :

Set a random seed in your simulation's HTML file. Refresh the page a few times and look what happens. What is different now?

Configuring the grid

Field size

Open your file MyFirstSimulation.html from the previous tutorial in the web browser. You should see a black cell floating inside a square gray area, which is the field or "grid" the simulation runs in.

The size of this grid is controlled by field_size in the config object. The entry

// Grid settings
field_size : [100,100],

ensures that the simulation is performed on a 100 x 100 pixel (2D) grid. We can change the size of this grid by changing the numbers x and y between the square brackets: [x,y], where x controls the grid's width and y controls its height. For example, changing this setting to:

    // Grid settings
    field_size : [150,100], 

makes the grid slightly broader, but equally high as before.

Exercise :

How would you make a grid with a width of 100 pixels and a height of 200 pixels? Open your file MyFirstSimulation.html in your favourite text editor, change the field_size in the appropriate manner, and save the file. Now open MyFirstSimulation.html in your web browser (or just refresh the page if you had already done so). Did it work?

Grid boundaries

Have a look at the following simulation:

When the cell leaves the grid on the right, we see that it re-enters on the left. We call this a grid with periodic boundaries. Because grid boundaries are linked, the cell feels as though it is roaming an infinite space.

In the CPM object, the grid boundary conditions can be set through the torus property in the configuration object like this:

// CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature
        torus : [true,true],

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

When torus=true, this means that the grid boundaries are linked as in the example above (this is also the default when we don't supply the torus property in the configuration settings). If we don't want linked boundaries, we can change this value to false:

// CPM parameters and configuration
    conf : {
        T : 20,                                // CPM temperature
        torus : [false,false],

        // Adhesion parameters:
        J: [[0,20], [20,100]] ,

        // VolumeConstraint parameters
        LAMBDA_V : [0,50],                // VolumeConstraint importance per cellkind
        V : [0,500]                        // Target volume of each cellkind
    },

However, what we get is not quite what we intended:

The problem here is that we can't just say that there are boundaries without telling the CPM how to deal with those boundaries. A pixel at the border of the grid will have fewer neighboring pixels to contribute to its adhesion or surface energy, which will give artefacts. In reality, any physical boundary a cell might encounter would contribute to adhesion energy.

To solve such problems, it is possible to create a layer of boundary pixels at the edges of the grid, to which the adhesion can be explicitly defined. See this example for details and implementation (once you see the simulation, right click on the page (outside of the simulation grid) and select "view page source" to see code).

Configuring the Simulation object

The Simulation object automatically takes care of many aspects of the simulation, such as seeding cells on the grid, running the simulation for a number of steps, drawing the output images and computing output statistics. We'll discuss how to tune some of these aspects here.

Have a look at the documentation for the constructor of the simulation class here. All the options listed under simsettings here are options to configure the simulation, which we will discuss here.

Controlling the simulation

Using the option NRCELLS, we can specify how many cells of each CellKind we want to seed on the grid at the beginning of the simulation. Note that this needs to be an array with one element for each non-background cellkind on the grid. This means this array is one element shorter than most typical CPM parameters (e.g. LAMBDA_V) which are specified for each cellkind including background. If the lengths of these different arrays are not consistent with each other, the CPM will get confused about how many cellkinds you want on the grid. It will crash.

By default, the simulation class seeds them in random positions. We'll discuss how to seed cells in more customized locations in a later tutorial. But for now, try modifying the NRCELLS property of your simulation and see what happens.

The RUNTIME property determines for how many Monte Carlo Steps (MCS) the simulation will run. This property is actually only used by node simulations. You can use the RUNTIME_BROWSER property in the browser, but then you must use that in your step() function like this:

function step(){
    sim.step()
    meter.tick()
    if( sim.conf["RUNTIME_BROWSER"] == "Inf" | sim.time+1 < sim.conf["RUNTIME_BROWSER"] ){
        requestAnimationFrame( step )
    }
}

This way, the simulation will only keep running until RUNTIME_BROWSER is reached. If we wouldn't do this, the browser simulation would just keep running infinitely (or in practice, until your patience or your computer's battery runs out and you close the browser window).

Controlling the visualization

The Simulation class contains a default method drawCanvas() that takes care of drawing the grid for you. You can control the way this function draws the grid via the simsettings object as well, by setting:

  • CANVASCOLOR, the color code to draw the background in. If left unspecified, this is white ("FFFFFF"").
  • CELLCOLOR, the color code to draw the cells in. If left unspecified, this is black ("000000""). If you do wish to specify it, you must provide an array with a color for each CellKind just like we did for NRCELLS above. E.g. for one black and one red cell kind, that would be ["000000","FF0000"]
  • ACTCOLOR, whether or not to draw the activity gradient if the cells have an ActivityConstraint. If left unspecified, this is set to false for each CellKind.
  • SHOWBORDERS, whether or not to draw the borders of each cell as well as their area. This is false by default.
  • BORDERCOL, the color to draw the cell borders in, once more specified for each CellKind. This is only used whenever SHOWBORDERS is set to true.
  • zoomthe factor by which to enlarge the image (note that using a higher zoom factor may slow down your simulation, since it takes more time to draw the grid in a larger image. If a simulation is fast enough, this won't be an issue, but if you notice your simulations being too slow you might want to adjust this factor).

For all the options involving color, this online color picker may come in handy.

Exercise :

Given all the information we have covered so far, can you reproduce a simulation that looks roughly like the one below?

Controlling outputs

The simulation class helps you by generating outputs automatically. This happens inside the function createOutputs(), which does two things:

  1. Drawing things on the screen (browser version) or on an output PNG image (node version)
  2. Computing and logging statistics to the console (both versions)

In node, you can control the following aspects of the PNG images that are written:

  • SAVEIMG, are any images saved at all? Defaults to false;
  • IMGFRAMERATE, how often should an image be saved? Defaults to 1 (save an image every simulation step), but you might set it to e.g. 10 to save an image only once every 10 steps. This may significantly speed up your simulations since drawing images can be a speed bottleneck. Very often, drawing every step is not necessary to get a smooth movie in the end;
  • SAVEPATH, in which folder should images be stored? You have to make sure this folder exists, or you will get an error. Although this option is documented as "optional", this is only true because you don't have to give it when SAVEIMG=false. If you set SAVEIMG=true, you must provide this option.
  • EXPNAME, a name used to generate the name of the output images. These will be stored as [SAVEPATH]/[EXPNAME]-t[STEP].png. Defaults to "mysim".

Note that the options above only apply to node, since the browser version does not support writing PNG images. If you want to make a movie from the browser version, just record your screen. If you wish to change the IMGFRAMERATE of your browser simulation, you can adjust your step() function (see also this tutorial) to something like this:

function step(){
    // Run IMGFRAMERATE simulation steps before rendering
    for( let s = 0; s < config.simsettings.IMGFRAMERATE; s++ ){
        sim.step()
    }
    requestAnimationFrame( step )
}

Note that sim.step() will still call drawCanvas() every step, but the grid will only be rendered once every IMGFRAMERATE steps. Since rendering is often the bottleneck, this will still speed up your simulation most of the times.

The createOutputs() also takes care of logging statistics to the console. By default, it logs the simulation step, the cellID, the CellKind, and the centroid of every cell on the grid. To adjust this, you can overwrite this method, which we will discuss in a later tutorial.

Two settings in the simsettings object control the logging of statistics:

  • LOGSTATS, specifying for browser and node separately whether statistics should be logged to the console. Defaults to { browser: false, node: true };
  • LOGRATE, specifying how often statistics are computed and logged. Defaults to 1 (log every simulation step), but you might set it to e.g. 10 to log only once every 10 steps.

Adding CPM Constraints

One of the strengths of the CPM is that it can incorporate different processes of interest as separate "rules" given to the model. These rules can be mixed and matched to get a simulation of interest, making the CPM a very flexible framework. Artistoo implements a diverse set of model rules, called Constraints. This tutorial will walk you through how to use these constraints in your models.

Soft versus hard constraints

In Artistoo, we distinguish two types of constraints: "hard" and "soft" constraints.

Soft constraints

"Soft" constraints are the classical CPM energy rules. They are the terms of the model's Hamiltonian, or energy equation. When a copy attempt would break such a rule, this becomes visible as an energetic penalty making that copy attempt less likely. But a soft constraint is more a guideline than an actual rule; the model will mostly adhere to it, but not always.

For example, suppose we have a cell with a volume constraint, and that cell is already larger than its target volume. A copy attempt that would add another pixel to that cell would be associated with a $ΔH_\text{vol} \gt 0$. However, that does not mean that this copy attempt will always fail to go through. First, it may be that there are other constraints acting on the model for which this copy attempt is favourable, compensating for the volume penalty such that the global $ΔH$ is still negative. Second, even if the global $ΔH$ is positive, a copy attempt may still be accepted with a chance $e^{-ΔH T}$. Thus, soft constraints make some changes more likely to happen than others, but they can balance each other out and are not always listened to.

Most CPM constraints are soft constraints. But sometimes, there are things we would like to forbid completely. For that, we can use a hard constraint.

Hard constraints

Whereas soft constraints are more guidelines than rules, "hard" constraints are set in stone. Before the $ΔH$ of a copy attempt is even computed, Artistoo checks whether it violates one of the hard constraints. If it does, it won't go through - ever.

Not many constraints are hard constraints, and in a typical CPM, you should be careful using them. But examples are the HardVolumeRangeConstraint, the BarrierConstraint, and the hard ConnectivityConstraint. See also this example of a 'particle' following a random walk, where we constrained the volume such that the cell can only be one or two pixels big:

Adding a constraint to a model

There are two ways of adding a constraint to your model, which we will explain here.

Method 1: via the conf object

The first method is to add the relevant constraint parameters to the conf object. This is the easiest method to add a constraint, but it only works for a few of the most used constraints (see this page for a list of the constraints for which this works).

For example:

let myCPM = new CPM.CPM(
    [ 200, 200 ],
    {
        T : 20,
        J : [[0,20],[20,0]],
        V : [0,500],
        LAMBDA_V: [ 0, 50]
    }

)

will automatically create a CPM with an adhesion and a volume constraint inside it. If we open the browser console from a page with an Artistoo simulation in it, e.g. this one, we can check this. Go to the page, right click anywhere on the page outside of the grid, select "inspect" and then "console". Copy-paste the above code in the console and hit enter. If you now type:

myCPM.soft_constraints

and hit enter, you should see that there are two soft constraints in the model now: an Adhesion and a VolumeConstraint.

This works the same if we use the Simulation class, because the config.conf is passed along internally to the CPM constructor. So if we type:

let mySim = new CPM.Simulation(
    {
        field_size : [200,200],
        conf : {
            T : 20,
            J : [[0,20],[20,0]],
            V : [0,500],
            LAMBDA_V: [ 0, 50]
}, simsettings : { NRCELLS : [1] } } ) mySim.C.soft_constraints

we again see our two constraints appear.

Method 2: via the add() method

Note that, as mentioned, the first method does not work for all constraints. If we try to add an AttractionPointConstraint (another soft constraint) in this way:

let myCPM = new CPM.CPM(
    [ 200, 200 ],
    {
        T : 20,
        J : [[0,20],[20,0]],
        V : [0,500],
        LAMBDA_V: [ 0, 50],
        LAMBDA_ATTRACTIONPOINT : [0,100],
        ATTRACTIONPOINT: [100,100]
    }

)

then if we type:

myCPM.soft_constraints

we still only have our Adhesion and a VolumeConstraint. The AttractionPointConstrainthas not been added.

To add this constraint, we must use the add() method of the CPM class. If we have already put the relevant parameters in the conf object, this is easy:

//add constraint
myCPM.add( new CPM.AttractionPointConstraint( myCPM.conf ) )
myCPM.soft_constraints

you should now see an AttractionPointConstraint in the list of soft constraints. We've given the entire conf object of the CPM to the constraint's constructor, but it will just ignore the parameters that are not relevant to this constraint. However, we can also just give it it's own conf object. For example, we might add a second attraction point like this:

//add another constraint
myCPM.add( new CPM.AttractionPointConstraint( {
        LAMBDA_ATTRACTIONPOINT : [0,100],
        ATTRACTIONPOINT: [150,150]
} ) ) myCPM.soft_constraints

You should now see two attraction point constraints in your list.

A word of warning: when you add a new constraint, the model will use it. It won't check if another constraint of the same type was already present. For the example above, that's perfectly fine because we might want to have two different points of attraction. However, for other constraints such as the Adhesion, this might not be what you want (basically, this will be the same as having only one adhesion constraint where the parameters are twice as large. But someone checking your code may overlook your second adhesion constraint and think that the adhesion parameters are set lower than they actually are...).

Accessing a constraint in your model

Once you've put a constraint in your model, you may want to access it from outside. The method getConstraint() allows you to do that. For example:

//get volumeconstraint
myCPM.getConstraint( "VolumeConstraint" )

will return the VolumeConstraint that is currently in your model.

If there are multiple constraints of the same name in your model, this method automatically gives you the first one:

//get the first attraction point constraint
myCPM.getConstraint( "AttractionPointConstraint" )

This yields the first attraction point we added (from the browser console, you can click the little triangle on the left to see more about the object. If you look at its conf object, you see that this is the settings we gave to our first attraction point constraint).

If we want to access the second attraction point, we can type:

//get the second attraction point constraint
myCPM.getConstraint( "AttractionPointConstraint", 1 )

(Note here that JavaScript uses zero-based indexing, so an element with number 1 is actually the second element)

Implementation details

Constraints in Artistoo are implemented as separate classes, of which an instance can be added to a CPM model (See above for an explanation how). The base Constraint class underlies all of them, and has two subclasses: SoftConstraint and HardConstraint. These are never used by themselves, but can be extended to create a constraint (see also this tutorial to see how to develop custom modules).

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 initialization, 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 visualization of your grid;
  • drawBelow and drawOnTop, use this if you do want to use the default visualization 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 initialization

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

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 visualization

Your simulation might also contain custom visualization methods. For example, the following example uses custom visualization 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 visualizations. 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().

Sharing Your Simulations

This tutorial will describe how you can share your simulations in a zero-install setting. Note that this tutorial applies to the browser (HTML) simulations only, since command line (node) simulations do require installation of libraries and are therefore not zero-install.

Step 1: Create a self-contained directory

For sharing, we will make a directory that contains all required components. Anyone who has that folder and opens your simulation in that folder will be able to see your model in its full glory, as long as they don't mess with the directory structure or move/remove any files.

Assuming you have created an empty directory mySimulation, now add the following files to it:

  • MySimulation.html, the html file containing your simulation;
  • artistoo.js, the script with the Artistoo code (don't just make a link, but actually copy the code into this folder);

If you're using the fpsmeter, you also need to add the fpsmeter.min.js file from here (the same goes for any other scripts you are loading from your HTML file).

Step 2: Adjust relative paths

In your HTML file MySimulation.html, adjust any paths to scripts you are loading so that they refer to the local copy in your self-contained folder. To check that it all works, open the HTML file in your browser -- the simulation should run. If it doesn't, right click, select 'inspect' > 'console' to see any error messages that come up (if your simulation worked before you moved it, any error messages are probably due to a missing file or a path that needs updating).

Just to be sure, move the directory to some other place on your computer and check if the simulation still runs. If it does, you're good to go.

Step 3: Share

Once you have a working simulation in a self-contained directory, you can zip that directory and send it to someone else. They can unpack the folder on their own computer and double click the HTML file to run in their own browser. Alternatively, you can copy the folder to your own server and make your simulation available via a URL.

Custom Modules

This tutorial will show you how to extend the Artistoo code with your own custom modules. More detailed information will follow in a later version of this manual, but for now we will show some examples of how you can develop your own custom stats. You can use the same principle to develop your own CPM energy constraints, or even grids (tutorials will follow later).

A custom statistic

In this tutorial, we will implement a custom statistic as an example of a custom module. Suppose that in the ActModel example, we want to record the percentage of the cell's pixels that have a non-zero activity. Since no such statistic currently exists in Artistoo, we'll have to develop a custom module.

We'll build a simulation of a single migrating cell as in the following example:

We will extend this example with our custom statistic.

We start from the code as written in the example. You can find the full code here (once you follow the link the the simulation, right click and choose "view page source"), but focussing on the code between the <script></script> tags and leaving out the details in the config object, the simulation looks something like this:

let config = {
            ...
    }
}
//    ---------------------------------- 
let sim, meter


function initialize(){
    sim = new CPM.Simulation( config, {} )
    meter = new FPSMeter({left:"auto", right:"5px"})
    step()
}


function step(){
    sim.step()
    meter.tick()
    requestAnimationFrame( step )
}

Step 1: Create a custom extension of the Stat class

We can write a custom statistic by coding a new class. In doing so, we will extend the base Stat class. This base class takes care of things like attaching your stat to your model for you, so you don't need to worry about that.

To implement a new class in the code, we must add it to the code from step 1:

let config = {
            ...
    }
}
// --------------------------
let sim, meter


function initialize(){
    sim = new CPM.Simulation( config, {} )
    meter = new FPSMeter({left:"auto", right:"5px"})
    step()
}

class PercentageActive extends CPM.Stat {

}


function step(){
    sim.step()
    meter.tick()
    requestAnimationFrame( step )
}

This will create a new class for a statistic called PercentageActive, which will behave just like the other stats implemented in Artistoo. This means you can request its value by using sim.C.getStat( PercentageActive ) somewhere, but we'll get to that later. For now, let's focus on what code should go between the now empty braces.

Step 2: Implement a compute() method

Looking at the documentation for the Stat class, we see that the important method is the compute() method. In the base Stat class, this method throws an error, because it is something that should be implemented separately for each stat. Let's add it inside our new class:

class PercentageActive extends CPM.Stat {
    compute(){
        return 0
    }
}

Our code will no longer throw an error, but it is also not very useful yet. We can have a look at the code from an existing statistic to see how we can generate some useful output. Have a look at the compute() method from the Connectedness statistic (full code here):


    // The compute method of Connectedness creates an object with 
    // connectedness of each cell on the grid.
    // @return {CellObject} object with for each cell on the grid
    // a connectedness value. 
    compute(){
        // initialize the object
        let connectedness = { }
        // The this.M.pixels() iterator returns coordinates and cellid for all 
        // non-background pixels on the grid. See the appropriate Grid class for
        // its implementation.
        for( let ci of this.M.cellIDs() ){
            connectedness[ci] = this.connectednessOfCell( ci )
        }
        return connectedness
    }

Let's look at what is happening here. Most statistics we want to compute are actually properties of individual cells, but we may have more than one of those in our simulation. This means we must compute the statistic for each cell. Statistics therefore typically return an object, where each cell gets its own entry. The key of this entry is the cell's cellID, and the value is the computed statistic for that cell.

In the code above, this is also what happens. We first make the empty object {}, and then set an entry for each cellID on the grid in the loop. The actual values for each cell are computed by an additional method in the class called connectednessOfCell(), which takes the cellID as input argument.

Let's apply the same structure to our own statistic. We get:

class PercentageActive extends CPM.Stat {

    computePercentageOfCell( cid ){
        return 0
    }

    compute(){

        // Create an object for the output, then add stat for each cell in the loop.
        let percentages = {}
        for( let cid of this.M.cellIDs() ){
            percentages[cid] = this.computePercentageOfCell( cid )
        }

        return percentages

    }
}

Step 3: Implement a computeForCell() method

Now, how do we get the method computePercentageOfCell() to return the correct percentage of active pixels? For that, we need to know two things of each cell:

  1. The number of pixels of that cell currently active, and
  2. The total number of pixels of that cell.

If we had those, we could compute the percentage of active pixels like this:

computePercentageOfCell( cid ){

    // divide by total number of pixels and multiply with 100 to get percentage
    return ( 100 * activePixels / totalPixels )
}

To know how many pixels a cell has and how many of those are active, let's start by looking at which pixels actually are part of the cell. Luckily, there already is a statistic called PixelsByCell which does exactly that. Its return value is an object with a key for each cellID, and as a value an array with all pixels belonging to that cell (given as an ArrayCoordinate).

We first note that the length of this array equals the number of pixels belonging to that cell, which we can use directly inside the code for computePercentageOfCell():

computePercentageOfCell( cid ){

    // Get the pixels of each cell:
    // Note that we need the 'CPM.' prefix since we're accessing this from outside.
    const cellpixels = this.M.getStat( CPM.PixelsByCell )

    // Get the array of pixels for this cell
    const current_pixels = cellpixels[cid]

    // The length of this array tells us the number of pixels:
    const totalPixels = current_pixels.length

    // divide by total number of pixels and multiply with 100 to get percentage
    return ( 100 * activePixels / totalPixels )
}

Note here that this.M contains the current CPM model which we want to compute the statistic on. Models have a method getStat, which calls the compute method of the statistic given as an argument. Since we are accessing this from outside, we need the "CPM." prefix.

Now, we just need the number of active pixels. We should be able to find that somewhere in our ActivityConstraint, since it's used there to compute the Hamiltonian. We find that there is a method pxact that we can use, which takes the IndexCoordinate of a pixel as argument and then returns it's current activity value.

Let's put that information to use. We write:

computePercentageOfCell( cid ){


        // Get the pixels of each cell:
        // Note that we need the 'CPM.' prefix since we're accessing this from outside.
        const cellpixels = this.M.getStat( CPM.PixelsByCell )

        // Get the array of pixels for this cell
        const current_pixels = cellpixels[cid]

        // The length of this array tells us the number of pixels:
        const totalPixels = current_pixels.length

        // Loop over pixels of the current cell and count the active ones:
        let activePixels = 0
        for( let i = 0; i < current_pixels.length; i++ ){
            // PixelsByCell returns ArrayCoordinates, but we need to convert those
            // to IndexCoordinates to look up the activity using the pxact() method.
            const pos = this.M.grid.p2i( current_pixels[i] )

            // increase the counter if pxact() returns an activity > 0
            if( this.M.getConstraint( "ActivityConstraint" ).pxact( pos ) > 0 ){
                activePixels++
            }
        }

        // divide by total number of pixels and multiply with 100 to get percentage
        return ( 100 * activePixels / totalPixels )

    }

Step 4 (Optional): clean-up

Note: we now compute the PixelsByCell every time the computePercentageOfCell method is called, even though it is one single object with pixels for all cells already in there. Ideally, we'd therefore only call it once. We clean up the code to compute PixelsByCell at the beginning of the computemethod, and get the following complete code for the new class:

class PercentageActive extends CPM.Stat {

    computePercentageOfCell( cid, cellpixels ){

        // Get the array of pixels for this cell
        const current_pixels = cellpixels[cid]

        // The length of this array tells us the number of pixels:
        const totalPixels = current_pixels.length

        // Loop over pixels of the current cell and count the active ones:
        let activePixels = 0
        for( let i = 0; i < current_pixels.length; i++ ){
            // PixelsByCell returns ArrayCoordinates, but we need to convert those
            // to IndexCoordinates to look up the activity using the pxact() method.
            const pos = this.M.grid.p2i( current_pixels[i] )

            // increase the counter if pxact() returns an activity > 0
            if( this.M.getConstraint( "ActivityConstraint" ).pxact( pos ) > 0 ){
                activePixels++
            }
        }

        // divide by total number of pixels and multiply with 100 to get percentage
        return ( 100 * activePixels / totalPixels )

    }

    compute(){
        // Get object with arrays of pixels for each cell on the grid, and get
        // the array for the current cell.
        const cellpixels = this.M.getStat( CPM.PixelsByCell ) 

        // Create an object for the output, then add stat for each cell in the loop.
        let percentages = {}
        for( let cid of this.M.cellIDs() ){
            percentages[cid] = this.computePercentageOfCell( cid, cellpixels )
        }

        return percentages

    }
}

Note: In the current implementation of statistics in Artistoo this is not really necessary, since statistics are actually cached when they are computed. Even if the method is called many times, the stat is only computed once unless the grid changes. But let's do the cleanup step anyway since it is clearer from the code that way that the object will only be computed once.

We now have a custom statistic class that we can use. All that's left now is to make sure that this statistic is actually computed and reported somewhere in our simulation.

Step 5: Use statistic

To use our new statistic, we will overwrite the logStats() method of the Simulation class. Rather than reporting the centroids as is done by default, we now log the percentage active pixels as returned by our new stat:

function logStats() {
        // compute percentages for all cells
        const allpercentages = this.C.getStat( PercentageActive )

        for( let cid of this.C.cellIDs() ){

            let theperc = allpercentages[cid]

            console.log( this.time + "\t" + cid + "\t" + 
                this.C.cellKind(cid) + "\t" + theperc )

        }

}

The full code becomes (where we pass our new logStats method along when we construct our simulation):

let config = {
    field_size : [200,200],
        conf : {
        torus : [true,true],
seed : 1,
T : 10,
J: [[0,10], [10,0]], LAMBDA_V: [0,5],
V: [0,500], LAMBDA_P: [0,2],
P : [0,260],
LAMBDA_ACT : [0,300],
MAX_ACT : [0,30],
ACT_MEAN : "geometric"
}, simsettings : { NRCELLS : [1],
BURNIN : 500, RUNTIME : 1000, CANVASCOLOR : "eaecef", CELLCOLOR : ["000000"], ACTCOLOR : [true],
SHOWBORDERS : [false],
zoom :2, SAVEIMG : false, // Note that we set this to true for the browser to see the // effect of our new stat: STATSOUT : { browser: true, node: true }, LOGRATE : 10
} } let sim, meter function initialize(){ // our simulation gets the new logStats method to overwrite the old one: sim = new CPM.Simulation( config, { logStats: logStats } ) meter = new FPSMeter({left:"auto", right:"5px"}) step() } class PercentageActive extends CPM.Stat { computePercentageOfCell( cid, cellpixels ){ const current_pixels = cellpixels[cid] const totalPixels = current_pixels.length let activePixels = 0 for( let i = 0; i < current_pixels.length; i++ ){ const pos = this.M.grid.p2i( current_pixels[i] ) if( this.M.getConstraint( "ActivityConstraint" ).pxact( pos ) > 0 ){ activePixels++ } } return ( 100 * activePixels / totalPixels ) } compute(){ const cellpixels = this.M.getStat( CPM.PixelsByCell ) let percentages = {} for( let cid of this.M.cellIDs() ){ percentages[cid] = this.computePercentageOfCell( cid, cellpixels ) } return percentages } } function logStats() { const allpercentages = this.C.getStat( PercentageActive ) for( let cid of this.C.cellIDs() ){ let theperc = allpercentages[cid] console.log( this.time + "\t" + cid + "\t" + this.C.cellKind(cid) + "\t" + theperc ) } } function step(){ sim.step() meter.tick() requestAnimationFrame( step ) }

The end result is this (right click inside the simulation, choose 'inspect' and then 'console' to see the output):

A custom constraint

Just like you might want to develop a custom statistic, you might also want to develop your own model constraints. A detailed tutorial will follow, but for now, you can try applying the same principle as described above for the statistics. But instead of the Stat class, you will have to extend either the SoftConstraint or the HardConstraint class (see also this tutorial for more information). Have a look at their code here and here to see which methods your class extension will have to overwrite. Of course, you can have a look at the code for one of the existing constraints for inspiration as well. Once you have written your constraint inside your simulation file as explained for the custom statistics above, you can add it using the add() method as explained here.