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 )
}

We will now make changes to this page to implement our custom statistic.

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.