-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcoop_merging.py
More file actions
365 lines (307 loc) · 13.1 KB
/
coop_merging.py
File metadata and controls
365 lines (307 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import numpy as np
import scipy
import matplotlib.pyplot as plt
import math
import random
import copy
import statistics
from scipy.optimize import curve_fit
from scipy.integrate import odeint
from numba import jit
import numba as nb
from numbalsoda import lsoda_sig, lsoda
from numba import njit, cfunc
#Deterministic Growth
@cfunc(lsoda_sig)
def rhs(t, N, dN, par):
dN[0]=N[0]*(1 - par[0] + par[0]*(par[1]*(N[0])/(N[0]+N[1])-par[2]))*(1-(N[0]+N[1])/par[3])
dN[1]=N[1]*(1 - par[0] + par[0]*(par[1]*N[0]/(N[0]+N[1])))*(1-(N[0]+N[1])/par[3])
funcptr = rhs.address
#Deterministic Growth
@njit
def growth_event(in_numbers,w,b,c,t,K):
N_demes,N_types=np.shape(in_numbers)
numbers=np.zeros((N_demes,N_types),dtype=np.float64)
par=np.array([w,b,c,K])
t_values = np.linspace(0,t,1000)
for i in range(N_demes):
usol = lsoda(funcptr, (in_numbers[i,:]).astype(np.float64), t_values, data = par)[0]
numbers[i,:]=(usol[-1])
return numbers
#Stochastic Growth
@njit
def count_nb_ele_nonzero(arr):
compt = 0
for i in range(len(arr)):
if arr[i] != 0:
compt += 1
return compt
@njit
def put_to0_negative_ele(arr):
for i in range(len(arr)):
if arr[i] < 0:
arr[i] = 0
return arr
@njit
def initialization10(Nm0, Nw0, D): #We start from one fully mutant deme in a wild-type structure; the mutant deme corresponds to the element of index 0 in the arrays Nm and Nw
Nm = np.zeros(D)
Nm[0] = Nm0
Nw = np.zeros(D)
for i in range(D):
Nw[i]=(Nm0+Nw0)-Nm[i]
#print('Initial state ', 'Nm0: ', Nm, ' ', 'Nw0: ', Nw)
return Nm, Nw
@njit
def fitnessM(b, c, w, Nm, Nw, D): #We create a np array containing the mutant fitnesses for each deme
fm = np.zeros(D)
for i in range(D):
fm[i] = 1 - w + w * ((b*(Nm[i])/(Nm[i]+Nw[i])-c))
return fm
@njit
def fitnessW(b, c, w, Nm, Nw, D): #We create a np array containing the wild-type fitnesses for each deme
fw = np.zeros(D)
for i in range(D):
fw[i] = 1 - w + w * b*Nm[i]/(Nm[i]+Nw[i])
return fw
@njit
def compute_division_reactionrate(Nm, Nw, f, K, D):
k = np.zeros(D)
for i in range(D):
k[i] = f[i]*(1-(Nw[i]+Nm[i])/K)
return k
@njit
def compute_total_rate(Nm, Nw, kw_p, km_p, D):
#kw_p: division reaction rate for the wild-types
#km_p: division reaction rate for the mutants
ktot = 0
for i in range(D):
ktot += kw_p[i]*Nw[i] + km_p[i]*Nm[i]
return ktot
@njit
def build_changetower(D): #We create an array change_tower in which we store all modifications which can happen at one Gillespie step
nb_reac = 2*D
ind_arr = np.array([D, 2*D], dtype=np.int64)
change_tower = np.empty((nb_reac,2), dtype=np.int64)
change_tower[0,:] = [0,+1]
for i in range(1,ind_arr[0]):
change_tower[i,:] = [0,+1] #Means here that we have 1 more wild-type in the deme i as we are looking at wild-type division reactions
for i in range(ind_arr[0], ind_arr[1]):
change_tower[i,:] = [+1,0] #Means here that we have 1 more mutant in the deme i as we are looking at mutant division reactions
return change_tower
@njit
def build_samplingtower(Nm, Nw, kw_p, km_p, D): #We create an array reac_tower in which we store all the possible reactions and from which we will randomly choose an event to happen in the simulation
nb_reac = 2*D #Total number of possible reactions in Gillespie algorithm
ind_arr = np.array([D, 2*D], dtype=np.int64) #Reactions are grouped by package, first the mutants divisions in the D demes, second the wild-types divisions in the D demes
reac_tower = np.zeros(nb_reac, dtype=np.float64)
ktot = compute_total_rate(Nm, Nw, kw_p, km_p, D)
reac_tower[0] = kw_p[0]*Nw[0]/ktot
for i in range(1,ind_arr[0]):
reac_tower[i] = reac_tower[i-1] + kw_p[i]*Nw[i]/ktot
for i in range(ind_arr[0], ind_arr[1]):
reac_tower[i] = reac_tower[i-1] + km_p[i-ind_arr[0]]*Nm[i-ind_arr[0]]/ktot
return reac_tower
#Gillespie Algorithm
@njit
def finalpopulation(b, c, w, Nm, Nw, K, D, t):
nb_loop = 0
time = 0
# Initialization
N_C = np.zeros((D, 0), dtype=np.float64)
N_F = np.zeros((D, 0), dtype=np.float64)
times = [0.0]
ind_arr = np.array([D, 2*D])
change_tower = build_changetower(D)
while time < t:
nb_loop += 1
fm = fitnessM(b, c, w, Nm, Nw, D)
fw = fitnessW(b, c, w, Nm, Nw, D)
kw_p = compute_division_reactionrate(Nm, Nw, fw, K, D)
kw_p = put_to0_negative_ele(kw_p)
km_p = compute_division_reactionrate(Nm, Nw, fm, K, D)
km_p = put_to0_negative_ele(km_p)
ktot = compute_total_rate(Nm, Nw, kw_p, km_p, D)
reac_tower = build_samplingtower(Nm, Nw, kw_p, km_p, D)
r = np.random.uniform(0, 1)
dt = (1 / ktot) * np.log(1 / r)
time += dt
r_ = np.random.uniform(0, 1)
ir = 0
while reac_tower[ir] <= r_:
ir += 1
if ir < ind_arr[1]:
addNm = change_tower[ir, 0]
addNw = change_tower[ir, 1]
if ir < D:
Nm[ir] += addNm
Nw[ir] += addNw
elif ir < 2 * D:
Nm[ir - D] += addNm
Nw[ir - D] += addNw
times.append(time)
Nm_reshaped = np.copy(Nm).reshape(D, 1)
Nw_reshaped = np.copy(Nw).reshape(D, 1)
N_C = np.hstack((N_C, Nm_reshaped))
N_F = np.hstack((N_F, Nw_reshaped))
return N_C, N_F, np.array(times)
@njit
def growth_stochastique(in_numbers, b, c, w, K, t):
in_numbers = in_numbers.astype(np.float64)
D, N_types = in_numbers.shape
Nm0 = in_numbers[:, 0]
Nw0 = in_numbers[:, 1]
mutants, cheaters, temps = finalpopulation(b, c, w, Nm0, Nw0, K, D, t)
return mutants, cheaters, temps
#End Stochastic Growth
#Migration and dilution event, simpson=True will sample from each deme proprotionally to its size after growth.
#Here we implement the binomial version, as described in the paper.
@njit
def dilution_migration_event(in_numbers,migration_matrix,Nmin,simpson=False):
N_demes, N_types= np.shape(in_numbers)
#Final numbers that will be kept at the end of the migration/dilution phase
new_numbers=np.zeros((N_demes, N_types), dtype=np.int64)
for j in np.arange(N_demes):
Nj=Nmin
probabilities = np.zeros((N_demes,2),dtype=np.float64)
for k in range(N_demes):
probabilities[k,0]= max(min((in_numbers[k,0]/np.sum(in_numbers[k,:]))*migration_matrix[k,j],1),0)
probabilities[k,1]= max(min((1-in_numbers[k,0]/np.sum(in_numbers[k,:]))*migration_matrix[k,j],1),0)
proba = np.reshape(probabilities,2*N_demes)
#while np.sum(proba)>1:
#print(np.sum(proba))
#proba = proba/np.sum(proba)
for l in range(len(proba)):
if proba[l]>1e-10:
proba[l]-= 1e-15
break
samples = np.zeros(2*N_demes)
samples = np.random.multinomial(Nj, proba)
samples_reshape = samples.reshape(-1, 2)
new_numbers[j,0]= np.sum(samples_reshape[:,0])
new_numbers[j,1]= np.sum(samples_reshape[:,1])
#Updating the numbers in j with the migrants that arrived from i
return new_numbers
#Checking if the mutant is completely extinct
@njit
def extinct_mutant(numbers):
N_demes = numbers.shape[0]
for i in range(N_demes):
if numbers[i,0]>0:
return False
return True
#Checking if the wild-type is completely extinct
@njit
def extinct_wild(numbers):
N_demes, N_types= numbers.shape
for i in range(N_demes):
for j in range(N_types -1):
if numbers[i,j+1]>0:
return False
return True
#We do a number of cycles starting from inital numbers in_numbers ; until the mutant is fixed or dies out
#nb_cycles : a priori number of how many cycles we do before observing fixation or extinction
#dilution_number = bottleneck
#growth_factor = t the growth parameter
@njit
def cycle(in_numbers, fitness_parameters, nb_cycles, growth_factor, dilution_number, determinism, start_follow_numbers, size_follow_numbers, start_cycle, print_frequency, save_dynamics=False, simpson=False):
N_demes, N_types= in_numbers.shape
if start_follow_numbers is None:
follow_numbers=np.zeros((size_follow_numbers,N_demes,N_types), dtype=np.int64)
else:
follow_numbers=start_follow_numbers.copy()
fixation=True
numbers=in_numbers.copy()
w,b,c,K=fitness_parameters
#Booleans that check if mutants or wild-types are extinct
keep_going=True
for i in range(nb_cycles):
end_cycle = nb_cycles
if determinism == True: #Deterministic growth
numbers=growth_event(numbers,w,b,c,growth_factor,K)
else: #Stochastic growth
mutants,cheaters,temps = growth_stochastique(numbers, b, c, w, K, growth_factor)
temps=temps[1:]
numbers=np.zeros((N_demes,N_types),dtype=np.float64)
numbers[:,0]=mutants[:,-1]
numbers[:,1]=cheaters[:,-1]
#print(numbers)
migration_matrix=np.ones((N_demes,N_demes),dtype=np.float64)
for j in range(N_demes):
migration_matrix[j,:]= np.ones(N_demes)*(numbers[j,0]+numbers[j,1])/np.sum(numbers[:,0]+numbers[:,1])
#print(migration_matrix)
numbers=dilution_migration_event(numbers,migration_matrix,dilution_number,simpson=False)
if (start_cycle+i)%print_frequency==0 and ((i+start_cycle)/print_frequency)<size_follow_numbers:
follow_numbers[int(i+start_cycle), :, :]=numbers
if extinct_mutant(numbers):
keep_going=False
fixation=False
end_cycle=i+start_cycle
follow_numbers[int((i+start_cycle))+1:]=numbers
break
if extinct_wild(numbers):
keep_going=False
fixation=True
#print("Fixation occurred at cycle ", i+start_cycle)
end_cycle=i+start_cycle
follow_numbers[int((i+start_cycle))+1:]=numbers
break
if keep_going:
follow_numbers, end_cycle, fixation= cycle(numbers, fitness_parameters, nb_cycles, growth_factor, dilution_number, determinism, follow_numbers, size_follow_numbers, start_cycle+end_cycle, print_frequency, save_dynamics, simpson=False)
return follow_numbers, end_cycle, fixation
#We compute the fixation probability starting from in_numbers, on nb_sim simulations.
#For each simulation we use function cycle, and see if the mutant is fixed or not
@njit
def fixation_probability(in_numbers, fitness_parameters, nb_sim, growth_factor, dilution_number, determinism, simpson,nb_cycles=1000,equal_contribution=False,size_follow_numbers=10000, print_frequency=1, save_dynamics=False,folder=None):
fix_count=0
average_fix_cycle=np.zeros(nb_sim)
average_ex_cycle=np.zeros(nb_sim)
for i in range(nb_sim):
#if i % 10000 == 0:
#print("Simulation", i)
start_cycle=0
start_follow_numbers=None
follow_numbers, end_cycle, fixation = cycle(in_numbers, fitness_parameters, nb_cycles, growth_factor, dilution_number, determinism, start_follow_numbers, size_follow_numbers, start_cycle, print_frequency, save_dynamics=False, simpson=False)
if fixation :
fix_count+=1
average_fix_cycle[i] = end_cycle
#if save_dynamics:
#if fix_count<10000:
#np.savez(folder+"/fix_"+str(fix_count),follow_numbers)
else:
average_ex_cycle[i] = end_cycle
#if save_dynamics:
#if i-fix_count<10000:
#np.savez(folder+"/ex_"+str(i-fix_count),follow_numbers)
ex_count=nb_sim-fix_count
if fix_count>0:
fixation_cycle = np.sum(average_fix_cycle)/fix_count
else :
fixation_cycle = 0
if ex_count>0:
extinction_cycle = np.sum(average_ex_cycle)/ex_count
else:
extinction_cycle=0
proba = fix_count/nb_sim
return extinction_cycle,fixation_cycle, proba
#Computing the fixation probability
def calculate_fixation_probability(bvalue):
w = 0.001
c = 1
nb_sim = 1000
t = 3
K=1000
bottleneck = 10
in_num_graph = np.array([[1.,9.],[0.,10.],[0.,10.],[0.,10.],[0.,10.]]).astype(np.int64) #Clique-structured population of D demes, bottleneck size B=10, starting with one mutant
#in_num_mixed=np.array([[1.,49.]]).astype(np.int64) #Well-mixed population of size D*B, starting with one mutant
parameters = w, bvalue, c,K
_, _, fixation_value = fixation_probability(in_num_graph, parameters, nb_sim, t, bottleneck,False,False,1000,False,10000, 1,False,None) #Deterministic or stochastic growth according to a boolean variable
return fixation_value
def execution():
c = 1
bvalues = np.linspace(c,50,20) #Testing different values of benefit-to-cost ratio b/c
bvalues=np.sort(bvalues)
fixation_values_list=np.zeros((20))
for i in range(20):
fixation_values_list[i]=calculate_fixation_probability(bvalues[i])
return fixation_values_list
fixation_values = execution() #Fixation probabilities
print(fixation_values)