Skip to content

Instantly share code, notes, and snippets.

@tonyhschu
Last active Sep 13, 2019
Embed
What would you like to do?
Antibiotic Resistance Simulation
license: mit

Built with blockbuilder.org

Inspired by this Harvard antibiotics resistance study I wanted to build a simulation of evolution.

In this simulation the cells each produce a number, determined by its "genes", which are two random numbers added together. As they reproduce, sometimes those genes mutate, and the cells produce a new number. When they mutate, they are assigned a different color in the visualization.

The "petri dish" is divided into 9 sections, like in the Harvard study. In place of exponentially increasing amounts of antibiotics in each column, the survival criteria is whether the cell's number is divisible by an increasingly large number.

This is my admission entry to visFest 2016 =)

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
const WIDTH = 960
const HEIGHT = 500
const PRIMES =[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101, 103, 107,109,113,127,131,137,139,149,151,157]
const OPERATORS = [
function(a, b) { return a + b },
function(a, b) { return a - b },
function(a, b) { return a * b },
function(a, b) { return a / b },
function(a, b) { return a % b }
]
const CRITERION = [
function(n) { return true },
function(n) { return n > 110 },
function(n) { return n % 5 === 0 },
function(n) { return n % 15 === 0 },
function(n) { return n % 210 === 0 },
function(n) { return n % 14 === 0 },
function(n) { return n % 7 === 0 },
function(n) { return n > 150 },
function(n) { return true }
]
const MUTATION_RATE = 0.0008
const clampColor = function(input) {
return Math.round(Math.max(0, Math.min(255, input)))
}
const wrap = function(n) {
if (n < 0) { return false } // return n + WIDTH * HEIGHT }
if (n > WIDTH * HEIGHT) { return false } // return n - WIDTH * HEIGHT }
return n
}
const adjacencyTransform = [
function(n) { return (n % WIDTH !== 0) ? wrap(n - WIDTH - 1) : false },
function(n) { return wrap(n - WIDTH) },
function(n) { return ((n + 1) % WIDTH !== 0) ? wrap(n - WIDTH + 1) : false },
function(n) { return (n % WIDTH !== 0) ? wrap(n - 1) : false },
function(n) { return ((n + 1) % WIDTH !== 0) ? wrap(n + 1) : false },
function(n) { return (n % WIDTH !== 0) ? wrap(n + WIDTH - 1) : false },
function(n) { return wrap(n + WIDTH) },
function(n) { return ((n + 1) % WIDTH !== 0) ? wrap(n + WIDTH + 1) : false },
]
// Set up
var slots, cells, liveCells, pxToPaint, tribes, lineage
var candidate = function(parent) {
var genes = {
numbers: parent.genes.numbers.map(function(n) { return n }),
operators: parent.genes.operators.map(function(n) { return n })
}
var colorCache = Object.assign({}, parent.color)
var fitness = parent.fitness
var generation = parent.generation + 1
var hue = parent.hue
// Mutate
if (Math.random() < MUTATION_RATE) {
var numberSlot = Math.floor(Math.random() * genes.numbers.length)
var primeToPick = Math.floor(Math.random() * PRIMES.length)
genes.numbers[numberSlot] = PRIMES[primeToPick]
fitness = genes.operators.reduce(function(n, currentOp, i) {
return OPERATORS[currentOp](n, genes.numbers[i + 1])
}, genes.numbers[0])
hue = Math.round(Math.random() * 60 - 30 + 120 + parent.hue)
//hue = (tribes.length % 8) * 45
generation = 0
}
var hsl = d3.hsl(
hue,
0.75,
0.48 - Math.sin(generation / 15) * 0.03
)
var rgb = d3.rgb(hsl)
color = {
r: clampColor(rgb.r),
g: clampColor(rgb.g),
b: clampColor(rgb.b)
}
var child = {
generation: generation,
genes: genes,
cooldown: genes.numbers.length + genes.operators.length,
hue: hue,
color: color,
fitness: fitness,
tribeId: parent.tribeId
}
return child
}
var reproduce = function(child, coordinate) {
var adjacentAvalible = adjacencyTransform.map(function(fn) {
return fn(coordinate)
}).filter(function(co) {
return co && slots[co] === null
})
if (child.generation === 0) {
if (typeof child.tribeId === 'number') {
// if it isn't one of the originals
var parentTribeId = child.tribeId
lineage.push({
parent: parentTribeId,
child: tribes.length
})
}
child.tribeId = tribes.length
tribes.push([coordinate])
} else {
tribes[child.tribeId].push(coordinate)
}
child.coordinate = coordinate
child.adjacentAvalible = adjacentAvalible
cells.push(child)
liveCells.push(child)
slots[coordinate] = cells.length - 1
pxToPaint.push(coordinate)
}
var reproduceThrow = function(parent, coordinate) {
if (slots[coordinate] !== null) {
var index = parent.adjacentAvalible.indexOf(coordinate)
parent.adjacentAvalible.splice(index, 1)
return
}
var column = Math.floor((coordinate % WIDTH) / WIDTH * 9)
var criteria = CRITERION[column]
var newChild = candidate(parent)
if (criteria(newChild.fitness)) {
reproduce(newChild, coordinate)
}
}
// Feel free to change or delete any of the code you see in this editor!
var canvas = d3.select("body").append("canvas")
.attr("width", WIDTH)
.attr("height", HEIGHT);
var svg = d3.select("body").append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.style("position", "absolute")
.style("top", 0)
.style("left", 0)
var ctx = canvas.node().getContext("2d");
var imgData = ctx.getImageData(0, 0, WIDTH, HEIGHT)
var data = imgData.data
var resetSimulation = function() {
slots = d3.range(960 * 500).map(function() { return null })
cells = []
liveCells = []
tribes = []
lineage = []
pxToPaint = []
overlay()
var c1 = WIDTH * HEIGHT / 2 + 20
var c2 = WIDTH * HEIGHT / 2 - 20
var firstCell = {
generation: 0,
genes: {
numbers: [13, 0],
operators: [0]
},
cooldown: 0,
hue: Math.round(360 * Math.random()),
color: {
r: 155,
g: 155,
b: 155
},
fitness: 13
}
var secondCell = {
generation: 0,
genes: {
numbers: [13, 0],
operators: [0]
},
cooldown: 0,
hue: (firstCell.hue + 180) % 360,
color: {
r: 155,
g: 155,
b: 155
},
fitness: 13
}
reproduce(firstCell, c1)
reproduce(secondCell, c2)
slots.forEach(function(slot, i) {
var b = i * 4
data[b] = 255
data[b + 1] = 255
data[b + 2] = 255
})
ctx.putImageData(imgData, 0, 0)
window.requestAnimationFrame(tick)
}
var overlay = function() {
var keys = d3.range(tribes.length)
var significantTribes = keys.filter(function(key) {
return tribes[key].length > 20
})
var coordinates = significantTribes.map(function(key) {
var tribe = tribes[key]
var originalCoordinate = tribe[0]
var x = originalCoordinate % WIDTH
var y = Math.floor(originalCoordinate / WIDTH)
return { x: x, y: y }
})
var linkedCoordinates = lineage.filter(function(line) {
return significantTribes.indexOf(line.child) >= 0
})
.map(function(pair) {
var parent = tribes[pair.parent][0]
var child = tribes[pair.child][0]
return {
x1: parent % WIDTH,
y1: Math.floor(parent / WIDTH),
x2: child % WIDTH,
y2: Math.floor(child / WIDTH)
}
})
var circles = svg.selectAll('circle')
.data(coordinates)
circles.enter().append("circle")
.attr('r', 5)
.attr('fill', 'none')
.attr('stroke-width', 2.5)
.attr('stroke', '#ffffff')
.merge(circles)
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
circles.exit().remove()
var lines = svg.selectAll('line')
.data(linkedCoordinates)
lines.enter().append('line')
.attr('stroke', '#ffffff')
.attr('stroke-width', 1.5)
.merge(lines)
.attr('stroke-dasharray', function(d) {
var dx = d.x1 - d.x2
var dy = d.y1 - d.y2
var length = Math.sqrt(dx * dx + dy * dy)
return '0, 4, ' + (length - 12)
})
.attr('x1', function(d) { return d.x1 })
.attr('y1', function(d) { return d.y1 })
.attr('x2', function(d) { return d.x2 })
.attr('y2', function(d) { return d.y2 })
lines.exit().remove()
}
var tick = function() {
// Should Continue
pxToPaint = []
// Simulate
var cullCells = []
liveCells.forEach(function(cell, i) {
if (cell.adjacentAvalible.length <= 0) {
cullCells.push(i)
return
}
var target = cell.adjacentAvalible[Math.floor(cell.adjacentAvalible.length * Math.random())]
reproduceThrow(cell, target)
})
cullCells.reverse()
cullCells.forEach(function(id) {
liveCells.splice(id, 1)
})
// Paint
pxToPaint.forEach(function(coordinate) {
var r = coordinate * 4
var g = r + 1
var b = g + 1
var a = b + 1
var slot = slots[coordinate]
// if slot has a cell
var cell = cells[slot]
data[r] = cell.color.r
data[g] = cell.color.g
data[b] = cell.color.b
data[a] = 255
})
ctx.putImageData(imgData, 0, 0)
if (Math.random() > 0.9) {
overlay()
}
if (liveCells.length > 0) {
window.requestAnimationFrame(tick)
} else {
console.log('done')
setTimeout(resetSimulation, 5000)
}
}
resetSimulation()
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment