cacatoo
Version:
Building, exploring, and sharing spatially structured models
1,085 lines (962 loc) • 91.5 kB
JavaScript
'use strict';
/**
* Gridpoint is what Gridmodels are made of. Contains everything that may happen in 1 locality.
*/
class Gridpoint {
/**
* The constructor function for a @Gridpoint object. Takes an optional template to copy primitives from. (NOTE!! Other types of objects are NOT deep copied by default)
* If you need synchronous updating with complex objects (for whatever reason), replate line 18 with line 19. This will slow things down quite a bit, so ony use this
* if you really need it. A better option is to use asynchronous updating so you won't have to worry about this at all :)
* @param {Gridpoint} template Optional template to make a new @Gridpoint from
*/
constructor(template) {
for (var prop in template)
this[prop] = template[prop]; // Shallow copy. It's fast, but be careful with syncronous updating!
// this[prop] = copy(template[prop]) // Deep copy. Takes much more time, and you'll likely end up copying much more than necessary. Use only if you're sure you need it!
}
}
/**
* Graph is a wrapper-class for a Dygraph element (see https://dygraphs.com/). It is attached to the DOM-windows, and stores all values to be plotted, colours, title, axis names, etc.
*/
class Graph {
/**
* The constructor function for a @Canvas object.
* @param {Array} labels array of strings containing the labels for datapoints (e.g. for the legend)
* @param {Array} values Array of floats to plot (here plotted over time)
* @param {Array} colours Array of colours to use for plotting
* @param {String} title Title of the plot
* @param {Object} opts dictionary-style list of opts to pass onto dygraphs
*/
constructor(labels, values, colours, title, opts) {
if (typeof window == undefined) throw "Using dygraphs with cashJS only works in browser-mode"
this.labels = labels;
this.data = [values];
this.title = title;
this.num_dps = this.labels.length; // number of data points for this graphs
this.elem = document.createElement("div");
this.elem.className = "graph-holder";
this.colours = [];
for (let v in colours) {
if (v == "Time") continue
else if (colours[v] == undefined) this.colours.push("#000000");
else if (colours[v][0] + colours[v][1] + colours[v][2] == 765) this.colours.push("#dddddd");
else this.colours.push(rgbToHex(colours[v][0], colours[v][1], colours[v][2]));
}
document.body.appendChild(this.elem);
document.getElementById("graph_holder").appendChild(this.elem);
this.g = new Dygraph(this.elem, this.data,
{
title: this.title,
showRoller: false,
ylabel: this.labels.length == 2 ? this.labels[1] : "",
width: 500,
height: 200,
xlabel: this.labels[0],
drawPoints: opts && opts.drawPoints || false,
pointSize: opts ? (opts.pointSize ? opts.pointSize : 0) : 0,
strokePattern: opts ? (opts.strokePattern ? opts.strokePattern : null) : null,
dateWindow: [0, 100],
axisLabelFontSize: 10,
valueRange: [0.000,],
strokeWidth: opts ? opts.strokeWidth : 3,
colors: this.colours,
labels: this.labels
});
}
/** Push data to your graph-element
* @param {array} array of floats to be added to the dygraph object (stored in 'data')
*/
push_data(data_array) {
this.data.push(data_array);
}
reset_plot() {
this.data = [this.data[0]];
this.g.updateOptions(
{
'file': [this.data]
});
}
/**
* Update the graph axes
*/
update() {
let max_x = 0;
let min_x = 999999999999;
for (let i of this.data) {
if (i[0] > max_x) max_x = i[0];
if (i[0] < min_x) min_x = i[0];
}
this.g.updateOptions(
{
'file': this.data,
dateWindow: [min_x, max_x]
});
}
}
/*
Functions below are to make sure dygraphs understands the colours used by Cacatoo (converts to hex)
*/
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
/**
* The ODE class is used to call the odex.js library and numerically solve ODEs
*/
class ODE {
/**
* The constructor function for a @ODE object.
* @param {function} eq Function that describes the ODE (see examples starting with ode)
* @param {Array} state_vector Initial state vector
* @param {Array} pars Array of parameters for the ODEs
* @param {Array} diff_rates Array of rates at which each state diffuses to neighbouring grid point (Has to be less than 0.25!)
* @param {String} ode_name Name of this ODE
*/
constructor(eq, state_vector, pars, diff_rates, ode_name, acceptable_error) {
this.name = ode_name;
this.eq = eq;
this.state = state_vector;
this.diff_rates = diff_rates;
this.pars = pars;
this.solver = new Solver(state_vector.length);
if (acceptable_error !== undefined) this.solver.absoluteTolerance = this.solver.relativeTolerance = acceptable_error;
}
/**
* Numerically solve the ODE
* @param {float} delta_t Step size
* @param {bool} opt_pos When enabled, negative values are set to 0 automatically
*/
solveTimestep(delta_t = 0.1, pos = false) {
let newstate = this.solver.solve(
this.eq(...this.pars), // function to solve and its pars (... unlists the array as a list of args)
0, // Initial x value
this.state, // Initial y value(s)
delta_t // Final x value
).y;
if (pos) for (var i = 0; i < newstate.length; i++) if (newstate[i] < 0.000001) newstate[i] = 0.0;
this.state = newstate;
}
/**
* Prints the current state to the console
*/
print_state() {
console.log(this.state);
}
}
/*
I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace
so it's better encapsulated. Now you can have multiple random number generators
and they won't stomp all over eachother's state.
If you want to use this as a substitute for Math.random(), use the random()
method like so:
var m = new MersenneTwister();
var randomNumber = m.random();
You can also call the other genrand_{foo}() methods on the instance.
If you want to use a specific seed in order to get a repeatable random
sequence, pass an integer into the constructor:
var m = new MersenneTwister(123);
and that will always produce the same random sequence.
Sean McCullough (banksean@gmail.com)
*/
/*
A C-program for MT19937, with initialization improved 2002/1/26.
Coded by Takuji Nishimura and Makoto Matsumoto.
Before using, initialize the state by using init_genrand(seed)
or init_by_array(init_key, key_length).
Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Any feedback is very welcome.
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)
*/
function MersenneTwister(seed) {
if (seed == undefined) {
seed = new Date().getTime();
}
/* Period parameters */
this.N = 624;
this.M = 397;
this.MATRIX_A = 0x9908b0df; /* constant vector a */
this.UPPER_MASK = 0x80000000; /* most significant w-r bits */
this.LOWER_MASK = 0x7fffffff; /* least significant r bits */
this.mt = new Array(this.N); /* the array for the state vector */
this.mti=this.N+1; /* mti==N+1 means mt[N] is not initialized */
this.init_genrand(seed);
}
/* initializes mt[N] with a seed */
MersenneTwister.prototype.init_genrand = function(s) {
this.mt[0] = s >>> 0;
for (this.mti=1; this.mti<this.N; this.mti++) {
var s = this.mt[this.mti-1] ^ (this.mt[this.mti-1] >>> 30);
this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)
+ this.mti;
/* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
/* In the previous versions, MSBs of the seed affect */
/* only MSBs of the array mt[]. */
/* 2002/01/09 modified by Makoto Matsumoto */
this.mt[this.mti] >>>= 0;
/* for >32 bit machines */
}
};
/* initialize by an array with array-length */
/* init_key is the array for initializing keys */
/* key_length is its length */
/* slight change for C++, 2004/2/26 */
MersenneTwister.prototype.init_by_array = function(init_key, key_length) {
var i, j, k;
this.init_genrand(19650218);
i=1; j=0;
k = (this.N>key_length ? this.N : key_length);
for (; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30);
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525)))
+ init_key[j] + j; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++; j++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
if (j>=key_length) j=0;
}
for (k=this.N-1; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30);
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941))
- i; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
}
this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
};
/* generates a random number on [0,0xffffffff]-interval */
MersenneTwister.prototype.genrand_int32 = function() {
var y;
var mag01 = new Array(0x0, this.MATRIX_A);
/* mag01[x] = x * MATRIX_A for x=0,1 */
if (this.mti >= this.N) { /* generate N words at one time */
var kk;
if (this.mti == this.N+1) /* if init_genrand() has not been called, */
this.init_genrand(5489); /* a default initial seed is used */
for (kk=0;kk<this.N-this.M;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
}
for (;kk<this.N-1;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+(this.M-this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
}
y = (this.mt[this.N-1]&this.UPPER_MASK)|(this.mt[0]&this.LOWER_MASK);
this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y & 0x1];
this.mti = 0;
}
y = this.mt[this.mti++];
/* Tempering */
y ^= (y >>> 11);
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= (y >>> 18);
return y >>> 0;
};
/* generates a random number on [0,0x7fffffff]-interval */
MersenneTwister.prototype.genrand_int31 = function() {
return (this.genrand_int32()>>>1);
};
/* generates a random number on [0,1]-real-interval */
MersenneTwister.prototype.genrand_real1 = function() {
return this.genrand_int32()*(1.0/4294967295.0);
/* divided by 2^32-1 */
};
/* generates a random int between [min,max] */
MersenneTwister.prototype.genrand_int = function(min,max) {
return min+Math.floor(this.genrand_real1()*(max));
};
/* generates a random number on [0,1)-real-interval */
MersenneTwister.prototype.random = function() {
return this.genrand_int32()*(1.0/4294967296.0);
/* divided by 2^32 */
};
/* generates a random number on (0,1)-real-interval */
MersenneTwister.prototype.genrand_real3 = function() {
return (this.genrand_int32() + 0.5)*(1.0/4294967296.0);
/* divided by 2^32 */
};
/* generates a random number on [0,1) with 53-bit resolution*/
MersenneTwister.prototype.genrand_res53 = function() {
var a=this.genrand_int32()>>>5, b=this.genrand_int32()>>>6;
return (a*67108864.0+b)*(1.0/9007199254740992.0);
};
/**
* Gridmodel is the main (currently only) type of model in Cacatoo. Most of these models
* will look and feel like CAs, but GridModels can also contain ODEs with diffusion, making
* them more like PDEs.
*/
class Gridmodel {
/**
* The constructor function for a @Gridmodel object. Takes the same config dictionary as used in @Simulation
* @param {string} name The name of your model. This is how it will be listed in @Simulation 's properties
* @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo GridModel.
* @param {MersenneTwister} rng A random number generator (MersenneTwister object)
*/
constructor(name, config, rng) {
this.name = name;
this.time = 0;
this.grid = MakeGrid(config.ncol, config.nrow); // Initialises an (empty) grid
this.nc = config.ncol || 200;
this.nr = config.nrow || 200;
this.wrap = config.wrap || [true, true];
this.rng = rng;
this.statecolours = this.setupColours(config.statecolours); // Makes sure the statecolours in the config dict are parsed (see below)
this.lims = {};
this.scale = config.scale || 1;
this.graph_update = config.graph_update || 20;
this.graph_interval = config.graph_interval || 2;
this.margolus_phase = 0;
// Store a simple array to get neighbours from the N, E, S, W, NW, NE, SW, SE (analogous to Cash2.1)
this.moore = [[0, 0], // SELF _____________
[0, -1], // NORTH | 5 | 1 | 6 |
[1, 0], // EAST | 4 | 0 | 2 |
[0, 1], // SOUTH | 7 | 3 | 8 |
[-1, 0], // WEST _____________
[-1, -1], // NW
[1, -1], // NE
[-1, 1], // SW
[1, 1] // SE
];
this.graphs = {}; // Object containing all graphs belonging to this model (HTML usage only)
this.canvases = {}; // Object containing all Canvases belonging to this model (HTML usage only)
}
/** Initiate a dictionary with colour arrays [R,G,B] used by Graph and Canvas classes
* @param {statecols} object - given object can be in two forms
* | either {state:colour} tuple (e.g. 'alive':'white', see gol.html)
* | or {state:object} where objects are {val:'colour},
* | e.g. {'species':{0:"black", 1:"#DDDDDD", 2:"red"}}, see cheater.html
*/
setupColours(statecols) {
let return_dict = {};
if (statecols == null) // If the user did not define statecols (yet)
return return_dict
let colours = dict_reverse(statecols) || { 'val': 1 };
for (const [statekey, statedict] of Object.entries(colours)) {
if (statedict == 'default') {
return_dict[statekey] = default_colours; // Defined below
}
else if (typeof statedict === 'string' || statedict instanceof String) // For if
{
return_dict[statekey] = stringToRGB(statedict);
}
else {
let c = {};
for (const [key, val] of Object.entries(statedict)) {
if (Array.isArray(val)) c[key] = val;
else c[key] = stringToRGB(val);
}
return_dict[statekey] = c;
}
}
return return_dict
}
/** Initiate a gradient of colours for a property.
* @param {string} property The name of the property to which the colour is assigned
* @param {int} n How many colours the gradient consists off
* For example usage, see colourViridis below
*/
colourGradient(property, n) {
let n_arrays = arguments.length - 2;
if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays")
let segment_len = n / (n_arrays-1);
let color_dict = this.statecolours[property];
let total = 0;
if (typeof color_dict != 'undefined')
total = Object.keys(this.statecolours[property]).length;
else
color_dict = {};
color_dict[0] = [0, 0, 0];
for (let arr = 0; arr < n_arrays -1 ; arr++) {
let arr1 = arguments[2 + arr];
let arr2 = arguments[2 + arr + 1];
for (let i = 0; i < segment_len; i++) {
let r, g, b;
if (arr2[0] > arr1[0]) r = Math.floor(arr1[0] + (arr2[0] - arr1[0]) * (i / (segment_len - 1)));
else r = Math.floor(arr1[0] - (arr1[0] - arr2[0]) * (i / (segment_len - 1)));
if (arr2[1] > arr1[1]) g = Math.floor(arr1[1] + (arr2[1] - arr1[1]) * (i / (segment_len - 1)));
else g = Math.floor(arr1[0] - (arr1[1] - arr2[1]) * (i / (segment_len - 1)));
if (arr2[2] > arr1[2]) b = Math.floor(arr1[2] + (arr2[2] - arr1[2]) * (i / (segment_len - 1)));
else b = Math.floor(arr1[2] - (arr1[2] - arr2[2]) * (i / (segment_len - 1)));
color_dict[Math.floor(i + arr * segment_len + total) + 1] = [r, g, b];
}
// total += segment_len
}
this.statecolours[property] = color_dict;
}
/** Initiate a gradient of colours for a property, using the Viridis colour scheme (purpleblue-ish to green to yellow) or Inferno (black to orange to yellow)
* @param {string} property The name of the property to which the colour is assigned
* @param {int} n How many colours the gradient consists off
* @param {bool} rev Reverse the viridis colour gradient
*/
colourViridis(property, n, rev = false, option="viridis") {
if(option=="viridis"){
if (!rev) this.colourGradient(property, n, [68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]); // Viridis
else this.colourGradient(property, n, [253, 231, 37], [93, 201, 99], [33, 144, 140], [59, 82, 139], [68, 1, 84]); // Viridis
}
else if(option=="inferno"){
if (!rev) this.colourGradient(property, n, [20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]); // Inferno
else this.colourGradient(property, n, [246, 215, 70], [229, 92, 45], [132, 32, 107], [20, 11, 52]); // Inferno
}
}
/** Print the entire grid to the console */
printgrid() {
console.table(this.grid);
}
/** The most important function in GridModel: how to determine the next state of a gridpoint?
* By default, nextState is empty. It should be defined by the user (see examples)
* @param {int} i Position of grid point to update (column)
* @param {int} j Position of grid point to update (row)
*/
nextState(i, j) {
throw 'Nextstate function of \'' + this.name + '\' undefined';
}
/** Synchronously apply the nextState function (defined by user) to the entire grid
* Synchronous means that all grid points will be updated simultaneously. This is ensured
* by making a back-up grid, which will serve as a reference to know the state in the previous
* time step. First all grid points are updated based on the back-up. Only then will the
* actual grid be changed.
*/
synchronous() // Do one step (synchronous) of this grid
{
let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Old state based on current grid
let newstate = MakeGrid(this.nc, this.nr); // New state == empty grid
for (let i = 0; i < this.nc; i++) {
for (let j = 0; j < this.nr; j++) {
this.nextState(i, j); // Update this.grid
newstate[i][j] = this.grid[i][j]; // Set this.grid to newstate
this.grid[i][j] = oldstate[i][j]; // Reset this.grid to old state
}
}
this.grid = newstate;
this.time++;
}
/** Like the synchronous function above, but can not take a custom user-defined function rather
* than the default next-state function. Technically one should be able to refarctor this by making
* the default function of synchronous "nextstate". But this works. :)
*/
apply_sync(func) {
let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Old state based on current grid
let newstate = MakeGrid(this.nc, this.nr); // New state == empty grid
for (let i = 0; i < this.nc; i++) {
for (let j = 0; j < this.nr; j++) {
func(i, j); // Update this.grid
newstate[i][j] = this.grid[i][j]; // Set this.grid to newstate
this.grid[i][j] = oldstate[i][j]; // Reset this.grid to old state
}
}
this.grid = newstate;
}
/** Asynchronously apply the nextState function (defined by user) to the entire grid
* Asynchronous means that all grid points will be updated in a random order. For this
* first the update_order will be determined (this.set_update_order). Afterwards, the nextState
* will be applied in that order. This means that some cells may update while all their neighours
* are still un-updated, and other cells will update while all their neighbours are already done.
*/
asynchronous() {
this.set_update_order();
for (let n = 0; n < this.nc * this.nr; n++) {
let m = this.upd_order[n];
let i = m % this.nc;
let j = Math.floor(m / this.nc);
this.nextState(i, j);
}
this.time++;
// Don't have to copy the grid here. Just cycle through i,j in random order and apply nextState :)
}
/** Analogous to apply_sync(func), but asynchronous */
apply_async(func) {
this.set_update_order();
for (let n = 0; n < this.nc * this.nr; n++) {
let m = this.upd_order[n];
let i = m % this.nc;
let j = Math.floor(m / this.nc);
func(i, j);
}
}
/** If called for the first time, make an update order (list of ints), otherwise just shuffle it. */
set_update_order() {
if (typeof this.upd_order === 'undefined') // "Static" variable, only create this array once and reuse it
{
this.upd_order = [];
for (let n = 0; n < this.nc * this.nr; n++) {
this.upd_order.push(n);
}
}
shuffle(this.upd_order, this.rng); // Shuffle the update order
}
/** The update is, like nextState, user-defined (hence, empty by default).
* It should contains all functions that one wants to apply every time step
* (e.g. grid manipulations and printing statistics)
* For example, and update function could look like:
* this.synchronous() // Update all cells
* this.MargolusDiffusion() // Apply Toffoli Margolus diffusion algorithm
* this.plotPopsizes('species',[1,2,3]) // Plot the population sizes
*/
update() {
throw 'Update function of \'' + this.name + '\' undefined';
}
/** Get the gridpoint at coordinates i,j
* Makes sure wrapping is applied if necessary
* @param {int} i position (column) for the focal gridpoint
* @param {int} j position (row) for the focal gridpoint
*/
getGridpoint(i, j) {
let x = i;
if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right
let y = j;
if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom
if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object
else return this.grid[x][y]
}
/** Change the gridpoint at position i,j into gp
* Makes sure wrapping is applied if necessary
* @param {int} i position (column) for the focal gridpoint
* @param {int} j position (row) for the focal gridpoint
* @param {Gridpoint} @Gridpoint object to set the gp to
*/
setGridpoint(i, j, gp) {
let x = i;
if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right
let y = j;
if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom
if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) this.grid[x][y] = undefined; // TODO!!!!!!!! Return border-state instead!
else this.grid[x][y] = gp;
}
/** Get the x,y coordinates of a neighbour in an array.
* Makes sure wrapping is applied if necessary
*/
getNeighXY(i, j) {
let x = i;
if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right
let y = j;
if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom
if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object
else return [x, y]
}
/** Get array of grid points with val in property (Neu4, Neu5, Moore8, Moore9 depending on range-array)
* @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
* but can be mixed to make grids interact.
* @param {int} col position (column) for the focal gridpoint
* @param {int} row position (row) for the focal gridpoint
* @param {string} property the property that is counted
* @param {int} val value 'property' should have
* @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
* @return {int} The number of grid points with "property" set to "val"
* Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
* If one wants to count all the "cheater" surrounding a gridpoint in cheater.js in the Moore8 neighbourhood
* one needs to look for value '3' in the property 'species':
* this.getNeighbours(this,10,10,3,'species',[1-8]);
* or
* this.getMoore8(this,10,10,3,'species')
*/
getNeighbours(model,col,row,property,val,range) {
let gps = [];
for (let n = range[0]; n <= range[1]; n++) {
let i = model.moore[n][0];
let j = model.moore[n][1];
let neigh = model.getGridpoint(col + i, row + j);
if (neigh != undefined && neigh[property] == val)
gps.push(neigh);
}
return gps;
}
/** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
getMoore8(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,8]) }
/** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
getMoore9(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,8]) }
/** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
getNeumann4(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,4]) }
/** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */
getNeumann5(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,4]) }
/** From a list of grid points, e.g. from getNeighbours(), sample one weighted by a property. This is analogous
* to spinning a "roulette wheel". Also see a hard-coded versino of this in the "cheater" example
* @param {Array} gps Array of gps to sample from (e.g. living individuals in neighbourhood)
* @param {string} property The property used to weigh gps (e.g. fitness)
* @param {float} non Scales the probability of not returning any gp.
*/
rouletteWheel(gps, property, non = 0.0) {
let sum_property = non;
for (let i = 0; i < gps.length; i++) sum_property += gps[i][property]; // Now we have the sum of weight + a constant (non)
let randomnr = this.rng.genrand_real1() * sum_property; // Sample a randomnr between 0 and sum_property
let cumsum = 0.0; // This will keep track of the cumulative sum of weights
for (let i = 0; i < gps.length; i++) {
cumsum += gps[i][property];
if (randomnr < cumsum) return gps[i]
}
return
}
/** Sum the properties of grid points in the neighbourhood (Neu4, Neu5, Moore8, Moore9 depending on range-array)
* @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
* but can be mixed to make grids interact.
* @param {int} col position (column) for the focal gridpoint
* @param {int} row position (row) for the focal gridpoint
* @param {string} property the property that is counted
* @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
* @return {int} The number of grid points with "property" set to "val"
* Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
* For example, if one wants to sum all the "fitness" surrounding a gridpoint in the Neumann neighbourhood, use
* this.sumNeighbours(this,10,10,'fitness',[1-4]);
* or
* this.sumNeumann4(this,10,10,'fitness')
*/
sumNeighbours(model, col, row, property, range) {
let count = 0;
for (let n = range[0]; n <= range[1]; n++) {
let i = model.moore[n][0];
let j = model.moore[n][1];
let gp = model.getGridpoint(col + i, row + j);
if(gp != undefined) count += model.getGridpoint(col + i, row + j)[property];
}
return count;
}
/** sumNeighbours for range 1-8 (see sumNeighbours) */
sumMoore8(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,8]) }
/** sumNeighbours for range 0-8 (see sumNeighbours) */
sumMoore9(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,8]) }
/** sumNeighbours for range 1-4 (see sumNeighbours) */
sumNeumann4(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,4]) }
/** sumNeighbours for range 0-4 (see sumNeighbours) */
sumNeumann5(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,4]) }
/** Count the number of neighbours with 'val' in 'property' (Neu4, Neu5, Moore8, Moore9 depending on range-array)
* @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
* but can be mixed to make grids interact.
* @param {int} col position (column) for the focal gridpoint
* @param {int} row position (row) for the focal gridpoint
* @param {string} property the property that is counted
* @param {int} val value property must have to be counted
* @param {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc)
* @return {int} The number of grid points with "property" set to "val"
* Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.)
* For example, if one wants to count all the "alive" individuals in the Moore 9 neighbourhood, use
* this.countNeighbours(this,10,10,1,'alive',[0-8]);
* or
* this.countMoore9(this,10,10,1,'alive');
*/
countNeighbours(model, col, row, property, val, range) {
let count = 0;
for (let n = range[0]; n <= range[1]; n++) {
let i = model.moore[n][0];
let j = model.moore[n][1];
if (model.getGridpoint(col + i, row + j)[property]==val) count++;
}
return count;
}
/** sumNeighbours for range 1-8 (see sumNeighbours) */
countMoore8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) }
/** sumNeighbours for range 0-8 (see sumNeighbours) */
countMoore9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) }
/** sumNeighbours for range 1-4 (see sumNeighbours) */
countNeumann4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) }
/** sumNeighbours for range 0-4 (see sumNeighbours) */
countNeumann5(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,4]) }
/** Return a random neighbour from the neighbourhood defined by range array
* @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this),
* but can be mixed to make grids interact.
* @param {int} col position (column) for the focal gridpoint
* @param {int} row position (row) for the focal gridpoint
* @param {Array} range from which to sample (1-8 is Moore8, 0-4 is Neu5, etc.)
*/
randomMoore(grid, col, row,range) {
let rand = this.rng.genrand_int(range[0], range[1]);
let i = this.moore[rand][0];
let j = this.moore[rand][1];
let neigh = grid.getGridpoint(col + i, row + j);
while (neigh == undefined) neigh = this.randomMoore(grid, col, row,range);
return neigh
}
/** sumNeighbours for range 1-8 (see sumNeighbours) */
randomMoore8(model, col, row) { return this.randomMoore(model, col, row, [1,8]) }
/** sumNeighbours for range 0-8 (see sumNeighbours) */
randomMoore9(model, col, row) { return this.randomMoore(model, col, row, [0,8]) }
/** sumNeighbours for range 1-4 (see sumNeighbours) */
randomNeu4(model, col, row) { return this.randomMoore(model, col, row, [1,4]) }
/** sumNeighbours for range 0-4 (see sumNeighbours) */
randomNeu5(model, col, row) { return this.randomMoore(model, col, row, [0,4]) }
/** Diffuse ODE states on the grid. Because ODEs are stored by reference inside gridpoint, the
* states of the ODEs have to be first stored (copied) into a 4D array (x,y,ODE,state-vector),
* which is then used to update the grid.
*/
diffuseODEstates() {
let newstates_2 = CopyGridODEs(this.nc, this.nr, this.grid); // Generates a 4D array of [i][j][o][s] (i-coord,j-coord,relevant ode,state-vector)
for (let i = 0; i < this.nc; i += 1) // every column
{
for (let j = 0; j < this.nr; j += 1) // every row
{
for (let o = 0; o < this.grid[i][j].ODEs.length; o++) // every ode
{
for (let s = 0; s < this.grid[i][j].ODEs[o].state.length; s++) // every state
{
let rate = this.grid[i][j].ODEs[o].diff_rates[s];
let sum_in = 0.0;
for (let n = 1; n <= 4; n++) // Every neighbour (neumann)
{
let moore = this.moore[n];
let xy = this.getNeighXY(i + moore[0], j + moore[1]);
if (typeof xy == "undefined") continue
let neigh = this.grid[xy[0]][xy[1]];
sum_in += neigh.ODEs[o].state[s] * rate;
newstates_2[xy[0]][xy[1]][o][s] -= neigh.ODEs[o].state[s] * rate;
}
newstates_2[i][j][o][s] += sum_in;
}
}
}
}
for (let i = 0; i < this.nc; i += 1) // every column
for (let j = 0; j < this.nr; j += 1) // every row
for (let o = 0; o < this.grid[i][j].ODEs.length; o++)
for (let s = 0; s < this.grid[i][j].ODEs[o].state.length; s++)
this.grid[i][j].ODEs[o].state[s] = newstates_2[i][j][o][s];
}
/** Assign each gridpoint a new random position on the grid. This simulated mixing,
* but does not guarantee a "well-mixed" system per se (interactions are still)
* calculated based on neighbourhoods.
*/
perfectMix() {
let all_gridpoints = [];
for (let i = 0; i < this.nc; i++)
for (let j = 0; j < this.nr; j++)
all_gridpoints.push(this.getGridpoint(i, j));
all_gridpoints = shuffle(all_gridpoints, this.rng);
for (let i = 0; i < all_gridpoints.length; i++)
this.setGridpoint(i % this.nc, Math.floor(i / this.nc), all_gridpoints[i]);
return "Perfectly mixed the grid"
}
/** Apply diffusion algorithm for grid-based models described in Toffoli & Margolus' book "Cellular automata machines"
* The idea is to subdivide the grid into 2x2 neighbourhoods, and rotate them (randomly CW or CCW). To avoid particles
* simply being stuck in their own 2x2 subspace, different 2x2 subspaces are taken each iteration (CW in even iterations,
* CCW in odd iterations)
*/
MargolusDiffusion() {
//
// A B
// D C
// a = backup of A
// rotate cw or ccw randomly
let even = this.margolus_phase % 2 == 0;
if ((this.nc % 2 + this.nr % 2) > 0) throw "Do not use margolusDiffusion with an uneven number of cols / rows!"
for (let i = 0 + even; i < this.nc; i += 2) {
if(i> this.nc-2) continue
for (let j = 0 + even; j < this.nr; j += 2) {
if(j> this.nr-2) continue
// console.log(i,j)
let old_A = new Gridpoint(this.grid[i][j]);
let A = this.getGridpoint(i, j);
let B = this.getGridpoint(i + 1, j);
let C = this.getGridpoint(i + 1, j + 1);
let D = this.getGridpoint(i, j + 1);
if (this.rng.random() < 0.5) // CW = clockwise rotation
{
A = D;
D = C;
C = B;
B = old_A;
}
else {
A = B; // CCW = counter clockwise rotation
B = C;
C = D;
D = old_A;
}
this.setGridpoint(i, j, A);
this.setGridpoint(i + 1, j, B);
this.setGridpoint(i + 1, j + 1, C);
this.setGridpoint(i, j + 1, D);
}
}
this.margolus_phase++;
}
/**
* Adds a dygraph-plot to your DOM (if the DOM is loaded)
* @param {Array} graph_labels Array of strings for the graph legend
* @param {Array} graph_values Array of floats to plot (here plotted over time)
* @param {Array} cols Array of colours to use for plotting
* @param {String} title Title of the plot
* @param {Object} opts dictionary-style list of opts to pass onto dygraphs
*/
plotArray(graph_labels, graph_values, cols, title, opts) {
if (typeof window == 'undefined') return
if (!(title in this.graphs)) {
cols = parseColours(cols);
graph_values.unshift(this.time);
graph_labels.unshift("Time");
this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
}
else {
if (this.time % this.graph_interval == 0) {
graph_values.unshift(this.time);
graph_labels.unshift("Time");
this.graphs[title].push_data(graph_values);
}
if (this.time % this.graph_update == 0) {
this.graphs[title].update();
}
}
}
/**
* Adds a dygraph-plot to your DOM (if the DOM is loaded)
* @param {Array} graph_labels Array of strings for the graph legend
* @param {Array} graph_values Array of 2 floats to plot (first value for x-axis, second value for y-axis)
* @param {Array} cols Array of colours to use for plotting
* @param {String} title Title of the plot
* @param {Object} opts dictionary-style list of opts to pass onto dygraphs
*/
plotXY(graph_labels, graph_values, cols, title, opts) {
if (typeof window == 'undefined') return
if (!(title in this.graphs)) {
cols = parseColours(cols);
this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts);
}
else {
if (this.time % this.graph_interval == 0) {
this.graphs[title].push_data(graph_values);
}
if (this.time % this.graph_update == 0) {
this.graphs[title].update();
}
}
}
/**
* Easy function to add a pop-sizes plot (wrapper for plotArrays)
* @param {String} property What property to plot (needs to exist in your model, e.g. "species" or "alive")
* @param {Array} values Which values are plotted (e.g. [1,3,4,6])
*/
plotPopsizes(property, values) {
if (typeof window == 'undefined') return
if (this.time % this.graph_interval != 0 && this.graphs[`Population sizes (${this.name})`] !== undefined) return
// Wrapper for plotXY function, which expects labels, values, colours, and a title for the plot:
// Labels
let graph_labels = [];
for (let val of values) { graph_labels.push(property + '_' + val); }
// Values
let popsizes = this.getPopsizes(property, values);
//popsizes.unshift(this.time)
let graph_values = popsizes;
// Colours
let colours = [];
for (let c of values) {
//console.log(this.statecolours[property][c])
if (this.statecolours[property].constructor != Object)
colours.push(this.statecolours[property]);
else
colours.push(this.statecolours[property][c]);
}
// Title
let title = "Population sizes (" + this.name + ")";
this.plotArray(graph_labels, graph_values, colours, title);
//this.graph = new Graph(graph_labels,graph_values,colours,"Population sizes ("+this.name+")")
}
/**
* Easy function to add a ODE states (wrapper for plot array)
* @param {String} ODE name Which ODE to plot the states for
* @param {Array} values Which states are plotted (if undefined, all of them are plotted)
*/
plotODEstates(odename, values, colours) {
if (typeof window == 'undefined') return
if (this.time % this.graph_interval != 0 && this.graphs[`Average ODE states (${this.name})`] !== undefined) return
// Labels
let graph_labels = [];
for (let val of values) { graph_labels.push(odename + '_' + val); }
// Values
let ode_states = this.getODEstates(odename, values);
// Title
let title = "Average ODE states (" + this.name + ")";
this.plotArray(graph_labels, ode_states, colours, title);
}
resetPlots() {
this.time = 0;
for (let g in this.graphs) {
this.graphs[g].reset_plot();
}
}
/**
* Returns an array with the population sizes of different types
* @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive")
* @param {Array} values Which values are counted and returned (e.g. [1,3,4,6])
*/
getPopsizes(property, values) {
let sum = Array(values.length).fill(0);
for (let i = 0; i < this.nc; i++) {
for (let j = 0; j < this.nr; j++) {
for (let val in values)
if (this.grid[i][j][property] == values[val]) sum[val]++;
}
}
return sum;
}
/**
* Returns an array with the population sizes of different types
* @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive")
* @param {Array} values Which values are counted and returned (e.g. [1,3,4,6])
*/
getODEstates(odename, values) {
let sum = Array(values.length).fill(0);
for (let i = 0; i < this.nc; i++)
for (let j = 0; j < this.nr; j++)
for (let val in values)
sum[val] += this.grid[i][j][odename].state[val] / (this.nc * this.nr);
return sum;
}
/**
* Attaches an ODE to all GPs in the model. Each gridpoint has it's own ODE.
* @param {function} eq Function that describes the ODEs, see examples starting with "ode"
* @param {Object} conf dictionary style configuration of your ODEs (initial state, parameters, etc.)
*/
attachODE(eq, conf) {
for (let i = 0; i < this.nc; i++) {
for (let j = 0; j < this.nr; j++) {
let ode = new ODE(eq, conf.init_states, conf.parameters, conf.diffusion_rates, conf.ode_name, conf.acceptable_error);
if (typeof this.grid[i][j].ODEs == "undefined") this.grid[i][j].ODEs = []; // If list doesnt exist yet
this.grid[i][j].ODEs.push(ode);
if (conf.ode_name) this.grid[i][j][conf.ode_name] = ode;
}
}
}
/**
* Numerically solve the ODEs for each grid point
* @param {float} delta_t Step size
* @param {bool} opt_pos When enabled, negative values are set to 0 automatically
*/
solveAllODEs(delta_t = 0.1, opt_pos = false) {
for (let i = 0; i < this.nc; i++) {
for (let j = 0; j < this.nr; j++) {
for (let ode of this.grid[i][j].ODEs) {
ode.solveTimestep(delta_t, opt_pos);
}
}
}
}
/**
* Print the entire grid to the console. Not always recommended, but useful for debugging :D
* @param {float} property What property is printed
* @param {float} fract Subset to be printed (from the top-left)
*/
printGrid(property, fract)