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 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:
- The number of pixels of that cell currently active, and
- 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 its 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 compute
method, 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 thelogStats()
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.