piegy 1.1.6__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piegy/C_core/piegyc.so +0 -0
- piegy/__version__.py +2 -0
- piegy/data_tools.py +16 -21
- piegy/simulation.py +109 -821
- piegy/simulation_py.py +808 -0
- piegy/test_var.py +2 -2
- {piegy-1.1.6.dist-info → piegy-2.0.1.dist-info}/METADATA +1 -1
- piegy-2.0.1.dist-info/RECORD +18 -0
- piegy-1.1.6.dist-info/RECORD +0 -16
- {piegy-1.1.6.dist-info → piegy-2.0.1.dist-info}/WHEEL +0 -0
- {piegy-1.1.6.dist-info → piegy-2.0.1.dist-info}/licenses/LICENSE.txt +0 -0
- {piegy-1.1.6.dist-info → piegy-2.0.1.dist-info}/top_level.txt +0 -0
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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,
|
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
|
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
|
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
|
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()
|
304
|
-
|
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
|
310
|
-
self.max_record = int(self.maxtime / self.record_itv)
|
311
|
-
self.compress_itv = 1
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
self.U =
|
316
|
-
self.V =
|
317
|
-
self.Upi =
|
318
|
-
self.Vpi =
|
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 += '
|
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
|
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,
|
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(
|
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 =
|
517
|
-
V_reduced = np.zeros((self.N, self.M, self.max_record), dtype =
|
518
|
-
Upi_reduced = np.zeros((self.N, self.M, self.max_record), dtype =
|
519
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
921
|
-
|
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
|
-
|
955
|
-
|
956
|
-
|
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
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
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
|
-
|
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
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
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
|
438
|
+
print_pct = print_pct, seed = seed)
|
1152
439
|
|
1153
440
|
return mod
|
1154
441
|
|
1155
442
|
|
443
|
+
|