piegy 1.0.0__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.
piegy/model.py ADDED
@@ -0,0 +1,1168 @@
1
+ '''
2
+ Main Module of Stochastic Model
3
+ -------------------------------
4
+
5
+ Contains all the necessary tools to build a model and run simulations based on Gillespie Algorithm.
6
+
7
+ Classes:
8
+ - patch: (Private) Simulates a single patch in the N x M space. Assume no spatial structure within a patch.
9
+ All spatial movements are based on patches.
10
+ - simulation: Stores input parameters and data generated during simulation.
11
+
12
+
13
+ Functions:
14
+ - find_nb_zero_flux: (Private) Return pointers to a patch's neighbors under zero-flux (no-boundary) boundary condition.
15
+ - find_nb_periodical: (Private) Return pointers to a patch's neighbors under periodical (with-booundary) boundary condition.
16
+ - find_event: (Private) Pick a random event to happen.
17
+ - make_signal_zero_flux: (Private) Expand event index (return value of find_event) to a detailed signal, under zero-flux boundary condition.
18
+ - make_signal_periodical: (Private) Expand event index (return value of find_event) to a detailed signal, under periodical boundary condition.
19
+ - single_init: (Private) Initialize a single simulation. Meaning of 'single': see single_test and run.
20
+ - single_test: (Private) Run a single simulation.
21
+ 'single' means a single round of simulation. You can run many single rounds and then take the average --- that's done by <run> function.
22
+ - run: Run multiple simulations and then take the average. All the simulation will use the same parameters.
23
+ Set a seed for reproducible results.
24
+ - demo_model: Returns a demo model (a simulation object).
25
+
26
+ NOTE: Only simulation class and run function are intended for direct usages.
27
+ '''
28
+
29
+
30
+ import math
31
+ import numpy as np
32
+ from timeit import default_timer as timer
33
+
34
+
35
+ # data type used by simulation.U and simulation.V
36
+ UV_DTYPE = 'float32'
37
+
38
+ # data type used by simulation.U_pi and V_pi
39
+ PI_DTYPE = 'float64'
40
+
41
+ # data type for storing rates in single_test an single_init
42
+ RATES_DTYPE = 'float64'
43
+
44
+ # store e locally, slightly increases speed
45
+ MATH_E = math.e
46
+
47
+
48
+ class patch:
49
+ '''
50
+ A single patch in the N x M space.
51
+ Interacts with neighboring patches, assuming no spatial structure within a patch.
52
+ Initialized in single_init function.
53
+
54
+ Class Functions:
55
+
56
+ __init__:
57
+ Inputs:
58
+ U, V: initial value of U and V
59
+ matrix: payoff matrix for U and V. The canonical form is 2x2, here we ask for a flattened 1x4 form.
60
+ patch_var: np.array of [mu1, mu2, w1, w2, kappa1, kappa2]
61
+
62
+ __str__:
63
+ Print patch object in a nice way.
64
+
65
+ set_nb_pointers:
66
+ Set pointers to neighbors of this patch object.
67
+
68
+ update_pi:
69
+ Update U_pi, V_pi and payoff rates (payoff rates are the first two numbers in self.pi_death_rates).
70
+
71
+ update_k:
72
+ Update natural death rates (the last two numbers in self.pi_death_rates).
73
+
74
+ update_mig:
75
+ Update migration rates.
76
+
77
+ get_pi_death_rates, get_mig_rates:
78
+ Return respective members.
79
+
80
+ change_popu:
81
+ Change U, V based on input signal.
82
+ '''
83
+
84
+ 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]):
85
+
86
+ self.U = U # int, U population. Initialized upon creating object.
87
+ self.V = V # int, V population
88
+ self.U_pi = 0 # float, payoff
89
+ self.V_pi = 0
90
+
91
+ self.matrix = matrix # np.array or list, len = 4, payoff matrix
92
+ self.mu1 = patch_var[0] # float, how much proportion of the population migrates (U) each time
93
+ self.mu2 = patch_var[1]
94
+ self.w1 = patch_var[2] # float, strength of payoff-driven effect. Larger w <=> stronger payoff-driven motion
95
+ self.w2 = patch_var[3]
96
+ self.kappa1 = patch_var[4] # float, carrying capacity, determines death rates
97
+ self.kappa2 = patch_var[5]
98
+
99
+ self.nb = None # list of patch objects (pointers), point to neighbors, initialized seperatedly (after all patches are created)
100
+ self.pi_death_rates = [0 for _ in range(4)] # list, len = 4, rates of payoff & death
101
+ # first two are payoff rates, second two are death rates
102
+ self.mig_rates = [0 for _ in range(8)] # list, len = 8, migration rates, stored in an order: up, down, left, right,
103
+ # first 4 are U's mig_rate, the last 4 are V's
104
+ self.sum_pi_death_rates = 0 # float, sum of pi_death_rates
105
+ self.sum_mig_rates = 0 # float, sum of mig_rates
106
+
107
+
108
+ def __str__(self):
109
+ self_str = ''
110
+ self_str += 'U, V = ' + str(self.U) + ', ' + str(self.V) + '\n'
111
+ self_str += 'pi = ' + str(self.U_pi) + ', ' + str(self.V_pi) + '\n'
112
+ self_str += 'matrix = ' + str(self.matrix) + '\n'
113
+ self_str += 'mu1, mu2 = ' + str(self.mu1) + ', ' + str(self.mu2) + '\n'
114
+ self_str += 'w1, w2 = ' + str(self.w1) + ', ' + str(self.w2) + '\n'
115
+ self_str += 'kappa1, kappa2 = ' + str(self.kappa1) + ', ' + str(self.kappa2) + '\n'
116
+ self_str += '\n'
117
+ self_str += 'nb = ' + str(self.nb)
118
+ self_str += 'pi_death_rates = ' + str(self.pi_death_rates) + '\n'
119
+ self_str += 'mig_rates = ' + str(self.mig_rates) + '\n'
120
+ self_str += 'sum_pi_death_rates = ' + str(self.sum_pi_death_rates) + '\n'
121
+ self_str += 'sum_mig_rates = ' + str(self.sum_mig_rates) + '\n'
122
+
123
+ return self_str
124
+
125
+
126
+ def set_nb_pointers(self, nb):
127
+ # nb is a list of pointers (point to patches)
128
+ # nb is passed from the simulation class
129
+ self.nb = nb
130
+
131
+
132
+ def update_pi_k(self):
133
+ # calculate payoff and natural death rates
134
+
135
+ U = self.U # bring the values to front
136
+ V = self.V
137
+ sum_minus_1 = U + V - 1 # this value is used several times
138
+
139
+ if sum_minus_1 > 0:
140
+ # interaction happens only if there is more than 1 individual
141
+
142
+ if U != 0:
143
+ # no payoff if U == 0
144
+ self.U_pi = (U - 1) / sum_minus_1 * self.matrix[0] + V / sum_minus_1 * self.matrix[1]
145
+ else:
146
+ self.U_pi = 0
147
+
148
+ if V != 0:
149
+ self.V_pi = U / sum_minus_1 * self.matrix[2] + (V - 1) / sum_minus_1 * self.matrix[3]
150
+ else:
151
+ self.V_pi = 0
152
+
153
+ else:
154
+ # no interaction, hence no payoff, if only 1 individual
155
+ self.U_pi = 0
156
+ self.V_pi = 0
157
+
158
+ # update payoff rates
159
+ self.pi_death_rates[0] = abs(U * self.U_pi)
160
+ self.pi_death_rates[1] = abs(V * self.V_pi)
161
+
162
+ # update natural death rates
163
+ self.pi_death_rates[2] = self.kappa1 * U * (sum_minus_1 + 1)
164
+ self.pi_death_rates[3] = self.kappa2 * V * (sum_minus_1 + 1)
165
+
166
+ # update sum of rates
167
+ self.sum_pi_death_rates = sum(self.pi_death_rates)
168
+
169
+
170
+ def update_mig(self):
171
+ # calculate migration rates
172
+
173
+ # store the 'weight' of migration, i.e. value of f/g functions for neighbors
174
+ U_weight = [0, 0, 0, 0]
175
+ V_weight = [0, 0, 0, 0]
176
+
177
+ for i in range(4):
178
+ if self.nb[i] != None:
179
+ U_weight[i] = 1 + pow(MATH_E, self.w1 * self.nb[i].U_pi) # use your own functions!
180
+ V_weight[i] = 1 + pow(MATH_E, self.w2 * self.nb[i].V_pi)
181
+
182
+ mu1_U = self.mu1 * self.U
183
+ mu2_V = self.mu2 * self.V
184
+
185
+ mu1_U_divide_sum = mu1_U / sum(U_weight)
186
+ mu2_V_divide_sum = mu2_V / sum(V_weight)
187
+
188
+ for i in range(4):
189
+ self.mig_rates[i] = mu1_U_divide_sum * U_weight[i]
190
+ self.mig_rates[i + 4] = mu2_V_divide_sum * V_weight[i]
191
+
192
+ # update sum of rates
193
+ self.sum_mig_rates = mu1_U + mu2_V
194
+
195
+
196
+ def get_sum_rates(self):
197
+ # return sum of all 12 rates
198
+ return self.sum_pi_death_rates + self.sum_mig_rates
199
+
200
+
201
+ def find_event(self, expected_sum):
202
+ # find the event within the 12 events based on expected sum-of-rates within this patch
203
+
204
+ if expected_sum < self.sum_pi_death_rates:
205
+ # in the first 4 events (payoff and death events)
206
+ event = 0
207
+ current_sum = 0
208
+ while current_sum < expected_sum:
209
+ current_sum += self.pi_death_rates[event]
210
+ event += 1
211
+ event -= 1
212
+
213
+ else:
214
+ # in the last 8 events (migration events):
215
+ event = 0
216
+ current_sum = self.sum_pi_death_rates
217
+ while current_sum < expected_sum:
218
+ current_sum += self.mig_rates[event]
219
+ event += 1
220
+ event += 3 # i.e., -= 1, then += 4 (to account for the first 4 payoff & death rates)
221
+
222
+ return event
223
+
224
+
225
+ def change_popu(self, s):
226
+ # convert s (a signal, passed from simulation class) to a change in population
227
+
228
+ # s = 0, 1, 2 are for U
229
+ # s = 0 for migration IN, receive an immigrant
230
+ if s == 0:
231
+ self.U += 1 # receive an immigrant
232
+ # s = 1 for migration OUT / death due to carrying capacity
233
+ elif s == 1:
234
+ if self.U > 0:
235
+ self.U -= 1
236
+ # s = 2 for natural birth / death, due to payoff
237
+ elif s == 2:
238
+ if self.U_pi > 0:
239
+ self.U += 1 # natural growth due to payoff
240
+ elif self.U > 0:
241
+ self.U -= 1 # natural death due to payoff
242
+
243
+ # s = 3, 4, 5 are for V
244
+ elif s == 3:
245
+ self.V += 1
246
+ elif s == 4:
247
+ if self.V > 0:
248
+ self.V -= 1
249
+ else:
250
+ if self.V_pi > 0:
251
+ self.V += 1
252
+ elif self.V > 0:
253
+ self.V -= 1
254
+
255
+
256
+
257
+ class simulation:
258
+ '''
259
+ Store simulation data and input parameters.
260
+ Initialize a simulation object to run simulations.
261
+
262
+ Public Class Functions:
263
+
264
+ __init__:
265
+ Create a simulation object. Also initialize data storage.
266
+
267
+ __str__:
268
+ Print simulation object in a nice way.
269
+
270
+ copy:
271
+ Return a deep copy of self. Can choose whether to copy data as well. Default is to copy.
272
+
273
+ clear:
274
+ clear all data stored, set U, V, U_pi, V_pi to zero arrays
275
+
276
+ change_maxtime:
277
+ Changes maxtime of self. Update data storage as well.
278
+
279
+ set_seed:
280
+ Set a new seed.
281
+
282
+ compress:
283
+ compress data by only storing average values
284
+ '''
285
+
286
+ def __init__(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct = 25, seed = None, UV_dtype = UV_DTYPE, pi_dtype = PI_DTYPE):
287
+
288
+ self.check_valid_input(N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct, seed)
289
+
290
+ self.N = N # int, N x M is spatial dimension
291
+ self.M = M # int, can't be 1. If want to make 1D space, use N = 1. And this model doesn't work for 1x1 space (causes NaN)
292
+ self.maxtime = maxtime # float or int, run simulation for how long time
293
+ self.record_itv = record_itv # float, record data every record_itv of time
294
+ self.sim_time = sim_time # int, run this many of rounds (of single_test)
295
+ self.boundary = boundary # bool, the N x M space have boundary or not (i.e., zero-flux (True) or periodical (False))
296
+ self.I = np.array(I) # N x M x 2 np.array, initial population. Two init-popu for every patch (U and V)
297
+ self.X = np.array(X) # N x M x 4 np.array, matrices. The '4' comes from 2x2 matrix flattened to 1D
298
+ self.P = np.array(P) # N x M x 6 np.array, 'patch variables', i.e., mu1&2, w1&2, kappa1&2
299
+ self.print_pct = print_pct # int, print how much percent is done, need to be non-zero
300
+ self.seed = seed # non-negative int, seed for random generator
301
+ self.UV_dtype = UV_dtype # what data type to store population, should be a float format. This value is passed to np.array.
302
+ # Default is 'float32', use lower accuracy to reduce data size.
303
+ self.pi_dtype = pi_dtype # what data type to store payoff, should be a float format. This value is passed to np.array.
304
+ # Default is 'float64'
305
+
306
+ self.init_storage() # initialize storage bins. Put in a separate function because might want to change maxtime
307
+ # and that doesn't need to initialze the whole object again
308
+
309
+
310
+ def init_storage(self):
311
+ # initialize storage bins
312
+ self.data_empty = True # whether data storage bins are empty. model.run will refuse to run (raise error) if not empty.
313
+ self.max_record = int(self.maxtime / self.record_itv) # int, how many data points to store sin total
314
+ self.compress_itv = 1 # int, intended to reduce size of data (if not 1). Updated by compress_data function
315
+ # if set to an int, say 20, sim will take average over every 20 data points and save them as new data.
316
+ # May be used over and over again to recursively reduce data size.
317
+ # Default is 1, not to take average.
318
+ self.U = np.zeros((self.N, self.M, self.max_record), dtype = self.UV_dtype) # N x M x max_record np.array, float32, stores population of U in every patch over tiem
319
+ self.V = np.zeros((self.N, self.M, self.max_record), dtype = self.UV_dtype)
320
+ self.U_pi = np.zeros((self.N, self.M, self.max_record), dtype = self.pi_dtype) # similar to U, but for U's payoff and float 64
321
+ self.V_pi = np.zeros((self.N, self.M, self.max_record), dtype = self.pi_dtype)
322
+
323
+
324
+ def check_valid_input(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct, seed):
325
+ # check whether the inputs are valid
326
+ # seed, UV_dtype, pi_dtype is handled by numpy
327
+
328
+ if (N < 1) or (M < 1):
329
+ raise ValueError('N < 1 or M < 1')
330
+ if (N == 1) and (M == 1):
331
+ raise ValueError('Model fails for 1x1 space')
332
+ if (M == 1):
333
+ raise ValueError('Please set N = 1 for 1D space.')
334
+ if maxtime <= 0:
335
+ raise ValueError('Please set a positive number for maxtime')
336
+ if record_itv <= 0:
337
+ raise ValueError('Please set a positive number for record_itv')
338
+ if sim_time <= 0:
339
+ raise ValueError('Please set a positive number for sim_time')
340
+ if type(boundary) != bool:
341
+ raise TypeError('boundary not a bool. Please use True for zero-flux (with boundary) or False for periodical (no boundary)')
342
+
343
+ if (type(I) != list) and (type(I) != np.ndarray):
344
+ raise TypeError('Please set I as a list or np.ndarray')
345
+ if np.array(I).shape != (N, M, 2):
346
+ raise ValueError('Please set I as a N x M x 2 shape list or array. 2 is for init values of U, V at every patch')
347
+
348
+ if (type(X) != list) and (type(X) != np.ndarray):
349
+ raise TypeError('Please set X as a list or np.ndarray')
350
+ if np.array(X).shape != (N, M, 4):
351
+ raise ValueError('Please set X as a N x M x 4 shape list or array. 4 is for the flattened 2x2 payoff matrix')
352
+
353
+ if (type(P) != list) and (type(P) != np.ndarray):
354
+ raise TypeError('Please set P as a list or np.ndarray')
355
+ if np.array(P).shape != (N, M, 6):
356
+ raise ValueError('Please set P as a N x M x 6 shape list or array. 6 is for mu1, mu2, w1, w2, kappa1, kappa2')
357
+
358
+ if print_pct <= 0:
359
+ raise ValueError('Please use an int > 0 for print_pct or None for not printing progress.')
360
+
361
+ if type(seed) != int:
362
+ raise TypeError('Please use an int as seed')
363
+ if seed < 0:
364
+ raise ValueError('Please use a non-negative int as seed.')
365
+
366
+
367
+ def check_valid_data(self, data_empty, max_record, compress_itv, U, V, U_pi, V_pi):
368
+ # check whether a set of data is valid
369
+ if type(data_empty) != bool:
370
+ raise TypeError('data_empty not a bool')
371
+
372
+ if type(max_record) != int:
373
+ raise TypeError('max_record not an int')
374
+ if max_record < 0:
375
+ raise ValueError('max_record < 0')
376
+
377
+ if type(compress_itv) != int:
378
+ raise TypeError('compress_itv not an int')
379
+ if compress_itv < 0:
380
+ raise ValueError('compress_itv < 0')
381
+
382
+
383
+ def __str__(self):
384
+ # print this sim in a nice format
385
+
386
+ self_str = ''
387
+ self_str += 'N = ' + str(self.N) + '\n'
388
+ self_str += 'M = ' + str(self.M) + '\n'
389
+ self_str += 'maxtime = ' + str(self.maxtime) + '\n'
390
+ self_str += 'record_itv = ' + str(self.record_itv) + '\n'
391
+ self_str += 'sim_time = ' + str(self.sim_time) + '\n'
392
+ self_str += 'boundary = ' + str(self.boundary) + '\n'
393
+ self_str += 'print_pct = ' + str(self.print_pct) + '\n'
394
+ self_str += 'seed = ' + str(self.seed) + '\n'
395
+ self_str += 'UV_dtype = \'' + self.UV_dtype + '\'\n'
396
+ self_str += 'pi_dtype = \'' + self.pi_dtype + '\'\n'
397
+ self_str += 'compress_itv = ' + str(self.compress_itv) + '\n'
398
+ self_str += '\n'
399
+
400
+ # check whether I, X, P all same (compare all patches to (0, 0))
401
+ I_same = True
402
+ X_same = True
403
+ P_same = True
404
+ for i in range(self.N):
405
+ for j in range(self.M):
406
+ for k in range(2):
407
+ if self.I[i][j][k] != self.I[0][0][k]:
408
+ I_same = False
409
+ for k in range(4):
410
+ if self.X[i][j][k] != self.X[0][0][k]:
411
+ X_same = False
412
+ for k in range(6):
413
+ if self.P[i][j][k] != self.P[0][0][k]:
414
+ P_same = False
415
+
416
+ if I_same:
417
+ self_str += 'I all same: ' + str(self.I[0][0]) + '\n'
418
+ else:
419
+ self_str += 'I:\n'
420
+ for i in range(self.N):
421
+ for j in range(self.M):
422
+ self_str += str(self.I[i][j]) + ' '
423
+ self_str += '\n'
424
+ self_str += '\n'
425
+
426
+ if X_same:
427
+ self_str += 'X all same: ' + str(self.X[0][0]) + '\n'
428
+ else:
429
+ self_str += 'X:\n'
430
+ for i in range(self.N):
431
+ for j in range(self.M):
432
+ self_str += str(self.X[i][j]) + ' '
433
+ self_str += '\n'
434
+ self_str += '\n'
435
+
436
+ if P_same:
437
+ self_str += 'P all same: ' + str(self.P[0][0]) + '\n'
438
+ else:
439
+ self_str += 'P:\n'
440
+ for i in range(self.N):
441
+ for j in range(self.M):
442
+ self_str += str(self.P[i][j]) + ' '
443
+ self_str += '\n'
444
+
445
+ return self_str
446
+
447
+
448
+ def copy(self, copy_data = True):
449
+ # return deep copy of self
450
+ # copy_data decides whether to copy data as well
451
+ if type(copy_data) != bool:
452
+ raise TypeError('Please give a bool as argument: whether to copy data or not')
453
+
454
+ sim2 = simulation(N = self.N, M = self.M, maxtime = self.maxtime, record_itv = self.record_itv, sim_time = self.sim_time, boundary = self.boundary,
455
+ I = np.copy(self.I), X = np.copy(self.X), P = np.copy(self.P),
456
+ print_pct = self.print_pct, seed = self.seed, UV_dtype = self.UV_dtype, pi_dtype = self.pi_dtype)
457
+
458
+ if copy_data:
459
+ # copy data as well
460
+ sim2.set_data(self.data_empty, self.max_record, self.compress_itv, self.U, self.V, self.U_pi, self.V_pi)
461
+
462
+ return sim2
463
+
464
+
465
+ def calculate_ave(self):
466
+ # get the average value over sim_time many simulations
467
+ if self.sim_time != 1:
468
+ for i in range(self.N):
469
+ for j in range(self.M):
470
+ for t in range(self.max_record):
471
+ self.U[i][j][t] /= self.sim_time
472
+ self.V[i][j][t] /= self.sim_time
473
+ self.U_pi[i][j][t] /= self.sim_time
474
+ self.V_pi[i][j][t] /= self.sim_time
475
+
476
+
477
+ def change_maxtime(self, maxtime):
478
+ # change maxtime
479
+ if (type(maxtime) != float) and (type(maxtime) != int):
480
+ raise TypeError('Please pass in a float or int as the new maxtime.')
481
+ if maxtime <= 0:
482
+ raise ValueError('Please use a positive maxtime.')
483
+ self.maxtime = maxtime
484
+ self.init_storage()
485
+
486
+
487
+ def set_seed(self, seed):
488
+ # set seed
489
+ self.seed = seed
490
+
491
+
492
+ def reset_data(self):
493
+ # clear all data stored, set all to 0
494
+ self.init_storage()
495
+
496
+
497
+ def clear(self):
498
+ # clear data by simply reseting
499
+ self.reset_data()
500
+
501
+
502
+ def set_data(self, data_empty, max_record, compress_itv, U, V, U_pi, V_pi):
503
+ # set data to the given data values
504
+ # copies are made
505
+ self.check_valid_data(data_empty, max_record, compress_itv, U, V, U_pi, V_pi)
506
+
507
+ self.data_empty = data_empty
508
+ self.max_record = max_record
509
+ self.compress_itv = compress_itv
510
+ self.U = np.copy(U)
511
+ self.V = np.copy(V)
512
+ self.U_pi = np.copy(U_pi)
513
+ self.V_pi = np.copy(V_pi)
514
+
515
+
516
+ def compress(self, compress_itv = 5):
517
+ # compress data by only storing average values
518
+
519
+ if type(compress_itv) != int:
520
+ raise TypeError('Please use an int as compress_itv')
521
+ if compress_itv < 1:
522
+ raise ValueError('Please use record_itv >= 1')
523
+ if compress_itv == 1:
524
+ return
525
+
526
+ self.compress_itv *= compress_itv # may be reduced over and over again
527
+ self.max_record = int(self.max_record / compress_itv) # number of data points after reducing
528
+
529
+ U_reduced = np.zeros((self.N, self.M, self.max_record), dtype = self.UV_dtype)
530
+ V_reduced = np.zeros((self.N, self.M, self.max_record), dtype = self.UV_dtype)
531
+ U_pi_reduced = np.zeros((self.N, self.M, self.max_record), dtype = self.pi_dtype)
532
+ V_pi_reduced = np.zeros((self.N, self.M, self.max_record), dtype = self.pi_dtype)
533
+
534
+ for i in range(self.N):
535
+ for j in range(self.M):
536
+ for k in range(self.max_record):
537
+ lower = k * compress_itv # upper and lower bound of current record_itv
538
+ upper = lower + compress_itv
539
+ U_reduced[i][j][k] = np.mean(self.U[i, j, lower : upper])
540
+ V_reduced[i][j][k] = np.mean(self.V[i, j, lower : upper])
541
+ U_pi_reduced[i][j][k] = np.mean(self.U_pi[i, j, lower : upper])
542
+ V_pi_reduced[i][j][k] = np.mean(self.V_pi[i, j, lower : upper])
543
+
544
+ self.U = U_reduced
545
+ self.V = V_reduced
546
+ self.U_pi = U_pi_reduced
547
+ self.V_pi = V_pi_reduced
548
+
549
+
550
+
551
+
552
+ def find_nb_zero_flux(N, M, i, j):
553
+ '''
554
+ Find neighbors of patch (i, j) in zero-flux boundary condition. i.e., the space is square with boundary.
555
+ Return neighbors' indices in an order: up, down, left, right.
556
+ Index will be None if no neighbor exists in that direction.
557
+ '''
558
+ nb_indices = []
559
+
560
+ if i != 0:
561
+ nb_indices.append([i - 1, j]) # up
562
+ else:
563
+ nb_indices.append(None) # neighbor doesn't exist
564
+
565
+ if i != N - 1:
566
+ nb_indices.append([i + 1, j]) # down
567
+ else:
568
+ nb_indices.append(None)
569
+
570
+ if j != 0:
571
+ nb_indices.append([i, j - 1]) # left
572
+ else:
573
+ nb_indices.append(None)
574
+
575
+ if j != M - 1:
576
+ nb_indices.append([i, j + 1]) # right
577
+ else:
578
+ nb_indices.append(None)
579
+
580
+ return nb_indices
581
+
582
+
583
+
584
+
585
+ def find_nb_periodical(N, M, i, j):
586
+ '''
587
+ Find neighbors of patch (i, j) in periodical boundary condition. i.e., the space is a sphere.
588
+ Return neighbors' indices in an order: up, down, left, right.
589
+ If space not 1D, a neighbor always exists.
590
+ If space is 1D, say N = 1, we don't allow (0, j) to migrate up & down (self-self migration is considered invalid)
591
+ '''
592
+ nb_indices = []
593
+
594
+ # up
595
+ if N != 1:
596
+ if i != 0:
597
+ nb_indices.append([i - 1, j])
598
+ else:
599
+ nb_indices.append([N - 1, j])
600
+ else:
601
+ nb_indices.append(None) # can't migrate to itself
602
+
603
+ # down
604
+ if N != 1:
605
+ if i != N - 1:
606
+ nb_indices.append([i + 1, j])
607
+ else:
608
+ nb_indices.append([0, j])
609
+ else:
610
+ nb_indices.append(None)
611
+
612
+ # left
613
+ # No need to check M == 1 because we explicitly asked for M > 1
614
+ if j != 0:
615
+ nb_indices.append([i, j - 1])
616
+ else:
617
+ nb_indices.append([i, M - 1])
618
+
619
+ # right
620
+ if j != M - 1:
621
+ nb_indices.append([i, j + 1])
622
+ else:
623
+ nb_indices.append([i, 0])
624
+
625
+ return nb_indices
626
+
627
+
628
+
629
+
630
+ def find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates):
631
+ '''
632
+ Find which patch the event is in. Only patch index is found, patch.find_event find which event it is exactly.
633
+
634
+ Inputs:
635
+ expected_sum: a random number * sum of all rates. Essentially points to a random event.
636
+ We want to find the patch that contains this pointer.
637
+ patch_rates: a N x M np.array. Stores sum of the 12 rates in every patch.
638
+ sum_rates_by_row: a 1D np.array with len = N. Stores the sum of the M x 12 rates in every row.
639
+ sum_rates: sum of all N x M x 12 rates.
640
+
641
+ Returns:
642
+ row, col: row and column number of where the patch.
643
+ '''
644
+
645
+ # Find row first
646
+ if expected_sum < sum_rates / 2:
647
+ # search row forwards if in the first half of rows
648
+ current_sum = 0
649
+ row = 0
650
+ while current_sum < expected_sum:
651
+ current_sum += sum_rates_by_row[row]
652
+ row += 1
653
+ row -= 1
654
+ current_sum -= sum_rates_by_row[row] # need to subtract that row (which caused current sum to exceed expected_sum)
655
+ else:
656
+ # search row backwards if in the second half of rows
657
+ current_sum = sum_rates
658
+ row = len(patch_rates) - 1
659
+ while current_sum > expected_sum:
660
+ current_sum -= sum_rates_by_row[row]
661
+ row -= 1
662
+ row += 1
663
+ # don't need subtraction here, as current_sum is already < expected same
664
+
665
+ # Find col in that row
666
+ if (expected_sum - current_sum) < sum_rates_by_row[row] / 2:
667
+ # search col forwards if in the first half of that row
668
+ col = 0
669
+ while current_sum < expected_sum:
670
+ current_sum += patch_rates[row][col]
671
+ col += 1
672
+ col -= 1
673
+ current_sum -= patch_rates[row][col] # need a subtraction
674
+ else:
675
+ # search col backwards if in the second half of that row
676
+ current_sum += sum_rates_by_row[row]
677
+ col = len(patch_rates[0]) - 1
678
+ while current_sum > expected_sum:
679
+ current_sum -= patch_rates[row][col]
680
+ col -= 1
681
+ col += 1
682
+ # don't need subtraction
683
+
684
+ return row, col, current_sum
685
+
686
+
687
+
688
+
689
+ def make_signal_zero_flux(i, j, e):
690
+ '''
691
+ Find which patch to change what based on i, j, e (event number) value, for the zero-flux boundary condition
692
+
693
+ Inputs:
694
+ i, j is the position of the 'center' patch, e is which event to happen there.
695
+ Another patch might be influenced as well if a migration event was picked.
696
+
697
+ Possible values for e:
698
+ e = 0 or 1: natural change of U/V due to payoff.
699
+ Can be either brith or death (based on payoff is positive or negative).
700
+ Cooresponds to s = 2 or 5 in the patch class
701
+ e = 2 or 3: death of U/V due to carrying capacity.
702
+ Cooresponds to s = 1 or 4 in patch: make U/V -= 1
703
+ e = 4 ~ 7: migration events of U, patch (i, j) loses an individual, and another patch receives one.
704
+ we use the up-down-left-right rule for the direction. 4 means up, 5 means down, ...
705
+ Cooresponds to s = 0 for the mig-in patch (force U += 1), and s = 1 for the mig-out patch (force U -= 1)
706
+ e = 8 ~ 11: migration events of V.
707
+ Cooresponds to s = 3 for the mig-in patch (force V += 1), and s = 4 for the mig-out patch (force V -= 1)
708
+ '''
709
+ if e < 6:
710
+ if e == 0:
711
+ return [[i, j, 2]]
712
+ elif e == 1:
713
+ return [[i, j, 5]]
714
+ elif e == 2:
715
+ return [[i, j, 1]]
716
+ elif e == 3:
717
+ return [[i, j, 4]]
718
+ elif e == 4:
719
+ return [[i, j, 1], [i - 1, j, 0]]
720
+ else:
721
+ return [[i, j, 1], [i + 1, j, 0]]
722
+ else:
723
+ if e == 6:
724
+ return [[i, j, 1], [i, j - 1, 0]]
725
+ elif e == 7:
726
+ return [[i, j, 1], [i, j + 1, 0]]
727
+ elif e == 8:
728
+ return [[i, j, 4], [i - 1, j, 3]]
729
+ elif e == 9:
730
+ return [[i, j, 4], [i + 1, j, 3]]
731
+ elif e == 10:
732
+ return [[i, j, 4], [i, j - 1, 3]]
733
+ elif e == 11:
734
+ return [[i, j, 4], [i, j + 1, 3]]
735
+ else:
736
+ raise RuntimeError('A bug in code: invalid event number encountered:', e) # debug line
737
+
738
+
739
+
740
+ def make_signal_periodical(N, M, i, j, e):
741
+ '''
742
+ Find which patch to change what based on i, j, e value, for the periodical boundary condition
743
+ Similar to make_signal_zero_flux.
744
+ '''
745
+
746
+ if e < 6:
747
+ if e == 0:
748
+ return [[i, j, 2]]
749
+ elif e == 1:
750
+ return [[i, j, 5]]
751
+ elif e == 2:
752
+ return [[i, j, 1]]
753
+ elif e == 3:
754
+ return [[i, j, 4]]
755
+ elif e == 4:
756
+ if i != 0:
757
+ return [[i, j, 1], [i - 1, j, 0]]
758
+ else:
759
+ return [[i, j, 1], [N - 1, j, 0]]
760
+ else:
761
+ if i != N - 1:
762
+ return [[i, j, 1], [i + 1, j, 0]]
763
+ else:
764
+ return [[i, j, 1], [0, j, 0]]
765
+ else:
766
+ if e == 6:
767
+ if j != 0:
768
+ return [[i, j, 1], [i, j - 1, 0]]
769
+ else:
770
+ return [[i, j, 1], [i, M - 1, 0]]
771
+ elif e == 7:
772
+ if j != M - 1:
773
+ return [[i, j, 1], [i, j + 1, 0]]
774
+ else:
775
+ return [[i, j, 1], [i, 0, 0]]
776
+ elif e == 8:
777
+ if i != 0:
778
+ return [[i, j, 4], [i - 1, j, 3]]
779
+ else:
780
+ return [[i, j, 4], [N - 1, j, 3]]
781
+ elif e == 9:
782
+ if i != N - 1:
783
+ return [[i, j, 4], [i + 1, j, 3]]
784
+ else:
785
+ return [[i, j, 4], [0, j, 3]]
786
+ elif e == 10:
787
+ if j != 0:
788
+ return [[i, j, 4], [i, j - 1, 3]]
789
+ else:
790
+ return [[i, j, 4], [i, M - 1, 3]]
791
+ elif e == 11:
792
+ if j != M - 1:
793
+ return [[i, j, 4], [i, j + 1, 3]]
794
+ else:
795
+ return [[i, j, 4], [i, 0, 3]]
796
+ else:
797
+ raise RuntimeError('A bug in code: invalid event number encountered:', e) # debug line
798
+
799
+
800
+
801
+
802
+ def nb_need_change(ni, signal):
803
+ '''
804
+ Check whether a neighbor needs to change.
805
+ Two cases don't need change: either ni is None (doesn't exist) or in signal (is a last-change patch and already updated)
806
+
807
+ Inputs:
808
+ ni: index of a neighbor, might be None if patch doesn't exist.
809
+ signal: return value of make_signal_zero_flux or make_signal_periodical.
810
+
811
+ Returns:
812
+ True or False, whether the neighboring patch specified by ni needs change
813
+ '''
814
+
815
+ if ni == None:
816
+ return False
817
+
818
+ for si in signal:
819
+ if ni[0] == si[0] and ni[1] == si[1]:
820
+ return False
821
+
822
+ return True
823
+
824
+
825
+
826
+
827
+ def single_init(sim, rng):
828
+ '''
829
+ The first major function for the model.
830
+ Initialize all variables and run 1 round, then pass variables and results to single_test.
831
+
832
+ Input:
833
+ sim is a simulation object
834
+ rng is random number generator (np.random.default_rng), initialized by model.run
835
+ '''
836
+
837
+ #### Initialize Data Storage ####
838
+
839
+ world = [[patch(sim.I[i][j][0], sim.I[i][j][1], sim.X[i][j], sim.P[i][j]) for j in range(sim.M)] for i in range(sim.N)] # N x M patches
840
+ patch_rates = np.zeros((sim.N, sim.M), dtype = RATES_DTYPE) # every patch's sum-of-12-srates
841
+ sum_rates_by_row = np.zeros((sim.N), dtype = RATES_DTYPE) # every row's sum-of-patch, i.e., sum of 12 * M rates in every row.
842
+ sum_rates = 0 # sum of all N x M x 12 rates
843
+
844
+ signal = None
845
+
846
+ nb_indices = None
847
+ if sim.boundary:
848
+ nb_indices = [[find_nb_zero_flux(sim.N, sim.M, i, j) for j in range(sim.M)] for i in range(sim.N)]
849
+ else:
850
+ nb_indices = [[find_nb_periodical(sim.N, sim.M, i, j) for j in range(sim.M)] for i in range(sim.N)]
851
+
852
+ for i in range(sim.N):
853
+ for j in range(sim.M):
854
+ nb = []
855
+ for k in range(4):
856
+ if nb_indices[i][j][k] != None:
857
+ # append a pointer to the patch
858
+ nb.append(world[nb_indices[i][j][k][0]][nb_indices[i][j][k][1]])
859
+ else:
860
+ # nb doesn't exist
861
+ nb.append(None)
862
+ # pass it to patch class and store
863
+ world[i][j].set_nb_pointers(nb)
864
+
865
+
866
+ #### Begin Running ####
867
+
868
+ # initialize payoff & natural death rates
869
+ for i in range(sim.N):
870
+ for j in range(sim.M):
871
+ world[i][j].update_pi_k()
872
+
873
+ # initialize migration rates & the rates list
874
+ for i in range(sim.N):
875
+ for j in range(sim.M):
876
+ world[i][j].update_mig()
877
+ # store rates & sum of rates
878
+ patch_rates[i][j] = world[i][j].get_sum_rates()
879
+ sum_rates_by_row[i] = sum(patch_rates[i])
880
+
881
+ sum_rates = sum(sum_rates_by_row)
882
+
883
+ # pick the first random event
884
+ expected_sum = rng.random() * sum_rates
885
+ # find patch first
886
+ i0, j0, current_sum = find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates)
887
+ # then find which event in that patch
888
+ e0 = world[i0][j0].find_event(expected_sum - current_sum)
889
+
890
+ # initialize signal
891
+ if sim.boundary:
892
+ signal = make_signal_zero_flux(i0, j0, e0) # walls around world
893
+ else:
894
+ signal = make_signal_periodical(sim.N, sim.M, i0, j0, e0) # no walls around world
895
+
896
+ # change U&V based on signal
897
+ for si in signal:
898
+ world[si[0]][si[1]].change_popu(si[2])
899
+
900
+ # time increment
901
+ time = (1 / sum_rates) * math.log(1 / rng.random())
902
+
903
+ # record
904
+ if time > sim.record_itv:
905
+ record_index = int(time / sim.record_itv)
906
+ for i in range(sim.N):
907
+ for j in range(sim.M):
908
+ for k in range(record_index):
909
+ sim.U[i][j][k] += world[i][j].U
910
+ sim.V[i][j][k] += world[i][j].V
911
+ sim.U_pi[i][j][k] += world[i][j].U_pi
912
+ sim.V_pi[i][j][k] += world[i][j].V_pi
913
+ # we simply add to that entry, and later divide by sim_time to get the average (division in run function)
914
+
915
+ return time, world, nb_indices, patch_rates, sum_rates_by_row, sum_rates, signal
916
+
917
+
918
+
919
+
920
+ def single_test(sim, front_info, end_info, update_sum_frequency, rng):
921
+ '''
922
+ Runs a single simulation, from time = 0 to sim.maxtime.
923
+ run recursively calls single_test to get the average data.
924
+
925
+ Inputs:
926
+ sim: a simulation object, created by user and carries all parameters & storage bins.
927
+ front_info, end_info: passed by run to show messages, like the current round number in run. Not intended for direct usages.
928
+ update_sum_frequency: re-calculate sums this many times in simulation.
929
+ Our sums are gradually updated over time. So might have precision errors for large maxtime.
930
+ rng: np.random.default_rng. Initialized by model.run
931
+ '''
932
+
933
+ # initialize helper variables
934
+ # used to print progress, i.e., how much percent is done
935
+ one_time = sim.maxtime / max(100, update_sum_frequency)
936
+ one_progress = 0
937
+ if sim.print_pct != None:
938
+ # print progress, x%
939
+ print(front_info + ' 0%' + end_info, end = '\r')
940
+ one_progress = sim.maxtime * sim.print_pct / 100
941
+ else:
942
+ one_progress = 2 * sim.maxtime # not printing
943
+
944
+ # our sums (sum_rates_by_row and sum_rates) are gradually updated over time. This may have precision errors for large maxtime.
945
+ # So re-sum everything every some percentage of maxtime.
946
+ one_update_sum = sim.maxtime / update_sum_frequency
947
+
948
+ current_time = one_time
949
+ current_progress = one_progress
950
+ current_update_sum = one_update_sum
951
+
952
+ max_record = int(sim.maxtime / sim.record_itv)
953
+
954
+
955
+ # initialize
956
+ time, world, nb_indices, patch_rates, sum_rates_by_row, sum_rates, signal = single_init(sim, rng)
957
+ record_index = int(time / sim.record_itv)
958
+ # record_time is how much time has passed since the last record
959
+ # if record_time > record_itv:
960
+ # we count how many record_itvs are there in record_time, denote the number by multi_records
961
+ # then store the current data in multi_records number of cells in the list
962
+ # and subtract record_time by the multiple of record_itv, so that record_time < record_itv
963
+ record_time = time - record_index * sim.record_itv
964
+
965
+ ### Large while loop ###
966
+
967
+ while time < sim.maxtime:
968
+
969
+ # print progress & correct error of sum_rates
970
+ if time > current_time:
971
+ # a new 1% of time
972
+ current_time += one_time
973
+ if time > current_progress:
974
+ # print progress
975
+ print(front_info + ' ' + str(round(time / sim.maxtime * 100)) + '%' + end_info, end = '\r')
976
+ current_progress += one_progress
977
+
978
+ if time > current_update_sum:
979
+ current_update_sum += one_update_sum
980
+ for i in range(sim.N):
981
+ sum_rates_by_row[i] = sum(patch_rates[i])
982
+ sum_rates = sum(sum_rates_by_row)
983
+
984
+
985
+ # before updating last-changed patches, subtract old sum of rates (so as to update sum of rates by adding new rates later)
986
+ for si in signal:
987
+ # si[0] is row number, si[1] is col number
988
+ old_patch_rate = world[si[0]][si[1]].get_sum_rates()
989
+ sum_rates_by_row[si[0]] -= old_patch_rate
990
+ sum_rates -= old_patch_rate
991
+
992
+ # update last-changed patches
993
+ # update payoff and death rates first
994
+ for si in signal:
995
+ world[si[0]][si[1]].update_pi_k()
996
+ # then update migration rates, as mig_rates depend on neighbor's payoff
997
+ for si in signal:
998
+ world[si[0]][si[1]].update_mig()
999
+
1000
+ # update rates stored
1001
+ new_patch_rate = world[si[0]][si[1]].get_sum_rates()
1002
+ # update patch_rates
1003
+ patch_rates[si[0]][si[1]] = new_patch_rate
1004
+ # update sum_rate_by_row and sum_rates_by_row by adding new rates
1005
+ sum_rates_by_row[si[0]] += new_patch_rate
1006
+ sum_rates += new_patch_rate
1007
+
1008
+ # update neighbors of last-changed patches
1009
+ for si in signal:
1010
+ for ni in nb_indices[si[0]][si[1]]:
1011
+ # don't need to update if the patch is a last-change patch itself or None
1012
+ # use helper function to check
1013
+ if nb_need_change(ni, signal):
1014
+ # update migratino rates
1015
+ world[ni[0]][ni[1]].update_mig()
1016
+ # Note: no need to update patch_rates and sum of rates, as update_mig doesn't change total rates in a patch.
1017
+ # sum_mig_rate is decided by mu1 * U + mu2 * V, and pi_death_rate is not changed.
1018
+
1019
+ # pick the first random event
1020
+ expected_sum = rng.random() * sum_rates
1021
+ # find patch first
1022
+ i0, j0, current_sum = find_patch(expected_sum, patch_rates, sum_rates_by_row, sum_rates)
1023
+ # then find which event in that patch
1024
+ e0 = world[i0][j0].find_event(expected_sum - current_sum)
1025
+
1026
+ # make signal
1027
+ if sim.boundary:
1028
+ signal = make_signal_zero_flux(i0, j0, e0)
1029
+ else:
1030
+ signal = make_signal_periodical(sim.N, sim.M, i0, j0, e0)
1031
+
1032
+ # let the event happen
1033
+ for si in signal:
1034
+ world[si[0]][si[1]].change_popu(si[2])
1035
+
1036
+ # increase time
1037
+ r1 = rng.random()
1038
+ dt = (1 / sum_rates) * math.log(1 / r1)
1039
+ time += dt
1040
+ record_time += dt
1041
+
1042
+ if time < sim.maxtime:
1043
+ # if not exceeds maxtime
1044
+ if record_time > sim.record_itv:
1045
+ multi_records = int(record_time / sim.record_itv)
1046
+ record_time -= multi_records * sim.record_itv
1047
+
1048
+ for i in range(sim.N):
1049
+ for j in range(sim.M):
1050
+ for k in range(record_index, record_index + multi_records):
1051
+ sim.U[i][j][k] += world[i][j].U
1052
+ sim.V[i][j][k] += world[i][j].V
1053
+ sim.U_pi[i][j][k] += world[i][j].U_pi
1054
+ sim.V_pi[i][j][k] += world[i][j].V_pi
1055
+ record_index += multi_records
1056
+ else:
1057
+ # if already exceeds maxtime
1058
+ for i in range(sim.N):
1059
+ for j in range(sim.M):
1060
+ for k in range(record_index, max_record):
1061
+ sim.U[i][j][k] += world[i][j].U
1062
+ sim.V[i][j][k] += world[i][j].V
1063
+ sim.U_pi[i][j][k] += world[i][j].U_pi
1064
+ sim.V_pi[i][j][k] += world[i][j].V_pi
1065
+
1066
+ ### Large while loop ends ###
1067
+
1068
+ if sim.print_pct != None:
1069
+ print(front_info + ' 100%' + ' ' * 20, end = '\r') # empty spaces to overwrite predicted runtime
1070
+
1071
+
1072
+
1073
+
1074
+ def run(sim, predict_runtime = False, message = ''):
1075
+ '''
1076
+ Main function. Recursively calls single_test to run many simulations and then takes the average.
1077
+
1078
+ Inputs:
1079
+ - sim is a simulation object.
1080
+ - predict_runtime = False will not predict how much time still needed, set to True if you want to see.
1081
+ - message is used by some functions in figures.py to print messages.
1082
+ '''
1083
+
1084
+ if not sim.data_empty:
1085
+ raise RuntimeError('sim has non-empty data')
1086
+
1087
+ start = timer() # runtime
1088
+
1089
+ sim.data_empty = False
1090
+ rng = np.random.default_rng(sim.seed)
1091
+
1092
+ # passed to single_test to print progress
1093
+ if sim.print_pct == 0:
1094
+ sim.print_pct = 5 # default print_pct
1095
+
1096
+ update_sum_frequency = 4 # re-calculate sums this many times. See input desciption of single_test
1097
+
1098
+ ### simulations ###
1099
+ i = 0
1100
+
1101
+ while i < sim.sim_time:
1102
+ # use while loop so that can go backwards if got numerical issues
1103
+
1104
+ end_info = ''
1105
+ if predict_runtime:
1106
+ if i > 0:
1107
+ time_elapsed = timer() - start
1108
+ pred_runtime = time_elapsed / i * (sim.sim_time - i)
1109
+ end_info = ', ~' + str(round(pred_runtime, 2)) + 's left'
1110
+
1111
+ front_info = ''
1112
+ if sim.print_pct != None:
1113
+ front_info = message + 'round ' + str(i) + ':'
1114
+ print(front_info + ' ' * 30, end = '\r') # the blank spaces are to overwrite percentages, e.g. 36 %
1115
+
1116
+ try:
1117
+ single_test(sim, front_info, end_info, update_sum_frequency, rng)
1118
+ i += 1
1119
+ except IndexError:
1120
+ update_sum_frequency *= 4
1121
+ print('Numerical issue at round ' + str(i) + '. Trying higher precision now. See doc if err repeats')
1122
+ # not increasing i: redo current round.
1123
+
1124
+ ### simulations end ###
1125
+
1126
+ sim.calculate_ave()
1127
+
1128
+ stop = timer()
1129
+ print(' ' * 30, end = '\r') # overwrite all previous prints
1130
+ print(message + 'runtime: ' + str(round(stop - start, 2)) + ' s')
1131
+ return
1132
+
1133
+
1134
+
1135
+
1136
+ def demo_model():
1137
+ '''
1138
+ Returns a demo model.simulation object
1139
+ '''
1140
+
1141
+ N = 10 # Number of rows
1142
+ M = 10 # Number of cols
1143
+ maxtime = 300 # how long you want the model to run
1144
+ record_itv = 0.1 # how often to record data.
1145
+ sim_time = 1 # repeat simulation to reduce randomness
1146
+ boundary = True # boundary condition.
1147
+
1148
+ # initial population for the N x M patches.
1149
+ I = [[[44, 22] for _ in range(M)] for _ in range(N)]
1150
+
1151
+ # flattened payoff matrices, total resource is 0.4, cost of fighting is 0.1
1152
+ X = [[[-0.1, 0.4, 0, 0.2] for _ in range(M)] for _ in range(N)]
1153
+
1154
+ # patch variables
1155
+ P = [[[0.5, 0.5, 200, 200, 0.001, 0.001] for _ in range(M)] for _ in range(N)]
1156
+
1157
+ print_pct = 5 # print progress
1158
+ seed = 36 # seed for random number generation
1159
+ UV_dtype = 'float32' # data type for population
1160
+ pi_dyna = 'float64' # data type for payoff
1161
+
1162
+ # create a simulation object
1163
+ sim = simulation(N, M, maxtime, record_itv, sim_time, boundary, I, X, P,
1164
+ print_pct = print_pct, seed = seed, UV_dtype = UV_dtype, pi_dtype = pi_dyna)
1165
+
1166
+ return sim
1167
+
1168
+