Published
Edited
Feb 2, 2021
Importers
Insert cell
Insert cell
coalesce =function(height,nodes){
const parentNode = {height:height,children:nodes,id:uuid.v4()}
return parentNode;
}
Insert cell
class PopulationDemographic{
constructor({N0}){
this.N0=N0;
}
getT(tau){
throw Error("don't call base class methods")
}
getIntegral(t0,t1){
throw Error("don't call base class methods")
}
getDemographic(t){
throw Error("don't call base class methods")
}
}
Insert cell
class ExponetialPopulation extends PopulationDemographic{
constructor(options){
super(options);
this.g= options.g;
}
getT(tau){
if(this.g===0){
return tau
}
return (1/this.g)*(Math.log(1+this.g*tau))
}
getTau(t){
return (1/this.g) * (Math.exp(this.g*t)-1)
}
getIntegral(t0,t1){
if(this.g===0){
return (t1-t0)*(this.N0)
}else{
return 1/(this.N0*this.g)*(Math.exp(this.g*t1)-Math.exp(this.g*t0))
}
}
getDemographic(t){
return this.N0*Math.exp(-this.g*t)
}
}
Insert cell
class ConstantPopulation extends PopulationDemographic{
constructor(options){
super(options);
}
getT(tau){
return tau;
}
getTau(t){
return t;
}
getIntegral(t0,t1){
return (t1-t0)*(1/this.N0)
}
getDemographic(t){
return this.N0;
}
}
Insert cell
simulation=populationDemographicModel=>{

let currentHeight=0;

const helper = function(population){
//Filter population to just nodes that are in play and can coalesce
const activePopulation = population.filter(n=>n.height<=currentHeight);
// these are tips that have yet to be sampled.
const toBeSampled = population.filter(n=>n.height>currentHeight)
// this is the next tip to be sampled (if it exists)
const nextSample = toBeSampled.length>0?toBeSampled[d3_array.minIndex(toBeSampled,d=>d.height)]:null;
// If the population only has one node return the node. it is the root. We are done.
if(population.length===1){
const tree= new Figtree.Tree(population[0],{lengthsKnown:false,heightsKnown:true});
//convert to new times
tree.nodeList.forEach(n=>n.height=populationDemographicModel.getT(n.height));
return tree;
}
//If there is only 1 active population then we can't coalesce to move up to the next sample time
if(activePopulation.length===1 && nextSample){
currentHeight=nextSample.height;
return helper(population)
}
// if we get this far then there are nodes that can coalsce
const k = activePopulation.length;
const Ne = populationDemographicModel.N0;
// draw the wait time for the next coalescent event. the event will happen at currentHeight+tau
const tau = sampleExponetial(k*(k-1)/(2*Ne));
// If there is a node to sample still and this coalecent event happend after that node would have been sampled reject the event, move up to the sample time and try again
if(nextSample && currentHeight+tau>nextSample.height){
//didn't happen in time
currentHeight=nextSample.height;
return helper(population);
}else{
// there is a coalsenct event
// pick nodes to coalesce
const luckyDucks = sampleLineages(activePopulation);
// get time of coalescent
currentHeight+=tau;
// make parent node
const parent = coalesce(currentHeight,luckyDucks);
// replace the children nodes with their parent in the population and go again
const updatedPopulation = population.filter(n=>!luckyDucks.includes(n)).concat(parent);
return helper(updatedPopulation);
}
}
return helper;
}
Insert cell
function simulateCoalescentTree({demographic,maxTipHeight,numberOfTips}){
return compose(simulation(demographic),makeTips(demographic)(maxTipHeight))(numberOfTips);
}
Insert cell
function sampleLineages(lineages){
const samples = [lineages[Math.round(Math.random()*(lineages.length-1))]];
const remainingLineages = lineages.filter(l=>!samples.includes(l))
return samples.concat(remainingLineages[Math.round(Math.random()*(remainingLineages.length-1))])
}
Insert cell
sampleExponetial=(lambda)=>{
return -1*Math.log(Math.random())/lambda
}
Insert cell
makeTips=populationDemographic=>(maxSampleHeight)=>numberofTips=>{
function makeTipsHelper(tips){
const heightDraw = Math.random()*maxSampleHeight;
// get Tau because the simulation will be run in the imaginary time tau and then converted to real
// time. This why the maxSampleHieght can be given in real time.
const newTips = tips.concat({height:populationDemographic.getTau(heightDraw),name:uuid.v4()})
if(newTips.length<numberofTips){
return makeTipsHelper(newTips)
}else{
return newTips;
}
}
return makeTipsHelper([{height:0,name:uuid.v4()}])
}
Insert cell
Insert cell
Insert cell
simtree = simulateCoalescentTree({demographic:new ExponetialPopulation({N0:100,g:2}),maxTipHeight:1,numberOfTips:10})
Insert cell
Insert cell
figure = {
const treeSVG = document.getElementById('figtree');

const layout = new Figtree.RectangularLayout(simtree);


const figTree = new Figtree.FigTree(treeSVG, layout, { top: 10, bottom: 70, left: 10, right: 150},
{ hoverBorder: 5, backgroundBorder: 2,
baubles: [ new Figtree.CircleBauble({r:20,vertexFilter:v=>v.degree===1})],
styles:{"nodes":{"fill":(d)=>colorRange[2]},
"branches":{"stroke":(d)=>colorRange[9],
"stroke-width":d=>4}}},
)


figTree.draw()

return figTree;
}
Insert cell
getIntervalList=(tree)=>{
let k=tree.nodeList.filter(n=>n.height===0).length
return tree.nodeList.sort((a,b)=>a.height-b.height).map((n,i,nodes)=> {if(n!==tree.rootNode){
const interval= {t0:n.height,
t1:nodes[i+1].height,
k:k,
type:(nodes[i+1].children?"coalescent":"sampling")};
interval.type==="coalescent"?k-=1:k+=1
return interval;
}
return null;
}).filter(n=>n)
}
Insert cell
coalescentLogLikelihood=(PopulationDemographic)=>intervals=>{
let logL =0;
for(const interval of intervals){
const intervalArea = PopulationDemographic.getIntegral(interval.t0,interval.t1);
const kChoose2 = interval.k*(interval.k-1)/2
logL += -kChoose2 * intervalArea;
if(interval.type==="coalescent"){
// logL += Math.log(kChoose2*1/PopulationDemographic.getDemographic(interval.t1));

// logL += Math.log(kChoose2)
// should we include the above term?
logL -= Math.log(PopulationDemographic.getDemographic(interval.t1))


}
}
return logL;
}
Insert cell
Insert cell
coalescentLogLikelihood(new ExponetialPopulation({N0:500,g:1}))(getIntervalList(simtree))
Insert cell
coalescentLogLikelihood(new ExponetialPopulation({N0:100,g:2}))(getIntervalList(simtree))
Insert cell
Insert cell
Insert cell
colorRange = [...d3.schemeTableau10,...d3.schemeSet3]
Insert cell
md`# libraries`
Insert cell
uuid = import('https://jspm.dev/uuid')
Insert cell
d3 = require('d3')
Insert cell
Figtree = import("https://cdn.jsdelivr.net/gh/rambaut/figtree.js@a92caa7/dist/figtree.esm.js")
Insert cell
// Figtree =import("https://cdn.jsdelivr.net/gh/rambaut/figtree.js@5799a65/dist/figtree.esm.js")
Insert cell
d3_array=require('d3-array')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more