argile 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- argile/__init__.py +4 -0
- argile/likelihood.py +596 -0
- argile/locate.py +265 -0
- argile/main.py +310 -0
- argile/migration_matrix.py +55 -0
- argile/world_map.py +233 -0
- argile-0.0.1.dist-info/METADATA +67 -0
- argile-0.0.1.dist-info/RECORD +11 -0
- argile-0.0.1.dist-info/WHEEL +5 -0
- argile-0.0.1.dist-info/licenses/LICENSE +21 -0
- argile-0.0.1.dist-info/top_level.txt +1 -0
argile/__init__.py
ADDED
argile/likelihood.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.linalg import matrix_power
|
|
3
|
+
from numba import njit, prange
|
|
4
|
+
import scipy
|
|
5
|
+
from .migration_matrix import build_transition_matrices
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@njit()
|
|
9
|
+
def _norm1(A):
|
|
10
|
+
"""1-norm (max absolute column sum) — numba's np.linalg.norm
|
|
11
|
+
doesn't reliably support ord=1 for 2D arrays, so do it by hand."""
|
|
12
|
+
n = A.shape[1]
|
|
13
|
+
max_sum = 0.0
|
|
14
|
+
for j in range(n):
|
|
15
|
+
s = 0.0
|
|
16
|
+
for i in range(A.shape[0]):
|
|
17
|
+
s += abs(A[i, j])
|
|
18
|
+
if s > max_sum:
|
|
19
|
+
max_sum = s
|
|
20
|
+
return max_sum
|
|
21
|
+
|
|
22
|
+
@njit()
|
|
23
|
+
def expm(A):
|
|
24
|
+
n = A.shape[0]
|
|
25
|
+
I = np.eye(n, dtype=A.dtype)
|
|
26
|
+
# Pade(13) coefficients (Higham, 2005)
|
|
27
|
+
b0, b1, b2, b3, b4, b5, b6, b7 = (
|
|
28
|
+
64764752532480000.0, 32382376266240000.0, 7771770303897600.0,
|
|
29
|
+
1187353796428800.0, 129060195264000.0, 10559470521600.0,
|
|
30
|
+
670442572800.0, 33522128640.0,
|
|
31
|
+
)
|
|
32
|
+
b8, b9, b10, b11, b12, b13 = (
|
|
33
|
+
1323241920.0, 40840800.0, 960960.0, 16380.0, 182.0, 1.0,
|
|
34
|
+
)
|
|
35
|
+
theta13 = 5.371920351148152
|
|
36
|
+
normA = _norm1(A)
|
|
37
|
+
s = 0
|
|
38
|
+
if normA > theta13:
|
|
39
|
+
s = int(np.ceil(np.log2(normA / theta13)))
|
|
40
|
+
if s < 0:
|
|
41
|
+
s = 0
|
|
42
|
+
A = A / (2.0 ** s)
|
|
43
|
+
A2 = A @ A
|
|
44
|
+
A4 = A2 @ A2
|
|
45
|
+
A6 = A2 @ A4
|
|
46
|
+
U = A @ (A6 @ (b13 * A6 + b11 * A4 + b9 * A2)
|
|
47
|
+
+ b7 * A6 + b5 * A4 + b3 * A2 + b1 * I)
|
|
48
|
+
V = (A6 @ (b12 * A6 + b10 * A4 + b8 * A2)
|
|
49
|
+
+ b6 * A6 + b4 * A4 + b2 * A2 + b0 * I)
|
|
50
|
+
P = U + V
|
|
51
|
+
Q = V - U
|
|
52
|
+
R = np.ascontiguousarray(np.linalg.solve(Q, P))
|
|
53
|
+
for _ in range(s):
|
|
54
|
+
R = R @ R
|
|
55
|
+
return R
|
|
56
|
+
|
|
57
|
+
@njit()
|
|
58
|
+
def calc_current_pos(id, messages, parents, coal_rate):
|
|
59
|
+
"""Calculates current node position as product of child messages
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
id : int
|
|
64
|
+
ID of node
|
|
65
|
+
messages : np.array
|
|
66
|
+
Messages being passed in tree
|
|
67
|
+
parents : np.array
|
|
68
|
+
Parent IDs for each node
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
current_pos : np.array
|
|
73
|
+
Probability distribution of node's current position given subtree below
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
children = np.where(parents==id)[0]
|
|
77
|
+
if len(children) == 0:
|
|
78
|
+
raise RuntimeError(f"Node {id} does not have any children. This should never happen.")
|
|
79
|
+
current_pos = messages[children[0]]
|
|
80
|
+
if len(children) > 1:
|
|
81
|
+
for i in range(1, len(children)):
|
|
82
|
+
current_pos = np.multiply(current_pos, messages[children[i]])
|
|
83
|
+
current_pos = np.multiply(coal_rate, current_pos)
|
|
84
|
+
return current_pos
|
|
85
|
+
|
|
86
|
+
@njit()
|
|
87
|
+
def calc_branch_message(
|
|
88
|
+
current_pos,
|
|
89
|
+
branch_above,
|
|
90
|
+
transition_matrices
|
|
91
|
+
):
|
|
92
|
+
"""Calculates the message to be passed along a branch above specified node
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
id : int
|
|
97
|
+
ID of node
|
|
98
|
+
current_pos : np.array
|
|
99
|
+
Probability distribution of node's current position given subtree below
|
|
100
|
+
branch_above : np.array
|
|
101
|
+
Branch lengths above each node split across epochs. Shape is #epochs x #nodes.
|
|
102
|
+
direction : string
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
current_pos : np.array
|
|
108
|
+
Probability distribution for location of lineage given subtree below. Length is #demes.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
included_epochs = np.where(branch_above > 0)[0]
|
|
112
|
+
for epoch in included_epochs:
|
|
113
|
+
trans_prob = expm(transition_matrices[epoch]*branch_above[epoch])
|
|
114
|
+
current_pos = np.dot(trans_prob, current_pos)
|
|
115
|
+
return current_pos
|
|
116
|
+
|
|
117
|
+
@njit()
|
|
118
|
+
def calc_backward_messages(
|
|
119
|
+
parents,
|
|
120
|
+
branch_above,
|
|
121
|
+
node_epoch,
|
|
122
|
+
ids_asc_time,
|
|
123
|
+
sample_locations_array,
|
|
124
|
+
sample_ids,
|
|
125
|
+
backward_transition_matrices,
|
|
126
|
+
coal_rates
|
|
127
|
+
):
|
|
128
|
+
""""""
|
|
129
|
+
|
|
130
|
+
num_demes = len(sample_locations_array[0])
|
|
131
|
+
messages = np.ones((len(parents), num_demes), dtype="float64")
|
|
132
|
+
loglikelihood = 0
|
|
133
|
+
for id in ids_asc_time:
|
|
134
|
+
if id in sample_ids:
|
|
135
|
+
current_pos = sample_locations_array[np.where(sample_ids==id)[0][0]]
|
|
136
|
+
else:
|
|
137
|
+
current_pos = calc_current_pos(
|
|
138
|
+
id,
|
|
139
|
+
messages,
|
|
140
|
+
parents,
|
|
141
|
+
coal_rates[node_epoch[id]]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Also rescale so that underflow is not a problem and track scaler
|
|
145
|
+
s = np.sum(current_pos)
|
|
146
|
+
current_pos /= s
|
|
147
|
+
loglikelihood += np.log(s)
|
|
148
|
+
|
|
149
|
+
parent = parents[id]
|
|
150
|
+
if parent != -1:
|
|
151
|
+
messages[id] = calc_branch_message(
|
|
152
|
+
current_pos,
|
|
153
|
+
branch_above[id],
|
|
154
|
+
backward_transition_matrices
|
|
155
|
+
)
|
|
156
|
+
else: # collect roots here
|
|
157
|
+
messages[id] = current_pos
|
|
158
|
+
|
|
159
|
+
return loglikelihood, messages
|
|
160
|
+
|
|
161
|
+
@njit()
|
|
162
|
+
def calc_branch_message_pre(
|
|
163
|
+
current_pos,
|
|
164
|
+
branch_above,
|
|
165
|
+
exponentiated_transition_matrices
|
|
166
|
+
):
|
|
167
|
+
"""Calculates the message to be passed along a branch above specified node
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
id : int
|
|
172
|
+
ID of node
|
|
173
|
+
current_pos : np.array
|
|
174
|
+
Probability distribution of node's current position given subtree below
|
|
175
|
+
branch_above : np.array
|
|
176
|
+
Branch lengths above each node split across epochs. Shape is #epochs x #nodes.
|
|
177
|
+
direction : string
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
current_pos : np.array
|
|
183
|
+
Probability distribution for location of lineage given subtree below. Length is #demes.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
included_epochs = np.where(branch_above > 0)[0]
|
|
187
|
+
for epoch in included_epochs:
|
|
188
|
+
trans_prob = matrix_power(exponentiated_transition_matrices[epoch], branch_above[epoch])
|
|
189
|
+
current_pos = np.dot(trans_prob, current_pos)
|
|
190
|
+
return current_pos
|
|
191
|
+
|
|
192
|
+
@njit()
|
|
193
|
+
def calc_backward_messages_pre(
|
|
194
|
+
parents,
|
|
195
|
+
branch_above,
|
|
196
|
+
node_epoch,
|
|
197
|
+
ids_asc_time,
|
|
198
|
+
sample_locations_array,
|
|
199
|
+
sample_ids,
|
|
200
|
+
exponentiated_backward_transition_matrices,
|
|
201
|
+
coal_rates
|
|
202
|
+
):
|
|
203
|
+
""""""
|
|
204
|
+
|
|
205
|
+
num_demes = len(sample_locations_array[0])
|
|
206
|
+
messages = np.ones((len(parents), num_demes), dtype="float64")
|
|
207
|
+
loglikelihood = 0
|
|
208
|
+
for id in ids_asc_time:
|
|
209
|
+
if id in sample_ids:
|
|
210
|
+
current_pos = sample_locations_array[np.where(sample_ids==id)[0][0]]
|
|
211
|
+
else:
|
|
212
|
+
current_pos = calc_current_pos(
|
|
213
|
+
id,
|
|
214
|
+
messages,
|
|
215
|
+
parents,
|
|
216
|
+
coal_rates[node_epoch[id]]
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Also rescale so that underflow is not a problem and track scaler
|
|
220
|
+
s = np.sum(current_pos)
|
|
221
|
+
current_pos /= s
|
|
222
|
+
loglikelihood += np.log(s)
|
|
223
|
+
|
|
224
|
+
parent = parents[id]
|
|
225
|
+
if parent != -1:
|
|
226
|
+
messages[id] = calc_branch_message_pre(
|
|
227
|
+
current_pos,
|
|
228
|
+
branch_above[id],
|
|
229
|
+
exponentiated_backward_transition_matrices
|
|
230
|
+
)
|
|
231
|
+
else: # collect roots here
|
|
232
|
+
messages[id] = current_pos
|
|
233
|
+
|
|
234
|
+
return loglikelihood, messages
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@njit()
|
|
238
|
+
def _calc_branch_message_pre_log(
|
|
239
|
+
current_pos,
|
|
240
|
+
branch_above,
|
|
241
|
+
exponentiated_transition_matrices,
|
|
242
|
+
epoch_durations,
|
|
243
|
+
precalculated_transitions_log,
|
|
244
|
+
):
|
|
245
|
+
"""Calculates the message to be passed along a branch above specified node
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
id : int
|
|
250
|
+
ID of node
|
|
251
|
+
current_pos : np.array
|
|
252
|
+
Probability distribution of node's current position given subtree below
|
|
253
|
+
unique_branch_lengths : list
|
|
254
|
+
Arrays containing unique branch lengths in each epoch
|
|
255
|
+
precalculated_transitions : list
|
|
256
|
+
Arrays of transition probabilities corresponding with the unique_branch_lengths, converted to log space
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
message : np.array
|
|
261
|
+
Probability distribution for location of lineage given subtree below. Length is #demes.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
included_epochs = np.where(branch_above > 0)[0]
|
|
265
|
+
for epoch in included_epochs:
|
|
266
|
+
if branch_above[epoch] == epoch_durations[epoch]:
|
|
267
|
+
trans_prob_log = precalculated_transitions_log[epoch]
|
|
268
|
+
else:
|
|
269
|
+
trans_prob_log = np.log(np.maximum(matrix_power(exponentiated_transition_matrices[epoch], branch_above[epoch]), 1e-99))
|
|
270
|
+
current_pos = logsumexp_custom(trans_prob_log + current_pos, axis=1)[np.newaxis, :]
|
|
271
|
+
return current_pos
|
|
272
|
+
|
|
273
|
+
@njit()
|
|
274
|
+
def likelihood_of_tree_pre_log(
|
|
275
|
+
parents,
|
|
276
|
+
branch_above,
|
|
277
|
+
node_epoch,
|
|
278
|
+
ids_asc_time,
|
|
279
|
+
sample_locations_array_log,
|
|
280
|
+
sample_ids,
|
|
281
|
+
exponentiated_backward_transition_matrices,
|
|
282
|
+
epoch_durations,
|
|
283
|
+
precalculated_transitions_log,
|
|
284
|
+
coal_rates_log
|
|
285
|
+
):
|
|
286
|
+
"""Computes log-likelihood of tree in log-space to avoid underflow
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
parents : np.array
|
|
291
|
+
Parent IDs for each node. Length is #nodes.
|
|
292
|
+
branch_above : np.array
|
|
293
|
+
Branch lengths above each node split across epochs. Shape is #epochs x #nodes.
|
|
294
|
+
node_epoch : np.array
|
|
295
|
+
Epochs of each node. Length is #nodes.
|
|
296
|
+
ids_asc_time : np.array
|
|
297
|
+
IDs of nodes in tree in time ascending order. Length is #nodes.
|
|
298
|
+
sample_locations_array_log : np.array
|
|
299
|
+
Vector representation of sample locations. Shape is #samples x #demes.
|
|
300
|
+
sample_ids : np.array
|
|
301
|
+
IDs of sample nodes in tree. Length is #samples.
|
|
302
|
+
backward_transition_matrices : np.array
|
|
303
|
+
|
|
304
|
+
coal_rates_log : numpy.ndarray
|
|
305
|
+
Recipricol of suitability values for each deme in each epoch, converted to log space. Shape is #epochs x #demes.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
loglikelihood : float
|
|
310
|
+
Log-likelihood of the tree
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
num_demes = len(sample_locations_array_log[0])
|
|
314
|
+
messages = np.zeros((len(parents), num_demes), dtype="float64")
|
|
315
|
+
loglikelihood = 0
|
|
316
|
+
for id in ids_asc_time:
|
|
317
|
+
if id in sample_ids:
|
|
318
|
+
current_pos = sample_locations_array_log[np.where(sample_ids==id)[0][0]][np.newaxis, :]
|
|
319
|
+
else:
|
|
320
|
+
current_pos = _calc_current_pos_log(
|
|
321
|
+
id,
|
|
322
|
+
messages,
|
|
323
|
+
parents,
|
|
324
|
+
coal_rates_log[node_epoch[id]]
|
|
325
|
+
)
|
|
326
|
+
messages[id] = _calc_branch_message_pre_log(
|
|
327
|
+
current_pos,
|
|
328
|
+
branch_above[id],
|
|
329
|
+
exponentiated_backward_transition_matrices,
|
|
330
|
+
epoch_durations,
|
|
331
|
+
precalculated_transitions_log
|
|
332
|
+
)
|
|
333
|
+
if parents[id] == -1:
|
|
334
|
+
loglikelihood += logsumexp_custom(current_pos, axis=1)[0]
|
|
335
|
+
return loglikelihood
|
|
336
|
+
|
|
337
|
+
@njit()
|
|
338
|
+
def _calc_current_pos_log(id, messages, parents, coal_rates):
|
|
339
|
+
"""Calculates current node position as product of child messages
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
id : int
|
|
344
|
+
ID of node
|
|
345
|
+
messages : np.array
|
|
346
|
+
Messages being passed in tree. Shape is #nodes x #demes.
|
|
347
|
+
parents : np.array
|
|
348
|
+
Parent IDs for each node. Length is #nodes.
|
|
349
|
+
coal_rates : numpy.ndarray
|
|
350
|
+
Recipricol of suitability values for each deme in the node's corresponding epoch, converted to log space
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
current_pos : np.array
|
|
355
|
+
Probability distribution of node's current position given subtree below. Length is #demes.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
current_pos = coal_rates + np.sum(messages[np.where(parents==id)[0]], axis=0)[np.newaxis, :]
|
|
359
|
+
return current_pos
|
|
360
|
+
|
|
361
|
+
@njit()
|
|
362
|
+
def logsumexp_custom(x, axis):
|
|
363
|
+
"""LogSumExp function that can be accessed from numba
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
x : numpy.ndarray
|
|
368
|
+
Vector to transform
|
|
369
|
+
axis : int
|
|
370
|
+
Which axis of the numpy.array to transform over
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
c = np.max(x)
|
|
374
|
+
return c + np.log(np.sum(np.exp(x - c), axis=axis))
|
|
375
|
+
|
|
376
|
+
@njit()
|
|
377
|
+
def _calc_branch_message_log(
|
|
378
|
+
current_pos,
|
|
379
|
+
branch_above,
|
|
380
|
+
transition_matrices
|
|
381
|
+
):
|
|
382
|
+
"""Calculates the message to be passed along a branch above specified node
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
id : int
|
|
387
|
+
ID of node
|
|
388
|
+
current_pos : np.array
|
|
389
|
+
Probability distribution of node's current position given subtree below
|
|
390
|
+
unique_branch_lengths : list
|
|
391
|
+
Arrays containing unique branch lengths in each epoch
|
|
392
|
+
precalculated_transitions : list
|
|
393
|
+
Arrays of transition probabilities corresponding with the unique_branch_lengths, converted to log space
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
message : np.array
|
|
398
|
+
Probability distribution for location of lineage given subtree below. Length is #demes.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
included_epochs = np.where(branch_above > 0)[0]
|
|
402
|
+
for epoch in included_epochs:
|
|
403
|
+
trans_prob_log = np.log(np.maximum(expm(transition_matrices[epoch]*branch_above[epoch]), 1e-99))
|
|
404
|
+
current_pos = logsumexp_custom(trans_prob_log + current_pos, axis=1)[np.newaxis, :]
|
|
405
|
+
return current_pos
|
|
406
|
+
|
|
407
|
+
@njit()
|
|
408
|
+
def likelihood_of_tree_log(
|
|
409
|
+
parents,
|
|
410
|
+
branch_above,
|
|
411
|
+
node_epoch,
|
|
412
|
+
ids_asc_time,
|
|
413
|
+
sample_locations_array_log,
|
|
414
|
+
sample_ids,
|
|
415
|
+
backward_transition_matrices,
|
|
416
|
+
coal_rates_log
|
|
417
|
+
):
|
|
418
|
+
"""Computes log-likelihood of tree in log-space to avoid underflow
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
parents : np.array
|
|
423
|
+
Parent IDs for each node. Length is #nodes.
|
|
424
|
+
branch_above : np.array
|
|
425
|
+
Branch lengths above each node split across epochs. Shape is #epochs x #nodes.
|
|
426
|
+
node_epoch : np.array
|
|
427
|
+
Epochs of each node. Length is #nodes.
|
|
428
|
+
ids_asc_time : np.array
|
|
429
|
+
IDs of nodes in tree in time ascending order. Length is #nodes.
|
|
430
|
+
sample_locations_array_log : np.array
|
|
431
|
+
Vector representation of sample locations. Shape is #samples x #demes.
|
|
432
|
+
sample_ids : np.array
|
|
433
|
+
IDs of sample nodes in tree. Length is #samples.
|
|
434
|
+
backward_transition_matrices : np.array
|
|
435
|
+
|
|
436
|
+
coal_rates_log : numpy.ndarray
|
|
437
|
+
Recipricol of suitability values for each deme in each epoch, converted to log space. Shape is #epochs x #demes.
|
|
438
|
+
|
|
439
|
+
Returns
|
|
440
|
+
-------
|
|
441
|
+
loglikelihood : float
|
|
442
|
+
Log-likelihood of the tree
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
num_demes = len(sample_locations_array_log[0])
|
|
446
|
+
messages = np.zeros((len(parents), num_demes), dtype="float64")
|
|
447
|
+
loglikelihood = 0
|
|
448
|
+
for id in ids_asc_time:
|
|
449
|
+
if id in sample_ids:
|
|
450
|
+
current_pos = sample_locations_array_log[np.where(sample_ids==id)[0][0]][np.newaxis, :]
|
|
451
|
+
else:
|
|
452
|
+
current_pos = _calc_current_pos_log(
|
|
453
|
+
id,
|
|
454
|
+
messages,
|
|
455
|
+
parents,
|
|
456
|
+
coal_rates_log[node_epoch[id]]
|
|
457
|
+
)
|
|
458
|
+
messages[id] = _calc_branch_message_log(
|
|
459
|
+
current_pos,
|
|
460
|
+
branch_above[id],
|
|
461
|
+
backward_transition_matrices
|
|
462
|
+
)
|
|
463
|
+
if parents[id] == -1:
|
|
464
|
+
loglikelihood += logsumexp_custom(current_pos, axis=1)[0]
|
|
465
|
+
return loglikelihood
|
|
466
|
+
|
|
467
|
+
@njit(parallel=True)
|
|
468
|
+
def _process_trees(
|
|
469
|
+
parents,
|
|
470
|
+
branch_above,
|
|
471
|
+
node_epoch,
|
|
472
|
+
ids_asc_time,
|
|
473
|
+
sample_locations_array_log,
|
|
474
|
+
sample_ids,
|
|
475
|
+
exponentiated_backward_transition_matrices,
|
|
476
|
+
epoch_durations,
|
|
477
|
+
precalculated_transitions_log,
|
|
478
|
+
coal_rates_log
|
|
479
|
+
):
|
|
480
|
+
"""
|
|
481
|
+
Parameters
|
|
482
|
+
----------
|
|
483
|
+
parents : list
|
|
484
|
+
Arrays containing ID of parent for each node, one array per tree
|
|
485
|
+
branch_above : list
|
|
486
|
+
Arrays containing branch above length (split across epochs) for each node, one array per tree
|
|
487
|
+
node_epoch : list
|
|
488
|
+
Arrays containing the epochs of each node, one array per tree
|
|
489
|
+
ids_asc_time : List
|
|
490
|
+
Arrays of nodes IDs in time ascending order, one array per tree
|
|
491
|
+
sample_locations_array : numpy.ndarray
|
|
492
|
+
Probability distribution vector for each sample location, converted to log space
|
|
493
|
+
sample_ids : numpy.ndarray
|
|
494
|
+
Order of sample IDs for `sample_locations_array`
|
|
495
|
+
backward_transition_matrices : np.array
|
|
496
|
+
|
|
497
|
+
coal_rates_log : numpy.ndarray
|
|
498
|
+
Recipricol of suitability values for each deme in each epoch, converted to log space
|
|
499
|
+
|
|
500
|
+
Returns
|
|
501
|
+
-------
|
|
502
|
+
composite_likelihood : float
|
|
503
|
+
Composite log-likelihood across the trees
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
composite_likelihood = 0
|
|
507
|
+
for i in prange(len(branch_above)):
|
|
508
|
+
like = likelihood_of_tree_pre_log(
|
|
509
|
+
parents=parents[i],
|
|
510
|
+
branch_above=branch_above[i],
|
|
511
|
+
node_epoch=node_epoch[i],
|
|
512
|
+
ids_asc_time=ids_asc_time[i],
|
|
513
|
+
sample_locations_array_log=sample_locations_array_log,
|
|
514
|
+
sample_ids=sample_ids,
|
|
515
|
+
exponentiated_backward_transition_matrices=exponentiated_backward_transition_matrices,
|
|
516
|
+
epoch_durations=epoch_durations,
|
|
517
|
+
precalculated_transitions_log=precalculated_transitions_log,
|
|
518
|
+
coal_rates_log=coal_rates_log
|
|
519
|
+
)
|
|
520
|
+
composite_likelihood += like
|
|
521
|
+
return composite_likelihood
|
|
522
|
+
|
|
523
|
+
def calc_composite_likelihood_for_parameters(
|
|
524
|
+
parameters,
|
|
525
|
+
world_map,
|
|
526
|
+
parents,
|
|
527
|
+
branch_above,
|
|
528
|
+
node_epoch,
|
|
529
|
+
ids_asc_time,
|
|
530
|
+
sample_locations_array_log,
|
|
531
|
+
sample_ids,
|
|
532
|
+
output_file=None,
|
|
533
|
+
verbose=False
|
|
534
|
+
):
|
|
535
|
+
"""
|
|
536
|
+
Parameters
|
|
537
|
+
----------
|
|
538
|
+
parameters : numpy.ndarray
|
|
539
|
+
Combination of parameters used to build the migration surface
|
|
540
|
+
world_map : argile.WorldMap
|
|
541
|
+
Custom object built using the `demes.tsv`, `connections.tsv`, and `samples.tsv` files
|
|
542
|
+
parents : list
|
|
543
|
+
Arrays containing ID of parent for each node, one array per tree
|
|
544
|
+
branch_above : list
|
|
545
|
+
Arrays containing branch above length (split across epochs) for each node, one array per tree
|
|
546
|
+
node_epoch : list
|
|
547
|
+
Arrays containing the epochs of each node, one array per tree
|
|
548
|
+
ids_asc_time : list
|
|
549
|
+
Arrays of nodes IDs in time ascending order, one array per tree
|
|
550
|
+
sample_locations_array_log : numpy.ndarray
|
|
551
|
+
Probability distribution vector for each sample location (generally 0 in all demes except one)
|
|
552
|
+
sample_ids : numpy.ndarray
|
|
553
|
+
Order of sample IDs for `sample_locations_array`
|
|
554
|
+
output_file : str
|
|
555
|
+
Path to an output file to write to (default is `None`, ignored)
|
|
556
|
+
verbose : bool
|
|
557
|
+
Whether to print log-likelihoods to the terminal (default is False)
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
composite_likelihood : float
|
|
562
|
+
Log-likelihood of the parameter combination
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
backward_transition_matrices = build_transition_matrices(world_map=world_map, parameters=parameters, direction="backward")
|
|
566
|
+
|
|
567
|
+
exponentiated_backward_transition_matrices = np.zeros(backward_transition_matrices.shape)
|
|
568
|
+
precalculated_full_epoch_branch_lengths = np.zeros(backward_transition_matrices.shape)
|
|
569
|
+
for i in range(len(backward_transition_matrices)):
|
|
570
|
+
exponentiated_backward_transition_matrices[i] = scipy.linalg.expm(backward_transition_matrices[i])
|
|
571
|
+
if world_map.epoch_durations[i] != -1:
|
|
572
|
+
precalculated_full_epoch_branch_lengths[i] = matrix_power(exponentiated_backward_transition_matrices[i], world_map.epoch_durations[i])
|
|
573
|
+
precalculated_full_epoch_branch_lengths_log = np.log(np.maximum(precalculated_full_epoch_branch_lengths, 1e-99))
|
|
574
|
+
|
|
575
|
+
pop_sizes = np.maximum(world_map.suitabilities, 1e-99)
|
|
576
|
+
coal_rates_log = np.log(1 / (np.maximum(pop_sizes, 0.1)))
|
|
577
|
+
|
|
578
|
+
composite_likelihood = _process_trees(
|
|
579
|
+
parents=parents,
|
|
580
|
+
branch_above=branch_above,
|
|
581
|
+
node_epoch=node_epoch,
|
|
582
|
+
ids_asc_time=ids_asc_time,
|
|
583
|
+
sample_locations_array_log=sample_locations_array_log,
|
|
584
|
+
sample_ids=sample_ids,
|
|
585
|
+
exponentiated_backward_transition_matrices=exponentiated_backward_transition_matrices,
|
|
586
|
+
epoch_durations=world_map.epoch_durations,
|
|
587
|
+
precalculated_transitions_log=precalculated_full_epoch_branch_lengths_log,
|
|
588
|
+
coal_rates_log=coal_rates_log
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if output_file is not None:
|
|
592
|
+
with open(output_file, "a") as outfile:
|
|
593
|
+
outfile.write(f"{parameters}\t{composite_likelihood}\n")
|
|
594
|
+
if verbose:
|
|
595
|
+
print(parameters, composite_likelihood, flush=True)
|
|
596
|
+
return composite_likelihood
|