UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

1,092 lines (966 loc) 241 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: cacatoo.js</title> <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"> </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! } } /** * 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 = values.length; // number of data points for this graphs this.elem = document.createElement("div"); this.elem.className = "graph-holder"; this.colours = []; for (let v of colours) { if (v == "Time") continue else if (v == undefined) this.colours.push("#000000"); else this.colours.push(rgbToHex$1(v[0], v[1], v[2])); } document.body.appendChild(this.elem); document.getElementById("graph_holder").appendChild(this.elem); let graph_opts = {title: this.title, showRoller: false, width: opts ? (opts.width != undefined ? opts.width : 500) : 500, labelsSeparateLines: true, height: opts ? (opts.height != undefined ? opts.height : 200) : 200, xlabel: this.labels[0], ylabel: this.labels.length == 2 ? this.labels[1] : "", drawPoints: opts ? (opts.drawPoints ? opts.drawPoints : false) : false, pointSize: opts ? (opts.pointSize ? opts.pointSize : 0) : 0, logscale: opts ? (opts.logscale ? opts.logscale : false) : false, strokePattern: opts ? (opts.strokePattern != undefined ? opts.strokePattern : null) : null, dateWindow: [0, 100], axisLabelFontSize: 10, valueRange: [opts ? (opts.min_y != undefined ? opts.min_y: 0):0, opts ? (opts.max_y != undefined ? opts.max_y: null):null], strokeWidth: opts ? (opts.strokeWidth != undefined ? opts.strokeWidth : 3) : 3, colors: this.colours, labels: labels.length == values.length ? this.labels: null, series: opts ? ( opts.series != undefined ? opts.series : null) : null }; for(var opt in opts){ graph_opts[opt] = opts[opt]; } this.g = new Dygraph(this.elem, this.data, graph_opts); } /** 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() { let first_dp = this.data[0]; this.data = []; let empty = Array(first_dp.length).fill(null); // null = "missing data" in Dygraph empty[0] = 0; // valid x so dateWindow doesn't invert this.data.push(empty); 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] == null) continue; if (i[0] > max_x) max_x = i[0]; if (i[0] &lt; min_x) min_x = i[0]; } if (min_x >= max_x) return; // ← ADD THIS: skip if no range yet (avoids [0,0] window) 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$1(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; } function rgbToHex$1(r, g, b) { return "#" + componentToHex$1(r) + componentToHex$1(g) + componentToHex$1(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 &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); } } /** * 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(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; if(hex.length == 7) { 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)] } if(hex.length == 9) { result = /^#?([a-f\d]{2})([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), parseInt(result[4], 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], 'darkgreen': [0, 128, 0], 'olive': [128, 128, 0], 'teal': [0, 128, 128], 'navy': [0, 0, 128] }; let c = colours[string]; if (c == undefined) throw new Error(`Cacatoo has no colour with name '${string}'`) return c } /** * Make sure all colours, even when of different types, are stored in the same format (RGB, as cacatoo uses internally) * @param {Array} cols array of strings, or [R,G,B]-arrays. Only strings are converted, other returned. */ function parseColours(cols) { let return_cols = []; for (let c of cols) { if (typeof c === 'string' || c instanceof String) { return_cols.push(stringToRGB(c)); } else { return_cols.push(c); } } return return_cols } /** * Compile a dict of default colours if nothing is given by the user. Reuses colours if more colours are needed. */ function default_colours(num_colours) { let colour_dict = [ [0, 0, 0], // black [255, 255, 255], // white [255, 0, 0], // red [0, 0, 255], // blue [0, 255, 0], //green [60, 60, 60], //darkgrey [180, 180, 180], //lightgrey [148, 0, 211], //violet [64, 224, 208], //turquoise [255, 165, 0], //orange [240, 200, 0], //gold [125, 125, 125], [255, 255, 0], // yellow [0, 255, 255], // cyan [192, 192, 192], // silver [0, 128, 0], //darkgreen [128, 128, 0], // olive [0, 128, 128], // teal [0, 0, 128]]; // navy let return_dict = {}; for(let i = 0; i &lt; num_colours; i++) { return_dict[i] = colour_dict[i%19]; } return return_dict } /** * A list of default colours if nothing is given by the user. */ function random_colours(num_colours,rng) { let return_dict = {}; return_dict[0] = [0,0,0]; for(let i = 1; i &lt; num_colours; i++) { return_dict[i] = [rng.genrand_int(0,255),rng.genrand_int(0,255),rng.genrand_int(0,255)]; } return return_dict } /** * Deep copy function. * @param {Object} aObject Object to be deep copied. This function still won't deep copy every possible object, so when enabling deep copying, make sure you put your debug-hat on! */ function copy(aObject) { if (!aObject) { return aObject; } let v; let bObject = Array.isArray(aObject) ? [] : {}; for (const k in aObject) { v = aObject[k]; bObject[k] = (typeof v === "object") ? copy(v) : v; } return bObject; } /** * Gridmodel is the main 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.nc = config.ncol || 200; this.nr = config.nrow || 200; this.grid = MakeGrid(this.nc, this.nr); // Initialises an (empty) grid this.grid_buffer = MakeGrid(this.nc, this.nr); // Initialises an (empty) grid this._old_grid = null; this._new_grid = null; this.wrap = config.wrap || [true, true]; this.rng = rng; this.random = () => { return this.rng.random()}; this.randomInt = (a,b) => { return this.rng.randomInt(a,b)}; this.statecolours = this.setupColours(config.statecolours,config.num_colours); // 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.bgcolour = config.bgcolour || 'black'; 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], // WEST | 2 | 0 | 3 | [1, 0], // EAST | 7 | 4 | 8 | [0, 1], // SOUTH _____________ [-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) } /** Replaces current grid with an empty grid */ clearGrid() { this.grid = MakeGrid(this.nc,this.nr); this._old_grid = null; this._new_grid = null; } /** * Saves the current grid in a JSON object. In browser mode, it will throw download-request, which may or may not * work depending on the security of the user's browser. * @param {string} filename The name of of the JSON file */ save_grid(filename) { console.log(`Saving grid in JSON file \'${filename}\'`); let gridjson = JSON.stringify(this.grid); if((typeof document !== "undefined")){ const a = document.createElement('a'); a.href = URL.createObjectURL( new Blob([gridjson], { type:'text/plain' }) ); a.download = filename; a.click(); console.warn("Cacatoo: download of grid in browser-mode may be blocked for security reasons."); return } else { try { var fs = require('fs'); } catch (e) { console.log('Cacatoo:save_grid: save_grid requires file-system module. Please install fs via \'npm install fs\''); } fs.writeFileSync(filename, gridjson, function(err) { if (err) { console.log(err); } }); } } /** * Reads a JSON file and loads a JSON object onto this gridmodel. Reading a local JSON file will not work in browser mode because of security reasons, * You can instead use 'addCheckpointButton' instead, which allows you to select a file from the browser manually. * @param {string} file Path to the json file */ load_grid(file) { if((typeof document !== "undefined")){ console.warn("Cacatoo: loading grids directly is not supported in browser-mode for security reasons. Use 'addCheckpointButton' instead. "); return } this.clearGrid(); console.log(`Loading grid for ${this.name} from file \'${file}\'`); try { var fs = require('fs'); } catch (e) { console.log('Cacatoo:load_grid: requires file-system module. Please install fs via \'npm install fs\''); } let filehandler = fs.readFileSync(file); let gridjson = JSON.parse(filehandler); this.grid_from_json(gridjson); } /** * Loads a JSON object onto this gridmodel. * @param {string} gridjson JSON object to build new grid from */ grid_from_json(gridjson) { for(let x in gridjson) for(let y in gridjson[x]) { let newgp = new Gridpoint(gridjson[x][y]); gridjson[x][y] = newgp; } this.grid = gridjson; } /** Print the entire grid to the console */ print_grid() { console.table(this.grid); } /** 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,num_colours=18) { let return_dict = {}; if (statecols == null) // If the user did not define statecols (yet) return return_dict["state"] = default_colours(num_colours) let colours = dict_reverse(statecols) || { 'val': 1 }; for (const [statekey, statedict] of Object.entries(colours)) { if (statedict == 'default') { return_dict[statekey] = default_colours(num_colours+1); } else if (statedict == 'random') { return_dict[statekey] = random_colours(num_colours+1,this.rng); } else if (statedict == 'viridis') { let colours = this.colourGradientArray(num_colours, 0,[68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]); return_dict[statekey] = colours; } else if (statedict == 'inferno') { let colours = this.colourGradientArray(num_colours, 0,[20, 11, 52], [132, 32, 107], [229, 92, 45], [246, 215, 70]); return_dict[statekey] = colours; } else if (statedict == 'rainbow') { let colours = this.colourGradientArray(num_colours, 0,[251, 169, 73], [250, 228, 66], [139, 212, 72], [42, 168, 242], [50,100,255]); return_dict[statekey] = colours; } else if (statedict == 'pride') { let colours = this.colourGradientArray(num_colours, 0,[228, 3, 3], [255, 140, 0], [255, 237, 0], [0, 128, 38], [0,76,255],[115,41,130]); return_dict[statekey] = colours; } else if (statedict == 'inferno_rev') { let colours = this.colourGradientArray(num_colours, 0, [246, 215, 70], [229, 92, 45], [132, 32, 107]); return_dict[statekey] = colours; } 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 (return array only) * @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 */ colourGradientArray(n,total) { let color_dict = {}; //color_dict[0] = [0, 0, 0] let n_arrays = arguments.length - 2; if (n_arrays &lt;= 1) throw new Error("colourGradient needs at least 2 arrays") let segment_len = Math.ceil(n / (n_arrays-1)); if(n &lt;= 10 &amp;&amp; n_arrays > 3) console.warn("Cacatoo warning: forming a complex gradient with only few colours... hoping for the best."); let total_added_colours = 0; for (let arr = 0; arr &lt; n_arrays - 1 ; arr++) { let arr1 = arguments[2 + arr]; let arr2 = arguments[2 + arr + 1]; 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[1] - (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)] = [Math.min(r,255), Math.min(g,255), Math.min(b,255)]; total_added_colours++; if(total_added_colours == n) break } color_dict[n] = arguments[arguments.length-1]; } return(color_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 offset = 2; let n_arrays = arguments.length - offset; if (n_arrays &lt;= 1) throw new Error("colourGradient needs at least 2 arrays") let color_dict = {}; let total = 0; if(this.statecolours !== undefined &amp;&amp; this.statecolours[property] !== undefined){ color_dict = this.statecolours[property]; total = Object.keys(this.statecolours[property]).length; } let all_arrays = []; for (let arr = 0; arr &lt; n_arrays ; arr++) all_arrays.push(arguments[offset + arr]); let new_dict = this.colourGradientArray(n,total,...all_arrays); this.statecolours[property] = {...color_dict,...new_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 } } /** 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} x Position of grid point to update (column) * @param {int} y Position of grid point to update (row) */ nextState(x, y) { 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() { let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Create a copy of the current grid let newstate = MakeGrid(this.nc, this.nr); // Create an empty grid for the next state for (let x = 0; x &lt; this.nc; x++) { for (let y = 0; y &lt; this.nr; y++) { this.nextState(x, y); // Update this.grid[x][y] newstate[x][y] = this.grid[x][y]; // Store new state in newstate this.grid[x][y] = oldstate[x][y]; // Restore original state } } this.grid = newstate; // Replace the current grid with the newly computed one } /** 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 x = 0; x &lt; this.nc; x++) { for (let y = 0; y &lt; this.nr; y++) { func(x, y); // Update this.grid newstate[x][y] = this.grid[x][y]; // Set this.grid to newstate this.grid[x][y] = oldstate[x][y]; // 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 x = m % this.nc; let y = Math.floor(m / this.nc); this.nextState(x, y); } // Don't have to copy the grid here. Just cycle through x,y 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 x = m % this.nc; let y = Math.floor(m / this.nc); func(x, y); } } /** 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'; } /** Get the gridpoint at coordinates x,y * Makes sure wrapping is applied if necessary * @param {int} xpos position (column) for the focal gridpoint * @param {int} ypos position (row) for the focal gridpoint */ getGridpoint(xpos, ypos) { let x = xpos; if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right let y = ypos; if (this.wrap[1]) y = (ypos + 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 x,y into gp (typically retrieved with 'getGridpoint') * Makes sure wrapping is applied if necessary * @param {int} x position (column) for the focal gridpoint * @param {int} y position (row) for the focal gridpoint * @param {Gridpoint} @Gridpoint object to set the gp to (result of 'getGridpoint') */ setGridpoint(xpos, ypos, gp) { let x = xpos; if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right let y = ypos; if (this.wrap[1]) y = (ypos + 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; else this.grid[x][y] = gp; } /** Return a copy of the gridpoint at position x,y * Makes sure wrapping is applied if necessary * @param {int} x position (column) for the focal gridpoint * @param {int} y position (row) for the focal gridpoint */ copyGridpoint(xpos, ypos) { let x = xpos; if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right let y = ypos; if (this.wrap[1]) y = (ypos + this.nr) % this.nr; // Wraps neighbours top-to-bottom if (x &lt; 0 || y &lt; 0 || x >= this.nc || y >= this.nr) return undefined else { return new Gridpoint(this.grid[x][y]) } } /** Copy properties from src gridpoint into dst gridpoint (reuses dst object) */ copyGP(dst, src) { for (var prop in src) dst[prop] = src[prop]; } /** Change the gridpoint at position x,y into gp * Makes sure wrapping is applied if necessary * @param {int} x position (column) for the focal gridpoint * @param {int} y position (row) for the focal gridpoint * @param {Gridpoint} @Gridpoint object to set the gp to */ copyIntoGridpoint(xpos, ypos, gp) { let x = xpos; if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right let y = ypos; if (this.wrap[1]) y = (ypos + 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; else { for (var prop in gp) this.grid[x][y][prop] = gp[prop]; } } /** Get the x,y coordinates of a neighbour in an array. * Makes sure wrapping is applied if necessary */ getNeighXY(xpos, ypos) { let x = xpos; if (this.wrap[0]) x = (xpos + this.nc) % this.nc; // Wraps neighbours left-to-right let y = ypos; if (this.wrap[1]) y = (ypos + 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] } /** Get a neighbour at compass direction * @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} direction the neighbour to return */ getNeighbour(model,col,row,direction) { let x = model.moore[direction][0]; let y = model.moore[direction][1]; return model.getGridpoint(col + x, row + 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 {Array} range which section of the neighbourhood must be counted? (see this.moore, e.g. 1-8 is Moore8, 0-4 is Neu5,etc) * To get all 8 neighbours, use range [1,8] * To get all neumann neighbours, use range [1,4] */ getAllNeighbours(model,col,row,range) { let gps = []; for (let n = range[0]; n &lt;= range[1]; n++) { let x = model.moore[n][0]; let y = model.moore[n][1]; let neigh = model.getGridpoint(col + x, row + y); gps.push(neigh); } return gps; } /** 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 &lt;= range[1]; n++) { let x = model.moore[n][0]; let y = model.moore[n][1]; let neigh = model.getGridpoint(col + x, row + y); if (neigh != undefined &amp;&amp; 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]) } getNeighbours8(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]) } getNeighbours9(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]) } getNeighbours4(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]) } getNeighbours5(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 &lt; gps.length; i++) sum_property += gps[i][property]; // Now we have the sum of weight + a constant (non) let randomnr = this.rng.genrand_real2() * 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 &lt; gps.length; i++) { cumsum += gps[i][property]; if (randomnr &lt; 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 &lt;= range[1]; n++) { let x = model.moore[n][0]; let y = model.moore[n][1]; let gp = model.getGridpoint(col + x, row + y); if(gp !== undefined &amp;&amp; gp[property] !== undefined) count += gp[property]; } return count; } /** sumNeighbours for range 1-8 (see sumNeighbours) */ sumMoore8(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,8]) } sumNeighbours8(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]) } sumNeighbours9(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]) } sumNeighbours4(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]) } sumNeighbours5(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 &lt;= range[1]; n++) { let x = model.moore[n][0]; let y = model.moore[n][1]; let neigh = model.getGridpoint(col + x, row + y); if (neigh !== undefined &amp;&amp; neigh[property]==val) count++; } return count; } /** countNeighbours for range 1-8 (see countNeighbours) */ countMoore8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) } countNeighbours8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) } /** countNeighbours for range 0-8 (see countNeighbours) */ countMoore9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) } countNeighbours9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) } /** countNeighbours for range 1-4 (see countNeighbours) */ countNeumann4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) } countNeighbours4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) } /** countNeighbours for range 0-4 (see countNeighbours) */ countNeumann5(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,4]) } countNeighbours5(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.) */ randomNeighbour(grid, col, row,range) { let rand = this.rng.genrand_int(range[0], range[1]); let x = this.moore[rand][0]; let y = this.moore[rand][1]; let neigh = grid.getGridpoint(col + x, row + y); while (neigh == undefined) neigh = this.randomNeighbour(grid, col, row,range); return neigh } /** randomMoore for range 1-8 (see randomMoore) */ randomMoore8(model, col, row) { return this.randomNeighbour(model, col, row, [1,8]) } randomNeighbour8(model, col, row) { return this.randomNeighbour(model, col, row, [1,8]) } /** randomMoore for range 0-8 (see randomMoore) */ randomMoore9(model, col, row) { return this.randomNeighbour(model, col, row, [0,8]) } randomNeighbour9(model, col, row) { return this.randomNeighbour(model, col, row, [0,8]) } /** randomMoore for range 1-4 (see randomMoore) */ randomNeumann4(model, col, row) { return this.randomNeighbour(model, col, row, [1,4]) } randomNeighbour4(model, col, row) { return this.randomNeighbour(model, col, row, [1,4]) } /** randomMoore for range 0-4 (see randomMoore) */ randomNeumann5(model, col, row) { return this.randomNeighbour(model, col, row, [0,4]) } randomNeighbour5(model, col, row) { return this.randomNeighbour(model, col, row, [0,4]) } /** Diffuse continuous states on the grid. * * @param {string} state The name of the state to diffuse * but can be mixed to make grids interact. * @param {float} rate the rate of diffusion. (&lt;0.25) */ diffuseStates(state,rate) { if(rate > 0.25) { throw new Error("Cacatoo: rate for diffusion cannot be greater than 0.25, try multiple diffusion steps instead.") } let newstate = MakeGrid(this.nc, this.nr, this.grid); for (let x = 0; x &lt; this.nc; x += 1) // every column { for (let y = 0; y &lt; this.nr; y += 1) // every row { for (let n = 1; n &lt;= 4; n++) // Every neighbour (neumann) { let moore = this.moore[n]; let xy = this.getNeighXY(x + moore[0], y + moore[1]); if (typeof xy == "undefined") continue let neigh = this.grid[xy[0]][xy[1]]; newstate[x][y][state] += neigh[state] * rate; newstate[xy[0]][xy[1]][state] -= neigh[state] * rate; } } } for (let x = 0; x &lt; this.nc; x += 1) // every column for (let y = 0; y &lt; this.nr; y += 1) // every row this.grid[x][y][state] = newstate[x][y][state]; } /** Diffuse continuous states on the grid. * * @param {string} state The name of the state to diffuse * but can be mixed to make grids interact. * @param {float} rate the rate of diffusion. (&lt;0.25) */ diffuseStateVector(statevector,rate) { if(rate > 0.25) { throw new Error("Cacatoo: rate for diffusion cannot be greater than 0.25, try multiple diffusion steps instead.") } let newstate = MakeGrid(this.nc, this.nr); for (let x = 0; x &lt; this.nc; x += 1) // every column for (let y = 0; y &lt; this.nr; y += 1) // every row { newstate[x][y][statevector] = Array(this.grid[x][y][statevector].length).fill(0); for (let n = 1; n &lt;= 4; n++)