cacatoo
Version:
Building, exploring, and sharing spatially structured models
1,092 lines (966 loc) • 241 kB
HTML
<!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] < 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 < newstate.length; i++) if (newstate[i] < 0.000001) newstate[i] = 0.0;
this.state = newstate;
}
/**
* Prints the current state to the console
*/
print_state() {
console.log(this.state);
}
}
/**
* 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 < 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 < 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 <= 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);
}
}
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++)