Source code for treesap.transmission_neutral_coalescent
#!/usr/bin/env python3
from .common import check_end_conditions,check_transmission_event,Set
from numpy.random import exponential
from scipy.stats import truncexpon
from treeswift import Node,Tree
[docs]def transmission_neutral_coalescent_tree(transmission_network, sample_times, rate=1):
'''Sample a tree under the pure-neutral coalescent model constrained to a given transmission network with given patient sampling times.
Args:
``transmission_network`` (``list``): The transmission network as a ``list`` of ``(u,v,t)`` tuples denoting the transmission from ``u`` to ``v`` at time ``t``. The transmission network must be sorted in ascending order of time
``sample_times`` (``dict``): The times at which each individual was sampled (i.e., the times of the leaves) as a ``dict`` in which keys are individuals from ``transmission_network`` and the value associated with an individual ``u`` is a ``list`` of times at which ``u`` was sampled (i.e., the times of the leaves from individual ``u``)
``rate`` (``float``): The rate of the Poisson process of coalescing two lineages
Returns:
A ``Tree`` object storing the sampled pure-neutral tree, where leaves are labeled ``ID|u|t``, where ``ID`` is a unique identifier for the leaf, ``u`` is the corresponding individual from ``transmission_network``, and ``t`` is the sample time (which equals the leaf's distance from the root). If there are multiple seed individuals (infected beforehand), a tree will be output for each.
'''
if not isinstance(transmission_network,list):
raise TypeError("transmission_network must be a list, but it was a %s" % str(type(transmission_network)))
if not isinstance(sample_times,dict):
raise TypeError("sample_times must be a dict, but it was a %s" % str(type(sample_times)))
time = dict(); ID = 0; root = dict(); leaves = dict(); infected_by = dict(); to_visit = set(); infection_time = dict()
for i in range(len(transmission_network)-1,-1,-1):
check_transmission_event(transmission_network[i])
if i != 0 and transmission_network[i][2] < transmission_network[i-1][2]:
raise ValueError("transmission_network must be sorted in ascending order of time")
u,v,t = transmission_network[i]
if v in infection_time:
raise ValueError("Encountered duplicate transmission recipient: %s" % str(v))
else:
infection_time[v] = t
if u not in infected_by:
infected_by[u] = list()
if v not in infected_by:
infected_by[v] = list()
if v not in sample_times:
raise KeyError("Individual not in sample_times: %s" % str(v))
if not isinstance(sample_times[v],list):
if isinstance(sample_times[v],float) or isinstance(sample_times[v],int):
sample_times[v] = [sample_times[v]]
elif isinstance(sample_times[v],set):
sample_times[v] = list(sample_times[v])
else:
raise TypeError("Values in sample_times must be list")
if u is not None and u != 'None':
infected_by[u].append(v); to_visit.add(u)
leaves[v] = list()
for w in infected_by[v]:
if w not in root:
raise ValueError("Missing links in transmission_network")
leaves[v].append(root[w]); del root[w]
for st in sample_times[v]:
if not isinstance(st,float) and not isinstance(st,int):
raise TypeError("Values in sample_times must be list of float")
newnode = Node(label='%d|%s|%f' % (ID,str(v),st)); ID += 1; time[newnode] = st; leaves[v].append(newnode)
leaves[v].sort(key=lambda x: time[x], reverse=True); lineages = Set(); curr_time = time[leaves[v][0]]
for w in leaves[v]:
if time[w] < curr_time:
while len(lineages) > 1:
L = rate*len(lineages)*(len(lineages)-1)/2.; coal_time = curr_time - exponential(1./L)
if coal_time >= time[w]:
c1 = lineages.pop(); c2 = lineages.pop(); newnode = Node(); time[newnode] = coal_time; curr_time = coal_time; newnode.add_child(c1); newnode.add_child(c2); lineages.add(newnode)
curr_time = time[w]
lineages.add(w)
while len(lineages) != 1:
L = rate*len(lineages)*(len(lineages)-1)/2.; coal_time = curr_time - truncexpon.rvs(curr_time-t, scale=1./L)
c1 = lineages.pop(); c2 = lineages.pop(); newnode = Node(); time[newnode] = coal_time; curr_time = coal_time; newnode.add_child(c1); newnode.add_child(c2); lineages.add(newnode)
root[v] = lineages.pop(); to_visit.discard(v); del leaves[v]
if len(to_visit) != 0:
raise ValueError("Malformed transmission network. Missing the following seeds:\n%s" % '\n'.join(str(v) for v in to_visit))
out = list()
for k,r in root.items():
if r.is_root():
tmp = Tree(); tmp.root = r; out.append(tmp)
for node in tmp.traverse_preorder():
if node.is_root():
node.edge_length = time[node] - infection_time[k]
else:
node.edge_length = time[node] - time[node.parent]
return out