Generating trees
TreeTools
has three ways to generate trees:
- using a birth-death process (i.e. forward simulation);
- using a coalescent process (i.e. backward simulation);
- using a collection of pre-defined simple shapes.
All three methods are accessible through the TreeTools.Generate
submodule. For now, this is not exported: most functions below need to be prefaced by an explicit TreeTools.Generate
.
Birth-death process
This is rather straightforward:
julia> begin n = 25 # number of lineages to reach for completion b = 1 # birth rate d = 0.1 # death rate end;
julia> tree, completed = TreeTools.Generate.birth_death(n, b, d)
(Tree{TreeTools.EmptyData}: 49 nodes, 25 leaves, true)
julia> length(leaves(tree)) == n || !completed
true
The second output argument completed
is a boolean indicating whether the process reached the target n
lineages. If the death rate is non-zero, it is possible that all lineages die before completion.
The implemented process starts with one lineage (the root). Then, if there are $n$ lineages in the tree:
- with rate $b\cdot n$, trigger a birth event: pick a lineage at random and split it in two;
- with rate $d\cdot n$, trigger a death event: stop the lineage and place a leaf at its end;
- if
n
reaches the target value for completion or if all lineages are dead, stop the process by placing a leaf at the end of each lineage.
Coalescent process
There are two coalescent models implemented.
- The classical Kingman's process, with a rate of coalescence $\nu = n(n-1)/N$ for $n$ lineages, where $N$ is a parameter (the population size).
- The Yule process, with a rate of coalescence $\nu = b\cdot n$ for $n$ lineages, where $b$ can be interpreted as a birth rate. This is equivalent to a birth only forward process.
Generating a coalescent tree is a two-step process: first construct a Coalescent
object, then give it to the function genealogy
.
julia> kingman = TreeTools.Generate.KingmanCoalescent(n=25, N=10_000)
TreeTools.Generate.KingmanCoalescent(25, 10000.0)
julia> yule = TreeTools.Generate.YuleCoalescent(n=5, b=1)
TreeTools.Generate.YuleCoalescent(5, 1.0)
julia> tree = TreeTools.Generate.genealogy(kingman)
Tree{TreeTools.EmptyData}: 49 nodes, 25 leaves
julia> distance(root(tree), first(leaves(tree))) # ~2N on average
20931.794549823353
julia> tree = TreeTools.Generate.genealogy(yule)
________________________ 1 __________________________________________________| | | ,_ 2 | |______________________| _| |_ 4 | | _______________________________________________ 3 |___________________________| |_______________________________________________ 5
julia> distance(root(tree), first(leaves(tree))) # ~b log(n) on average
0.37721699073515935
It is possible to create custom coalescent. Below, we create a backward process with multiple mergers, with the following properties:
- the rate of merging is $\rho$, independent of the number of lineages $n$;
- when a merge occurs, an average fraction $\beta$ of the $n$ lineages is involved (sampled using a binomial).
First, we define the corresponding coalescent type. Importantly, the field n
must be present in the struct
, and is interpreted as the number of remaining lineages.
julia> @kwdef mutable struct MultipleMerger <: TreeTools.Generate.Coalescent n::Int # number of lineages β::Float64 # parameters ρ::Float64 end
Main.MultipleMerger
Now, we need to define the choose_event
function that samples a merge event from this process. The return value should be a tuple k, t
with k
being the number of lineages involved in the merge and t
the time to the event.
julia> using Distributions
julia> import TreeTools.Generate.choose_event
julia> function choose_event(C::MultipleMerger) C.n <= 1 && throw(ArgumentError("Cannot choose coalescence event for $(C.n) lineage.")) t = 0. merger = 0 coin = Binomial(C.n, C.β) # this is from Distributions, while merger < 2 t += rand(Exponential(1/C.ρ)) merger = rand(coin) end return merger, t end
choose_event (generic function with 5 methods)
Now we can sample from this
julia> mm = MultipleMerger(n=15, β=.25, ρ=1)
Main.MultipleMerger(15, 0.25, 1.0)
julia> tree = TreeTools.Generate.genealogy(mm)
__________________ 1 ______________________________________________________| | | _________ 2 | |________| | |_________ 8 _| | _________________________________ 10 | | | | _________ 6 | | | |_______________________________________| |_________ 9 | ____________| | | |_________ 15 | | | | | | ______ 11 |__________| |__| | |______ 13 | | ______________ 5 | | |_______| ____________ 4 | | | |____________ 7 |_| | __ 3 | | |_________|__ 12 | |__ 14
julia> using StatsBase
julia> map(n -> length(children(n)), internals(tree)) |> countmap # if we're not unlucky, there should be some multiple mergers in here!
Dict{Int64, Int64} with 3 entries: 4 => 1 2 => 7 3 => 2
Basic shapes
There are three at the moment, with self explanatory names:
julia> star = TreeTools.Generate.star_tree(4, 1.)
____________________________________________________________________________ 1 | |____________________________________________________________________________ 2 _| |____________________________________________________________________________ 3 | |____________________________________________________________________________ 4
julia> ladder = TreeTools.Generate.ladder_tree(4, 1.)
____________________________________________________________________________ 4 _| | ___________________________________________________ 3 |________________________| | _________________________ 2 |_________________________| |_________________________ 1
julia> balanced = TreeTools.Generate.balanced_binary_tree(4, 1.)
______________________________________ 3 _____________________________________| | |______________________________________ 4 _| | ______________________________________ 1 |_____________________________________| |______________________________________ 2
The docstrings give a bit more detail.