UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

360 lines (304 loc) 18.1 kB
<!-- EXPERT EXAMPLE FILE This file shows the advanced useage of the Cacatoo library, using custom classes and methods. It is similar to classes_PoS.html, but with much more dynamics for the "genomes". The genomes contain self- replicating DNA elements (TEs), which cause fitness penalties for their host. --> <html> <script src="../../dist/cacatoo.js"></script> <!-- Include cacatoo library (compiled with rollup) --> <script src="../../lib/all.js"></script> <!-- Include other libraries (concattenated in 1 file) --> <script src="./PoS_classes.js"></script> <link rel="stylesheet" href="../../style/cacatoo.css"> <!-- Set style sheet --> <script> /*-----------------------Start user-defined code ---------------------*/ let sim; // PARAMETERS FOR FITNESS DEFINITION var init_es = 10 // es = essential --- if even a single one is missing = 0.0 fitness var init_ne = 0 // ne = non-essential --- for each non-essential genes gain "non_essential_gene_boon" fitness var init_nc = 30 var init_tra_rate = 0.85 var transposon_fitness_cost = 0.005 var non_essential_gene_boon = 0.1 // PARAMETERS FOR ECOLOGY var death_rate = 0.02 var non = 100 // PARAMETERS FOR EVOLUTION var gene_inactivation_rate = 0.001 var gene_deletion_rate = 0.001 var gene_duplication_rate = 0.001 var phi_mutation_rate = 0.01 // PARAMETERS FOR EDNA POOL var degr_rate_edna = 0.02 var diff_rate_edna = 0.01 // PARAMETERS FOR TRANSPOSON DYNAMICS var probability_TE_induced_damage = 1.0 var uptake_from_pool = 0.01 var jump_rate = 0.02 var size = 120 var scale = 2 var address_query = {} location.search.substr(1).split("&").forEach(function (item) { address_query[item.split("=")[0]] = item.split("=")[1] }) if (address_query.size) size = Math.min(address_query.size, 500) if (size > 300) scale = 1 else if (size > 200) scale = 2 function cacatoo() { let config = { title: "TEs and their host", description: "", maxtime: 200000, ncol: size, nrow: size, // dimensions of the grid to build wrap: [true, true], // Wrap boundary [COLS, ROWS] scale: scale, // scale of the grid (nxn pixels per grid cell) graph_interval: 10, statecolours: { alive: { 1: 'blue' } } // The background state '0' is never drawn } sim = new Simulation(config) sim.makeGridmodel("TE_model"); sim.createDisplay_continuous({model:"TE_model", property:"genomesize", label:"Genome size", minval:0, maxval:100, fill:"viridis"}) sim.createDisplay_continuous({model:"TE_model", property:"T_in_genomes", label:"TEs inside genomes", minval:0, maxval:100, fill:"viridis"}) sim.createDisplay_continuous({model:"TE_model", property:"T_in_eDNA", label:"TEs in eDNA pool", minval:0, maxval:100, fill:"viridis"}) sim.TE_model.initialise = function () { sim.initialGrid(sim.TE_model, "alive", 0) sim.initialGrid(sim.TE_model, "genomesize", 0, undefined, 1.0) sim.initialGrid(sim.TE_model, "T_in_eDNA", 0, undefined, 1.0) sim.initialGrid(sim.TE_model, "T_in_genomes", 0, undefined, 1.0) sim.max_g = 100 sim.max_t = 100 sim.TE_model.colourViridis("genomesize", sim.max_g) sim.TE_model.colourViridis("T_in_genomes", sim.max_t) sim.TE_model.colourViridis("T_in_eDNA", sim.max_t) sim.mixDNApool = false this.resetPlots() placeCell = function (gp, init_es, init_ne, init_nc, init_tra, init_tra_rate) { gp.alive = 1 gp.genome = new Genome() gp.genome.initialise(init_es, init_ne, init_nc, init_tra, init_tra_rate) gp.genomesize = Math.min(sim.max_g, gp.genome.chromosome.length) // A copy of the genome size is also stored within the grid point itself, so we can visualise it on the grid (capped at 100) gp.fitness = gp.genome.fitness // A copy of the genomes' fitness is stored within the grid point itself, so we can use it for the "rouletteWheel" function gp.T_in_genomes = Math.min(sim.max_t, gp.genome.nr_tra) // A copy of the genomes' fitness is stored within the grid point itself, so we can use it for the "rouletteWheel" function } for (let x = 0; x < sim.TE_model.nc; x++) for (let y = 0; y < sim.TE_model.nr; y++) { this.grid[x][y].eDNA = [] // Initialise empty eDNA pool this.grid[x][y].T_in_eDNA = 0 let size = 5 if ((Math.pow((x - sim.TE_model.nc / 2), 2) + Math.pow((y - sim.TE_model.nr / 2), 2)) < size) placeCell(sim.TE_model.grid[x][y], init_es, init_ne, init_nc, 10, init_tra_rate) else if ((Math.pow((x - sim.TE_model.nc / 2), 2) + Math.pow((y - sim.TE_model.nr / 2), 2)) < size * 50) placeCell(sim.TE_model.grid[x][y], init_es, init_ne, init_nc, 0, init_tra_rate) // else if((Math.pow((x-sim.TE_model.nc/2),2) + Math.pow((y-sim.TE_model.nr/2),2) ) < size*10 && y>sim.TE_model.nr/2) // placeCell(sim.TE_model.grid[x][y],init_es,init_ne,0,0) } } sim.TE_model.initialise() // Initialise for the first time (otherwise used for "RESET" button) // Define the next-state function. This example is stochastic growth in a petri dish sim.TE_model.nextState = function (x, y) { if (this.grid[x][y].alive == 0) { let neighbours = this.getMoore8(this, x, y, 'alive',1) if (neighbours.length > 0) { let winner = this.rouletteWheel(neighbours, 'fitness', non) if (winner != undefined) this.reproduce(x, y, winner) } } else if (this.rng.genrand_real1() < death_rate || this.grid[x][y].genome.fitness == 0) this.death_and_lysis(x, y) else { this.TEdynamicsI(x, y) this.TEdynamicsII(x, y) } // EDNA DYNAMICS if (this.grid[x][y].eDNA.length > 0) { for (let k = 0; k < this.grid[x][y].eDNA.length; k++) if (this.rng.genrand_real1() < degr_rate_edna) // degr this.grid[x][y].eDNA.splice(k, 1) this.grid[x][y].T_in_eDNA = Math.min(sim.max_t, this.grid[x][y].eDNA.length) // Track number of TEs for visualisation purposes } else this.grid[x][y].T_in_eDNA = undefined } // This function is asynchronously applied to the entire grid every time step. It uses a single random number to determine both IF a DNA fragment will move, // as well as WHERE it moves. sim.TE_model.diffuse_eDNA = function (x, y) { moveDNA = function (k, direction) { let coords = sim.TE_model.moore[direction] let target = sim.TE_model.getGridpoint(coords[0] + x, coords[1] + y) target.eDNA.push(sim.TE_model.grid[x][y].eDNA[k]) sim.TE_model.grid[x][y].eDNA.splice(k, 1) } for (let k = 0; k < sim.TE_model.grid[x][y].eDNA.length; k++) { let randomnr = sim.TE_model.rng.genrand_real1() if (randomnr < diff_rate_edna / 4) moveDNA(k, 1) else if (randomnr < 2 * diff_rate_edna / 4) moveDNA(k, 2) else if (randomnr < 3 * diff_rate_edna / 4) moveDNA(k, 3) else if (randomnr < 4 * diff_rate_edna / 4) moveDNA(k, 4) } } // A custom function for copying a cell into a gp at position x,y ("reproduction") sim.TE_model.reproduce = function (x, y, winner) { this.grid[x][y].alive = winner.alive this.grid[x][y].genome = winner.genome.copy(mutate = true) this.grid[x][y].genomesize = Math.min(sim.max_g, this.grid[x][y].genome.chromosome.length) this.grid[x][y].fitness = this.grid[x][y].genome.fitness this.grid[x][y].T_in_genomes = Math.min(sim.max_t, this.grid[x][y].genome.nr_tra) // A copy of the genomes' fitness is stored within the grid point itself, so we can use it for the "rouletteWheel" function } // A custom function for killing a gp at position x,y sim.TE_model.death_and_lysis = function (x, y) { this.grid[x][y].alive = 0 this.grid[x][y].genomesize = undefined this.grid[x][y].T_in_genomes = undefined for (let p = 0; p < this.grid[x][y].genome.chromosome.length; p++) { if (this.grid[x][y].genome.chromosome[p].type == "T") this.grid[x][y].eDNA.push(this.grid[x][y].genome.chromosome[p]) } } // A custom function for TE dynamics during the lifetime of a cell sim.TE_model.TEdynamicsI = function (x, y) { jumping = [] for (let p = 0; p < this.grid[x][y].genome.chromosome.length; p++) { if (this.grid[x][y].genome.chromosome[p].type == "T") { if (this.rng.genrand_real1() < jump_rate * this.grid[x][y].genome.chromosome[p].transposition_rate) jumping.push(this.grid[x][y].genome.chromosome[p]) } } for (let p = 0; p < jumping.length; p++) { let random_pos = Math.floor(this.rng.genrand_real1() * this.grid[x][y].genome.chromosome.length) let new_TE_copy = jumping[p].copy() if (this.rng.genrand_real1() < probability_TE_induced_damage) this.grid[x][y].genome.chromosome[random_pos].type = "." this.grid[x][y].genome.chromosome.splice(random_pos, 0, new_TE_copy) } //if(jumping.length > 0) this.grid[x][y].genome.calculate_fitness() } // A custom function for TE dynamics from the eDNA pool sim.TE_model.TEdynamicsII = function (x, y) { let nr_jumps = 0 for (let p = 0; p < this.grid[x][y].eDNA.length; p++) { let randomnr = this.rng.genrand_real1() let uptake = randomnr < uptake_from_pool let integration = randomnr < uptake_from_pool * this.grid[x][y].eDNA[p].transposition_rate if (uptake) { if (integration) { let random_pos = Math.floor(this.rng.genrand_real1() * this.grid[x][y].genome.chromosome.length) let new_TE_copy = this.grid[x][y].eDNA[p].copy() if (this.rng.genrand_real1() < probability_TE_induced_damage) this.grid[x][y].genome.chromosome[random_pos].type = "." this.grid[x][y].genome.chromosome.splice(random_pos, 0, new_TE_copy) nr_jumps++ } this.grid[x][y].eDNA.splice(p, 1) } } if (nr_jumps > 0) this.grid[x][y].genome.calculate_fitness() } // Custom function to mix only the eDNA sim.TE_model.mixeDNA = function () { let all_eDNA_gps = []; for (let x = 0; x < this.nc; x++) for (let y = 0; y < this.nr; y++) all_eDNA_gps.push(this.grid[x][y].eDNA) all_eDNA_gps = shuffle(all_eDNA_gps, this.rng) for (let x = 0; x < this.nc; x++) for (let y = 0; y < this.nr; y++) this.grid[x][y].eDNA = all_eDNA_gps.pop() // return "Perfectly mixed the grid" } sim.TE_model.update = function () { if (this.time % sim.config.graph_interval == 0) this.updateGraphs() this.synchronous() // Applied as many times as it can in 1/60th of a second this.apply_async(this.diffuse_eDNA) if (sim.mixDNApool == true) this.mixeDNA() } sim.TE_model.updateGraphs = function () { let num_alive = 0, gsizes = 0, hks = 0, nes = 0, non = 0, tra = 0, fitnesses = 0, sum_tra_rates = 0 for (let x = 0; x < sim.TE_model.nc; x++) for (let y = 0; y < sim.TE_model.nr; y++) { if (this.grid[x][y].alive == 1) { num_alive++ gsizes += this.grid[x][y].genome.chromosome.length fitnesses += this.grid[x][y].genome.fitness for (let p = 0; p < sim.TE_model.grid[x][y].genome.chromosome.length; p++) switch (sim.TE_model.grid[x][y].genome.chromosome[p].type) { case "G": hks++; break; case "g": nes++; break; case ".": non++; break; case "T": tra++; sum_tra_rates += sim.TE_model.grid[x][y].genome.chromosome[p].transposition_rate break; } } } this.plotArray(["Population size"], [num_alive], ["blue"], "Population size (nr. of living cells)") this.plotArray(["Genome size", "Essential", "Non-essential", "Non-coding", "Transposons"], [gsizes / num_alive, hks / num_alive, nes / num_alive, non / num_alive, tra / num_alive], ["black", "blue", "green", "grey", "red"], "Avg genome size and composition") this.plotArray(["Fitness"], [fitnesses / num_alive], ["black"], "Avg fitness") this.plotArray(["TE jump rate (phi)"], [sum_tra_rates / tra], ["red"], "Avg TE jump rate (phi)") } // sim.addHTML("canvas_holder", "<img src=\"legend.png\">") sim.addButton("Pause/continue", function () { sim.toggle_play() }) sim.addButton("Well-mix (all)", function () { sim.toggle_mix() }) sim.addButton("Well-mix (eDNA)", () => { sim.mixDNApool = !sim.mixDNApool }) sim.addButton("Restart", function () { sim.TE_model.initialise() }) sim.addButton("Reset", function () { location.reload(); }) sim.addHTML("form_holder", "<br>Ecological options:") sim.addSlider("death_rate", 0.0, 1.0, 0.001, "Stochastic death rate (d)") sim.addSlider("non", 0.0, 200.0, 1.00, "No-reproduction constant (eps)") sim.addSlider("diff_rate_edna", 0.0, 0.25, 0.001, "eDNA diffusion rate (D)") sim.addSlider("degr_rate_edna", 0.0, 1.0, 0.001, "eDNA degradation rate (q)") sim.addMovieButton(sim.TE_model,"Genome size") sim.addHTML("form_holder", "<br>TE options:") sim.addSlider("uptake_from_pool", 0.0, 1.0, 0.001, "Uptake rate from eDNA pool (u)") sim.addSlider("jump_rate", 0.0, 1.0, 0.001, "TE jump rate (j)") sim.addSlider("transposon_fitness_cost", 0.0, 1.0, 0.001, "TE fitness cost (c)") sim.addSlider("probability_TE_induced_damage", 0.0, 1.0, 0.01, "TE-damage propensity (b)") sim.addHTML("form_holder", "<br>Mutations:") sim.addSlider("phi_mutation_rate", 0.0, 1.0, 0.001, "TE (phi) mutation rate") sim.addSlider("gene_deletion_rate", 0.0, 0.1, 0.0001, "Gene deletion rate") sim.addSlider("gene_duplication_rate", 0.0, 0.1, 0.0001, "Gene duplication rate") sim.addSlider("gene_inactivation_rate", 0.0, 0.1, 0.0001, "Gene inactivation rate") sim.addHTML("form_holder", "<br>Initial conditions (restart required!):") sim.addSlider("init_es", 0.0, 100.0, 1, "Nr. essential genes") sim.addSlider("init_nc", 0.0, 300.0, 1, "Nr. non-coding elements") sim.addSlider("init_ne", 0.0, 100.0, 1, "Nr. non-essential functions") sim.addSlider("non_essential_gene_boon", 0.0, 1.0, 0.001, "Fitness added per non-essential function") sim.addSlider("init_tra_rate", 0.0, 1.0, 0.001, "Initial TE jump rate") sim.start() } /*-------------------------End user-defined code ---------------------*/ </script> <body onload="cacatoo()"> <div class="header" id="header"> <h2>Cacatoo</h2> </div> <div class="content" id="canvas_holder"> <center> <div style="width:40%;"> This is an interactive version of the TE-host coevolution model <br>(derived from van Dijk <i>et al.</i>, 2021)<br> <font size=2><br> Note: default parameters result in genome streamlining, preventing extinction even in small systems (<a href="?size=120">120x120</a>). When exploring parameters that (may) cause host extinction (e.g. disabling TE-damage), you may need to increase the system size to <a href="?size=250">250x250</a> or perhaps even <a href="?size=350">350x350</a>. </font> </center> </div> </div> <div class="content" id="graph_holder"> </div> <div class="content" id="form_holder"></div> <div class="footer" id="footer"></div> </body> </html>