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