piegy 2.1.0__cp38-cp38-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
piegy/simulation.py ADDED
@@ -0,0 +1,500 @@
1
+ '''
2
+ Main Module of Stochastic Simulation
3
+ -------------------------------------------
4
+
5
+ Contains all the necessary tools to build a model and run models based on Gillespie Algorithm.
6
+
7
+ Class & Functions:
8
+ - model: creates a stochastic mode. Run the model by ``simulation.run(mod)``
9
+ - run: runs stochastic simulation on a model.
10
+ - demo_model: returns a demo model.
11
+ - UV_expected_val: calculates the expected population of U, V at steady state, assuming no migration and any stochastic process.
12
+ - check_overflow_func: check whether an overflow might happen in simulation. This is usually done automatically when init-ing a model.
13
+ '''
14
+
15
+
16
+ import numpy as np
17
+ import os
18
+ import ctypes
19
+ from ctypes import c_size_t, c_uint32, c_int32, c_int64, c_double, c_bool, c_char_p, c_char
20
+ import numpy as np
21
+ from numpy.ctypeslib import ndpointer
22
+
23
+
24
+ # path to the C shared libary
25
+ C_LIB_PATH = os.path.join(os.path.dirname(__file__), 'C_core', 'piegyc.so')
26
+
27
+ # check whether overflow / too large values might be encountered
28
+ # these values are considered as exponents in exp()
29
+ EXP_OVERFLOW_BOUND = 709 # where exp(x) reaches overflow bound
30
+ EXP_TOO_LARGE_BOUND = 88 # where exp(x) reaches 1e20
31
+
32
+
33
+ '''
34
+ The C core
35
+ '''
36
+
37
+ class model_c(ctypes.Structure):
38
+ '''
39
+ The C-cored model
40
+ '''
41
+
42
+ _fields_ = [
43
+ ('N', c_size_t),
44
+ ('M', c_size_t),
45
+ ('maxtime', c_double),
46
+ ('record_itv', c_double),
47
+ ('sim_time', c_size_t),
48
+ ('boundary', c_bool),
49
+
50
+ ('I', ctypes.POINTER(c_uint32)),
51
+ ('X', ctypes.POINTER(c_double)),
52
+ ('P', ctypes.POINTER(c_double)),
53
+
54
+ ('print_pct', c_int32),
55
+ ('seed', c_int32),
56
+
57
+ ('data_empty', c_bool),
58
+ ('max_record', c_size_t),
59
+ ('arr_size', c_size_t),
60
+ ('compress_itv', c_uint32),
61
+
62
+ ('U1d', ctypes.POINTER(c_double)),
63
+ ('V1d', ctypes.POINTER(c_double)),
64
+ ('Upi_1d', ctypes.POINTER(c_double)),
65
+ ('Vpi_1d', ctypes.POINTER(c_double)),
66
+ ]
67
+ def get_array(self, name):
68
+ """Return internal data as NumPy array, e.g. .get_array('U')"""
69
+ ptr = getattr(self, name)
70
+ return np.ctypeslib.as_array(ptr, shape=(self.arr_size,))
71
+
72
+ lib = ctypes.CDLL(C_LIB_PATH, winmode = 0)
73
+ lib.mod_init.argtypes = [
74
+ ctypes.POINTER(model_c), c_size_t, c_size_t,
75
+ c_double, c_double, c_size_t, c_bool,
76
+ ndpointer(dtype=np.uint32, flags="C_CONTIGUOUS"),
77
+ ndpointer(dtype=np.float64, flags="C_CONTIGUOUS"),
78
+ ndpointer(dtype=np.float64, flags="C_CONTIGUOUS"),
79
+ c_int32, c_int32
80
+ ]
81
+ lib.mod_init.restype = c_bool
82
+
83
+ lib.mod_free_py.argtypes = [ctypes.POINTER(model_c)]
84
+ lib.mod_free_py.restype = None
85
+
86
+ lib.run.argtypes = [ctypes.POINTER(model_c), ctypes.POINTER(c_char), c_size_t]
87
+ lib.run.restype = None
88
+
89
+
90
+
91
+
92
+ '''
93
+ For access by Python
94
+ '''
95
+
96
+ class model:
97
+ '''
98
+ Store model data and input parameters.
99
+ Initialize a model object to run models.
100
+
101
+ Public Class Functions:
102
+
103
+ __init__:
104
+ Create a model object. Also initialize data storage.
105
+
106
+ __str__:
107
+ Print model object in a nice way.
108
+
109
+ copy:
110
+ Return a deep copy of self. Can choose whether to copy data as well. Default is to copy.
111
+
112
+ clear_data:
113
+ clear all data stored, set U, V, Upi, Vpi to zero arrays
114
+
115
+ change_maxtime:
116
+ Changes maxtime of self. Update data storage as well.
117
+
118
+ set_seed:
119
+ Set a new seed.
120
+
121
+ compress_data:
122
+ compress data by only storing average values
123
+ '''
124
+
125
+ def __init__(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct = 25, seed = None, check_overflow = True):
126
+
127
+ self.check_valid_input(N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct, seed, check_overflow)
128
+
129
+ self.N = N # int, N x M is spatial dimension
130
+ 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)
131
+ self.maxtime = maxtime # float or int, run model for how long time
132
+ self.record_itv = record_itv # float, record data every record_itv of time
133
+ self.sim_time = sim_time # int, run this many of rounds (of single_test)
134
+ self.boundary = boundary # bool, the N x M space have boundary or not (i.e., zero-flux (True) or periodical (False))
135
+ self.I = np.array(I) # N x M x 2 np.array, initial population. Two init-popu for every patch (U and V)
136
+ self.X = np.array(X) # N x M x 4 np.array, matrices. The '4' comes from 2x2 matrix flattened to 1D
137
+ self.P = np.array(P) # N x M x 6 np.array, 'patch variables', i.e., mu1&2, w1&2, kappa1&2
138
+ if print_pct == None:
139
+ self.print_pct = -1
140
+ else:
141
+ self.print_pct = print_pct # int, print how much percent is done, need to be non-zero
142
+ if seed == None:
143
+ self.seed = -1 # non-negative int, seed for random number generation
144
+ else:
145
+ self.seed = seed
146
+ self.check_overflow = check_overflow
147
+
148
+ if check_overflow:
149
+ check_overflow_func(self)
150
+
151
+ self.init_storage() # initialize storage bins. Put in a separate function because might want to change maxtime
152
+ # and that doesn't need to initialze the whole object again
153
+
154
+
155
+ def init_storage(self):
156
+ # initialize storage bins
157
+ self.data_empty = True # whether data storage bins are empty. model.run will refuse to run (raise error) if not empty.
158
+ self.max_record = int(self.maxtime / self.record_itv) # int, how many data points to store sin total
159
+ self.compress_itv = 1 # int, intended to reduce size of data (if not 1). Updated by compress_data function
160
+ # if set to an int, say 20, mod will take average over every 20 data points and save them as new data.
161
+ # May be used over and over again to recursively reduce data size.
162
+ # Default is 1, not to take average.
163
+ self.U = None # initialized by simulation.run or data_tools.read_data
164
+ self.V = None
165
+ self.Upi = None
166
+ self.Vpi = None
167
+
168
+
169
+ def check_valid_input(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct, seed, check_overflow):
170
+ # check whether the inputs are valid
171
+
172
+ if (N < 1) or (M < 1):
173
+ raise ValueError('N < 1 or M < 1')
174
+ if (N == 1) and (M == 1):
175
+ raise ValueError('Model fails for 1x1 space')
176
+ if (M == 1):
177
+ raise ValueError('Please set N = 1 for 1D space.')
178
+ if maxtime <= 0:
179
+ raise ValueError('Please set a positive number for maxtime')
180
+ if record_itv <= 0:
181
+ raise ValueError('Please set a positive number for record_itv')
182
+ if sim_time <= 0:
183
+ raise ValueError('Please set a positive number for sim_time')
184
+ if type(boundary) != bool:
185
+ raise TypeError('boundary not a bool. Please use True for zero-flux (with boundary) or False for periodical (no boundary)')
186
+
187
+ if (type(I) != list) and (type(I) != np.ndarray):
188
+ raise TypeError('Please set I as a list or np.ndarray')
189
+ if np.array(I).shape != (N, M, 2):
190
+ 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')
191
+
192
+ if (type(X) != list) and (type(X) != np.ndarray):
193
+ raise TypeError('Please set X as a list or np.ndarray')
194
+ if np.array(X).shape != (N, M, 4):
195
+ raise ValueError('Please set X as a N x M x 4 shape list or array. 4 is for the flattened 2x2 payoff matrix')
196
+
197
+ if (type(P) != list) and (type(P) != np.ndarray):
198
+ raise TypeError('Please set P as a list or np.ndarray')
199
+ if np.array(P).shape != (N, M, 6):
200
+ 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')
201
+
202
+ if not ((print_pct == None) or (isinstance(print_pct, int) and (print_pct >= -1))):
203
+ # if not the two acceptable values: None or >= -1 int
204
+ raise ValueError('Please use an int > 0 for print_pct or None for not printing progress.')
205
+
206
+ if not ((seed == None) or (isinstance(seed, int) and (seed >= -1))):
207
+ raise ValueError('Please use a non-negative int as seed, or use None for no seed.')
208
+
209
+ if not isinstance(check_overflow, bool):
210
+ raise ValueError('Please use a bool for check_overflow')
211
+
212
+
213
+ def check_valid_data(self, data_empty, max_record, compress_itv):
214
+ # check whether a set of data is valid, used when reading a saved model
215
+ if type(data_empty) != bool:
216
+ raise TypeError('data_empty not a bool')
217
+
218
+ if type(max_record) != int:
219
+ raise TypeError('max_record not an int')
220
+ if max_record < 0:
221
+ raise ValueError('max_record < 0')
222
+
223
+ if type(compress_itv) != int:
224
+ raise TypeError('compress_itv not an int')
225
+ if compress_itv < 0:
226
+ raise ValueError('compress_itv < 0')
227
+
228
+
229
+ def __str__(self):
230
+ # print this mod in a nice format
231
+
232
+ self_str = ''
233
+ self_str += 'N = ' + str(self.N) + '\n'
234
+ self_str += 'M = ' + str(self.M) + '\n'
235
+ self_str += 'maxtime = ' + str(self.maxtime) + '\n'
236
+ self_str += 'record_itv = ' + str(self.record_itv) + '\n'
237
+ self_str += 'sim_time = ' + str(self.sim_time) + '\n'
238
+ self_str += 'boundary = ' + str(self.boundary) + '\n'
239
+ self_str += 'print_pct = ' + str(self.print_pct) + '\n'
240
+ self_str += 'seed = ' + str(self.seed) + '\n'
241
+ self_str += 'check_overflow = ' + str(self.check_overflow) + '\n'
242
+ self_str += 'data_empty = ' + str(self.data_empty) + '\n'
243
+ self_str += 'compress_itv = ' + str(self.compress_itv) + '\n'
244
+ self_str += '\n'
245
+
246
+ # check whether I, X, P all same (compare all patches to (0, 0))
247
+ I_same = True
248
+ X_same = True
249
+ P_same = True
250
+ for i in range(self.N):
251
+ for j in range(self.M):
252
+ for k in range(2):
253
+ if self.I[i][j][k] != self.I[0][0][k]:
254
+ I_same = False
255
+ for k in range(4):
256
+ if self.X[i][j][k] != self.X[0][0][k]:
257
+ X_same = False
258
+ for k in range(6):
259
+ if self.P[i][j][k] != self.P[0][0][k]:
260
+ P_same = False
261
+
262
+ if I_same:
263
+ self_str += 'I all same: ' + str(self.I[0][0]) + '\n'
264
+ else:
265
+ self_str += 'I:\n'
266
+ for i in range(self.N):
267
+ for j in range(self.M):
268
+ self_str += str(self.I[i][j]) + ' '
269
+ self_str += '\n'
270
+ self_str += '\n'
271
+
272
+ if X_same:
273
+ self_str += 'X all same: ' + str(self.X[0][0]) + '\n'
274
+ else:
275
+ self_str += 'X:\n'
276
+ for i in range(self.N):
277
+ for j in range(self.M):
278
+ self_str += str(self.X[i][j]) + ' '
279
+ self_str += '\n'
280
+ self_str += '\n'
281
+
282
+ if P_same:
283
+ self_str += 'P all same: ' + str(self.P[0][0]) + '\n'
284
+ else:
285
+ self_str += 'P:\n'
286
+ for i in range(self.N):
287
+ for j in range(self.M):
288
+ self_str += str(self.P[i][j]) + ' '
289
+ self_str += '\n'
290
+
291
+ return self_str
292
+
293
+
294
+ def copy(self, copy_data = True):
295
+ # return deep copy of self
296
+ # copy_data decides whether to copy data as well
297
+ if type(copy_data) != bool:
298
+ raise TypeError('Please give a bool as argument: whether to copy data or not')
299
+
300
+ sim2 = model(N = self.N, M = self.M, maxtime = self.maxtime, record_itv = self.record_itv, sim_time = self.sim_time, boundary = self.boundary,
301
+ I = np.copy(self.I), X = np.copy(self.X), P = np.copy(self.P),
302
+ print_pct = self.print_pct, seed = self.seed, check_overflow = self.check_overflow)
303
+
304
+ if copy_data:
305
+ # copy data as well
306
+ sim2.set_data(self.data_empty, self.max_record, self.compress_itv, self.U, self.V, self.Upi, self.Vpi)
307
+
308
+ return sim2
309
+
310
+
311
+ def calculate_ave(self):
312
+ # get the average value over sim_time many models
313
+ if self.sim_time != 1:
314
+ for i in range(self.N):
315
+ for j in range(self.M):
316
+ for t in range(self.max_record):
317
+ self.U[i][j][t] /= self.sim_time
318
+ self.V[i][j][t] /= self.sim_time
319
+ self.Upi[i][j][t] /= self.sim_time
320
+ self.Vpi[i][j][t] /= self.sim_time
321
+
322
+
323
+ def change_maxtime(self, maxtime):
324
+ # change maxtime
325
+ if (type(maxtime) != float) and (type(maxtime) != int):
326
+ raise TypeError('Please pass in a float or int as the new maxtime.')
327
+ if maxtime <= 0:
328
+ raise ValueError('Please use a positive maxtime.')
329
+ self.maxtime = maxtime
330
+ self.init_storage()
331
+
332
+
333
+ def set_seed(self, seed):
334
+ # set seed
335
+ self.seed = seed
336
+
337
+
338
+ def clear_data(self):
339
+ # clear all data stored, set all to 0
340
+ self.init_storage()
341
+
342
+
343
+ def set_data(self, data_empty, max_record, compress_itv, U, V, Upi, Vpi):
344
+ # set data to the given data values
345
+ # copies are made
346
+ self.check_valid_data(data_empty, max_record, compress_itv)
347
+
348
+ self.data_empty = data_empty
349
+ self.max_record = max_record
350
+ self.compress_itv = compress_itv
351
+ self.U = np.copy(U)
352
+ self.V = np.copy(V)
353
+ self.Upi = np.copy(Upi)
354
+ self.Vpi = np.copy(Vpi)
355
+
356
+
357
+ def compress_data(self, compress_itv = 5):
358
+ # compress data by only storing average values
359
+ if self.data_empty:
360
+ raise RuntimeError('Model has empty data. Cannot compress')
361
+
362
+ if type(compress_itv) != int:
363
+ raise TypeError('Please use an int as compress_itv')
364
+ if compress_itv < 1:
365
+ raise ValueError('Please use record_itv >= 1')
366
+ if compress_itv == 1:
367
+ return
368
+
369
+ self.compress_itv *= compress_itv # may be reduced over and over again
370
+ self.max_record = int(self.max_record / compress_itv) # number of data points after reducing
371
+
372
+ U_reduced = np.zeros((self.N, self.M, self.max_record), dtype = np.float64)
373
+ V_reduced = np.zeros((self.N, self.M, self.max_record), dtype = np.float64)
374
+ Upi_reduced = np.zeros((self.N, self.M, self.max_record), dtype = np.float64)
375
+ Vpi_reduced = np.zeros((self.N, self.M, self.max_record), dtype = np.float64)
376
+
377
+ for i in range(self.N):
378
+ for j in range(self.M):
379
+ for k in range(self.max_record):
380
+ lower = k * compress_itv # upper and lower bound of current record_itv
381
+ upper = lower + compress_itv
382
+ U_reduced[i][j][k] = np.mean(self.U[i, j, lower : upper])
383
+ V_reduced[i][j][k] = np.mean(self.V[i, j, lower : upper])
384
+ Upi_reduced[i][j][k] = np.mean(self.Upi[i, j, lower : upper])
385
+ Vpi_reduced[i][j][k] = np.mean(self.Vpi[i, j, lower : upper])
386
+
387
+ self.U = U_reduced
388
+ self.V = V_reduced
389
+ self.Upi = Upi_reduced
390
+ self.Vpi = Vpi_reduced
391
+
392
+
393
+
394
+ def run(mod, message = ""):
395
+ '''
396
+ C-cored simulation
397
+ '''
398
+
399
+ if not mod.data_empty:
400
+ raise ValueError('mod has non-empty data.')
401
+
402
+ msg_len = len(message)
403
+ msg_bytes = message.encode('utf-8')
404
+ msg_buffer = ctypes.create_string_buffer(msg_bytes, msg_len)
405
+
406
+ I = np.ascontiguousarray(mod.I.flatten(), dtype = np.uint32)
407
+ X = np.ascontiguousarray(mod.X.flatten(), dtype = np.float64)
408
+ P = np.ascontiguousarray(mod.P.flatten(), dtype = np.float64)
409
+
410
+ mod_c = model_c()
411
+ success = lib.mod_init(ctypes.byref(mod_c),
412
+ mod.N, mod.M, mod.maxtime, mod.record_itv, mod.sim_time, mod.boundary,
413
+ I, X, P, mod.print_pct, mod.seed)
414
+ if not success:
415
+ raise RuntimeError('mod_init failed')
416
+
417
+ lib.run(ctypes.byref(mod_c), msg_buffer, msg_len)
418
+
419
+ mod.set_data(False, mod.max_record, 1, mod_c.get_array('U1d').reshape(mod.N, mod.M, mod.max_record),
420
+ mod_c.get_array('V1d').reshape(mod.N, mod.M, mod.max_record),
421
+ mod_c.get_array('Upi_1d').reshape(mod.N, mod.M, mod.max_record),
422
+ mod_c.get_array('Vpi_1d').reshape(mod.N, mod.M, mod.max_record))
423
+
424
+ lib.mod_free_py(ctypes.byref(mod_c))
425
+ del mod_c
426
+
427
+
428
+
429
+ def demo_model():
430
+ '''
431
+ Returns a demo model.model object
432
+ '''
433
+
434
+ N = 10 # Number of rows
435
+ M = 10 # Number of cols
436
+ maxtime = 30 # how long you want the model to run
437
+ record_itv = 0.1 # how often to record data.
438
+ sim_time = 1 # repeat model to reduce randomness
439
+ boundary = True # boundary condition.
440
+
441
+ # initial population for the N x M patches.
442
+ I = [[[44, 22] for _ in range(M)] for _ in range(N)]
443
+
444
+ # flattened payoff matrices, total resource is 0.4, cost of fighting is 0.1
445
+ X = [[[-1, 4, 0, 2] for _ in range(M)] for _ in range(N)]
446
+
447
+ # patch variables
448
+ P = [[[0.5, 0.5, 1, 1, 0.001, 0.001] for _ in range(M)] for _ in range(N)]
449
+
450
+ print_pct = 25 # print progress
451
+ seed = 36 # seed for random number generation
452
+
453
+ # create a model object
454
+ mod = model(N, M, maxtime, record_itv, sim_time, boundary, I, X, P,
455
+ print_pct = print_pct, seed = seed)
456
+
457
+ return mod
458
+
459
+
460
+
461
+ def UV_expected_val(mod):
462
+ '''
463
+ Calculate expected U & V population and payoff based on matrices, assume no migration and any stochastic process.
464
+ To differentiate from UV_expected in figures.py: this one return arrays (values).
465
+ '''
466
+
467
+ U_expected = np.zeros((mod.N, mod.M)) # expected U population
468
+ V_expected = np.zeros((mod.N, mod.M)) # expected V population
469
+ pi_expected = np.zeros((mod.N, mod.M)) # expected payoff, which are equal for U and V
470
+
471
+ for i in range(mod.N):
472
+ for j in range(mod.M):
473
+ # say matrix = [a, b, c, d]
474
+ # U_proportion = (d - b) / (a - b - c + d)
475
+ U_prop = (mod.X[i][j][3] - mod.X[i][j][1]) / (mod.X[i][j][0] - mod.X[i][j][1] - mod.X[i][j][2] + mod.X[i][j][3])
476
+ # equilibrium payoff, U_payoff = V_payoff
477
+ eq_payoff = U_prop * mod.X[i][j][0] + (1 - U_prop) * mod.X[i][j][1]
478
+
479
+ # payoff / kappa * proportion
480
+ U_expected[i][j] = eq_payoff / mod.P[i][j][4] * U_prop
481
+ V_expected[i][j] = eq_payoff / mod.P[i][j][5] * (1 - U_prop)
482
+ pi_expected[i][j] = eq_payoff
483
+
484
+ return U_expected, V_expected, pi_expected
485
+
486
+
487
+
488
+ def check_overflow_func(mod):
489
+ _, _, pi_expected = UV_expected_val(mod)
490
+ for i in range(mod.N):
491
+ for j in range(mod.M):
492
+ w1_pi = pi_expected[i][j] * mod.P[i][j][2] # w1 * U_pi
493
+ w2_pi = pi_expected[i][j] * mod.P[i][j][3] # w2 * V_pi
494
+ if ((w1_pi > EXP_OVERFLOW_BOUND) or (w2_pi > EXP_OVERFLOW_BOUND)):
495
+ print("Warning: might cause overflow. \n\t w1, w2, or payoff matrix values too large")
496
+ return
497
+ if ((w1_pi > EXP_TOO_LARGE_BOUND) or (w2_pi > EXP_TOO_LARGE_BOUND)):
498
+ print("Warning: might encounter large values > 3e38 in simulation. \n\t w1, w2, or payoff matrix values too large")
499
+ return
500
+