1 'use strict'; 2 3 /** 4 * Gridpoint is what Gridmodels are made of. Contains everything that may happen in 1 locality. 5 */ 6 7 class Gridpoint { 8 /** 9 * 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) 10 * 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 11 * if you really need it. A better option is to use asynchronous updating so you won't have to worry about this at all :) 12 * @param {Gridpoint} template Optional template to make a new @Gridpoint from 13 */ 14 constructor(template) { 15 for (var prop in template) 16 this[prop] = template[prop]; // Shallow copy. It's fast, but be careful with syncronous updating! 17 // this[prop] = copy(template[prop]) // Deep copy. Takes much more time, and you'll likely end up copying much more than necessary. Use only if you're sure you need it! 18 } 19 } 20 21 /** 22 * 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. 23 */ 24 25 class Graph { 26 /** 27 * The constructor function for a @Canvas object. 28 * @param {Array} labels array of strings containing the labels for datapoints (e.g. for the legend) 29 * @param {Array} values Array of floats to plot (here plotted over time) 30 * @param {Array} colours Array of colours to use for plotting 31 * @param {String} title Title of the plot 32 * @param {Object} opts dictionary-style list of opts to pass onto dygraphs 33 */ 34 constructor(labels, values, colours, title, opts) { 35 36 if (typeof window == undefined) throw "Using dygraphs with cashJS only works in browser-mode" 37 this.labels = labels; 38 this.data = [values]; 39 this.title = title; 40 this.num_dps = this.labels.length; // number of data points for this graphs 41 this.elem = document.createElement("div"); 42 this.elem.className = "graph-holder"; 43 this.colours = []; 44 45 for (let v in colours) { 46 if (v == "Time") continue 47 else if (colours[v] == undefined) this.colours.push("#000000"); 48 else if (colours[v][0] + colours[v][1] + colours[v][2] == 765) this.colours.push("#dddddd"); 49 else this.colours.push(rgbToHex(colours[v][0], colours[v][1], colours[v][2])); 50 } 51 52 document.body.appendChild(this.elem); 53 document.getElementById("graph_holder").appendChild(this.elem); 54 this.g = new Dygraph(this.elem, this.data, 55 { 56 title: this.title, 57 showRoller: false, 58 ylabel: this.labels.length == 2 ? this.labels[1] : "", 59 width: 500, 60 height: 200, 61 xlabel: this.labels[0], 62 drawPoints: opts && opts.drawPoints || false, 63 pointSize: opts ? (opts.pointSize ? opts.pointSize : 0) : 0, 64 strokePattern: opts ? (opts.strokePattern ? opts.strokePattern : null) : null, 65 dateWindow: [0, 100], 66 axisLabelFontSize: 10, 67 valueRange: [0.000,], 68 strokeWidth: opts ? opts.strokeWidth : 3, 69 colors: this.colours, 70 labels: this.labels 71 }); 72 } 73 74 75 /** Push data to your graph-element 76 * @param {array} array of floats to be added to the dygraph object (stored in 'data') 77 */ 78 push_data(data_array) { 79 this.data.push(data_array); 80 } 81 82 reset_plot() { 83 this.data = [this.data[0]]; 84 this.g.updateOptions( 85 { 86 'file': [this.data] 87 }); 88 } 89 90 /** 91 * Update the graph axes 92 */ 93 update() { 94 let max_x = 0; 95 let min_x = 999999999999; 96 for (let i of this.data) { 97 if (i[0] > max_x) max_x = i[0]; 98 if (i[0] < min_x) min_x = i[0]; 99 } 100 this.g.updateOptions( 101 { 102 'file': this.data, 103 dateWindow: [min_x, max_x] 104 }); 105 106 } 107 } 108 109 /* 110 Functions below are to make sure dygraphs understands the colours used by Cacatoo (converts to hex) 111 */ 112 function componentToHex(c) { 113 var hex = c.toString(16); 114 return hex.length == 1 ? "0" + hex : hex; 115 } 116 117 function rgbToHex(r, g, b) { 118 return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); 119 } 120 121 /** 122 * The ODE class is used to call the odex.js library and numerically solve ODEs 123 */ 124 125 class ODE { 126 /** 127 * The constructor function for a @ODE object. 128 * @param {function} eq Function that describes the ODE (see examples starting with ode) 129 * @param {Array} state_vector Initial state vector 130 * @param {Array} pars Array of parameters for the ODEs 131 * @param {Array} diff_rates Array of rates at which each state diffuses to neighbouring grid point (Has to be less than 0.25!) 132 * @param {String} ode_name Name of this ODE 133 */ 134 constructor(eq, state_vector, pars, diff_rates, ode_name, acceptable_error) { 135 this.name = ode_name; 136 this.eq = eq; 137 this.state = state_vector; 138 this.diff_rates = diff_rates; 139 this.pars = pars; 140 this.solver = new Solver(state_vector.length); 141 if (acceptable_error !== undefined) this.solver.absoluteTolerance = this.solver.relativeTolerance = acceptable_error; 142 } 143 144 /** 145 * Numerically solve the ODE 146 * @param {float} delta_t Step size 147 * @param {bool} opt_pos When enabled, negative values are set to 0 automatically 148 */ 149 solveTimestep(delta_t = 0.1, pos = false) { 150 let newstate = this.solver.solve( 151 this.eq(...this.pars), // function to solve and its pars (... unlists the array as a list of args) 152 0, // Initial x value 153 this.state, // Initial y value(s) 154 delta_t // Final x value 155 ).y; 156 if (pos) for (var i = 0; i < newstate.length; i++) if (newstate[i] < 0.000001) newstate[i] = 0.0; 157 this.state = newstate; 158 } 159 /** 160 * Prints the current state to the console 161 */ 162 print_state() { 163 console.log(this.state); 164 } 165 } 166 167 /* 168 I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace 169 so it's better encapsulated. Now you can have multiple random number generators 170 and they won't stomp all over eachother's state. 171 172 If you want to use this as a substitute for Math.random(), use the random() 173 method like so: 174 175 var m = new MersenneTwister(); 176 var randomNumber = m.random(); 177 178 You can also call the other genrand_{foo}() methods on the instance. 179 If you want to use a specific seed in order to get a repeatable random 180 sequence, pass an integer into the constructor: 181 var m = new MersenneTwister(123); 182 and that will always produce the same random sequence. 183 Sean McCullough (banksean@gmail.com) 184 */ 185 186 /* 187 A C-program for MT19937, with initialization improved 2002/1/26. 188 Coded by Takuji Nishimura and Makoto Matsumoto. 189 190 Before using, initialize the state by using init_genrand(seed) 191 or init_by_array(init_key, key_length). 192 193 Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, 194 All rights reserved. 195 196 Redistribution and use in source and binary forms, with or without 197 modification, are permitted provided that the following conditions 198 are met: 199 200 1. Redistributions of source code must retain the above copyright 201 notice, this list of conditions and the following disclaimer. 202 203 2. Redistributions in binary form must reproduce the above copyright 204 notice, this list of conditions and the following disclaimer in the 205 documentation and/or other materials provided with the distribution. 206 207 3. The names of its contributors may not be used to endorse or promote 208 products derived from this software without specific prior written 209 permission. 210 211 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 212 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 213 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 214 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 215 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 216 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 217 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 218 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 219 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 220 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 221 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 222 223 224 Any feedback is very welcome. 225 http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html 226 email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) 227 */ 228 229 function MersenneTwister(seed) { 230 if (seed == undefined) { 231 seed = new Date().getTime(); 232 } 233 /* Period parameters */ 234 this.N = 624; 235 this.M = 397; 236 this.MATRIX_A = 0x9908b0df; /* constant vector a */ 237 this.UPPER_MASK = 0x80000000; /* most significant w-r bits */ 238 this.LOWER_MASK = 0x7fffffff; /* least significant r bits */ 239 240 this.mt = new Array(this.N); /* the array for the state vector */ 241 this.mti=this.N+1; /* mti==N+1 means mt[N] is not initialized */ 242 243 this.init_genrand(seed); 244 } 245 246 /* initializes mt[N] with a seed */ 247 MersenneTwister.prototype.init_genrand = function(s) { 248 this.mt[0] = s >>> 0; 249 for (this.mti=1; this.mti<this.N; this.mti++) { 250 var s = this.mt[this.mti-1] ^ (this.mt[this.mti-1] >>> 30); 251 this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) 252 + this.mti; 253 /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ 254 /* In the previous versions, MSBs of the seed affect */ 255 /* only MSBs of the array mt[]. */ 256 /* 2002/01/09 modified by Makoto Matsumoto */ 257 this.mt[this.mti] >>>= 0; 258 /* for >32 bit machines */ 259 } 260 }; 261 262 /* initialize by an array with array-length */ 263 /* init_key is the array for initializing keys */ 264 /* key_length is its length */ 265 /* slight change for C++, 2004/2/26 */ 266 MersenneTwister.prototype.init_by_array = function(init_key, key_length) { 267 var i, j, k; 268 this.init_genrand(19650218); 269 i=1; j=0; 270 k = (this.N>key_length ? this.N : key_length); 271 for (; k; k--) { 272 var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30); 273 this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525))) 274 + init_key[j] + j; /* non linear */ 275 this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ 276 i++; j++; 277 if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; } 278 if (j>=key_length) j=0; 279 } 280 for (k=this.N-1; k; k--) { 281 var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30); 282 this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) 283 - i; /* non linear */ 284 this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ 285 i++; 286 if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; } 287 } 288 289 this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ 290 }; 291 292 /* generates a random number on [0,0xffffffff]-interval */ 293 MersenneTwister.prototype.genrand_int32 = function() { 294 var y; 295 var mag01 = new Array(0x0, this.MATRIX_A); 296 /* mag01[x] = x * MATRIX_A for x=0,1 */ 297 298 if (this.mti >= this.N) { /* generate N words at one time */ 299 var kk; 300 301 if (this.mti == this.N+1) /* if init_genrand() has not been called, */ 302 this.init_genrand(5489); /* a default initial seed is used */ 303 304 for (kk=0;kk<this.N-this.M;kk++) { 305 y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK); 306 this.mt[kk] = this.mt[kk+this.M] ^ (y >>> 1) ^ mag01[y & 0x1]; 307 } 308 for (;kk<this.N-1;kk++) { 309 y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK); 310 this.mt[kk] = this.mt[kk+(this.M-this.N)] ^ (y >>> 1) ^ mag01[y & 0x1]; 311 } 312 y = (this.mt[this.N-1]&this.UPPER_MASK)|(this.mt[0]&this.LOWER_MASK); 313 this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y & 0x1]; 314 315 this.mti = 0; 316 } 317 318 y = this.mt[this.mti++]; 319 320 /* Tempering */ 321 y ^= (y >>> 11); 322 y ^= (y << 7) & 0x9d2c5680; 323 y ^= (y << 15) & 0xefc60000; 324 y ^= (y >>> 18); 325 326 return y >>> 0; 327 }; 328 329 /* generates a random number on [0,0x7fffffff]-interval */ 330 MersenneTwister.prototype.genrand_int31 = function() { 331 return (this.genrand_int32()>>>1); 332 }; 333 334 /* generates a random number on [0,1]-real-interval */ 335 MersenneTwister.prototype.genrand_real1 = function() { 336 return this.genrand_int32()*(1.0/4294967295.0); 337 /* divided by 2^32-1 */ 338 }; 339 340 /* generates a random int between [min,max] */ 341 MersenneTwister.prototype.genrand_int = function(min,max) { 342 return min+Math.floor(this.genrand_real1()*(max)); 343 }; 344 345 /* generates a random number on [0,1)-real-interval */ 346 MersenneTwister.prototype.random = function() { 347 return this.genrand_int32()*(1.0/4294967296.0); 348 /* divided by 2^32 */ 349 }; 350 351 /* generates a random number on (0,1)-real-interval */ 352 MersenneTwister.prototype.genrand_real3 = function() { 353 return (this.genrand_int32() + 0.5)*(1.0/4294967296.0); 354 /* divided by 2^32 */ 355 }; 356 357 /* generates a random number on [0,1) with 53-bit resolution*/ 358 MersenneTwister.prototype.genrand_res53 = function() { 359 var a=this.genrand_int32()>>>5, b=this.genrand_int32()>>>6; 360 return (a*67108864.0+b)*(1.0/9007199254740992.0); 361 }; 362 363 /** 364 * Gridmodel is the main (currently only) type of model in Cacatoo. Most of these models 365 * will look and feel like CAs, but GridModels can also contain ODEs with diffusion, making 366 * them more like PDEs. 367 */ 368 369 class Gridmodel { 370 /** 371 * The constructor function for a @Gridmodel object. Takes the same config dictionary as used in @Simulation 372 * @param {string} name The name of your model. This is how it will be listed in @Simulation 's properties 373 * @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo GridModel. 374 * @param {MersenneTwister} rng A random number generator (MersenneTwister object) 375 */ 376 constructor(name, config, rng) { 377 this.name = name; 378 this.time = 0; 379 this.grid = MakeGrid(config.ncol, config.nrow); // Initialises an (empty) grid 380 this.nc = config.ncol || 200; 381 this.nr = config.nrow || 200; 382 this.wrap = config.wrap || [true, true]; 383 this.rng = rng; 384 this.statecolours = this.setupColours(config.statecolours); // Makes sure the statecolours in the config dict are parsed (see below) 385 this.lims = {}; 386 this.scale = config.scale || 1; 387 this.graph_update = config.graph_update || 20; 388 this.graph_interval = config.graph_interval || 2; 389 390 this.margolus_phase = 0; 391 // Store a simple array to get neighbours from the N, E, S, W, NW, NE, SW, SE (analogous to Cash2.1) 392 this.moore = [[0, 0], // SELF _____________ 393 [0, -1], // NORTH | 5 | 1 | 6 | 394 [1, 0], // EAST | 4 | 0 | 2 | 395 [0, 1], // SOUTH | 7 | 3 | 8 | 396 [-1, 0], // WEST _____________ 397 [-1, -1], // NW 398 [1, -1], // NE 399 [-1, 1], // SW 400 [1, 1] // SE 401 ]; 402 403 this.graphs = {}; // Object containing all graphs belonging to this model (HTML usage only) 404 this.canvases = {}; // Object containing all Canvases belonging to this model (HTML usage only) 405 } 406 407 /** Initiate a dictionary with colour arrays [R,G,B] used by Graph and Canvas classes 408 * @param {statecols} object - given object can be in two forms 409 * | either {state:colour} tuple (e.g. 'alive':'white', see gol.html) 410 * | or {state:object} where objects are {val:'colour}, 411 * | e.g. {'species':{0:"black", 1:"#DDDDDD", 2:"red"}}, see cheater.html 412 */ 413 setupColours(statecols) { 414 let return_dict = {}; 415 if (statecols == null) // If the user did not define statecols (yet) 416 return return_dict 417 let colours = dict_reverse(statecols) || { 'val': 1 }; 418 419 420 for (const [statekey, statedict] of Object.entries(colours)) { 421 if (statedict == 'default') { 422 return_dict[statekey] = default_colours; // Defined below 423 } 424 else if (typeof statedict === 'string' || statedict instanceof String) // For if 425 { 426 return_dict[statekey] = stringToRGB(statedict); 427 } 428 else { 429 let c = {}; 430 for (const [key, val] of Object.entries(statedict)) { 431 if (Array.isArray(val)) c[key] = val; 432 else c[key] = stringToRGB(val); 433 } 434 return_dict[statekey] = c; 435 } 436 } 437 return return_dict 438 } 439 440 441 /** Initiate a gradient of colours for a property. 442 * @param {string} property The name of the property to which the colour is assigned 443 * @param {int} n How many colours the gradient consists off 444 * For example usage, see colourViridis below 445 */ 446 colourGradient(property, n) { 447 let n_arrays = arguments.length - 2; 448 if (n_arrays <= 1) throw new Error("colourGradient needs at least 2 arrays") 449 450 let segment_len = n / (n_arrays); 451 452 let color_dict = this.statecolours[property]; 453 let total = 0; 454 if (typeof color_dict != 'undefined') 455 total = Object.keys(this.statecolours[property]).length; 456 else 457 color_dict = {}; 458 459 for (let arr = 0; arr < n_arrays - 1; arr++) { 460 let arr1 = arguments[2 + arr]; 461 let arr2 = arguments[2 + arr + 1]; 462 /// HIER BEN IK MEE BEZIG!!! 463 for (let i = 0; i < segment_len; i++) { 464 let r, g, b; 465 if (arr2[0] > arr1[0]) r = Math.floor(arr1[0] + (arr2[0] - arr1[0]) * (i / (segment_len - 1))); 466 else r = Math.floor(arr1[0] - (arr1[0] - arr2[0]) * (i / (segment_len - 1))); 467 if (arr2[1] > arr1[1]) g = Math.floor(arr1[1] + (arr2[1] - arr1[1]) * (i / (segment_len - 1))); 468 else g = Math.floor(arr1[0] - (arr1[1] - arr2[1]) * (i / (segment_len - 1))); 469 if (arr2[2] > arr1[2]) b = Math.floor(arr1[2] + (arr2[2] - arr1[2]) * (i / (segment_len - 1))); 470 else b = Math.floor(arr1[2] - (arr1[2] - arr2[2]) * (i / (segment_len - 1))); 471 472 color_dict[Math.floor(i + arr * segment_len + total) + 1] = [r, g, b]; 473 } 474 // total += segment_len 475 } 476 this.statecolours[property] = color_dict; 477 } 478 479 /** Initiate a gradient of colours for a property, using the Viridis colour scheme (purpleblue-ish to green to yellow) 480 * @param {string} property The name of the property to which the colour is assigned 481 * @param {int} n How many colours the gradient consists off 482 * @param {bool} rev Reverse the viridis colour gradient 483 */ 484 colourViridis(property, n, rev = false) { 485 if (!rev) this.colourGradient(property, n, [68, 1, 84], [59, 82, 139], [33, 144, 140], [93, 201, 99], [253, 231, 37]); // Viridis 486 else this.colourGradient(property, n, [253, 231, 37], [93, 201, 99], [33, 144, 140], [59, 82, 139], [68, 1, 84]); // Viridis 487 } 488 489 /** Print the entire grid to the console */ 490 printgrid() { 491 console.table(this.grid); 492 } 493 494 /** The most important function in GridModel: how to determine the next state of a gridpoint? 495 * By default, nextState is empty. It should be defined by the user (see examples) 496 * @param {int} i Position of grid point to update (column) 497 * @param {int} j Position of grid point to update (row) 498 */ 499 nextState(i, j) { 500 throw 'Nextstate function of \'' + this.name + '\' undefined'; 501 } 502 503 /** Synchronously apply the nextState function (defined by user) to the entire grid 504 * Synchronous means that all grid points will be updated simultaneously. This is ensured 505 * by making a back-up grid, which will serve as a reference to know the state in the previous 506 * time step. First all grid points are updated based on the back-up. Only then will the 507 * actual grid be changed. 508 */ 509 synchronous() // Do one step (synchronous) of this grid 510 { 511 let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Old state based on current grid 512 let newstate = MakeGrid(this.nc, this.nr); // New state == empty grid 513 514 for (let i = 0; i < this.nc; i++) { 515 for (let j = 0; j < this.nr; j++) { 516 this.nextState(i, j); // Update this.grid 517 newstate[i][j] = this.grid[i][j]; // Set this.grid to newstate 518 this.grid[i][j] = oldstate[i][j]; // Reset this.grid to old state 519 } 520 } 521 this.grid = newstate; 522 this.time++; 523 } 524 525 /** Like the synchronous function above, but can not take a custom user-defined function rather 526 * than the default next-state function. Technically one should be able to refarctor this by making 527 * the default function of synchronous "nextstate". But this works. :) 528 */ 529 apply_sync(func) { 530 let oldstate = MakeGrid(this.nc, this.nr, this.grid); // Old state based on current grid 531 let newstate = MakeGrid(this.nc, this.nr); // New state == empty grid 532 for (let i = 0; i < this.nc; i++) { 533 for (let j = 0; j < this.nr; j++) { 534 func(i, j); // Update this.grid 535 newstate[i][j] = this.grid[i][j]; // Set this.grid to newstate 536 this.grid[i][j] = oldstate[i][j]; // Reset this.grid to old state 537 } 538 } 539 this.grid = newstate; 540 } 541 542 /** Asynchronously apply the nextState function (defined by user) to the entire grid 543 * Asynchronous means that all grid points will be updated in a random order. For this 544 * first the update_order will be determined (this.set_update_order). Afterwards, the nextState 545 * will be applied in that order. This means that some cells may update while all their neighours 546 * are still un-updated, and other cells will update while all their neighbours are already done. 547 */ 548 asynchronous() { 549 this.set_update_order(); 550 for (let n = 0; n < this.nc * this.nr; n++) { 551 let m = this.upd_order[n]; 552 let i = m % this.nc; 553 let j = Math.floor(m / this.nc); 554 this.nextState(i, j); 555 } 556 this.time++; 557 // Don't have to copy the grid here. Just cycle through i,j in random order and apply nextState :) 558 } 559 560 /** Analogous to apply_sync(func), but asynchronous */ 561 apply_async(func) { 562 this.set_update_order(); 563 for (let n = 0; n < this.nc * this.nr; n++) { 564 let m = this.upd_order[n]; 565 let i = m % this.nc; 566 let j = Math.floor(m / this.nc); 567 func(i, j); 568 } 569 } 570 571 /** If called for the first time, make an update order (list of ints), otherwise just shuffle it. */ 572 set_update_order() { 573 if (typeof this.upd_order === 'undefined') // "Static" variable, only create this array once and reuse it 574 { 575 this.upd_order = []; 576 for (let n = 0; n < this.nc * this.nr; n++) { 577 this.upd_order.push(n); 578 } 579 } 580 shuffle(this.upd_order, this.rng); // Shuffle the update order 581 } 582 583 /** The update is, like nextState, user-defined (hence, empty by default). 584 * It should contains all functions that one wants to apply every time step 585 * (e.g. grid manipulations and printing statistics) 586 * For example, and update function could look like: 587 * this.synchronous() // Update all cells 588 * this.MargolusDiffusion() // Apply Toffoli Margolus diffusion algorithm 589 * this.plotPopsizes('species',[1,2,3]) // Plot the population sizes 590 */ 591 update() { 592 throw 'Update function of \'' + this.name + '\' undefined'; 593 } 594 595 /** Get the gridpoint at coordinates i,j 596 * Makes sure wrapping is applied if necessary 597 * @param {int} i position (column) for the focal gridpoint 598 * @param {int} j position (row) for the focal gridpoint 599 */ 600 getGridpoint(i, j) { 601 let x = i; 602 if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right 603 let y = j; 604 if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom 605 if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object 606 else return this.grid[x][y] 607 } 608 609 /** Change the gridpoint at position i,j into gp 610 * Makes sure wrapping is applied if necessary 611 * @param {int} i position (column) for the focal gridpoint 612 * @param {int} j position (row) for the focal gridpoint 613 * @param {Gridpoint} @Gridpoint object to set the gp to 614 */ 615 setGridpoint(i, j, gp) { 616 let x = i; 617 if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right 618 let y = j; 619 if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom 620 621 if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) this.grid[x][y] = undefined; // TODO!!!!!!!! Return border-state instead! 622 else this.grid[x][y] = gp; 623 } 624 625 /** Get the x,y coordinates of a neighbour in an array. 626 * Makes sure wrapping is applied if necessary 627 */ 628 getNeighXY(i, j) { 629 let x = i; 630 if (this.wrap[0]) x = (i + this.nc) % this.nc; // Wraps neighbours left-to-right 631 let y = j; 632 if (this.wrap[1]) y = (j + this.nr) % this.nr; // Wraps neighbours top-to-bottom 633 if (x < 0 || y < 0 || x >= this.nc || y >= this.nr) return undefined // If sampling neighbour outside of the grid, return empty object 634 else return [x, y] 635 } 636 637 /** Get array of grid points with val in property (Neu4, Neu5, Moore8, Moore9 depending on range-array) 638 * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this), 639 * but can be mixed to make grids interact. 640 * @param {int} col position (column) for the focal gridpoint 641 * @param {int} row position (row) for the focal gridpoint 642 * @param {string} property the property that is counted 643 * @param {int} val value 'property' should have 644 * @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) 645 * @return {int} The number of grid points with "property" set to "val" 646 * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.) 647 * If one wants to count all the "cheater" surrounding a gridpoint in cheater.js in the Moore8 neighbourhood 648 * one needs to look for value '3' in the property 'species': 649 * this.getNeighbours(this,10,10,3,'species',[1-8]); 650 * or 651 * this.getMoore8(this,10,10,3,'species') 652 */ 653 getNeighbours(model,col,row,property,val,range) { 654 let gps = []; 655 for (let n = range[0]; n <= range[1]; n++) { 656 let i = model.moore[n][0]; 657 let j = model.moore[n][1]; 658 let neigh = model.getGridpoint(col + i, row + j); 659 if (neigh != undefined && neigh[property] == val) 660 gps.push(neigh); 661 } 662 return gps; 663 } 664 665 /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */ 666 getMoore8(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,8]) } 667 /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */ 668 getMoore9(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,8]) } 669 /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */ 670 getNeumann4(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[1,4]) } 671 /** getNeighbours for the Moore8 neighbourhood (range 1-8 in function getNeighbours) */ 672 getNeumann5(model, col, row, property,val) { return this.getNeighbours(model,col,row,property,val,[0,4]) } 673 674 /** From a list of grid points, e.g. from getNeighbours(), sample one weighted by a property. This is analogous 675 * to spinning a "roulette wheel". Also see a hard-coded versino of this in the "cheater" example 676 * @param {Array} gps Array of gps to sample from (e.g. living individuals in neighbourhood) 677 * @param {string} property The property used to weigh gps (e.g. fitness) 678 * @param {float} non Scales the probability of not returning any gp. 679 */ 680 rouletteWheel(gps, property, non = 0.0) { 681 let sum_property = non; 682 for (let i = 0; i < gps.length; i++) sum_property += gps[i][property]; // Now we have the sum of weight + a constant (non) 683 let randomnr = this.rng.genrand_real1() * sum_property; // Sample a randomnr between 0 and sum_property 684 let cumsum = 0.0; // This will keep track of the cumulative sum of weights 685 for (let i = 0; i < gps.length; i++) { 686 cumsum += gps[i][property]; 687 if (randomnr < cumsum) return gps[i] 688 } 689 return 690 } 691 692 /** Sum the properties of grid points in the neighbourhood (Neu4, Neu5, Moore8, Moore9 depending on range-array) 693 * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this), 694 * but can be mixed to make grids interact. 695 * @param {int} col position (column) for the focal gridpoint 696 * @param {int} row position (row) for the focal gridpoint 697 * @param {string} property the property that is counted 698 * @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) 699 * @return {int} The number of grid points with "property" set to "val" 700 * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.) 701 * For example, if one wants to sum all the "fitness" surrounding a gridpoint in the Neumann neighbourhood, use 702 * this.sumNeighbours(this,10,10,'fitness',[1-4]); 703 * or 704 * this.sumNeumann4(this,10,10,'fitness') 705 */ 706 sumNeighbours(model, col, row, property, range) { 707 let count = 0; 708 for (let n = range[0]; n <= range[1]; n++) { 709 let i = model.moore[n][0]; 710 let j = model.moore[n][1]; 711 let gp = model.getGridpoint(col + i, row + j); 712 if(gp != undefined) count += model.getGridpoint(col + i, row + j)[property]; 713 } 714 return count; 715 } 716 717 /** sumNeighbours for range 1-8 (see sumNeighbours) */ 718 sumMoore8(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,8]) } 719 /** sumNeighbours for range 0-8 (see sumNeighbours) */ 720 sumMoore9(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,8]) } 721 /** sumNeighbours for range 1-4 (see sumNeighbours) */ 722 sumNeumann4(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [1,4]) } 723 /** sumNeighbours for range 0-4 (see sumNeighbours) */ 724 sumNeumann5(grid, col, row, property) { return this.sumNeighbours(grid, col, row, property, [0,4]) } 725 726 727 /** Count the number of neighbours with 'val' in 'property' (Neu4, Neu5, Moore8, Moore9 depending on range-array) 728 * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this), 729 * but can be mixed to make grids interact. 730 * @param {int} col position (column) for the focal gridpoint 731 * @param {int} row position (row) for the focal gridpoint 732 * @param {string} property the property that is counted 733 * @param {int} val value property must have to be counted 734 * @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) 735 * @return {int} The number of grid points with "property" set to "val" 736 * Below, 4 version of this functions are overloaded (Moore8, Moore9, Neumann4, etc.) 737 * For example, if one wants to count all the "alive" individuals in the Moore 9 neighbourhood, use 738 * this.countNeighbours(this,10,10,1,'alive',[0-8]); 739 * or 740 * this.countMoore9(this,10,10,1,'alive'); 741 */ 742 countNeighbours(model, col, row, property, val, range) { 743 let count = 0; 744 for (let n = range[0]; n <= range[1]; n++) { 745 let i = model.moore[n][0]; 746 let j = model.moore[n][1]; 747 if (model.getGridpoint(col + i, row + j)[property]==val) count++; 748 } 749 return count; 750 } 751 752 /** sumNeighbours for range 1-8 (see sumNeighbours) */ 753 countMoore8(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,8]) } 754 /** sumNeighbours for range 0-8 (see sumNeighbours) */ 755 countMoore9(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,8]) } 756 /** sumNeighbours for range 1-4 (see sumNeighbours) */ 757 countNeumann4(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [1,4]) } 758 /** sumNeighbours for range 0-4 (see sumNeighbours) */ 759 countNeumann5(model, col, row, property, val) { return this.countNeighbours(model, col, row, property, val, [0,4]) } 760 761 762 /** Return a random neighbour from the neighbourhood defined by range array 763 * @param {GridModel} grid The gridmodel used to check neighbours. Usually the gridmodel itself (i.e., this), 764 * but can be mixed to make grids interact. 765 * @param {int} col position (column) for the focal gridpoint 766 * @param {int} row position (row) for the focal gridpoint 767 * @param {Array} range from which to sample (1-8 is Moore8, 0-4 is Neu5, etc.) 768 */ 769 randomMoore(grid, col, row,range) { 770 let rand = this.rng.genrand_int(range[0], range[1]); 771 let i = this.moore[rand][0]; 772 let j = this.moore[rand][1]; 773 let neigh = grid.getGridpoint(col + i, row + j); 774 while (neigh == undefined) neigh = this.randomMoore(grid, col, row,range); 775 return neigh 776 } 777 778 /** sumNeighbours for range 1-8 (see sumNeighbours) */ 779 randomMoore8(model, col, row) { return this.randomMoore(model, col, row, [1,8]) } 780 /** sumNeighbours for range 0-8 (see sumNeighbours) */ 781 randomMoore9(model, col, row) { return this.randomMoore(model, col, row, [0,8]) } 782 /** sumNeighbours for range 1-4 (see sumNeighbours) */ 783 randomNeu4(model, col, row) { return this.randomMoore(model, col, row, [1,4]) } 784 /** sumNeighbours for range 0-4 (see sumNeighbours) */ 785 randomNeu5(model, col, row) { return this.randomMoore(model, col, row, [0,4]) } 786 787 788 /** Diffuse ODE states on the grid. Because ODEs are stored by reference inside gridpoint, the 789 * states of the ODEs have to be first stored (copied) into a 4D array (x,y,ODE,state-vector), 790 * which is then used to update the grid. 791 */ 792 diffuseOdeStates() { 793 let newstates_2 = CopyGridODEs(this.nc, this.nr, this.grid); // Generates a 4D array of [i][j][o][s] (i-coord,j-coord,relevant ode,state-vector) 794 795 for (let i = 0; i < this.nc; i += 1) // every column 796 { 797 for (let j = 0; j < this.nr; j += 1) // every row 798 { 799 for (let o = 0; o < this.grid[i][j].ODEs.length; o++) // every ode 800 { 801 for (let s = 0; s < this.grid[i][j].ODEs[o].state.length; s++) // every state 802 { 803 let rate = this.grid[i][j].ODEs[o].diff_rates[s]; 804 let sum_in = 0.0; 805 for (let n = 1; n <= 4; n++) // Every neighbour (neumann) 806 { 807 let moore = this.moore[n]; 808 let xy = this.getNeighXY(i + moore[0], j + moore[1]); 809 if (typeof xy == "undefined") continue 810 let neigh = this.grid[xy[0]][xy[1]]; 811 sum_in += neigh.ODEs[o].state[s] * rate; 812 newstates_2[xy[0]][xy[1]][o][s] -= neigh.ODEs[o].state[s] * rate; 813 } 814 newstates_2[i][j][o][s] += sum_in; 815 } 816 } 817 } 818 } 819 820 for (let i = 0; i < this.nc; i += 1) // every column 821 for (let j = 0; j < this.nr; j += 1) // every row 822 for (let o = 0; o < this.grid[i][j].ODEs.length; o++) 823 for (let s = 0; s < this.grid[i][j].ODEs[o].state.length; s++) 824 this.grid[i][j].ODEs[o].state[s] = newstates_2[i][j][o][s]; 825 826 } 827 828 /** Assign each gridpoint a new random position on the grid. This simulated mixing, 829 * but does not guarantee a "well-mixed" system per se (interactions are still) 830 * calculated based on neighbourhoods. 831 */ 832 perfectMix() { 833 let all_gridpoints = []; 834 for (let i = 0; i < this.nc; i++) 835 for (let j = 0; j < this.nr; j++) 836 all_gridpoints.push(this.getGridpoint(i, j)); 837 838 all_gridpoints = shuffle(all_gridpoints, this.rng); 839 840 for (let i = 0; i < all_gridpoints.length; i++) 841 this.setGridpoint(i % this.nc, Math.floor(i / this.nc), all_gridpoints[i]); 842 return "Perfectly mixed the grid" 843 } 844 845 /** Apply diffusion algorithm for grid-based models described in Toffoli & Margolus' book "Cellular automata machines" 846 * The idea is to subdivide the grid into 2x2 neighbourhoods, and rotate them (randomly CW or CCW). To avoid particles 847 * simply being stuck in their own 2x2 subspace, different 2x2 subspaces are taken each iteration (CW in even iterations, 848 * CCW in odd iterations) 849 */ 850 MargolusDiffusion() { 851 // 852 // A B 853 // D C 854 // a = backup of A 855 // rotate cw or ccw randomly 856 let even = this.margolus_phase % 2 == 0; 857 if ((this.nc % 2 + this.nr % 2) > 0) throw "Do not use margolusDiffusion with an uneven number of cols / rows!" 858 859 for (let i = 0 + even; i < this.nc; i += 2) { 860 if(i> this.nc-2) continue 861 for (let j = 0 + even; j < this.nr; j += 2) { 862 if(j> this.nr-2) continue 863 // console.log(i,j) 864 let old_A = new Gridpoint(this.grid[i][j]); 865 let A = this.getGridpoint(i, j); 866 let B = this.getGridpoint(i + 1, j); 867 let C = this.getGridpoint(i + 1, j + 1); 868 let D = this.getGridpoint(i, j + 1); 869 870 if (this.rng.random() < 0.5) // CW = clockwise rotation 871 { 872 A = D; 873 D = C; 874 C = B; 875 B = old_A; 876 } 877 else { 878 A = B; // CCW = counter clockwise rotation 879 B = C; 880 C = D; 881 D = old_A; 882 } 883 this.setGridpoint(i, j, A); 884 this.setGridpoint(i + 1, j, B); 885 this.setGridpoint(i + 1, j + 1, C); 886 this.setGridpoint(i, j + 1, D); 887 } 888 } 889 this.margolus_phase++; 890 } 891 892 /** 893 * Adds a dygraph-plot to your DOM (if the DOM is loaded) 894 * @param {Array} graph_labels Array of strings for the graph legend 895 * @param {Array} graph_values Array of floats to plot (here plotted over time) 896 * @param {Array} cols Array of colours to use for plotting 897 * @param {String} title Title of the plot 898 * @param {Object} opts dictionary-style list of opts to pass onto dygraphs 899 */ 900 plotArray(graph_labels, graph_values, cols, title, opts) { 901 if (typeof window == 'undefined') return 902 if (!(title in this.graphs)) { 903 cols = parseColours(cols); 904 graph_values.unshift(this.time); 905 graph_labels.unshift("Time"); 906 this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts); 907 } 908 else { 909 if (this.time % this.graph_interval == 0) { 910 graph_values.unshift(this.time); 911 graph_labels.unshift("Time"); 912 this.graphs[title].push_data(graph_values); 913 } 914 if (this.time % this.graph_update == 0) { 915 this.graphs[title].update(); 916 } 917 } 918 } 919 920 /** 921 * Adds a dygraph-plot to your DOM (if the DOM is loaded) 922 * @param {Array} graph_labels Array of strings for the graph legend 923 * @param {Array} graph_values Array of 2 floats to plot (first value for x-axis, second value for y-axis) 924 * @param {Array} cols Array of colours to use for plotting 925 * @param {String} title Title of the plot 926 * @param {Object} opts dictionary-style list of opts to pass onto dygraphs 927 */ 928 plotXY(graph_labels, graph_values, cols, title, opts) { 929 if (typeof window == 'undefined') return 930 if (!(title in this.graphs)) { 931 cols = parseColours(cols); 932 this.graphs[title] = new Graph(graph_labels, graph_values, cols, title, opts); 933 } 934 else { 935 if (this.time % this.graph_interval == 0) { 936 this.graphs[title].push_data(graph_values); 937 } 938 if (this.time % this.graph_update == 0) { 939 this.graphs[title].update(); 940 } 941 } 942 943 } 944 945 /** 946 * Easy function to add a pop-sizes plot (wrapper for plotArrays) 947 * @param {String} property What property to plot (needs to exist in your model, e.g. "species" or "alive") 948 * @param {Array} values Which values are plotted (e.g. [1,3,4,6]) 949 */ 950 plotPopsizes(property, values) { 951 if (typeof window == 'undefined') return 952 if (this.time % this.graph_interval != 0 && this.graphs[`Population sizes (${this.name})`] !== undefined) return 953 // Wrapper for plotXY function, which expects labels, values, colours, and a title for the plot: 954 // Labels 955 let graph_labels = []; 956 for (let val of values) { graph_labels.push(property + '_' + val); } 957 958 // Values 959 let popsizes = this.getPopsizes(property, values); 960 //popsizes.unshift(this.time) 961 let graph_values = popsizes; 962 963 // Colours 964 let colours = []; 965 966 for (let c of values) { 967 //console.log(this.statecolours[property][c]) 968 if (this.statecolours[property].constructor != Object) 969 colours.push(this.statecolours[property]); 970 else 971 colours.push(this.statecolours[property][c]); 972 } 973 // Title 974 let title = "Population sizes (" + this.name + ")"; 975 976 this.plotArray(graph_labels, graph_values, colours, title); 977 978 979 980 //this.graph = new Graph(graph_labels,graph_values,colours,"Population sizes ("+this.name+")") 981 } 982 983 /** 984 * Easy function to add a ODE states (wrapper for plot array) 985 * @param {String} ODE name Which ODE to plot the states for 986 * @param {Array} values Which states are plotted (if undefined, all of them are plotted) 987 */ 988 plotODEstates(odename, values, colours) { 989 if (typeof window == 'undefined') return 990 if (this.time % this.graph_interval != 0 && this.graphs[`Average ODE states (${this.name})`] !== undefined) return 991 // Labels 992 let graph_labels = []; 993 for (let val of values) { graph_labels.push(odename + '_' + val); } 994 // Values 995 let ode_states = this.getODEstates(odename, values); 996 // Title 997 let title = "Average ODE states (" + this.name + ")"; 998 this.plotArray(graph_labels, ode_states, colours, title); 999 } 1000 1001 resetPlots() { 1002 this.time = 0; 1003 for (let g in this.graphs) { 1004 this.graphs[g].reset_plot(); 1005 } 1006 } 1007 /** 1008 * Returns an array with the population sizes of different types 1009 * @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive") 1010 * @param {Array} values Which values are counted and returned (e.g. [1,3,4,6]) 1011 */ 1012 getPopsizes(property, values) { 1013 let sum = Array(values.length).fill(0); 1014 for (let i = 0; i < this.nc; i++) { 1015 for (let j = 0; j < this.nr; j++) { 1016 for (let val in values) 1017 if (this.grid[i][j][property] == values[val]) sum[val]++; 1018 } 1019 } 1020 return sum; 1021 } 1022 1023 /** 1024 * Returns an array with the population sizes of different types 1025 * @param {String} property Return popsizes for this property (needs to exist in your model, e.g. "species" or "alive") 1026 * @param {Array} values Which values are counted and returned (e.g. [1,3,4,6]) 1027 */ 1028 getODEstates(odename, values) { 1029 let sum = Array(values.length).fill(0); 1030 for (let i = 0; i < this.nc; i++) 1031 for (let j = 0; j < this.nr; j++) 1032 for (let val in values) 1033 sum[val] += this.grid[i][j][odename].state[val] / (this.nc * this.nr); 1034 return sum; 1035 } 1036 1037 1038 1039 /** 1040 * Attaches an ODE to all GPs in the model. Each gridpoint has it's own ODE. 1041 * @param {function} eq Function that describes the ODEs, see examples starting with "ode" 1042 * @param {Object} conf dictionary style configuration of your ODEs (initial state, parameters, etc.) 1043 */ 1044 attachODE(eq, conf) { 1045 for (let i = 0; i < this.nc; i++) { 1046 for (let j = 0; j < this.nr; j++) { 1047 let ode = new ODE(eq, conf.init_states, conf.parameters, conf.diffusion_rates, conf.ode_name, conf.acceptable_error); 1048 if (typeof this.grid[i][j].ODEs == "undefined") this.grid[i][j].ODEs = []; // If list doesnt exist yet 1049 this.grid[i][j].ODEs.push(ode); 1050 if (conf.ode_name) this.grid[i][j][conf.ode_name] = ode; 1051 } 1052 } 1053 } 1054 1055 /** 1056 * Numerically solve the ODEs for each grid point 1057 * @param {float} delta_t Step size 1058 * @param {bool} opt_pos When enabled, negative values are set to 0 automatically 1059 */ 1060 solve_all_odes(delta_t = 0.1, opt_pos = false) { 1061 for (let i = 0; i < this.nc; i++) { 1062 for (let j = 0; j < this.nr; j++) { 1063 for (let ode of this.grid[i][j].ODEs) { 1064 ode.solveTimestep(delta_t, opt_pos); 1065 } 1066 } 1067 } 1068 } 1069 1070 /** 1071 * Print the entire grid to the console. Not always recommended, but useful for debugging :D 1072 * @param {float} property What property is printed 1073 * @param {float} fract Subset to be printed (from the top-left) 1074 */ 1075 printGrid(property, fract) { 1076 let ncol = this.nc; 1077 let nrow = this.nr; 1078 1079 if (fract != undefined) ncol *= fract, nrow *= fract; 1080 let grid = new Array(nrow); // Makes a column or <rows> long --> grid[cols] 1081 for (let i = 0; i < ncol; i++) 1082 grid[i] = new Array(ncol); // Insert a row of <cols> long --> grid[cols][rows] 1083 for (let j = 0; j < nrow; j++) 1084 grid[i][j] = this.grid[i][j][property]; 1085 1086 console.table(grid); 1087 } 1088 } 1089 1090 //////////////////////////////////////////////////////////////////////////////////////////////////// 1091 // The functions below are not methods of grid-model as they are never unique for a particular model. 1092 //////////////////////////////////////////////////////////////////////////////////////////////////// 1093 1094 /** 1095 * Make a grid, or when a template is given, a COPY of a grid. 1096 * @param {int} cols Width of the new grid 1097 * @param {int} rows Height of the new grid 1098 * @param {2DArray} template Template to be used for copying (if not set, a new empty grid is made) 1099 */ 1100 function MakeGrid(cols, rows, template) { 1101 let grid = new Array(rows); // Makes a column or <rows> long --> grid[cols] 1102 for (let i = 0; i < cols; i++) { 1103 grid[i] = new Array(cols); // Insert a row of <cols> long --> grid[cols][rows] 1104 for (let j = 0; j < rows; j++) { 1105 if (template) grid[i][j] = new Gridpoint(template[i][j]); // Make a deep or shallow copy of the GP 1106 else grid[i][j] = new Gridpoint(); 1107 } 1108 } 1109 1110 return grid; 1111 } 1112 1113 /** 1114 * Make a back-up of all the ODE states (for synchronous ODE updating) 1115 * @param {int} cols Width of the grid 1116 * @param {int} rows Height of the grid 1117 * @param {2DArray} template Get ODE states from here 1118 */ 1119 function CopyGridODEs(cols, rows, template) { 1120 let grid = new Array(rows); // Makes a column or <rows> long --> grid[cols] 1121 for (let i = 0; i < cols; i++) { 1122 grid[i] = new Array(cols); // Insert a row of <cols> long --> grid[cols][rows] 1123 for (let j = 0; j < rows; j++) { 1124 for (let o = 0; o < template[i][j].ODEs.length; o++) // every ode 1125 { 1126 grid[i][j] = []; 1127 let states = []; 1128 for (let s = 0; s < template[i][j].ODEs[o].state.length; s++) // every state 1129 states.push(template[i][j].ODEs[o].state[s]); 1130 grid[i][j][o] = states; 1131 } 1132 } 1133 } 1134 1135 return grid; 1136 } 1137 1138 /** 1139 * Reverse dictionary 1140 * @param {Object} obj dictionary-style object to reverse in order 1141 */ 1142 function dict_reverse(obj) { 1143 let new_obj = {}; 1144 let rev_obj = Object.keys(obj).reverse(); 1145 rev_obj.forEach(function (i) { 1146 new_obj[i] = obj[i]; 1147 }); 1148 return new_obj; 1149 } 1150 1151 /** 1152 * Randomly shuffle an array with custom RNG 1153 * @param {Array} array array to be shuffled 1154 * @param {MersenneTwister} rng MersenneTwister RNG 1155 */ 1156 function shuffle(array, rng) { 1157 let i = array.length; 1158 while (i--) { 1159 const ri = Math.floor(rng.random() * (i + 1)); 1160 [array[i], array[ri]] = [array[ri], array[i]]; 1161 } 1162 return array; 1163 } 1164 1165 /** 1166 * Convert colour string to RGB. Works for colour names ('red','blue' or other colours defined in cacatoo), but also for hexadecimal strings 1167 * @param {String} string string to convert to RGB 1168 */ 1169 function stringToRGB(string) { 1170 if (string[0] != '#') return nameToRGB(string) 1171 else return hexToRGB(string) 1172 } 1173 1174 /** 1175 * Convert hexadecimal to RGB 1176 * @param {String} hex string to convert to RGB 1177 */ 1178 function hexToRGB(hex) { 1179 var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 1180 return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] 1181 } 1182 1183 /** 1184 * Convert colour name to RGB 1185 * @param {String} name string to look up in the set of known colours (see below) 1186 */ 1187 function nameToRGB(string) { 1188 let colours = { 1189 'black': [0, 0, 0], 1190 'white': [255, 255, 255], 1191 'red': [255, 0, 0], 1192 'blue': [0, 0, 255], 1193 'green': [0, 255, 0], 1194 'darkgrey': [40, 40, 40], 1195 'lightgrey': [180, 180, 180], 1196 'violet': [148, 0, 211], 1197 'turquoise': [64, 224, 208], 1198 'orange': [255, 165, 0], 1199 'gold': [240, 200, 0], 1200 'grey': [125, 125, 125], 1201 'yellow': [255, 255, 0], 1202 'cyan': [0, 255, 255], 1203 'aqua': [0, 255, 255], 1204 'silver': [192, 192, 192], 1205 'nearwhite': [192, 192, 192], 1206 'purple': [128, 0, 128], 1207 'darkgreen': [0, 128, 0], 1208 'olive': [128, 128, 0], 1209 'teal': [0, 128, 128], 1210 'navy': [0, 0, 128] 1211 1212 }; 1213 let c = colours[string]; 1214 if (c == undefined) throw new Error(`Cacatoo has no colour with name '${string}'`) 1215 return c 1216 } 1217 1218 /** 1219 * Make sure all colours, even when of different types, are stored in the same format (RGB, as cacatoo uses internally) 1220 * @param {Array} cols array of strings, or [R,G,B]-arrays. Only strings are converted, other returned. 1221 */ 1222 1223 function parseColours(cols) { 1224 let return_cols = []; 1225 for (let c of cols) { 1226 if (typeof c === 'string' || c instanceof String) { 1227 return_cols.push(stringToRGB(c)); 1228 } 1229 else { 1230 return_cols.push(c); 1231 } 1232 } 1233 return return_cols 1234 } 1235 1236 /** 1237 * A list of default colours if nothing is given by the user. 1238 */ 1239 let default_colours = { 1240 0: [0, 0, 0], // black 1241 1: [255, 255, 255], // white 1242 2: [255, 0, 0], // red 1243 3: [0, 0, 255], // blue 1244 4: [0, 255, 0], //green 1245 5: [60, 60, 60], //darkgrey 1246 6: [180, 180, 180], //lightgrey 1247 7: [148, 0, 211], //violet 1248 8: [64, 224, 208], //turquoise 1249 9: [255, 165, 0], //orange 1250 10: [240, 200, 0], //gold 1251 11: [125, 125, 125], 1252 12: [255, 255, 0], // yellow 1253 13: [0, 255, 255], // cyan 1254 14: [192, 192, 192], // silver 1255 15: [0, 128, 0], //darkgreen 1256 16: [128, 128, 0], // olive 1257 17: [0, 128, 128], // teal 1258 18: [0, 0, 128] 1259 }; // navy 1260 1261 /** 1262 * Canvas is a wrapper-class for a HTML-canvas element. It is linked to a @Gridmodel object, and stores what from that @Gridmodel should be displayed (width, height, property, scale, etc.) 1263 */ 1264 1265 class Canvas { 1266 /** 1267 * The constructor function for a @Canvas object. 1268 * @param {Gridmodel} gridmodel The gridmodel to which this canvas belongs 1269 * @param {string} property the property that should be shown on the canvas 1270 * @param {int} height height of the canvas (in rows) 1271 * @param {int} width width of the canvas (in cols) 1272 * @param {scale} scale of the canvas (width/height of each gridpoint in pixels) 1273 */ 1274 constructor(gridmodel, prop, lab, height, width, scale) { 1275 this.label = lab; 1276 this.gridmodel = gridmodel; 1277 this.property = prop; 1278 this.height = height; 1279 this.width = width; 1280 this.scale = scale; 1281 this.bgcolor = 'black'; 1282 1283 if (typeof document !== "undefined") // In browser, crease a new HTML canvas-element to draw on 1284 { 1285 this.elem = document.createElement("canvas"); 1286 this.titlediv = document.createElement("div"); 1287 this.titlediv.innerHTML = "<font size = 2>" + this.label + "</font>"; 1288 this.canvasdiv = document.createElement("div"); 1289 this.canvasdiv.className = "grid-holder"; 1290 this.elem.className = "grid-holder"; 1291 this.elem.width = this.width * this.scale; 1292 this.elem.height = this.height * this.scale; 1293 this.canvasdiv.appendChild(this.elem); 1294 this.canvasdiv.appendChild(this.titlediv); 1295 document.getElementById("canvas_holder").appendChild(this.canvasdiv); 1296 this.ctx = this.elem.getContext("2d"); 1297 } 1298 1299 } 1300 1301 /** 1302 * Draw the state of the Gridmodel (for a specific property) onto the HTML element 1303 */ 1304 displaygrid() { 1305 let ctx = this.ctx; 1306 let scale = this.scale; 1307 let ncol = this.width; 1308 let nrow = this.height; 1309 let prop = this.property; 1310 ctx.clearRect(0, 0, scale * ncol, scale * nrow); 1311 1312 ctx.fillStyle = this.bgcolor; 1313 ctx.fillRect(0, 0, ncol * scale, nrow * scale); 1314 var id = ctx.getImageData(0, 0, scale * ncol, scale * nrow); 1315 var pixels = id.data; 1316 for (let i = 0; i < ncol; i++) // i are cols 1317 { 1318 for (let j = 0; j < nrow; j++) // j are rows 1319 { 1320 if (!(prop in this.gridmodel.grid[i][j])) continue 1321 let statecols = this.gridmodel.statecolours[prop]; 1322 1323 1324 1325 let value = this.gridmodel.grid[i][j][prop]; 1326 if(this.multiplier !== undefined) value *= this.multiplier; 1327 if(this.maxval && value>this.maxval) value = this.maxval; 1328 if(this.minval && value<this.minval) value = this.minval; 1329 1330 if (statecols[value] == undefined) // Don't draw the background state 1331 continue 1332 let idx; 1333 if (statecols.constructor == Object) { 1334 idx = statecols[value]; 1335 } 1336 else idx = statecols; 1337 1338 for (let n = 0; n < scale; n++) { 1339 for (let m = 0; m < scale; m++) { 1340 let x = i * scale + n; 1341 let y = j * scale + m; 1342 var off = (y * id.width + x) * 4; 1343 pixels[off] = idx[0]; 1344 pixels[off + 1] = idx[1]; 1345 pixels[off + 2] = idx[2]; 1346 } 1347 } 1348 1349 1350 } 1351 } 1352 ctx.putImageData(id, 0, 0); 1353 } 1354 } 1355 1356 /** 1357 * Simulation is the global class of Cacatoo, containing the main configuration 1358 * for making a grid-based model grid and displaying it in either browser or with 1359 * nodejs. 1360 */ 1361 class Simulation { 1362 /** 1363 * The constructor function for a @Simulation object. Takes a config dictionary. 1364 * and sets options accordingly. 1365 * @param {dictionary} config A dictionary (object) with all the necessary settings to setup a Cacatoo simulation. 1366 */ 1367 constructor(config) { 1368 this.config = config; 1369 this.rng = new MersenneTwister(config.seed || 53); 1370 this.sleep = config.sleep || 0; 1371 this.fps = config.fps * 1.4 || 60; 1372 this.limitfps = true; 1373 if (config.limitfps == false) this.limitfps = false; 1374 1375 // Three arrays for all the grids ('CAs'), canvases ('displays'), and graphs 1376 this.gridmodels = []; // All gridmodels in this simulation 1377 this.canvases = []; // Array with refs to all canvases (from all models) from this simulation 1378 this.graphs = []; // All graphs 1379 this.time = 0; 1380 } 1381 1382 /** 1383 * Generate a new GridModel within this simulation. 1384 * @param {string} name The name of your new model, e.g. "gol" for game of life. Cannot contain whitespaces. 1385 */ 1386 makeGridmodel(name) { 1387 if (name.indexOf(' ') >= 0) throw new Error("The name of a gridmodel cannot contain whitespaces.") 1388 let model = new Gridmodel(name, this.config, this.rng); // ,this.config.show_gridname weggecomment 1389 this[name] = model; // this = model["cheater"] = CA-obj 1390 this.gridmodels.push(model); 1391 } 1392 1393 /** 1394 * Create a display for a gridmodel, showing a certain property on it. 1395 * @param {string} name The name of an existing gridmodel to display 1396 * @param {string} property The name of the property to display 1397 * @param {string} customlab Overwrite the display name with something more descriptive 1398 * @param {integer} height Number of rows to display (default = ALL) 1399 * @param {integer} width Number of cols to display (default = ALL) 1400 * @param {integer} scale Scale of display (default inherited from @Simulation class) 1401 */ 1402 createDisplay(name, property, customlab, height, width, scale, x, y) { 1403 let label = customlab; 1404 if (customlab == undefined) label = `${name} (${property})`; // <ID>_NAME_(PROPERTY) 1405 let gridmodel = this[name]; 1406 if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`) 1407 if (height == undefined) height = gridmodel.nr; 1408 if (width == undefined) width = gridmodel.nc; 1409 if (scale == undefined) scale = gridmodel.scale; 1410 1411 let cnv = new Canvas(gridmodel, property, label, height, width, scale); 1412 gridmodel.canvases[label] = cnv; // Add a reference to the canvas to the gridmodel 1413 this.canvases.push(cnv); // Add a reference to the canvas to the sim 1414 const canvas = cnv.elem; 1415 canvas.addEventListener('mousedown', (e) => { this.getCursorPosition(canvas, e, scale); }); 1416 cnv.displaygrid(); 1417 } 1418 1419 /** 1420 * Create a display for a gridmodel, showing a certain property on it. 1421 * @param {string} name The name of an existing gridmodel to display 1422 * @param {string} property The name of the property to display 1423 * @param {string} customlab Overwrite the display name with something more descriptive 1424 * @param {integer} height Number of rows to display (default = ALL) 1425 * @param {integer} width Number of cols to display (default = ALL) 1426 * @param {integer} scale Scale of display (default inherited from @Simulation class) 1427 */ 1428 createDisplay_c(config) { 1429 1430 let name = config.model; 1431 1432 let property = config.property; 1433 1434 1435 let label = config.label; 1436 if (label == undefined) label = `${name} (${property})`; // <ID>_NAME_(PROPERTY) 1437 let gridmodel = this[name]; 1438 if (gridmodel == undefined) throw new Error(`There is no GridModel with the name ${name}`) 1439 1440 let height = config.height || this[name].nr; 1441 let width = config.width || this[name].nc; 1442 let scale = config.scale || this[name].scale; 1443 1444 1445 let maxval = config.maxval | 100; 1446 let minval = config.minval; 1447 let multiplier = config.multiplier; 1448 1449 1450 if(config.fill == "viridis") this[name].colourViridis(property, maxval); 1451 else this[name].colourGradient(property, maxval, [0, 0, 0], [0, 0, 150], [100,100,150]); 1452 1453 let cnv = new Canvas(gridmodel, property, label, height, width, scale); 1454 gridmodel.canvases[label] = cnv; // Add a reference to the canvas to the gridmodel 1455 if (maxval != undefined) cnv.maxval = maxval; 1456 if (minval != undefined) cnv.minval = minval; 1457 if (multiplier != undefined) cnv.multiplier = multiplier; 1458 1459 this.canvases.push(cnv); // Add a reference to the canvas to the sim 1460 const canvas = cnv.elem; 1461 canvas.addEventListener('mousedown', (e) => { this.getCursorPosition(canvas, e, scale); }); 1462 cnv.displaygrid(); 1463 } 1464 1465 1466 /** 1467 * Create a display for a gridmodel, showing a certain property on it. 1468 * @param {canvas} canvas A (constant) canvas object 1469 * @param {event-handler} event Event handler (mousedown) 1470 * @param {scale} scale The zoom (scale) of the grid to grab the correct grid point 1471 */ 1472 getCursorPosition(canvas, event, scale) { 1473 const rect = canvas.getBoundingClientRect(); 1474 const x = Math.floor(Math.max(0, event.clientX - rect.left) / scale); 1475 const y = Math.floor(Math.max(0, event.clientY - rect.top) / scale); 1476 console.log(`You have clicked the grid at position ${x},${y}`); 1477 for (let model of this.gridmodels) { 1478 console.log(`This corresponds to gridpoint...`); 1479 console.log(model.grid[x][y]); 1480 console.log(`... in model ${model.name}`); 1481 } 1482 } 1483 1484 1485 1486 /** 1487 * Update all the grid models one step. Apply optional mixing 1488 */ 1489 step() { 1490 for (let i = 0; i < this.gridmodels.length; i++) 1491 this.gridmodels[i].update(); 1492 } 1493 1494 /** 1495 * Apply global events to all grids in the model. 1496 * (only perfectmix currently... :D) 1497 */ 1498 events() { 1499 for (let i = 0; i < this.gridmodels.length; i++) { 1500 if (this.mix) this.gridmodels[i].perfectMix(); 1501 } 1502 } 1503 1504 /** 1505 * Display all the canvases linked to this simulation 1506 */ 1507 display() { 1508 for (let i = 0; i < this.canvases.length; i++) 1509 this.canvases[i].displaygrid(); 1510 } 1511 1512 /** 1513 * Start the simulation. start() detects whether the user is running the code from the browser of 1514 * in nodejs. In the browser, a GUI is provided to interact with the model. In nodejs the 1515 * programmer can simply wait for the result without wasting time on displaying intermediate stuff 1516 * (which can be slow) 1517 */ 1518 start() { 1519 let model = this; // Caching this, as function animate changes the this-scope to the scope of the animate-function 1520 if (typeof window != 'undefined') { 1521 let meter = new FPSMeter({ show: 'ms', left: "auto", top: "45px", right: "50px", graph: 1, history: 20, smoothing: 30 }); 1522 1523 document.title = `Cacatoo - ${this.config.title}`; 1524 1525 if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML = `<a target="blank" href="https://bramvandijk88.github.io/cacatoo/"><img class="logos" src="/images/elephant_cacatoo_small.png"></a>`; 1526 if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML += `<a target="blank" href="https://github.com/bramvandijk88/cacatoo"><img class="logos" style="padding-top:32px;" src="/images/gh.png"></a></img>`; 1527 if (document.getElementById("header") != null) document.getElementById("header").innerHTML = `<h2>Cacatoo - ${this.config.title}</h2><font size=2>${this.config.description}</font size>`; 1528 if (document.getElementById("footer") != null) document.getElementById("footer").innerHTML += "<h2>Cacatoo is a toolbox to explore individual-based models straight from your webbrowser. It is still in development. Feedback <a href=\"https://www.bramvandijk.com/contact\">very welcome.</a></h2>"; 1529 let simStartTime = performance.now(); 1530 1531 async function animate() { 1532 if (model.config.fastmode) // Fast-mode tracks the performance so that frames can be skipped / paused / etc. Has some overhead, so use wisely! 1533 { 1534 if (model.sleep > 0) await pause(model.sleep); 1535 1536 let t = 0; // Will track cumulative time per step in microseconds 1537 1538 while (t < 16.67 * 60 / model.fps) //(t < 16.67) results in 60 fps if possible 1539 { 1540 let startTime = performance.now(); 1541 model.step(); 1542 model.events(); 1543 let endTime = performance.now(); 1544 t += (endTime - startTime); 1545 model.time++; 1546 if (!model.limitfps) break 1547 } 1548 model.display(); 1549 meter.tick(); 1550 } 1551 else // A slightly more simple setup, but does not allow controls like frame-rate, skipping every nth frame, etc. 1552 { 1553 meter.tickStart(); 1554 model.step(); 1555 model.events(); 1556 model.display(); 1557 meter.tick(); 1558 model.time++; 1559 } 1560 1561 let frame = requestAnimationFrame(animate); 1562 if (model.time >= model.config.maxtime) { 1563 let simStopTime = performance.now(); 1564 console.log("Cacatoo completed after", Math.round(simStopTime - simStartTime) / 1000, "seconds"); 1565 cancelAnimationFrame(frame); 1566 } 1567 if (model.pause == true) { cancelAnimationFrame(frame); } 1568 1569 } 1570 1571 requestAnimationFrame(animate); 1572 } 1573 else { 1574 while (true) { 1575 model.step(); 1576 model.time++; 1577 } 1578 } 1579 } 1580 1581 /** 1582 * initialGrid populates a grid with states. The number of arguments 1583 * is flexible and defined the percentage of every state. For example, 1584 * initialGrid('grid','species',1,0.5,2,0.25) populates the grid with 1585 * two species (1 and 2), where species 1 occupies 50% of the grid, and 1586 * species 2 25%. 1587 * @param {@GridModel} grid The gridmodel containing the grid to be modified. 1588 * @param {String} property The name of the state to be set 1589 * @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n) 1590 * @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n) 1591 */ 1592 initialGrid(gridmodel, property) { 1593 let p = property || 'val'; 1594 let bg = 0; 1595 1596 for (let i = 0; i < gridmodel.nc; i++) // i are columns 1597 for (let j = 0; j < gridmodel.nr; j++) // j are rows 1598 gridmodel.grid[i][j][p] = bg; 1599 1600 for (let arg = 2; arg < arguments.length; arg += 2) // Parse remaining 2+ arguments to fill the grid 1601 for (let i = 0; i < gridmodel.nc; i++) // i are columns 1602 for (let j = 0; j < gridmodel.nr; j++) // j are rows 1603 if (this.rng.random() < arguments[arg + 1]) gridmodel.grid[i][j][p] = arguments[arg]; 1604 } 1605 1606 /** 1607 * populateGrid populates a grid with custom individuals. 1608 * @param {@GridModel} grid The gridmodel containing the grid to be modified. 1609 * @param {Array} individuals The properties for individuals 1..n 1610 * @param {Array} freqs The initial frequency of individuals 1..n 1611 */ 1612 populateGrid(gridmodel,individuals,freqs) 1613 { 1614 let sumfreqs =0; 1615 if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies") 1616 for(let i=0; i<freqs.length; i++) sumfreqs += freqs[i]; 1617 1618 for (let i = 0; i < gridmodel.nc; i++) // i are columns 1619 for (let j = 0; j < gridmodel.nr; j++){ // j are rows 1620 let cumsumfreq = 0; 1621 for (const property in individuals[0]) { 1622 gridmodel.grid[i][j][property] = 0; 1623 } 1624 for(let n=0; n<individuals.length; n++) 1625 { 1626 cumsumfreq += freqs[n]; 1627 if(this.rng.random() < cumsumfreq) { 1628 Object.assign(gridmodel.grid[i][j],individuals[n]); 1629 break 1630 } 1631 } 1632 } 1633 1634 } 1635 1636 /** 1637 * initialSpot populates a grid with states. Grid points close to a certain coordinate are set to state value, while 1638 * other cells are set to the bg-state of 0. 1639 * @param {@GridModel} grid The gridmodel containing the grid to be modified. 1640 * @param {String} property The name of the state to be set 1641 * @param {integer} value The value of the state to be set (optional argument with position 2, 4, 6, ..., n) 1642 * @param {float} fraction The chance the grid point is set to this state (optional argument with position 3, 5, 7, ..., n) 1643 */ 1644 initialSpot(gridmodel, property, value, size, x, y) { 1645 let p = property || 'val'; 1646 1647 // Draw a circle 1648 for (let i = 0; i < gridmodel.nc; i++) // i are columns 1649 for (let j = 0; j < gridmodel.nr; j++) // j are rows 1650 { 1651 if ((Math.pow((i - x), 2) + Math.pow((j - y), 2)) < size) 1652 gridmodel.grid[i % gridmodel.nc][j % gridmodel.nr][p] = value; 1653 else 1654 gridmodel.grid[i % gridmodel.nc][j % gridmodel.nr][p] = 0; 1655 } 1656 } 1657 1658 /** 1659 * populateSpot populates a spot with custom individuals. 1660 * @param {@GridModel} grid The gridmodel containing the grid to be modified. 1661 * @param {Array} individuals The properties for individuals 1..n 1662 * @param {Array} freqs The initial frequency of individuals 1..n 1663 */ 1664 populateSpot(gridmodel,individuals, freqs,size, x, y) 1665 { 1666 let sumfreqs =0; 1667 if(individuals.length != freqs.length) throw new Error("populateGrid should have as many individuals as frequencies") 1668 for(let i=0; i<freqs.length; i++) sumfreqs += freqs[i]; 1669 1670 // Draw a circle 1671 for (let i = 0; i < gridmodel.nc; i++) // i are columns 1672 for (let j = 0; j < gridmodel.nr; j++) // j are rows 1673 { 1674 for (const property in individuals[0]) gridmodel.grid[i][j][property] = 0; 1675 1676 if ((Math.pow((i - x), 2) + Math.pow((j - y), 2)) < size) 1677 { 1678 let cumsumfreq = 0; 1679 for(let n=0; n<individuals.length; n++) 1680 { 1681 cumsumfreq += freqs[n]; 1682 if(this.rng.random() < cumsumfreq) { 1683 Object.assign(gridmodel.grid[i % gridmodel.nc][j % gridmodel.nr],individuals[n]); 1684 break 1685 } 1686 } 1687 } 1688 } 1689 1690 } 1691 1692 1693 /** 1694 * addButton adds a HTML button which can be linked to a function by the user. 1695 * @param {string} text Text displayed on the button 1696 * @param {function} func Function to be linked to the button 1697 */ 1698 addButton(text, func) { 1699 if (typeof window == 'undefined') return 1700 let button = document.createElement("button"); 1701 button.innerHTML = text; 1702 button.addEventListener("click", func, true); 1703 document.getElementById("form_holder").appendChild(button); 1704 } 1705 1706 /** 1707 * addSlider adds a HTML slider to the DOM-environment which allows the user 1708 * to modify a model parameter at runtime. 1709 * @param {string} parameter The name of the (global!) parameter to link to the slider 1710 * @param {float} [min] Minimal value of the slider 1711 * @param {float} [max] Maximum value of the slider 1712 * @param {float} [step] Step-size when modifying 1713 */ 1714 addSlider(parameter, min = 0.0, max = 2.0, step = 0.01, label) { 1715 let lab = label || parameter; 1716 if (typeof window == "undefined") return 1717 if (window[parameter] === undefined) { console.warn(`addSlider: parameter ${parameter} not found. No slider made.`); return; } 1718 let container = document.createElement("div"); 1719 container.classList.add("form-container"); 1720 1721 let slider = document.createElement("input"); 1722 let numeric = document.createElement("input"); 1723 container.innerHTML += "<div style='width:100%;height:20px;font-size:12px;'><b>" + lab + ":</b></div>"; 1724 1725 // Setting slider variables / handler 1726 slider.type = 'range'; 1727 slider.classList.add("slider"); 1728 slider.min = min; 1729 slider.max = max; 1730 slider.step = step; 1731 slider.value = window[parameter]; 1732 slider.oninput = function () { 1733 let value = parseFloat(slider.value); 1734 window[parameter] = parseFloat(value); 1735 numeric.value = value; 1736 }; 1737 1738 // Setting number variables / handler 1739 numeric.type = 'number'; 1740 numeric.classList.add("number"); 1741 numeric.min = min; 1742 numeric.max = max; 1743 numeric.step = step; 1744 numeric.value = window[parameter]; 1745 numeric.onchange = function () { 1746 let value = parseFloat(numeric.value); 1747 if (value > this.max) value = this.max; 1748 if (value < this.min) value = this.min; 1749 window[parameter] = parseFloat(value); 1750 numeric.value = value; 1751 slider.value = value; 1752 }; 1753 container.appendChild(slider); 1754 container.appendChild(numeric); 1755 document.getElementById("form_holder").appendChild(container); 1756 } 1757 addHTML(div, html) { 1758 if (typeof window == "undefined") return 1759 let container = document.createElement("div"); 1760 container.innerHTML += html; 1761 document.getElementById(div).appendChild(container); 1762 } 1763 log(msg, target) { 1764 if (typeof window == "undefined") console.log(msg); 1765 else if (typeof target == "undefined") console.log(msg); 1766 else document.getElementById(target).innerHTML += `${msg}<br>`; 1767 } 1768 /** 1769 * addPatternButton adds a pattern button to the HTML environment which allows the user 1770 * to load a PNG which then sets the state of 'proparty' for the @GridModel. 1771 * (currently only supports black and white image) 1772 * @param {@GridModel} targetgrid The gridmodel containing the grid to be modified. 1773 * @param {String} property The name of the state to be set 1774 */ 1775 addPatternButton(targetgrid, property) { 1776 let imageLoader = document.createElement("input"); 1777 imageLoader.type = "file"; 1778 imageLoader.id = "imageLoader"; 1779 let sim = this; 1780 imageLoader.style = "display:none"; 1781 imageLoader.name = "imageLoader"; 1782 document.getElementById("form_holder").appendChild(imageLoader); 1783 let label = document.createElement("label"); 1784 label.setAttribute("for", "imageLoader"); 1785 label.style = "background-color: rgb(217, 234, 245);border-radius: 10px;border: 2px solid rgb(177, 209, 231);padding:7px;font-size:12px;margin:10px;width:128px;"; 1786 label.innerHTML = "<font size=2>Select your own initial state</font>"; 1787 document.getElementById("form_holder").appendChild(label); 1788 let canvas = document.createElement('canvas'); 1789 canvas.name = "imageCanvas"; 1790 let ctx = canvas.getContext('2d'); 1791 function handleImage(e) { 1792 let reader = new FileReader(); 1793 let grid_data; 1794 1795 let grid = e.currentTarget.grid; 1796 reader.onload = function (event) { 1797 var img = new Image(); 1798 img.onload = function () { 1799 canvas.width = img.width; 1800 canvas.height = img.height; 1801 ctx.drawImage(img, 0, 0); 1802 1803 grid_data = get2DFromCanvas(canvas); 1804 1805 for (let i = 0; i < grid.nc; i++) for (let j = 0; j < grid.nr; j++) grid.grid[i][j].alive = 0; 1806 for (let i = 0; i < grid_data[0].length; i++) // i are columns 1807 for (let j = 0; j < grid_data.length; j++) // j are rows 1808 { 1809 grid.grid[Math.floor(i + grid.nc / 2 - img.width / 2)][Math.floor(j + grid.nr / 2 - img.height / 2)][property] = grid_data[j][i]; 1810 } 1811 sim.display(); 1812 1813 }; 1814 img.src = event.target.result; 1815 1816 }; 1817 reader.readAsDataURL(e.target.files[0]); 1818 document.getElementById("imageLoader").value = ""; 1819 } 1820 imageLoader.addEventListener('change', handleImage, false); 1821 imageLoader.grid = targetgrid; // Bind a grid to imageLoader 1822 } 1823 1824 /** 1825 * initialPattern takes a @GridModel and loads a pattern from a PNG file. Note that this 1826 * will only work when Cacatoo is ran on a server due to security issues. If you want to 1827 * use this feature locally, there are plugins for most browser to host a simple local 1828 * webserver. 1829 * (currently only supports black and white image) 1830 */ 1831 initialPattern(grid, property, image_path, x, y) { 1832 let sim = this; 1833 if (typeof window != undefined) { 1834 for (let i = 0; i < grid.nc; i++) for (let j = 0; j < grid.nr; j++) grid.grid[i][j][property] = 0; 1835 let tempcanv = document.createElement("canvas"); 1836 let tempctx = tempcanv.getContext('2d'); 1837 var tempimg = new Image(); 1838 tempimg.onload = function () { 1839 tempcanv.width = tempimg.width; 1840 tempcanv.height = tempimg.height; 1841 tempctx.drawImage(tempimg, 0, 0); 1842 let grid_data = get2DFromCanvas(tempcanv); 1843 if (x + tempimg.width >= grid.nc || y + tempimg.height >= grid.nr) throw RangeError("Cannot place pattern outside of the canvas") 1844 for (let i = 0; i < grid_data[0].length; i++) // i are columns 1845 for (let j = 0; j < grid_data.length; j++) // j are rows 1846 { 1847 grid.grid[x + i][y + j][property] = grid_data[j][i]; 1848 } 1849 sim.display(); 1850 }; 1851 1852 tempimg.src = image_path; 1853 tempimg.crossOrigin = "anonymous"; 1854 1855 } 1856 else { 1857 console.error("initialPattern currently only supported in browser-mode"); 1858 } 1859 1860 } 1861 1862 /** 1863 * Toggle the mix option 1864 */ 1865 toggle_mix() { 1866 if (this.mix) this.mix = false; 1867 else this.mix = true; 1868 } 1869 1870 /** 1871 * Toggle the pause option. Restart the model if pause is disabled. 1872 */ 1873 toggle_play() { 1874 if (this.pause) this.pause = false; 1875 else this.pause = true; 1876 if (!this.pause) this.start(); 1877 } 1878 1879 /** 1880 * colourRamp interpolates between two arrays to get a smooth colour scale. 1881 * @param {array} arr1 Array of R,G,B values to start fromtargetgrid The gridmodel containing the grid to be modified. 1882 * @param {array} arr2 Array of R,B,B values to transition towards 1883 * @param {integer} n number of steps taken 1884 * @return {dict} A dictionary (i.e. named JS object) of colours 1885 */ 1886 colourRamp(arr1, arr2, n) { 1887 let return_dict = {}; 1888 for (let i = 0; i < n; i++) { 1889 1890 return_dict[i] = [Math.floor(arr1[0] + arr2[0] * (i / n)), 1891 Math.floor(arr1[1] + arr2[1] * (i / n)), 1892 Math.floor(arr1[2] + arr2[2] * (i / n))]; 1893 } 1894 return return_dict 1895 } 1896 } 1897 1898 1899 /** 1900 * Below are a few global functions that are used by Simulation classes, but not a method of a Simulation instance per se 1901 */ 1902 1903 1904 //Delay for a number of milliseconds 1905 const pause = (timeoutMsec) => new Promise(resolve => setTimeout(resolve, timeoutMsec)); 1906 1907 /** 1908 * Reconstruct a 2D array based on a canvas 1909 * @param {canvas} canvas HTML canvas element to convert to a 2D grid for Cacatoo 1910 * @return {2DArray} Returns a 2D array (i.e. a grid) with the states 1911 */ 1912 function get2DFromCanvas(canvas) { 1913 let width = canvas.width; 1914 let height = canvas.height; 1915 let ctx = canvas.getContext('2d'); 1916 let img1 = ctx.getImageData(0, 0, width, height); 1917 let binary = new Array(img1.data.length); 1918 let idx = 0; 1919 for (var i = 0; i < img1.data.length; i += 4) { 1920 let num = [img1.data[i], img1.data[i + 1], img1.data[i + 2]]; 1921 let state; 1922 // console.log(num) 1923 if (JSON.stringify(num) == JSON.stringify([0, 0, 0])) state = 0; 1924 else if (JSON.stringify(num) == JSON.stringify([255, 255, 255])) state = 1; 1925 else if (JSON.stringify(num) == JSON.stringify([255, 0, 0])) state = 2; 1926 else if (JSON.stringify(num) == JSON.stringify([0, 0, 255])) state = 3; 1927 else throw RangeError("Colour in your pattern does not exist in Cacatoo") 1928 binary[idx] = state; 1929 idx++; 1930 } 1931 1932 const arr2D = []; 1933 let rows = 0; 1934 while (rows < height) { 1935 arr2D.push(binary.splice(0, width)); 1936 rows++; 1937 } 1938 return arr2D 1939 } 1940 1941 1942 try 1943 { 1944 module.exports = Simulation; 1945 } 1946 catch(err) 1947 { 1948 // do nothing 1949 } 1950