UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

1,027 lines (913 loc) 55.8 kB
import Gridpoint from "./gridpoint.js" import Graph from './graph.js' import ODE from "./ode.js" import * as utility from './utility.js' /** * 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"] = utility.default_colours(num_colours) let colours = utility.dict_reverse(statecols) || { 'val': 1 } for (const [statekey, statedict] of Object.entries(colours)) { if (statedict == 'default') { return_dict[statekey] = utility.default_colours(num_colours+1) } else if (statedict == 'random') { return_dict[statekey] = utility.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] = utility.stringToRGB(statedict) } else { let c = {} for (const [key, val] of Object.entries(statedict)) { if (Array.isArray(val)) c[key] = val else c[key] = utility.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 <= 1) throw new Error("colourGradient needs at least 2 arrays") let segment_len = Math.ceil(n / (n_arrays-1)) if(n <= 10 && 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 < 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[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 <= 1) throw new Error("colourGradient needs at least 2 arrays") let color_dict = {} let total = 0 if(this.statecolours !== undefined && this.statecolours[property] !== undefined){ color_dict = this.statecolours[property] total = Object.keys(this.statecolours[property]).length } let all_arrays = [] for (let arr = 0; arr < 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 < this.nc; x++) { for (let y = 0; y < 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 < this.nc; x++) { for (let y = 0; y < 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 < 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 < 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 < this.nc * this.nr; n++) { this.upd_order.push(n) } } utility.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 < 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 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 < 0 || y < 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 < 0 || y < 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 < 0 || y < 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 < 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 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 <= 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 <= 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 && 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 < 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 < 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 x = model.moore[n][0] let y = model.moore[n][1] let gp = model.getGridpoint(col + x, row + y) if(gp !== undefined && 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 <= 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 && 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. (<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 < this.nc; x += 1) // every column { for (let y = 0; y < this.nr; y += 1) // every row { for (let n = 1; n <= 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 < this.nc; x += 1) // every column for (let y = 0; y < 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. (<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 < this.nc; x += 1) // every column for (let y = 0; y < this.nr; y += 1) // every row { newstate[x][y][statevector] = Array(this.grid[x][y][statevector].length).fill(0) for (let n = 1; n <= 4; n++) for(let state of Object.keys(this.grid[x][y][statevector])) newstate[x][y][statevector][state] = this.grid[x][y][statevector][state] } for (let x = 0; x < this.nc; x += 1) // every column { for (let y = 0; y < this.nr; y += 1) // every row { for (let n = 1; n <= 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]] for(let state of Object.keys(this.grid[x][y][statevector])) { newstate[x][y][statevector][state] += neigh[statevector][state] * rate newstate[xy[0]][xy[1]][statevector][state] -= neigh[statevector][state] * rate } } } } for (let x = 0; x < this.nc; x += 1) // every column for (let y = 0; y < this.nr; y += 1) // every row for (let n = 1; n <= 4; n++) for(let state of Object.keys(this.grid[x][y][statevector])) this.grid[x][y][statevector][state] = newstate[x][y][statevector][state] } /** 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 [x][y][o][s] (x-coord,y-coord,relevant ode,state-vector) for (let x = 0; x < this.nc; x += 1) // every column { for (let y = 0; y < this.nr; y += 1) // every row { for (let o = 0; o < this.grid[x][y].ODEs.length; o++) // every ode { for (let s = 0; s < this.grid[x][y].ODEs[o].state.length; s++) // every state { let rate = this.grid[x][y].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(x + moore[0], y + 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[x][y][o][s] += sum_in } } } } for (let x = 0; x < this.nc; x += 1) // every column for (let y = 0; y < this.nr; y += 1) // every row for (let o = 0; o < this.grid[x][y].ODEs.length; o++) for (let s = 0; s < this.grid[x][y].ODEs[o].state.length; s++) this.grid[x][y].ODEs[o].state[s] = newstates_2[x][y][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 local) * calculated based on neighbourhoods. */ perfectMix() { let all_gridpoints = []; for (let x = 0; x < this.nc; x++) for (let y = 0; y < this.nr; y++) all_gridpoints.push(this.getGridpoint(x, y)) all_gridpoints = utility.shuffle(all_gridpoints, this.rng) for (let x = 0; x < all_gridpoints.length; x++) this.setGridpoint(x % this.nc, Math.floor(x / this.nc), all_gridpoints[x]) 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!" let x_off = this.wrap[0] ? 1 : 2; let y_off = this.wrap[1] ? 1 : 2; for (let x = 0 + even; x < this.nc; x += 2) { if(x> this.nc-x_off) continue for (let y = 0 + even; y < this.nr; y += 2) { if(y> this.nr-y_off) continue let old_A = new Gridpoint(this.grid[x][y]); let A = this.getGridpoint(x, y) let B = this.getGridpoint(x + 1, y) let C = this.getGridpoint(x + 1, y + 1) let D = this.getGridpoint(x, y + 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(x, y, A) this.setGridpoint(x + 1, y, B) this.setGridpoint(x + 1, y + 1, C) this.setGridpoint(x, y + 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 = utility.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_values Array of floats to plot (here plotted over time) * @param {String} title Title of the plot * @param {Object} opts dictionary-style list of opts to pass onto dygraphs */ plotPoints(graph_values, title, opts) { let graph_labels = Array.from({length: graph_values.length}, (v, i) => 'sample'+(i+1)) let cols = Array.from({length: graph_values.length}, (v, i) => 'black') let seriesname = 'average' let sum = 0 let num = 0 // Get average of all defined values for(let n = 0; n< graph_values.length; n++){ if(graph_values[n] !== undefined) { sum += graph_values[n] num++ } } let avg = (sum / num) || 0; graph_values.unshift(avg) graph_labels.unshift(seriesname) cols.unshift("#418b4e") if(opts == undefined) opts = {} opts.drawPoints = true opts.strokeWidth = 0 opts.pointSize = 1 opts.labelsDivWidth = 0 opts.series = {[seriesname]: {strokeWidth: 3.0, strokeColor:"green", drawPoints: false, pointSize: 0, highlightCircleSize: 3 }} if (typeof window == 'undefined') return if (!(title in this.graphs)) { cols = utility.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 = utility.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, opts) { 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) let graph_values = popsizes // Colours let colours = [] for (let c of values) { 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 + ")" if(opts && opts.title) title = opts.title this.plotArray(graph_labels, graph_values, colours, title, opts) //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) } drawSlide(canvasname,prefix="grid_") { let canvas = this.canvases[canvasname].elem // Grab the canvas element let timestamp = sim.time.toString() timestamp = timestamp.padStart(5, "0") canvas.toBlob(function(blob) { saveAs(