UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

1,211 lines (1,078 loc) 77.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: cacatoo.js</title> <link rel="icon" type="image/png" href="images/favicon.png" /> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> <script src="scripts/jquery.js"></script> <script> $(function(){ $("#Navigator").load("navigator.html"); }); </script> </head> <body> <div id="main"> <h1 class="page-title">Source: cacatoo.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'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 &amp;&amp; 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); } /** * 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]&lt;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) { 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); } /** * 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 &lt; newstate.length; i++) if(newstate[i] &lt; 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&lt;this.N; this.mti++) { var s = this.mt[this.mti-1] ^ (this.mt[this.mti-1] >>> 30); this.mt[this.mti] = (((((s &amp; 0xffff0000) >>> 16) * 1812433253) &lt;&lt; 16) + (s &amp; 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 &amp; 0xffff0000) >>> 16) * 1664525) &lt;&lt; 16) + ((s &amp; 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 &amp; 0xffff0000) >>> 16) * 1566083941) &lt;&lt; 16) + (s &amp; 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&lt;this.N-this.M;kk++) { y = (this.mt[kk]&amp;this.UPPER_MASK)|(this.mt[kk+1]&amp;this.LOWER_MASK); this.mt[kk] = this.mt[kk+this.M] ^ (y >>> 1) ^ mag01[y &amp; 0x1]; } for (;kk&lt;this.N-1;kk++) { y = (this.mt[kk]&amp;this.UPPER_MASK)|(this.mt[kk+1]&amp;this.LOWER_MASK); this.mt[kk] = this.mt[kk+(this.M-this.N)] ^ (y >>> 1) ^ mag01[y &amp; 0x1]; } y = (this.mt[this.N-1]&amp;this.UPPER_MASK)|(this.mt[0]&amp;this.LOWER_MASK); this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y &amp; 0x1]; this.mti = 0; } y = this.mt[this.mti++]; /* Tempering */ y ^= (y >>> 11); y ^= (y &lt;&lt; 7) &amp; 0x9d2c5680; y ^= (y &lt;&lt; 15) &amp; 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.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$1(statedict); } else { let c = {}; for(const [key,val] of Object.entries(statedict)) { let hex = stringToRGB$1(val); c[key] = hex; } 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 &lt;= 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 = {}; for(let arr=0;arr&lt;n_arrays-1;arr++) { let arr1 = arguments[2+arr]; let arr2 = arguments[2+arr+1]; /// HIER BEN IK MEE BEZIG!!! for(let i=0;i&lt;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[i+arr*segment_len+total] = [ 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) * @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) { 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 } /** 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&lt;this.nc;i++) { for(let j=0;j&lt;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&lt;this.nc;i++) { for(let j=0;j&lt;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 &lt; 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 &lt; 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 &lt; 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'; } /** Count the number of grid points in the Moore (9) neighbourhood which have * a certain value (val) for a certain property. * @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 {int} val what value should the GP have to be counted * @param {string} property the property that is counted * * For example, if one wants to count all the "cheater" surrounding a gridpoint in cheater.js, * one needs to look for value '3' in the property 'species': * this.countMoore9(this,10,10,3,'species'); */ countMoore9(grid,col,row,val,property) { let count = 0; for(let v=-1;v&lt;2;v++) // Check +/-1 vertically { for(let h=-1;h&lt;2;h++) // Check +/-1 horizontally { let x = col+h; if(grid.wrap[0]) x = (col+h+grid.nc) % grid.nc; // Wraps neighbours left-to-right let y = row+v; if(grid.wrap[1]) y = (row+v+grid.nr) % grid.nr; // Wraps neighbours top-to-bottom if(x&lt;0||y&lt;0||x>=grid.nc||y>=grid.nr) continue let nval = grid.grid[x][y][property]; if(nval == val) count++; // Add value } } return count; } /** Count the number of grid points in the Moore (8) neighbourhood which have * a certain value (val) for a certain property. * @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 {int} val what value should the GP have to be counted * @param {string} property the property that is counted * * For example, if one wants to count all the "cheater" surrounding a gridpoint in cheater.js, * one needs to look for value '3' in the property 'species': * this.countMoore8(this,10,10,3,'species'); */ countMoore8(grid,col,row,val,property) { let count = this.countMoore9(grid,col,row,val,property); let minus_this = grid.grid[col][row][property] == val; if(minus_this) count--; return count } /** Return a random neighbour from the Moore (8) neighbourhood * @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 */ randomMoore8(grid,col,row) { let rand = this.rng.genrand_int(1,8); 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.randomMoore8(grid,col,row); return neigh } /** Return a random neighbour from the Moore (9) neighbourhood (self inclusive) * @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 */ randomMoore9(grid,col,row) { let rand = model.rng.genrand_int(0,8); let i = moore[rand][0]; let j = moore[rand][1]; let neigh = grid.getGridpoint(col+i,row+j); while(neigh == undefined) neigh = this.randomMoore8(grid,col,row); return neigh } /** 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&lt;0||y&lt;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&lt;0||y&lt;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&lt;0||y&lt;0||x>=this.nc||y>=this.nr) return undefined // If sampling neighbour outside of the grid, return empty object else return [x,y] } /** 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&lt;this.nc;i+=1) // every column { for(let j=0;j&lt;this.nr;j+=1) // every row { for(let o=0;o&lt;this.grid[i][j].ODEs.length;o++) // every ode { for(let s=0;s&lt;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&lt;=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; // sum_in += 0.1 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&lt;this.nc;i+=1) // every column for(let j=0;j&lt;this.nr;j+=1) // every row for(let o=0;o&lt;this.grid[i][j].ODEs.length;o++) for(let s=0;s&lt;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&lt;this.nc;i++) for(let j=0;j&lt;this.nr;j++) all_gridpoints.push(this.getGridpoint(i,j)); all_gridpoints = shuffle(all_gridpoints, this.rng); for(let i=0;i&lt;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 &amp; 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() { if(!this.wrap[0] || !this.wrap[1]) { console.log("Current implementation of Margolus diffusion requires wrapped boundaries."); throw new Error("Current implementation of Margolus diffusion requires wrapped boundaries.") } // // 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&lt;this.nc;i+=2) { for(let j=0+even;j&lt;this.nr;j+=2) { // 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() &lt; 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 &amp;&amp; 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+")") } /** * 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&lt; this.nc; i++) { for(let j=0;j&lt;this.nr;j++) { for(let val in values) if(this.grid[i][j][property] == values[val]) sum[val]++; } } 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&lt;this.nc; i++) { for(let j=0;j&lt;this.nr;j++) { let ode = new ODE(eq,conf.init_states,conf.parameters,conf.diffusion_rates,conf.ode_name); 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&lt;this.nc; i++) { for(let j=0;j&lt;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) { let ncol = this.nc; let nrow = this.nr; if(fract != undefined) ncol*=fract, nrow*=fract; let grid = new Array(nrow); // Makes a column or &lt;rows> long --> grid[cols] for(let i = 0; i&lt; ncol; i++) grid[i] = new Array(ncol); // Insert a row of &lt;cols> long --> grid[cols][rows] for(let j=0;j&lt;nrow;j++) grid[i][j] = this.grid[i][j][property]; console.table(grid); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // The functions below are not methods of grid-model as they are never unique for a particular model. //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Make a grid, or when a template is given, a COPY of a grid. * @param {int} cols Width of the new grid * @param {int} rows Height of the new grid * @param {2DArray} template Template to be used for copying (if not set, a new empty grid is made) */ function MakeGrid(cols,rows,template) { let grid = new Array(rows); // Makes a column or &lt;rows> long --> grid[cols] for(let i = 0; i&lt; cols; i++) { grid[i] = new Array(cols); // Insert a row of &lt;cols> long --> grid[cols][rows] for(let j=0;j&lt;rows;j++) { if(template) grid[i][j] = new Gridpoint(template[i][j]); // Make a deep or shallow copy of the GP else grid[i][j] = new Gridpoint(); } } return grid; } /** * Make a back-up of all the ODE states (for synchronous ODE updating) * @param {int} cols Width of the grid * @param {int} rows Height of the grid * @param {2DArray} template Get ODE states from here */ function CopyGridODEs(cols,rows,template) { let grid = new Array(rows); // Makes a column or &lt;rows> long --> grid[cols] for(let i = 0; i&lt; cols; i++) { grid[i] = new Array(cols); // Insert a row of &lt;cols> long --> grid[cols][rows] for(let j=0;j&lt;rows;j++) { for(let o=0;o&lt;template[i][j].ODEs.length;o++) // every ode { grid[i][j] = []; let states = []; for(let s=0;s&lt;template[i][j].ODEs[o].state.length;s++) // every state states.push(template[i][j].ODEs[o].state[s]); grid[i][j][o] = states; } } } return grid; } /** * Reverse dictionary * @param {Object} obj dictionary-style object to reverse in order */ function dict_reverse(obj) { let new_obj= {}; let rev_obj = Object.keys(obj).reverse(); rev_obj.forEach(function(i) { new_obj[i] = obj[i]; }); return new_obj; } /** * Randomly shuffle an array with custom RNG * @param {Array} array array to be shuffled * @param {MersenneTwister} rng MersenneTwister RNG */ function shuffle(array,rng) { let i = array.length; while (i--) { const ri = Math.floor(rng.random() * (i + 1)); [array[i], array[ri]] = [array[ri], array[i]]; } return array; } /** * Convert colour string to RGB. Works for colour names ('red','blue' or other colours defined in cacatoo), but also for hexadecimal strings * @param {String} string string to convert to RGB */ function stringToRGB$1(string) { if(string[0] != '#') return nameToRGB(string) else return hexToRGB(string) } /** * Convert hexadecimal to RGB * @param {String} hex string to convert to RGB */ function hexToRGB(hex) { var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return [parseInt(result[1],16),parseInt(result[2],16),parseInt(result[3],16)] } /** * Convert colour name to RGB * @param {String} name string to look up in the set of known colours (see below) */ function nameToRGB(string) { let colours = {'black': [0,0,0], 'white': [255,255,255], 'red': [255,0,0], 'blue': [0,0,255], 'green': [0,255,0], 'darkgrey': [40,40,40], 'lightgrey': [180,180,180], 'violet': [148, 0, 211], 'turquoise': [64, 224, 208], 'orange': [255, 165, 0], 'gold': [240,200,0], 'grey': [125,125,125], 'yellow': [255,255,0], 'cyan': [0,255,255], 'aqua': [0,255,255], 'silver': [192,192,192], 'nearwhite': [192,192,192], 'purple': [128,0,128],