piegy 2.1.0__cp38-cp38-win32.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.
piegy/simulation_py.py ADDED
@@ -0,0 +1,817 @@
1
+ '''
2
+ Functions and class below are for Python-based simulations. Not maintained after v1.1.6 on Jun 26, 2025.
3
+ But you can still run them by calling:
4
+
5
+ >>> run_py(mod)
6
+ '''
7
+
8
+ from . import simulation
9
+ model = simulation.model
10
+
11
+ import math
12
+ import numpy as np
13
+ from timeit import default_timer as timer
14
+
15
+
16
+ class patch:
17
+ '''
18
+ A single patch in the N x M space.
19
+ Interacts with neighboring patches, assuming no spatial structure within a patch.
20
+ Initialized in single_init function.
21
+
22
+ Class Functions:
23
+
24
+ __init__:
25
+ Inputs:
26
+ U, V: initial value of U and V
27
+ matrix: payoff matrix for U and V. The canonical form is 2x2, here we ask for a flattened 1x4 form.
28
+ patch_var: np.array of [mu1, mu2, w1, w2, kappa1, kappa2]
29
+
30
+ __str__:
31
+ Print patch object in a nice way.
32
+
33
+ set_nb_pointers:
34
+ Set pointers to neighbors of this patch object.
35
+
36
+ update_pi:
37
+ Update Upi, Vpi and payoff rates (payoff rates are the first two numbers in self.pi_death_rates).
38
+
39
+ update_k:
40
+ Update natural death rates (the last two numbers in self.pi_death_rates).
41
+
42
+ update_mig:
43
+ Update migration rates.
44
+
45
+ get_pi_death_rates, get_mig_rates:
46
+ Return respective members.
47
+
48
+ change_popu:
49
+ Change U, V based on input signal.
50
+ '''
51
+
52
+ def __init__(self, U, V, matrix = [-0.1, 0.4, 0, 0.2], patch_var = [0.5, 0.5, 100, 100, 0.001, 0.001]):
53
+
54
+ self.U = U # int, U population. Initialized upon creating object.
55
+ self.V = V # int, V population
56
+ self.Upi = 0 # float, payoff
57
+ self.Vpi = 0
58
+
59
+ self.matrix = matrix # np.array or list, len = 4, payoff matrix
60
+ self.mu1 = patch_var[0] # float, how much proportion of the population migrates (U) each time
61
+ self.mu2 = patch_var[1]
62
+ self.w1 = patch_var[2] # float, strength of payoff-driven effect. Larger w <=> stronger payoff-driven motion
63
+ self.w2 = patch_var[3]
64
+ self.kappa1 = patch_var[4] # float, carrying capacity, determines death rates
65
+ self.kappa2 = patch_var[5]
66
+
67
+ self.nb = None # list of patch objects (pointers), point to neighbors, initialized seperatedly (after all patches are created)
68
+ self.pi_death_rates = [0 for _ in range(4)] # list, len = 4, rates of payoff & death
69
+ # first two are payoff rates, second two are death rates
70
+ self.mig_rates = [0 for _ in range(8)] # list, len = 8, migration rates, stored in an order: up, down, left, right,
71
+ # first 4 are U's mig_rate, the last 4 are V's
72
+ self.sum_pi_death_rates = 0 # float, sum of pi_death_rates
73
+ self.sum_mig_rates = 0 # float, sum of mig_rates
74
+
75
+
76
+ def __str__(self):
77
+ self_str = ''
78
+ self_str += 'U, V = ' + str(self.U) + ', ' + str(self.V) + '\n'
79
+ self_str += 'pi = ' + str(self.Upi) + ', ' + str(self.Vpi) + '\n'
80
+ self_str += 'matrix = ' + str(self.matrix) + '\n'
81
+ self_str += 'mu1, mu2 = ' + str(self.mu1) + ', ' + str(self.mu2) + '\n'
82
+ self_str += 'w1, w2 = ' + str(self.w1) + ', ' + str(self.w2) + '\n'
83
+ self_str += 'kappa1, kappa2 = ' + str(self.kappa1) + ', ' + str(self.kappa2) + '\n'
84
+ self_str += '\n'
85
+ self_str += 'nb = ' + str(self.nb)
86
+ self_str += 'pi_death_rates = ' + str(self.pi_death_rates) + '\n'
87
+ self_str += 'mig_rates = ' + str(self.mig_rates) + '\n'
88
+ self_str += 'sum_pi_death_rates = ' + str(self.sum_pi_death_rates) + '\n'
89
+ self_str += 'sum_mig_rates = ' + str(self.sum_mig_rates) + '\n'
90
+
91
+ return self_str
92
+
93
+
94
+ def set_nb_pointers(self, nb):
95
+ # nb is a list of pointers (point to patches)
96
+ # nb is passed from the model class
97
+ self.nb = nb
98
+
99
+
100
+ def update_pi_k(self):
101
+ # calculate payoff and natural death rates
102
+
103
+ U = self.U # bring the values to front
104
+ V = self.V
105
+ sum_minus_1 = U + V - 1 # this value is used several times
106
+
107
+ if sum_minus_1 > 0:
108
+ # interaction happens only if there is more than 1 individual
109
+
110
+ if U != 0:
111
+ # no payoff if U == 0
112
+ self.Upi = (U - 1) / sum_minus_1 * self.matrix[0] + V / sum_minus_1 * self.matrix[1]
113
+ else:
114
+ self.Upi = 0
115
+
116
+ if V != 0:
117
+ self.Vpi = U / sum_minus_1 * self.matrix[2] + (V - 1) / sum_minus_1 * self.matrix[3]
118
+ else:
119
+ self.Vpi = 0
120
+
121
+ else:
122
+ # no interaction, hence no payoff, if only 1 individual
123
+ self.Upi = 0
124
+ self.Vpi = 0
125
+
126
+ # update payoff rates
127
+ self.pi_death_rates[0] = abs(U * self.Upi)
128
+ self.pi_death_rates[1] = abs(V * self.Vpi)
129
+
130
+ # update natural death rates
131
+ self.pi_death_rates[2] = self.kappa1 * U * (sum_minus_1 + 1)
132
+ self.pi_death_rates[3] = self.kappa2 * V * (sum_minus_1 + 1)
133
+
134
+ # update sum of rates
135
+ self.sum_pi_death_rates = sum(self.pi_death_rates)
136
+
137
+
138
+ def update_mig(self):
139
+ # calculate migration rates
140
+
141
+ # store the 'weight' of migration, i.e. value of f/g functions for neighbors
142
+ U_weight = [0, 0, 0, 0]
143
+ V_weight = [0, 0, 0, 0]
144
+
145
+ for i in range(4):
146
+ if self.nb[i] != None:
147
+ U_weight[i] = 1 + pow(math.e, self.w1 * self.nb[i].Upi)
148
+ V_weight[i] = 1 + pow(math.e, self.w2 * self.nb[i].Vpi)
149
+
150
+ mu1_U = self.mu1 * self.U
151
+ mu2_V = self.mu2 * self.V
152
+
153
+ mu1_U_divide_sum = mu1_U / sum(U_weight)
154
+ mu2_V_divide_sum = mu2_V / sum(V_weight)
155
+
156
+ for i in range(4):
157
+ self.mig_rates[i] = mu1_U_divide_sum * U_weight[i]
158
+ self.mig_rates[i + 4] = mu2_V_divide_sum * V_weight[i]
159
+
160
+ # update sum of rates
161
+ self.sum_mig_rates = mu1_U + mu2_V
162
+
163
+
164
+ def get_sum_rates(self):
165
+ # return sum of all 12 rates
166
+ return self.sum_pi_death_rates + self.sum_mig_rates
167
+
168
+
169
+ def find_event(self, expected_sum):
170
+ # find the event within the 12 events based on expected sum-of-rates within this patch
171
+
172
+ if expected_sum > (self.sum_pi_death_rates + self.sum_mig_rates):
173
+ print("patch rate not large enough")
174
+
175
+ if expected_sum < self.sum_pi_death_rates:
176
+ # in the first 4 events (payoff and death events)
177
+ event = 0
178
+ current_sum = 0
179
+ while current_sum < expected_sum:
180
+ current_sum += self.pi_death_rates[event]
181
+ event += 1
182
+ event -= 1
183
+
184
+ else:
185
+ # in the last 8 events (migration events):
186
+ event = 0
187
+ current_sum = self.sum_pi_death_rates
188
+ while current_sum < expected_sum:
189
+ current_sum += self.mig_rates[event]
190
+ event += 1
191
+ event += 3 # i.e., -= 1, then += 4 (to account for the first 4 payoff & death rates)
192
+
193
+ return event
194
+
195
+
196
+ def change_popu(self, s):
197
+ # convert s (a signal, passed from model class) to a change in population
198
+
199
+ # s = 0, 1, 2 are for U
200
+ # s = 0 for migration IN, receive an immigrant
201
+ if s == 0:
202
+ self.U += 1 # receive an immigrant
203
+ # s = 1 for migration OUT / death due to carrying capacity
204
+ elif s == 1:
205
+ if self.U > 0:
206
+ self.U -= 1
207
+ # s = 2 for natural birth / death, due to payoff
208
+ elif s == 2:
209
+ if self.Upi > 0:
210
+ self.U += 1 # natural growth due to payoff
211
+ elif self.U > 0:
212
+ self.U -= 1 # natural death due to payoff
213
+
214
+ # s = 3, 4, 5 are for V
215
+ elif s == 3:
216
+ self.V += 1
217
+ elif s == 4:
218
+ if self.V > 0:
219
+ self.V -= 1
220
+ else:
221
+ if self.Vpi > 0:
222
+ self.V += 1
223
+ elif self.V > 0:
224
+ self.V -= 1
225
+
226
+
227
+
228
+ def find_nb_zero_flux(N, M, i, j):
229
+ '''
230
+ Find neighbors of patch (i, j) in zero-flux boundary condition. i.e., the space is square with boundary.
231
+ Return neighbors' indices in an order: up, down, left, right.
232
+ Index will be None if no neighbor exists in that direction.
233
+ '''
234
+ nb_indices = []
235
+
236
+ if i != 0:
237
+ nb_indices.append([i - 1, j]) # up
238
+ else:
239
+ nb_indices.append(None) # neighbor doesn't exist
240
+
241
+ if i != N - 1:
242
+ nb_indices.append([i + 1, j]) # down
243
+ else:
244
+ nb_indices.append(None)
245
+
246
+ if j != 0:
247
+ nb_indices.append([i, j - 1]) # left
248
+ else:
249
+ nb_indices.append(None)
250
+
251
+ if j != M - 1:
252
+ nb_indices.append([i, j + 1]) # right
253
+ else:
254
+ nb_indices.append(None)
255
+
256
+ return nb_indices
257
+
258
+
259
+
260
+
261
+ def find_nb_periodical(N, M, i, j):
262
+ '''
263
+ Find neighbors of patch (i, j) in periodical boundary condition. i.e., the space is a sphere.
264
+ Return neighbors' indices in an order: up, down, left, right.
265
+ If space not 1D, a neighbor always exists.
266
+ If space is 1D, say N = 1, we don't allow (0, j) to migrate up & down (self-self migration is considered invalid)
267
+ '''
268
+ nb_indices = []
269
+
270
+ # up
271
+ if N != 1:
272
+ if i != 0:
273
+ nb_indices.append([i - 1, j])
274
+ else:
275
+ nb_indices.append([N - 1, j])
276
+ else:
277
+ nb_indices.append(None) # can't migrate to itself
278
+
279
+ # down
280
+ if N != 1:
281
+ if i != N - 1:
282
+ nb_indices.append([i + 1, j])
283
+ else:
284
+ nb_indices.append([0, j])
285
+ else:
286
+ nb_indices.append(None)
287
+
288
+ # left
289
+ # No need to check M == 1 because we explicitly asked for M > 1
290
+ if j != 0:
291
+ nb_indices.append([i, j - 1])
292
+ else:
293
+ nb_indices.append([i, M - 1])
294
+
295
+ # right
296
+ if j != M - 1:
297
+ nb_indices.append([i, j + 1])
298
+ else:
299
+ nb_indices.append([i, 0])
300
+
301
+ return nb_indices
302
+
303
+
304
+
305
+
306
+ def find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates):
307
+ '''
308
+ Find which patch the event is in. Only patch index is found, patch.find_event find which event it is exactly.
309
+
310
+ Inputs:
311
+ expected_sum: a random number * sum of all rates. Essentially points to a random event.
312
+ We want to find the patch that contains this pointer.
313
+ patch_rates: a N x M np.array. Stores sum of the 12 rates in every patch.
314
+ sum_rates_by_row: a 1D np.array with len = N. Stores the sum of the M x 12 rates in every row.
315
+ sum_rates: sum of all N x M x 12 rates.
316
+
317
+ Returns:
318
+ row, col: row and column number of where the patch.
319
+ '''
320
+
321
+ # Find row first
322
+ if expected_sum < sum_rates / 2:
323
+ # search row forwards if in the first half of rows
324
+ current_sum = 0
325
+ row = 0
326
+ while current_sum < expected_sum:
327
+ current_sum += sum_rates_by_row[row]
328
+ row += 1
329
+ row -= 1
330
+ current_sum -= sum_rates_by_row[row] # need to subtract that row (which caused current sum to exceed expected_sum)
331
+ else:
332
+ # search row backwards if in the second half of rows
333
+ current_sum = sum_rates
334
+ row = len(patch_rates) - 1
335
+ while current_sum > expected_sum:
336
+ current_sum -= sum_rates_by_row[row]
337
+ row -= 1
338
+ row += 1
339
+ # don't need subtraction here, as current_sum is already < expected same
340
+
341
+ # Find col in that row
342
+ if (expected_sum - current_sum) < sum_rates_by_row[row] / 2:
343
+ # search col forwards if in the first half of that row
344
+ col = 0
345
+ while current_sum < expected_sum:
346
+ current_sum += patch_rates[row][col]
347
+ col += 1
348
+ col -= 1
349
+ current_sum -= patch_rates[row][col] # need a subtraction
350
+ else:
351
+ # search col backwards if in the second half of that row
352
+ current_sum += sum_rates_by_row[row]
353
+ col = len(patch_rates[0]) - 1
354
+ while current_sum > expected_sum:
355
+ current_sum -= patch_rates[row][col]
356
+ col -= 1
357
+ col += 1
358
+ # don't need subtraction
359
+
360
+ return row, col, current_sum
361
+
362
+
363
+
364
+
365
+ def make_signal_zero_flux(i, j, e):
366
+ '''
367
+ Find which patch to change what based on i, j, e (event number) value, for the zero-flux boundary condition
368
+
369
+ Inputs:
370
+ i, j is the position of the 'center' patch, e is which event to happen there.
371
+ Another patch might be influenced as well if a migration event was picked.
372
+
373
+ Possible values for e:
374
+ e = 0 or 1: natural change of U/V due to payoff.
375
+ Can be either brith or death (based on payoff is positive or negative).
376
+ Cooresponds to s = 2 or 5 in the patch class
377
+ e = 2 or 3: death of U/V due to carrying capacity.
378
+ Cooresponds to s = 1 or 4 in patch: make U/V -= 1
379
+ e = 4 ~ 7: migration events of U, patch (i, j) loses an individual, and another patch receives one.
380
+ we use the up-down-left-right rule for the direction. 4 means up, 5 means down, ...
381
+ Cooresponds to s = 0 for the mig-in patch (force U += 1), and s = 1 for the mig-out patch (force U -= 1)
382
+ e = 8 ~ 11: migration events of V.
383
+ Cooresponds to s = 3 for the mig-in patch (force V += 1), and s = 4 for the mig-out patch (force V -= 1)
384
+ '''
385
+ if e < 6:
386
+ if e == 0:
387
+ return [[i, j, 2]]
388
+ elif e == 1:
389
+ return [[i, j, 5]]
390
+ elif e == 2:
391
+ return [[i, j, 1]]
392
+ elif e == 3:
393
+ return [[i, j, 4]]
394
+ elif e == 4:
395
+ return [[i, j, 1], [i - 1, j, 0]]
396
+ else:
397
+ return [[i, j, 1], [i + 1, j, 0]]
398
+ else:
399
+ if e == 6:
400
+ return [[i, j, 1], [i, j - 1, 0]]
401
+ elif e == 7:
402
+ return [[i, j, 1], [i, j + 1, 0]]
403
+ elif e == 8:
404
+ return [[i, j, 4], [i - 1, j, 3]]
405
+ elif e == 9:
406
+ return [[i, j, 4], [i + 1, j, 3]]
407
+ elif e == 10:
408
+ return [[i, j, 4], [i, j - 1, 3]]
409
+ elif e == 11:
410
+ return [[i, j, 4], [i, j + 1, 3]]
411
+ else:
412
+ raise RuntimeError('A bug in code: invalid event number encountered:', e) # debug line
413
+
414
+
415
+
416
+ def make_signal_periodical(N, M, i, j, e):
417
+ '''
418
+ Find which patch to change what based on i, j, e value, for the periodical boundary condition
419
+ Similar to make_signal_zero_flux.
420
+ '''
421
+
422
+ if e < 6:
423
+ if e == 0:
424
+ return [[i, j, 2]]
425
+ elif e == 1:
426
+ return [[i, j, 5]]
427
+ elif e == 2:
428
+ return [[i, j, 1]]
429
+ elif e == 3:
430
+ return [[i, j, 4]]
431
+ elif e == 4:
432
+ if i != 0:
433
+ return [[i, j, 1], [i - 1, j, 0]]
434
+ else:
435
+ return [[i, j, 1], [N - 1, j, 0]]
436
+ else:
437
+ if i != N - 1:
438
+ return [[i, j, 1], [i + 1, j, 0]]
439
+ else:
440
+ return [[i, j, 1], [0, j, 0]]
441
+ else:
442
+ if e == 6:
443
+ if j != 0:
444
+ return [[i, j, 1], [i, j - 1, 0]]
445
+ else:
446
+ return [[i, j, 1], [i, M - 1, 0]]
447
+ elif e == 7:
448
+ if j != M - 1:
449
+ return [[i, j, 1], [i, j + 1, 0]]
450
+ else:
451
+ return [[i, j, 1], [i, 0, 0]]
452
+ elif e == 8:
453
+ if i != 0:
454
+ return [[i, j, 4], [i - 1, j, 3]]
455
+ else:
456
+ return [[i, j, 4], [N - 1, j, 3]]
457
+ elif e == 9:
458
+ if i != N - 1:
459
+ return [[i, j, 4], [i + 1, j, 3]]
460
+ else:
461
+ return [[i, j, 4], [0, j, 3]]
462
+ elif e == 10:
463
+ if j != 0:
464
+ return [[i, j, 4], [i, j - 1, 3]]
465
+ else:
466
+ return [[i, j, 4], [i, M - 1, 3]]
467
+ elif e == 11:
468
+ if j != M - 1:
469
+ return [[i, j, 4], [i, j + 1, 3]]
470
+ else:
471
+ return [[i, j, 4], [i, 0, 3]]
472
+ else:
473
+ raise RuntimeError('A bug in code: invalid event number encountered:', e) # debug line
474
+
475
+
476
+
477
+
478
+ def nb_need_change(ni, signal):
479
+ '''
480
+ Check whether a neighbor needs to change.
481
+ Two cases don't need change: either ni is None (doesn't exist) or in signal (is a last-change patch and already updated)
482
+
483
+ Inputs:
484
+ ni: index of a neighbor, might be None if patch doesn't exist.
485
+ signal: return value of make_signal_zero_flux or make_signal_periodical.
486
+
487
+ Returns:
488
+ True or False, whether the neighboring patch specified by ni needs change
489
+ '''
490
+
491
+ if ni == None:
492
+ return False
493
+
494
+ for si in signal:
495
+ if ni[0] == si[0] and ni[1] == si[1]:
496
+ return False
497
+
498
+ return True
499
+
500
+
501
+
502
+
503
+ def single_init(mod, rng):
504
+ '''
505
+ The first major function for the model.
506
+ Initialize all variables and run 1 round, then pass variables and results to single_test.
507
+
508
+ Input:
509
+ mod is a model object
510
+ rng is random number generator (np.random.default_rng), initialized by model.run
511
+ '''
512
+
513
+ #### Initialize Data Storage ####
514
+
515
+ world = [[patch(mod.I[i][j][0], mod.I[i][j][1], mod.X[i][j], mod.P[i][j]) for j in range(mod.M)] for i in range(mod.N)] # N x M patches
516
+ patch_rates = np.zeros((mod.N, mod.M), dtype = np.float64) # every patch's sum-of-12-srates
517
+ sum_rates_by_row = np.zeros((mod.N), dtype = np.float64) # every row's sum-of-patch, i.e., sum of 12 * M rates in every row.
518
+ sum_rates = 0 # sum of all N x M x 12 rates
519
+
520
+ signal = None
521
+
522
+ nb_indices = None
523
+ if mod.boundary:
524
+ nb_indices = [[find_nb_zero_flux(mod.N, mod.M, i, j) for j in range(mod.M)] for i in range(mod.N)]
525
+ else:
526
+ nb_indices = [[find_nb_periodical(mod.N, mod.M, i, j) for j in range(mod.M)] for i in range(mod.N)]
527
+
528
+ for i in range(mod.N):
529
+ for j in range(mod.M):
530
+ nb = []
531
+ for k in range(4):
532
+ if nb_indices[i][j][k] != None:
533
+ # append a pointer to the patch
534
+ nb.append(world[nb_indices[i][j][k][0]][nb_indices[i][j][k][1]])
535
+ else:
536
+ # nb doesn't exist
537
+ nb.append(None)
538
+ # pass it to patch class and store
539
+ world[i][j].set_nb_pointers(nb)
540
+
541
+
542
+ #### Begin Running ####
543
+
544
+ # initialize payoff & natural death rates
545
+ for i in range(mod.N):
546
+ for j in range(mod.M):
547
+ world[i][j].update_pi_k()
548
+
549
+ # initialize migration rates & the rates list
550
+ for i in range(mod.N):
551
+ for j in range(mod.M):
552
+ world[i][j].update_mig()
553
+ # store rates & sum of rates
554
+ patch_rates[i][j] = world[i][j].get_sum_rates()
555
+ sum_rates_by_row[i] = sum(patch_rates[i])
556
+
557
+ sum_rates = sum(sum_rates_by_row)
558
+
559
+ # pick the first random event
560
+ expected_sum = rng.random() * sum_rates
561
+ # find patch first
562
+ i0, j0, current_sum = find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates)
563
+ # then find which event in that patch
564
+ e0 = world[i0][j0].find_event(expected_sum - current_sum)
565
+
566
+ # initialize signal
567
+ if mod.boundary:
568
+ signal = make_signal_zero_flux(i0, j0, e0) # walls around world
569
+ else:
570
+ signal = make_signal_periodical(mod.N, mod.M, i0, j0, e0) # no walls around world
571
+
572
+ # change U&V based on signal
573
+ for si in signal:
574
+ world[si[0]][si[1]].change_popu(si[2])
575
+
576
+ # time increment
577
+ time = (1 / sum_rates) * math.log(1 / rng.random())
578
+
579
+ # record
580
+ if time > mod.record_itv:
581
+ record_index = int(time / mod.record_itv)
582
+ for i in range(mod.N):
583
+ for j in range(mod.M):
584
+ for k in range(record_index):
585
+ mod.U[i][j][k] += world[i][j].U
586
+ mod.V[i][j][k] += world[i][j].V
587
+ mod.Upi[i][j][k] += world[i][j].Upi
588
+ mod.Vpi[i][j][k] += world[i][j].Vpi
589
+ # we simply add to that entry, and later divide by sim_time to get the average (division in run function)
590
+
591
+ return time, world, nb_indices, patch_rates, sum_rates_by_row, sum_rates, signal
592
+
593
+
594
+
595
+
596
+ def single_test(mod, front_info, end_info, update_sum_frequency, rng):
597
+ '''
598
+ Runs a single model, from time = 0 to mod.maxtime.
599
+ run recursively calls single_test to get the average data.
600
+
601
+ Inputs:
602
+ sim: a model object, created by user and carries all parameters & storage bins.
603
+ front_info, end_info: passed by run to show messages, like the current round number in run. Not intended for direct usages.
604
+ update_sum_frequency: re-calculate sums this many times in model.
605
+ Our sums are gradually updated over time. So might have precision errors for large maxtime.
606
+ rng: np.random.default_rng. Initialized by model.run
607
+ '''
608
+
609
+ # initialize helper variables
610
+ # used to print progress, i.e., how much percent is done
611
+ one_time = mod.maxtime / max(100, update_sum_frequency)
612
+ one_progress = 0
613
+ if mod.print_pct != None:
614
+ # print progress, x%
615
+ print(front_info + ' 0%' + end_info, end = '\r')
616
+ one_progress = mod.maxtime * mod.print_pct / 100
617
+ else:
618
+ one_progress = 2 * mod.maxtime # not printing
619
+
620
+ # our sums (sum_rates_by_row and sum_rates) are gradually updated over time. This may have precision errors for large maxtime.
621
+ # So re-sum everything every some percentage of maxtime.
622
+ one_update_sum = mod.maxtime / update_sum_frequency
623
+
624
+ current_time = one_time
625
+ current_progress = one_progress
626
+ current_update_sum = one_update_sum
627
+
628
+ max_record = int(mod.maxtime / mod.record_itv)
629
+
630
+
631
+ # initialize
632
+ time, world, nb_indices, patch_rates, sum_rates_by_row, sum_rates, signal = single_init(mod, rng)
633
+ record_index = int(time / mod.record_itv)
634
+ # record_time is how much time has passed since the last record
635
+ # if record_time > record_itv:
636
+ # we count how many record_itvs are there in record_time, denote the number by multi_records
637
+ # then store the current data in multi_records number of cells in the list
638
+ # and subtract record_time by the multiple of record_itv, so that record_time < record_itv
639
+ record_time = time - record_index * mod.record_itv
640
+
641
+ ### Large while loop ###
642
+
643
+ while time < mod.maxtime:
644
+
645
+ # print progress & correct error of sum_rates
646
+ if time > current_time:
647
+ # a new 1% of time
648
+ current_time += one_time
649
+ if time > current_progress:
650
+ # print progress
651
+ print(front_info + ' ' + str(round(time / mod.maxtime * 100)) + '%' + end_info, end = '\r')
652
+ current_progress += one_progress
653
+
654
+ if time > current_update_sum:
655
+ current_update_sum += one_update_sum
656
+ for i in range(mod.N):
657
+ sum_rates_by_row[i] = sum(patch_rates[i])
658
+ sum_rates = sum(sum_rates_by_row)
659
+
660
+
661
+ # before updating last-changed patches, subtract old sum of rates (so as to update sum of rates by adding new rates later)
662
+ for si in signal:
663
+ # si[0] is row number, si[1] is col number
664
+ old_patch_rate = world[si[0]][si[1]].get_sum_rates()
665
+ sum_rates_by_row[si[0]] -= old_patch_rate
666
+ sum_rates -= old_patch_rate
667
+
668
+ # update last-changed patches
669
+ # update payoff and death rates first
670
+ for si in signal:
671
+ world[si[0]][si[1]].update_pi_k()
672
+ # then update migration rates, as mig_rates depend on neighbor's payoff
673
+ for si in signal:
674
+ world[si[0]][si[1]].update_mig()
675
+
676
+ # update rates stored
677
+ new_patch_rate = world[si[0]][si[1]].get_sum_rates()
678
+ # update patch_rates
679
+ patch_rates[si[0]][si[1]] = new_patch_rate
680
+ # update sum_rate_by_row and sum_rates_by_row by adding new rates
681
+ sum_rates_by_row[si[0]] += new_patch_rate
682
+ sum_rates += new_patch_rate
683
+
684
+ # update neighbors of last-changed patches
685
+ for si in signal:
686
+ for ni in nb_indices[si[0]][si[1]]:
687
+ # don't need to update if the patch is a last-change patch itself or None
688
+ # use helper function to check
689
+ if nb_need_change(ni, signal):
690
+ # update migratino rates
691
+ world[ni[0]][ni[1]].update_mig()
692
+ # Note: no need to update patch_rates and sum of rates, as update_mig doesn't change total rates in a patch.
693
+ # sum_mig_rate is decided by mu1 * U + mu2 * V, and pi_death_rate is not changed.
694
+
695
+ # pick the first random event
696
+ expected_sum = rng.random() * sum_rates
697
+ # find patch first
698
+ i0, j0, current_sum = find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates)
699
+ # then find which event in that patch
700
+ e0 = world[i0][j0].find_event(expected_sum - current_sum)
701
+
702
+ # make signal
703
+ if mod.boundary:
704
+ signal = make_signal_zero_flux(i0, j0, e0)
705
+ else:
706
+ signal = make_signal_periodical(mod.N, mod.M, i0, j0, e0)
707
+
708
+ # let the event happen
709
+ for si in signal:
710
+ world[si[0]][si[1]].change_popu(si[2])
711
+
712
+ # increase time
713
+ r1 = rng.random()
714
+ dt = (1 / sum_rates) * math.log(1 / r1)
715
+ time += dt
716
+ record_time += dt
717
+
718
+ if time < mod.maxtime:
719
+ # if not exceeds maxtime
720
+ if record_time > mod.record_itv:
721
+ multi_records = int(record_time / mod.record_itv)
722
+ record_time -= multi_records * mod.record_itv
723
+
724
+ for i in range(mod.N):
725
+ for j in range(mod.M):
726
+ for k in range(record_index, record_index + multi_records):
727
+ mod.U[i][j][k] += world[i][j].U
728
+ mod.V[i][j][k] += world[i][j].V
729
+ mod.Upi[i][j][k] += world[i][j].Upi
730
+ mod.Vpi[i][j][k] += world[i][j].Vpi
731
+ record_index += multi_records
732
+ else:
733
+ # if already exceeds maxtime
734
+ for i in range(mod.N):
735
+ for j in range(mod.M):
736
+ for k in range(record_index, max_record):
737
+ mod.U[i][j][k] += world[i][j].U
738
+ mod.V[i][j][k] += world[i][j].V
739
+ mod.Upi[i][j][k] += world[i][j].Upi
740
+ mod.Vpi[i][j][k] += world[i][j].Vpi
741
+
742
+ ### Large while loop ends ###
743
+
744
+ if mod.print_pct != None:
745
+ print(front_info + ' 100%' + ' ' * 20, end = '\r') # empty spaces to overwrite predicted runtime
746
+
747
+
748
+
749
+
750
+ def run_py(mod, predict_runtime = False, message = ''):
751
+ '''
752
+ Main function. Recursively calls single_test to run many models and then takes the average.
753
+
754
+ Inputs:
755
+ - mod is a model object.
756
+ - predict_runtime = False will not predict how much time still needed, set to True if you want to see.
757
+ - message is used by some functions in figures.py to print messages.
758
+ '''
759
+
760
+ if not mod.data_empty:
761
+ raise RuntimeError('mod has non-empty data')
762
+
763
+ mod.U = np.zeros((mod.N, mod.M, mod.max_record))
764
+ mod.V = np.zeros((mod.N, mod.M, mod.max_record))
765
+ mod.Vpi = np.zeros((mod.N, mod.M, mod.max_record))
766
+ mod.Upi = np.zeros((mod.N, mod.M, mod.max_record))
767
+
768
+ start = timer() # runtime
769
+
770
+ mod.data_empty = False
771
+ rng = np.random.default_rng(mod.seed)
772
+
773
+ # passed to single_test to print progress
774
+ if mod.print_pct == 0:
775
+ mod.print_pct = 5 # default print_pct
776
+
777
+ update_sum_frequency = 4 # re-calculate sums this many times. See input desciption of single_test
778
+
779
+ ### models ###
780
+ i = 0
781
+
782
+ while i < mod.sim_time:
783
+ # use while loop so that can go backwards if got numerical issues
784
+
785
+ end_info = ''
786
+ if predict_runtime:
787
+ if i > 0:
788
+ time_elapsed = timer() - start
789
+ pred_runtime = time_elapsed / i * (mod.sim_time - i)
790
+ end_info = ', ~' + str(round(pred_runtime, 2)) + 's left'
791
+
792
+ front_info = ''
793
+ if mod.print_pct != None:
794
+ front_info = message + 'round ' + str(i) + ':'
795
+ print(front_info + ' ' * 30, end = '\r') # the blank spaces are to overwrite percentages, e.g. 36 %
796
+
797
+ try:
798
+ single_test(mod, front_info, end_info, update_sum_frequency, rng)
799
+ i += 1
800
+ except IndexError:
801
+ update_sum_frequency *= 4
802
+ print('Numerical issue at round ' + str(i) + '. Trying higher precision now. See doc if err repeats')
803
+ # not increasing i: redo current round.
804
+
805
+ ### models end ###
806
+
807
+ mod.calculate_ave()
808
+
809
+ stop = timer()
810
+ print(' ' * 30, end = '\r') # overwrite all previous prints
811
+ print(message + 'runtime: ' + str(round(stop - start, 2)) + ' s')
812
+ return
813
+
814
+
815
+
816
+
817
+