cacatoo
Version:
Building, exploring, and sharing spatially structured models
615 lines (557 loc) • 20 kB
HTML
<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) -->
<link rel="stylesheet" href="../../style/cacatoo.css"> <!-- Set style sheet -->
<head>
<title>Cacatoo</title>
</head>
<script>
/*-----------------------Start user-defined code ---------------------*/
let sim
var draw_cells = 1
// Phage parameters
var mu_phage_bitstrings = 0.001
var diff_rate_phages = 0.5
var burst_size = 10 // number of particles created upon infection
var burst_size_cost = 0 // for every extra tail-fiber, reduce burst size with this much
var max_phage_decay = 0.02 // phage decay is maximal in the bottom of the grid
var min_phage_decay = 0.0001 // minimal decay
var phage_influx = 0.0 // influx chance per grid point
var mu_tailfibers = 0.001 // add or remove a tail fiber
// Host level parameters
var N = 1000
var mu_host = 0.001
var uptake = 0.5
var host_birthrate = 0.02
var deathrate = 0.005
// Lock-and-key parameters
var bitstring_length = 100
var lock_and_key_length = 8 // Note: make sure phage-generalism is capped to bitstring_length/lock_and_key_length!!
var max_infection_chance = 0.1
var max_food_influx = 0.01
var food_decay = 0.99
function cacatoo() {
let simconfig = {
title: "Phage-host coevolution with bitstrings", // The name of your cacatoo-simulation
description: "Does resource abundance predict phage host-range? (project proposed by Bas Dutilh)", // And a description if you wish
maxtime: 200000, // How many time steps the model continues to run
// (note, the onscreen FPS may drop below 60 fps when using fast mode, although many more timesteps may be handled per second)
ncol: 80, // Number of columns (width of your grid)
nrow: 100, // Number of rows (height of your grid)
scale: 4, // Scale of the grid (nxn pixels per grid point)
sleep: 0,
wrap: [false, false],
graph_update: 50,
graph_interval: 10,
fpsmeter: false,
}
// FLOCKCONFIG EXAMPLE
// This example sets up a boid simulation with specific values for the currently implemented parameters
// Note however, all these parameters have defaults, so not all need to be set by the user.
let flockconfig = {
// Flock parameters
num_boids: 0, // Starting number of boids (flocking individuals)
shape: "dot", // Shape of the boids drawn (options: bird, arrow, line, rect, dot, ant)
click: "kill", // Clicking the boids pushes them away from the mouse
max_speed: 1, // Maximum velocity of boids
max_force: 1, // Maximum steering force applied to boids (separation/cohesion/alignment rules)
init_velocity: 0,
friction: 0.3, // Them ants are darn slippery :)
brownian: 0.01,
// Mouse parameters
mouse_radius: 10, // Radius of boids captured by the mouse overlay
draw_mouse_radius: "true", // Show a circle where the mouse is
draw_mouse_colour: "blue",
// Collision behaviour
physics: true,
num_colours: 200,
statecolours: {
barcode: "random",
},
collision_force: 0.1,
size: 1.5, // Size of the boids (scales drawing and colision detection)
// Optimalisation (speed) parameters
//qt_colour: "white", // Show quadtree (optimalisation by automatically tessalating the space)
qt_capacity: 3, // How many boids can be in one subspace of the quadtree before it is divided further
}
sim = new Simulation(simconfig) // Initialise the Cacatoo simulation
for(let d=0; d<10; d++){
console.log(get_infection_chance(d))
}
sim.makeGridmodel("environment") // Build a new Gridmodel within the simulation called "model"
sim.createDisplay_continuous({
model: "environment",
property: "food",
label: "Resources", // Createa a display for a continuous variable (ODE state for external resources)
minval: 0,
maxval: 1.0,
num_colours: 100,
decimals: 2,
fill: "viridis",
legend: true,
legendlabel: "concentration",
})
sim.createDisplay_continuous({
model: "environment",
property: "num_phages",
label: "Phage density", // Createa a display for a continuous variable (ODE state for external resources)
minval: 0,
maxval: 20,
num_colours: 100,
decimals: 2,
fill: "inferno",
legend: true,
legendlabel: "concentration",
})
sim.environment.colourGradient(
"tailfibers",
100,
[228, 3, 3],
[255, 140, 0],
[255, 237, 0],
[0, 128, 38],
[0, 76, 255],
[115, 41, 130],
)
sim.createDisplay_continuous({
model: "environment",
property: "tailfibers",
label: "Num. tailfibers of phages", // Createa a display for a continuous variable (ODE state for external resources)
minval: 1,
maxval: 10,
num_colours: 100,
decimals: 2,
legend: true,
legendlabel: "<-- specialist / generalist -->",
})
sim.makeFlockmodel("flock", flockconfig) // Add a flockmodel, which contains invidiuals (boids) in continuous space
sim.randomString = function (len) {
let bs = new Int8Array(len).fill(0)
for (let i = 0; i < len; i++) {
if (sim.rng.random() < 0.5) bs[i] = 1
}
return bs
}
sim.reset = function () {
sim.environment.resetPlots()
sim.initialGrid(sim.environment, "num_phages", 0, 0.0)
let init_phage_string = new Int8Array(bitstring_length).fill(0)
for (let i = 0; i < bitstring_length; i++)
if (sim.rng.random() < 0.5) init_phage_string[i] = 1
for (let x = 0; x < sim.ncol; x++) {
for (let y = 0; y < sim.nrow; y++) {
sim.environment.grid[x][y].food = 0.5
sim.environment.grid[x][y].phages = []
sim.environment.grid[x][y].phages.push(new Phage())
sim.environment.grid[x][y].num_phages =
sim.environment.grid[x][y].phages.length
sim.environment.grid[x][y].tailfibers = sim.rng.random() * 10
}
}
sim.flock.boids = []
sim.flock.populateSpot(
N / 10,
sim.nr / 2,
sim.nc / 2,
Math.min(sim.ncol, sim.nrow, 10),
)
for (let boid of sim.flock.boids) {
boid.food = 0.0
boid.fill = "yellow"
boid.bitstring = sim.randomString(bitstring_length)
}
}
sim.resetBarcodes = function () {
for (let boid of sim.flock.boids) boid.barcode = sim.rng.genrand_int(1, 100)
}
sim.reset()
sim.resetBarcodes()
sim.createFlockDisplay("flock", {
addToDisplay: sim.canvases[0],
legend: false,
property: "barcode",
legendlabel: "Host type",
strokeStyle: "black",
strokeWidth: 0,
minval: 0,
maxval: 100,
num_colours: 200,
nticks: 3,
decimals: 0,
})
sim.environment.update = function () {
this.synchronous() // Applied as many times as it can in 1/60th of a second
//this.perfectMix()
this.apply_async(this.diffuse_phages)
this.diffuseStates("food", 0.2)
let sum_phages = 0
let food_on_grid = 0
let food_in_cells = 0
let all_host_bitstrings = []
let all_phage_bitstrings = []
for (let boid of sim.flock.boids) {
food_in_cells += boid.food
all_host_bitstrings.push(boid.bitstring.join(""))
}
// Output the sorted entries
//bact_avg_food /= sim.flock.boids.length
for (
let x = 0;
x < this.nc;
x++ // x are columns
)
for (
let y = 0;
y < this.nr;
y++ // y are rows
) {
sum_phages += sim.environment.grid[x][y].phages.length
for (let p of sim.environment.grid[x][y].phages) {
all_phage_bitstrings.push(p.bitstring.join(""))
}
// if(sum_phages>1) sum_g /= sum_phages
food_on_grid += sim.environment.grid[x][y].food
}
let avg_generalism = 1 / sum_phages
const host_bitstring_counts = new Map()
const phage_bitstring_counts = new Map()
all_host_bitstrings.forEach((bitstring) => {
host_bitstring_counts.set(
bitstring,
(host_bitstring_counts.get(bitstring) || 0) + 1,
)
})
all_phage_bitstrings.forEach((bitstring) => {
phage_bitstring_counts.set(
bitstring,
(phage_bitstring_counts.get(bitstring) || 0) + 1,
)
})
let tailfibers = []
let num_tailfibers = []
if (sim.time % 50 == 0) {
for (
let x = 0;
x < this.nc;
x++ // x are columns
)
for (
let y = 0;
y < this.nr;
y++ // y are rows
)
for (let p of sim.environment.grid[x][y].phages) {
num_tailfibers.push(p.tailfibers.length)
for (let t of p.tailfibers) tailfibers.push(t)
}
tailfibers = shuffle(tailfibers)
tailfibers = shuffle(num_tailfibers)
tailfibers = tailfibers.slice(0, 100)
num_tailfibers = num_tailfibers.slice(0, 100)
const sorted_host_bitstrings = Array.from(
host_bitstring_counts.entries(),
).sort((a, b) => b[1] - a[1]) // Sort in descending order by count
const sorted_phage_bitstrings = Array.from(
phage_bitstring_counts.entries(),
).sort((a, b) => b[1] - a[1]) // Sort in descending order by count
const top_host_bitstrings = sorted_host_bitstrings.slice(0, 8)
const top_phage_bitstrings = sorted_phage_bitstrings.slice(0, 8)
// If you want just the bitstrings and their counts for further use
sim.log("<b>BITSTRING STATS:</b><br>", "output", (append = false))
top_host_bitstrings.forEach(([bitstring, count]) => {
sim.log(
`Top hosts bitstring: ${bitstring}, Count: ${count}`,
"output",
(append = true),
)
})
sim.log("<br>", "output", (append = true))
top_phage_bitstrings.forEach(([bitstring, count]) => {
sim.log(
`Top phage bitstring: ${bitstring}, Count: ${count}`,
"output",
(append = true),
)
})
sim.log(
`<br> Note: only the first ${lock_and_key_length} bits are used for lock-and-key. The rest can be used for more accurate clustering in downstream analyses.`,
"output",
(append = true),
)
if (sim.time % 10000 == 0) sim.resetBarcodes()
//this.plotPoints(tailfibers, "Used tailfibers", {labelsDivWidth: 0, width:350})
this.plotPoints(num_tailfibers, "Average num tailfibers", {
labelsDivWidth: 0,
width: 350,
})
}
const frequent_host_bitstrings = Array.from(host_bitstring_counts.entries())
.filter(([, count]) => count > 20)
.map(([bitstring]) => bitstring)
const frequent_phage_bitstrings = Array.from(
phage_bitstring_counts.entries(),
)
.filter(([, count]) => count > 20)
.map(([bitstring]) => bitstring)
this.plotArray(
["Bacterial population size"],
[sim.flock.boids.length],
["gold"],
"Population size host",
{ width: 350 },
)
this.plotArray(
["Food on grid", "Food in cells"],
[food_on_grid, food_in_cells],
["green", "darkgreen"],
"Food on grid / in bacteria",
{ width: 350 },
)
this.plotArray(
["Phage population size"],
[sum_phages],
["red"],
"Population size phages",
{ width: 350 },
)
this.plotArray(
["Number host bitstrings", "Number phage bitstrings"],
[frequent_host_bitstrings.length, frequent_phage_bitstrings.length],
["gold", "red"],
"Bitstrings with abundance > 20",
{ width: 350 },
)
}
sim.environment.nextState = function (x, y) {
sim.environment.grid[x][y].food += max_food_influx*(1-x/sim.ncol)
sim.environment.grid[x][y].food *= food_decay
if (sim.rng.random() < phage_influx)
sim.environment.grid[x][y].phages.push(new Phage())
for (let p = 0; p < sim.environment.grid[x][y].phages.length; p++) {
let decay = max_phage_decay*(1-Math.abs(y-sim.nrow)/sim.nrow) + min_phage_decay
if (sim.rng.random() < decay) {
sim.environment.grid[x][y].phages.splice(p, 1)
}
}
sim.environment.grid[x][y].num_phages =
sim.environment.grid[x][y].phages.length
sim.environment.grid[x][y].tailfibers = undefined
if (sim.environment.grid[x][y].num_phages > 0)
sim.environment.grid[x][y].tailfibers =
sim.environment.grid[x][y].phages[0].tailfibers.length
}
sim.movePhage = function (k, direction,x,y) {
let coords = sim.environment.moore[direction]
let target = sim.environment.getGridpoint(coords[0] + x, coords[1] + y)
if(target==undefined) return
target.phages.push(sim.environment.grid[x][y].phages[k])
sim.environment.grid[x][y].phages.splice(k, 1)
// target.num_phages++
// sim.environment.grid[x][y].num_phages--
}
sim.environment.diffuse_phages = function (x, y) {
for (let k = 0; k < sim.environment.grid[x][y].phages.length; k++) {
let randomnr = sim.environment.rng.genrand_real1()
if (randomnr < diff_rate_phages / 4) sim.movePhage(k, 1,x,y)
else if (randomnr < (2 * diff_rate_phages) / 4) sim.movePhage(k, 2,x,y)
else if (randomnr < (3 * diff_rate_phages) / 4) sim.movePhage(k, 3,x,y)
else if (randomnr < (4 * diff_rate_phages) / 4) sim.movePhage(k, 4,x,y)
}
}
sim.flock.update = function () {
for (let boid of this.boids) {
// Bacteria resource uptake
for (let i of sim.flock.getNearbyGridpoints(boid,sim.environment,boid.size + 2,)) {
let u = i.food * uptake
i.food -= u
boid.food += u
}
// X and Y coordinate on grid for this boid
//console.log('Fetching gridpoint for', boid, 'on grid', sim.environment)
let gp = this.getGridpoint(boid, sim.environment)
if(gp==undefined) continue // Boids that are not on the grid can't do anything :)
//else console.log("Cool")
let x = gp.x
let y = gp.y
// Bacteria reproduction
boid.food *= 0.99
let birthrate = boid.food * host_birthrate
if (sim.rng.random() < birthrate) {
let newboid = this.copyBoid(boid)
newboid.food /= 2
boid.food /= 2
newboid.bitstring = new Int8Array(bitstring_length).fill(0)
for (let i = 0; i < newboid.bitstring.length; i++) {
if (sim.rng.random() < mu_host) {
newboid.bitstring[i] = !boid.bitstring[i]
if (i < lock_and_key_length)
newboid.barcode = sim.rng.genrand_int(1, 100)
} else {
newboid.bitstring[i] = boid.bitstring[i]
}
}
//console.log(newboid.bitstring.join(''))
let angle = sim.rng.random() * Math.PI * 2
newboid.position.x += 0.5 * boid.size * Math.cos(angle)
newboid.position.y += 0.5 * boid.size * Math.sin(angle)
newboid.position.x = (newboid.position.x + sim.ncol) % sim.ncol
newboid.position.y = (newboid.position.y + sim.nrow) % sim.nrow
this.boids.push(newboid)
}
// Bacteria death
const index = this.boids.indexOf(boid)
if (sim.rng.random() < deathrate) {
this.boids.splice(index, 1)
} else {
// Bacteria infection
let new_phages = []
for (let p of sim.environment.grid[x][y].phages) {
for (let t of p.tailfibers) {
// Generalism counter.
let d = 0
for (let i = 0; i < lock_and_key_length; i++) {
d += p.bitstring[t + i] != boid.bitstring[i]
}
let infect_chance = get_infection_chance(d)
//console.log(p.bitstring,boid.bitstring,infect_chance)
if (sim.rng.random() < infect_chance) {
//if(d==0){
this.boids.splice(index, 1)
let penalty = (p.tailfibers.length - 1) * burst_size_cost
for (let b = 0; b < burst_size - penalty; b++) {
new_phages.push(new Phage(p))
}
break
}
}
}
sim.environment.grid[x][y].phages.push(...new_phages)
}
}
if (sim.time % 50 == 0)
console.log(
`Simulation step ${sim.time} has ${sim.flock.boids.length} cells`,
)
}
sim.start()
sim.addButton("Pause", function () {
sim.toggle_play()
})
sim.addButton("Restart", function () {
sim.reset()
})
sim.addButton("Reset barcodes", function () {
sim.resetBarcodes()
})
sim.addToggle("draw_cells", "Show bacteria", function () {
sim.flock.draw = !sim.flock.draw
})
sim.addHTML("form_holder", "<br>")
//sim.addSlider("range_helping",2,50,1,"Range helping")
sim.addSlider("mu_phages", 0.0, 0.1, 0.001, "Mutation rate (phages)")
sim.addSlider("mu_host", 0.0, 0.1, 0.001, "Mutation rate (host)")
sim.addSlider("diff_rate_phages", 0.0, 1.0, 0.001, "Diffusion phages")
sim.addHTML("form_holder", "<br>")
sim.addSlider("burst_size", 2, 20, 1, "Burst size")
sim.addSlider("phage_decay", 0.001, 0.1, 0.001, "Phage decay")
sim.addSlider("phage_influx", 0.0, 0.01, 0.0001, "Random phage influx")
}
// Phage class
// Phages contain a list of bitsrings. More bitstrings means more chance to infect bacteria,
// but this reduces the burst size (larger phage particles to fit the extra seq info)
class Phage {
// Gene constructor
constructor(parent) {
if (parent instanceof Phage) {
this.uid = phageIDs.next().value
this.generation = parent.generation
this.mutations = parent.mutations
// Mutate generalism
this.tailfibers = []
for (let i of parent.tailfibers) {
if (sim.rng.random() > mu_tailfibers) this.tailfibers.push(i)
}
if (sim.rng.random() < mu_tailfibers)
this.tailfibers.push(
sim.rng.genrand_int(0, bitstring_length - lock_and_key_length),
)
// console.log(this.tailfibers)
// Inherit / mutate bitstring
this.bitstring = new Int8Array(bitstring_length).fill(0)
for (let i = 0; i < this.bitstring.length; i++) {
this.bitstring[i] =
sim.rng.random() > mu_phage_bitstrings
? parent.bitstring[i]
: !parent.bitstring[i]
}
} else if(parent instanceof Int8Array){
this.generation = 0
this.mutations = 0
this.tailfibers = [20]
this.len = bitstring_length
this.uid = phageIDs.next() // Just so it has a unique identifier, no biological function
this.bitstring = parent
this.uid = phageIDs.next() // Just so it has a unique identifier, no biological function
}
else{
this.generation = 0
this.mutations = 0
this.tailfibers = [20]
this.len = bitstring_length
this.uid = phageIDs.next() // Just so it has a unique identifier, no biological function
this.bitstring = this.newBitstring(bitstring_length)
}
}
newBitstring(len) {
let bs = new Int8Array(len).fill(0)
for (let i = 0; i < len; i++) {
if (sim.rng.random() < 0.5) bs[i] = 1
}
return bs
}
copy() {
return new Phage(this.len)
}
}
/**
* Shuffles array in place.
* @param {Array} a items An array containing the items.
*/
function shuffle(a) {
var j, x, i
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(sim.rng.genrand_real2() * (i + 1))
x = a[i]
a[i] = a[j]
a[j] = x
}
return a
}
function* idGenerator() {
let id = 1
while (true) {
yield id
id++
}
}
function get_infection_chance(d){
//return (0.9*max_infection_chance - d*max_infection_chance / (d + h))
return (max_infection_chance*Math.exp(-2*d))
}
const phageIDs = idGenerator()
const hostIDs = idGenerator()
</script>
<body onload="cacatoo()">
<div class="header" id="header">
<h2>Cacatoo (example project)</h2>
</div>
<div class="content" id="canvas_holder"> </div>
<div class="content" id="graph_holder"> </div>
<div class="content" id="form_holder"></div>
<div class="content" id="examples">
<div class="output" id="output"></div>
</div>
<div class="footer" id="footer"></div>
</div>
</body>
</html>