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