piegy 1.1.6__tar.gz → 2.0.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piegy
3
- Version: 1.1.6
3
+ Version: 2.0.1
4
4
  Summary: Payoff-Driven Stochastic Spatial Model for Evolutionary Game Theory
5
5
  Author-email: Chenning Xu <cxu7@caltech.edu>
6
6
  License: BSD 3-Clause License
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'piegy'
7
- version = '1.1.6'
7
+ version = '2.0.1'
8
8
  description = 'Payoff-Driven Stochastic Spatial Model for Evolutionary Game Theory'
9
9
  readme = 'README.md'
10
10
  requires-python = '>=3.6'
@@ -42,3 +42,7 @@ package-dir = {'' = 'src'}
42
42
  [tool.setuptools.packages.find]
43
43
  where = ['src']
44
44
 
45
+ [tool.setuptools.package-data]
46
+ "piegy" = ["C_core/piegyc.so"]
47
+
48
+
Binary file
@@ -21,4 +21,6 @@ version history:
21
21
  1.1.5: update README.
22
22
  1.1.6: change name of variables in model class -- for compatability with the new C core. 1.1.6 is the last verion of v1. From v2 on, the piegy package has C core.
23
23
 
24
+ 2.0.0: update simulation core to C-based.
25
+ 2.0.1: re-upload, the C core is not included in package.
24
26
  '''
@@ -38,24 +38,19 @@ def save_data(mod, dirs = '', print_msg = True):
38
38
 
39
39
  data = []
40
40
 
41
- inputs1 = []
42
- inputs1.append(mod.N)
43
- inputs1.append(mod.M)
44
- inputs1.append(mod.maxtime)
45
- inputs1.append(mod.record_itv)
46
- inputs1.append(mod.sim_time)
47
- inputs1.append(mod.boundary)
48
- inputs1.append(mod.I.tolist())
49
- inputs1.append(mod.X.tolist())
50
- inputs1.append(mod.P.tolist())
51
- data.append(inputs1)
52
-
53
- inputs2 = []
54
- inputs2.append(mod.print_pct)
55
- inputs2.append(mod.seed)
56
- inputs2.append(mod.UV_dtype)
57
- inputs2.append(mod.pi_dtype)
58
- data.append(inputs2)
41
+ inputs = []
42
+ inputs.append(mod.N)
43
+ inputs.append(mod.M)
44
+ inputs.append(mod.maxtime)
45
+ inputs.append(mod.record_itv)
46
+ inputs.append(mod.sim_time)
47
+ inputs.append(mod.boundary)
48
+ inputs.append(mod.I.tolist())
49
+ inputs.append(mod.X.tolist())
50
+ inputs.append(mod.P.tolist())
51
+ inputs.append(mod.print_pct)
52
+ inputs.append(mod.seed)
53
+ data.append(inputs)
59
54
 
60
55
  # skipped rng
61
56
 
@@ -112,14 +107,14 @@ def read_data(dirs):
112
107
  try:
113
108
  mod = simulation.model(N = data[0][0], M = data[0][1], maxtime = data[0][2], record_itv = data[0][3],
114
109
  sim_time = data[0][4], boundary = data[0][5], I = data[0][6], X = data[0][7], P = data[0][8],
115
- print_pct = data[1][0], seed = data[1][1], UV_dtype = data[1][2], pi_dtype = data[1][3])
110
+ print_pct = data[0][9], seed = data[0][10])
116
111
  except:
117
112
  raise ValueError('Invalid input parameters saved in data')
118
113
 
119
114
  # outputs
120
115
  try:
121
- mod.set_data(data_empty = False, max_record = data[2][0], compress_itv = data[2][1],
122
- U = data[2][2], V = data[2][3], Upi = data[2][4], Vpi = data[2][5])
116
+ mod.set_data(data_empty = False, max_record = data[1][0], compress_itv = data[1][1],
117
+ U = data[1][2], V = data[1][3], Upi = data[1][4], Vpi = data[1][5])
123
118
  except:
124
119
  raise ValueError('Invalid model results saved in data')
125
120
 
@@ -0,0 +1,443 @@
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
+ Classes:
8
+ - patch: (Private) Simulates a single patch in the N x M space. Assume no spatial structure within a patch.
9
+ All spatial movements are based on patches.
10
+ - model: Stores input parameters and data generated during simulation.
11
+
12
+
13
+ Functions:
14
+ - find_nb_zero_flux: (Private) Return pointers to a patch's neighbors under zero-flux (no-boundary) boundary condition.
15
+ - find_nb_periodical: (Private) Return pointers to a patch's neighbors under periodical (with-booundary) boundary condition.
16
+ - find_event: (Private) Pick a random event to happen.
17
+ - make_signal_zero_flux: (Private) Expand event index (return value of find_event) to a detailed signal, under zero-flux boundary condition.
18
+ - make_signal_periodical: (Private) Expand event index (return value of find_event) to a detailed signal, under periodical boundary condition.
19
+ - single_init: (Private) Initialize a single model. Meaning of 'single': see single_test and run.
20
+ - single_test: (Private) Run a single model.
21
+ 'single' means a single round of model. You can run many single rounds and then take the average --- that's done by <run> function.
22
+ - run: Run multiple models and then take the average. All the model will use the same parameters.
23
+ Set a seed for reproducible results.
24
+ - demo_model: Returns a demo model (a model object).
25
+
26
+ NOTE: Only model class and run function are intended for direct usages.
27
+ '''
28
+
29
+
30
+ import numpy as np
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
36
+
37
+
38
+
39
+
40
+ '''
41
+ The C core
42
+ '''
43
+
44
+ # path to the C shared libary
45
+ C_LIB_PATH = os.path.join(os.path.dirname(__file__), 'C_core', 'piegyc.so')
46
+
47
+
48
+ class model_c(ctypes.Structure):
49
+ '''
50
+ The C-cored model
51
+ '''
52
+
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
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):
133
+
134
+ self.check_valid_input(N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct)
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
+ self.print_pct = print_pct # int, print how much percent is done, need to be non-zero
146
+ self.seed = seed # non-negative int, seed for random number generation
147
+
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
150
+
151
+
152
+ def init_storage(self):
153
+ # initialize storage bins
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
164
+
165
+
166
+ def check_valid_input(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct):
167
+ # check whether the inputs are valid
168
+
169
+ if (N < 1) or (M < 1):
170
+ raise ValueError('N < 1 or M < 1')
171
+ if (N == 1) and (M == 1):
172
+ raise ValueError('Model fails for 1x1 space')
173
+ if (M == 1):
174
+ raise ValueError('Please set N = 1 for 1D space.')
175
+ if maxtime <= 0:
176
+ raise ValueError('Please set a positive number for maxtime')
177
+ if record_itv <= 0:
178
+ raise ValueError('Please set a positive number for record_itv')
179
+ if sim_time <= 0:
180
+ raise ValueError('Please set a positive number for sim_time')
181
+ if type(boundary) != bool:
182
+ raise TypeError('boundary not a bool. Please use True for zero-flux (with boundary) or False for periodical (no boundary)')
183
+
184
+ if (type(I) != list) and (type(I) != np.ndarray):
185
+ raise TypeError('Please set I as a list or np.ndarray')
186
+ if np.array(I).shape != (N, M, 2):
187
+ 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')
188
+
189
+ if (type(X) != list) and (type(X) != np.ndarray):
190
+ raise TypeError('Please set X as a list or np.ndarray')
191
+ if np.array(X).shape != (N, M, 4):
192
+ raise ValueError('Please set X as a N x M x 4 shape list or array. 4 is for the flattened 2x2 payoff matrix')
193
+
194
+ if (type(P) != list) and (type(P) != np.ndarray):
195
+ raise TypeError('Please set P as a list or np.ndarray')
196
+ if np.array(P).shape != (N, M, 6):
197
+ 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')
198
+
199
+ if print_pct <= 0:
200
+ raise ValueError('Please use an int > 0 for print_pct or None for not printing progress.')
201
+
202
+
203
+ def check_valid_data(self, data_empty, max_record, compress_itv):
204
+ # check whether a set of data is valid, used when reading a saved model
205
+ if type(data_empty) != bool:
206
+ raise TypeError('data_empty not a bool')
207
+
208
+ if type(max_record) != int:
209
+ raise TypeError('max_record not an int')
210
+ if max_record < 0:
211
+ raise ValueError('max_record < 0')
212
+
213
+ if type(compress_itv) != int:
214
+ raise TypeError('compress_itv not an int')
215
+ if compress_itv < 0:
216
+ raise ValueError('compress_itv < 0')
217
+
218
+
219
+ def __str__(self):
220
+ # print this mod in a nice format
221
+
222
+ self_str = ''
223
+ self_str += 'N = ' + str(self.N) + '\n'
224
+ self_str += 'M = ' + str(self.M) + '\n'
225
+ self_str += 'maxtime = ' + str(self.maxtime) + '\n'
226
+ self_str += 'record_itv = ' + str(self.record_itv) + '\n'
227
+ self_str += 'sim_time = ' + str(self.sim_time) + '\n'
228
+ self_str += 'boundary = ' + str(self.boundary) + '\n'
229
+ self_str += 'print_pct = ' + str(self.print_pct) + '\n'
230
+ self_str += 'seed = ' + str(self.seed) + '\n'
231
+ self_str += 'data_empty = ' + str(self.data_empty) + '\n'
232
+ self_str += 'compress_itv = ' + str(self.compress_itv) + '\n'
233
+ self_str += '\n'
234
+
235
+ # check whether I, X, P all same (compare all patches to (0, 0))
236
+ I_same = True
237
+ X_same = True
238
+ P_same = True
239
+ for i in range(self.N):
240
+ for j in range(self.M):
241
+ for k in range(2):
242
+ if self.I[i][j][k] != self.I[0][0][k]:
243
+ I_same = False
244
+ for k in range(4):
245
+ if self.X[i][j][k] != self.X[0][0][k]:
246
+ X_same = False
247
+ for k in range(6):
248
+ if self.P[i][j][k] != self.P[0][0][k]:
249
+ P_same = False
250
+
251
+ if I_same:
252
+ self_str += 'I all same: ' + str(self.I[0][0]) + '\n'
253
+ else:
254
+ self_str += 'I:\n'
255
+ for i in range(self.N):
256
+ for j in range(self.M):
257
+ self_str += str(self.I[i][j]) + ' '
258
+ self_str += '\n'
259
+ self_str += '\n'
260
+
261
+ if X_same:
262
+ self_str += 'X all same: ' + str(self.X[0][0]) + '\n'
263
+ else:
264
+ self_str += 'X:\n'
265
+ for i in range(self.N):
266
+ for j in range(self.M):
267
+ self_str += str(self.X[i][j]) + ' '
268
+ self_str += '\n'
269
+ self_str += '\n'
270
+
271
+ if P_same:
272
+ self_str += 'P all same: ' + str(self.P[0][0]) + '\n'
273
+ else:
274
+ self_str += 'P:\n'
275
+ for i in range(self.N):
276
+ for j in range(self.M):
277
+ self_str += str(self.P[i][j]) + ' '
278
+ self_str += '\n'
279
+
280
+ return self_str
281
+
282
+
283
+ def copy(self, copy_data = True):
284
+ # return deep copy of self
285
+ # copy_data decides whether to copy data as well
286
+ if type(copy_data) != bool:
287
+ raise TypeError('Please give a bool as argument: whether to copy data or not')
288
+
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,
290
+ I = np.copy(self.I), X = np.copy(self.X), P = np.copy(self.P),
291
+ print_pct = self.print_pct, seed = self.seed)
292
+
293
+ if copy_data:
294
+ # copy data as well
295
+ sim2.set_data(self.data_empty, self.max_record, self.compress_itv, self.U, self.V, self.Upi, self.Vpi)
296
+
297
+ return sim2
298
+
299
+
300
+ def calculate_ave(self):
301
+ # get the average value over sim_time many models
302
+ if self.sim_time != 1:
303
+ for i in range(self.N):
304
+ for j in range(self.M):
305
+ for t in range(self.max_record):
306
+ self.U[i][j][t] /= self.sim_time
307
+ self.V[i][j][t] /= self.sim_time
308
+ self.Upi[i][j][t] /= self.sim_time
309
+ self.Vpi[i][j][t] /= self.sim_time
310
+
311
+
312
+ def change_maxtime(self, maxtime):
313
+ # change maxtime
314
+ if (type(maxtime) != float) and (type(maxtime) != int):
315
+ raise TypeError('Please pass in a float or int as the new maxtime.')
316
+ if maxtime <= 0:
317
+ raise ValueError('Please use a positive maxtime.')
318
+ self.maxtime = maxtime
319
+ self.init_storage()
320
+
321
+
322
+ def set_seed(self, seed):
323
+ # set seed
324
+ self.seed = seed
325
+
326
+
327
+ def clear_data(self):
328
+ # clear all data stored, set all to 0
329
+ self.init_storage()
330
+
331
+
332
+ def set_data(self, data_empty, max_record, compress_itv, U, V, Upi, Vpi):
333
+ # set data to the given data values
334
+ # copies are made
335
+ self.check_valid_data(data_empty, max_record, compress_itv)
336
+
337
+ self.data_empty = data_empty
338
+ self.max_record = max_record
339
+ self.compress_itv = compress_itv
340
+ self.U = np.copy(U)
341
+ self.V = np.copy(V)
342
+ self.Upi = np.copy(Upi)
343
+ self.Vpi = np.copy(Vpi)
344
+
345
+
346
+ def compress_data(self, compress_itv = 5):
347
+ # compress data by only storing average values
348
+ if self.data_empty:
349
+ raise RuntimeError('Model has empty data. Cannot compress')
350
+
351
+ if type(compress_itv) != int:
352
+ raise TypeError('Please use an int as compress_itv')
353
+ if compress_itv < 1:
354
+ raise ValueError('Please use record_itv >= 1')
355
+ if compress_itv == 1:
356
+ return
357
+
358
+ self.compress_itv *= compress_itv # may be reduced over and over again
359
+ self.max_record = int(self.max_record / compress_itv) # number of data points after reducing
360
+
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)
365
+
366
+ for i in range(self.N):
367
+ for j in range(self.M):
368
+ for k in range(self.max_record):
369
+ lower = k * compress_itv # upper and lower bound of current record_itv
370
+ upper = lower + compress_itv
371
+ U_reduced[i][j][k] = np.mean(self.U[i, j, lower : upper])
372
+ V_reduced[i][j][k] = np.mean(self.V[i, j, lower : upper])
373
+ Upi_reduced[i][j][k] = np.mean(self.Upi[i, j, lower : upper])
374
+ Vpi_reduced[i][j][k] = np.mean(self.Vpi[i, j, lower : upper])
375
+
376
+ self.U = U_reduced
377
+ self.V = V_reduced
378
+ self.Upi = Upi_reduced
379
+ self.Vpi = Vpi_reduced
380
+
381
+
382
+
383
+ def run(mod, message = ""):
384
+ '''
385
+ C-cored simulation
386
+ '''
387
+ msg_len = len(message)
388
+ msg_bytes = message.encode('utf-8')
389
+ msg_buffer = ctypes.create_string_buffer(msg_bytes, msg_len)
390
+
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)
394
+
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")
401
+
402
+ lib.run(ctypes.byref(mod_c), msg_buffer, msg_len)
403
+
404
+ mod.data_empty = False
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)
409
+
410
+
411
+
412
+ def demo_model():
413
+ '''
414
+ Returns a demo model.model object
415
+ '''
416
+
417
+ N = 10 # Number of rows
418
+ M = 10 # Number of cols
419
+ maxtime = 300 # how long you want the model to run
420
+ record_itv = 0.1 # how often to record data.
421
+ sim_time = 1 # repeat model to reduce randomness
422
+ boundary = True # boundary condition.
423
+
424
+ # initial population for the N x M patches.
425
+ I = [[[44, 22] for _ in range(M)] for _ in range(N)]
426
+
427
+ # flattened payoff matrices, total resource is 0.4, cost of fighting is 0.1
428
+ X = [[[-0.1, 0.4, 0, 0.2] for _ in range(M)] for _ in range(N)]
429
+
430
+ # patch variables
431
+ P = [[[0.5, 0.5, 200, 200, 0.001, 0.001] for _ in range(M)] for _ in range(N)]
432
+
433
+ print_pct = 5 # print progress
434
+ seed = 36 # seed for random number generation
435
+
436
+ # create a model object
437
+ mod = model(N, M, maxtime, record_itv, sim_time, boundary, I, X, P,
438
+ print_pct = print_pct, seed = seed)
439
+
440
+ return mod
441
+
442
+
443
+
@@ -1,47 +1,17 @@
1
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
- Classes:
8
- - patch: (Private) Simulates a single patch in the N x M space. Assume no spatial structure within a patch.
9
- All spatial movements are based on patches.
10
- - model: Stores input parameters and data generated during simulation.
11
-
12
-
13
- Functions:
14
- - find_nb_zero_flux: (Private) Return pointers to a patch's neighbors under zero-flux (no-boundary) boundary condition.
15
- - find_nb_periodical: (Private) Return pointers to a patch's neighbors under periodical (with-booundary) boundary condition.
16
- - find_event: (Private) Pick a random event to happen.
17
- - make_signal_zero_flux: (Private) Expand event index (return value of find_event) to a detailed signal, under zero-flux boundary condition.
18
- - make_signal_periodical: (Private) Expand event index (return value of find_event) to a detailed signal, under periodical boundary condition.
19
- - single_init: (Private) Initialize a single model. Meaning of 'single': see single_test and run.
20
- - single_test: (Private) Run a single model.
21
- 'single' means a single round of model. You can run many single rounds and then take the average --- that's done by <run> function.
22
- - run: Run multiple models and then take the average. All the model will use the same parameters.
23
- Set a seed for reproducible results.
24
- - demo_model: Returns a demo model (a model object).
25
-
26
- NOTE: Only model class and run function are intended for direct usages.
2
+ Functions and class below are for Python-based simulations. Not maintained after v1.1.6 on Jun 26, 2025.
3
+ But you can still run them by calling:
4
+
5
+ >>> run_py(mod)
27
6
  '''
28
7
 
8
+ from . import simulation
29
9
 
30
10
  import math
31
11
  import numpy as np
32
12
  from timeit import default_timer as timer
33
13
 
34
14
 
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
15
  class patch:
46
16
  '''
47
17
  A single patch in the N x M space.
@@ -63,7 +33,7 @@ class patch:
63
33
  Set pointers to neighbors of this patch object.
64
34
 
65
35
  update_pi:
66
- Update Upi, V_pi and payoff rates (payoff rates are the first two numbers in self.pi_death_rates).
36
+ Update Upi, Vpi and payoff rates (payoff rates are the first two numbers in self.pi_death_rates).
67
37
 
68
38
  update_k:
69
39
  Update natural death rates (the last two numbers in self.pi_death_rates).
@@ -248,291 +218,6 @@ class patch:
248
218
  self.V += 1
249
219
  elif self.V > 0:
250
220
  self.V -= 1
251
-
252
-
253
-
254
- class model:
255
- '''
256
- Store model data and input parameters.
257
- Initialize a model object to run models.
258
-
259
- Public Class Functions:
260
-
261
- __init__:
262
- Create a model object. Also initialize data storage.
263
-
264
- __str__:
265
- Print model object in a nice way.
266
-
267
- copy:
268
- Return a deep copy of self. Can choose whether to copy data as well. Default is to copy.
269
-
270
- clear_data:
271
- clear all data stored, set U, V, Upi, V_pi to zero arrays
272
-
273
- change_maxtime:
274
- Changes maxtime of self. Update data storage as well.
275
-
276
- set_seed:
277
- Set a new seed.
278
-
279
- compress_data:
280
- compress data by only storing average values
281
- '''
282
-
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):
284
-
285
- self.check_valid_input(N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct)
286
-
287
- self.N = N # int, N x M is spatial dimension
288
- 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
- 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
291
- self.sim_time = sim_time # int, run this many of rounds (of single_test)
292
- self.boundary = boundary # bool, the N x M space have boundary or not (i.e., zero-flux (True) or periodical (False))
293
- self.I = np.array(I) # N x M x 2 np.array, initial population. Two init-popu for every patch (U and V)
294
- self.X = np.array(X) # N x M x 4 np.array, matrices. The '4' comes from 2x2 matrix flattened to 1D
295
- self.P = np.array(P) # N x M x 6 np.array, 'patch variables', i.e., mu1&2, w1&2, kappa1&2
296
- 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'
302
-
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
305
-
306
-
307
- def init_storage(self):
308
- # 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)
319
-
320
-
321
- def check_valid_input(self, N, M, maxtime, record_itv, sim_time, boundary, I, X, P, print_pct):
322
- # check whether the inputs are valid
323
- # seed, UV_dtype, pi_dtype is handled by numpy
324
-
325
- if (N < 1) or (M < 1):
326
- raise ValueError('N < 1 or M < 1')
327
- if (N == 1) and (M == 1):
328
- raise ValueError('Model fails for 1x1 space')
329
- if (M == 1):
330
- raise ValueError('Please set N = 1 for 1D space.')
331
- if maxtime <= 0:
332
- raise ValueError('Please set a positive number for maxtime')
333
- if record_itv <= 0:
334
- raise ValueError('Please set a positive number for record_itv')
335
- if sim_time <= 0:
336
- raise ValueError('Please set a positive number for sim_time')
337
- if type(boundary) != bool:
338
- raise TypeError('boundary not a bool. Please use True for zero-flux (with boundary) or False for periodical (no boundary)')
339
-
340
- if (type(I) != list) and (type(I) != np.ndarray):
341
- raise TypeError('Please set I as a list or np.ndarray')
342
- if np.array(I).shape != (N, M, 2):
343
- 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')
344
-
345
- if (type(X) != list) and (type(X) != np.ndarray):
346
- raise TypeError('Please set X as a list or np.ndarray')
347
- if np.array(X).shape != (N, M, 4):
348
- raise ValueError('Please set X as a N x M x 4 shape list or array. 4 is for the flattened 2x2 payoff matrix')
349
-
350
- if (type(P) != list) and (type(P) != np.ndarray):
351
- raise TypeError('Please set P as a list or np.ndarray')
352
- if np.array(P).shape != (N, M, 6):
353
- 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')
354
-
355
- if print_pct <= 0:
356
- raise ValueError('Please use an int > 0 for print_pct or None for not printing progress.')
357
-
358
-
359
- def check_valid_data(self, data_empty, max_record, compress_itv):
360
- # check whether a set of data is valid, used when reading a saved model
361
- if type(data_empty) != bool:
362
- raise TypeError('data_empty not a bool')
363
-
364
- if type(max_record) != int:
365
- raise TypeError('max_record not an int')
366
- if max_record < 0:
367
- raise ValueError('max_record < 0')
368
-
369
- if type(compress_itv) != int:
370
- raise TypeError('compress_itv not an int')
371
- if compress_itv < 0:
372
- raise ValueError('compress_itv < 0')
373
-
374
-
375
- def __str__(self):
376
- # print this mod in a nice format
377
-
378
- self_str = ''
379
- self_str += 'N = ' + str(self.N) + '\n'
380
- self_str += 'M = ' + str(self.M) + '\n'
381
- self_str += 'maxtime = ' + str(self.maxtime) + '\n'
382
- self_str += 'record_itv = ' + str(self.record_itv) + '\n'
383
- self_str += 'sim_time = ' + str(self.sim_time) + '\n'
384
- self_str += 'boundary = ' + str(self.boundary) + '\n'
385
- self_str += 'print_pct = ' + str(self.print_pct) + '\n'
386
- 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'
389
- self_str += 'compress_itv = ' + str(self.compress_itv) + '\n'
390
- self_str += '\n'
391
-
392
- # check whether I, X, P all same (compare all patches to (0, 0))
393
- I_same = True
394
- X_same = True
395
- P_same = True
396
- for i in range(self.N):
397
- for j in range(self.M):
398
- for k in range(2):
399
- if self.I[i][j][k] != self.I[0][0][k]:
400
- I_same = False
401
- for k in range(4):
402
- if self.X[i][j][k] != self.X[0][0][k]:
403
- X_same = False
404
- for k in range(6):
405
- if self.P[i][j][k] != self.P[0][0][k]:
406
- P_same = False
407
-
408
- if I_same:
409
- self_str += 'I all same: ' + str(self.I[0][0]) + '\n'
410
- else:
411
- self_str += 'I:\n'
412
- for i in range(self.N):
413
- for j in range(self.M):
414
- self_str += str(self.I[i][j]) + ' '
415
- self_str += '\n'
416
- self_str += '\n'
417
-
418
- if X_same:
419
- self_str += 'X all same: ' + str(self.X[0][0]) + '\n'
420
- else:
421
- self_str += 'X:\n'
422
- for i in range(self.N):
423
- for j in range(self.M):
424
- self_str += str(self.X[i][j]) + ' '
425
- self_str += '\n'
426
- self_str += '\n'
427
-
428
- if P_same:
429
- self_str += 'P all same: ' + str(self.P[0][0]) + '\n'
430
- else:
431
- self_str += 'P:\n'
432
- for i in range(self.N):
433
- for j in range(self.M):
434
- self_str += str(self.P[i][j]) + ' '
435
- self_str += '\n'
436
-
437
- return self_str
438
-
439
-
440
- def copy(self, copy_data = True):
441
- # return deep copy of self
442
- # copy_data decides whether to copy data as well
443
- if type(copy_data) != bool:
444
- raise TypeError('Please give a bool as argument: whether to copy data or not')
445
-
446
- 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
- 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)
449
-
450
- if copy_data:
451
- # copy data as well
452
- sim2.set_data(self.data_empty, self.max_record, self.compress_itv, self.U, self.V, self.Upi, self.Vpi)
453
-
454
- return sim2
455
-
456
-
457
- def calculate_ave(self):
458
- # get the average value over sim_time many models
459
- if self.sim_time != 1:
460
- for i in range(self.N):
461
- for j in range(self.M):
462
- for t in range(self.max_record):
463
- self.U[i][j][t] /= self.sim_time
464
- self.V[i][j][t] /= self.sim_time
465
- self.Upi[i][j][t] /= self.sim_time
466
- self.Vpi[i][j][t] /= self.sim_time
467
-
468
-
469
- def change_maxtime(self, maxtime):
470
- # change maxtime
471
- if (type(maxtime) != float) and (type(maxtime) != int):
472
- raise TypeError('Please pass in a float or int as the new maxtime.')
473
- if maxtime <= 0:
474
- raise ValueError('Please use a positive maxtime.')
475
- self.maxtime = maxtime
476
- self.init_storage()
477
-
478
-
479
- def set_seed(self, seed):
480
- # set seed
481
- self.seed = seed
482
-
483
-
484
- def clear_data(self):
485
- # clear all data stored, set all to 0
486
- self.init_storage()
487
-
488
-
489
- def set_data(self, data_empty, max_record, compress_itv, U, V, Upi, V_pi):
490
- # set data to the given data values
491
- # copies are made
492
- self.check_valid_data(data_empty, max_record, compress_itv)
493
-
494
- self.data_empty = data_empty
495
- self.max_record = max_record
496
- self.compress_itv = compress_itv
497
- self.U = np.copy(U)
498
- self.V = np.copy(V)
499
- self.Upi = np.copy(Upi)
500
- self.Vpi = np.copy(V_pi)
501
-
502
-
503
- def compress_data(self, compress_itv = 5):
504
- # compress data by only storing average values
505
-
506
- if type(compress_itv) != int:
507
- raise TypeError('Please use an int as compress_itv')
508
- if compress_itv < 1:
509
- raise ValueError('Please use record_itv >= 1')
510
- if compress_itv == 1:
511
- return
512
-
513
- self.compress_itv *= compress_itv # may be reduced over and over again
514
- self.max_record = int(self.max_record / compress_itv) # number of data points after reducing
515
-
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)
520
-
521
- for i in range(self.N):
522
- for j in range(self.M):
523
- for k in range(self.max_record):
524
- lower = k * compress_itv # upper and lower bound of current record_itv
525
- upper = lower + compress_itv
526
- U_reduced[i][j][k] = np.mean(self.U[i, j, lower : upper])
527
- V_reduced[i][j][k] = np.mean(self.V[i, j, lower : upper])
528
- 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])
530
-
531
- self.U = U_reduced
532
- self.V = V_reduced
533
- self.Upi = Upi_reduced
534
- self.Vpi = V_pi_reduced
535
-
536
221
 
537
222
 
538
223
 
@@ -824,8 +509,8 @@ def single_init(mod, rng):
824
509
  #### Initialize Data Storage ####
825
510
 
826
511
  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.
512
+ patch_rates = np.zeros((mod.N, mod.M), dtype = np.float64) # every patch's sum-of-12-srates
513
+ sum_rates_by_row = np.zeros((mod.N), dtype = np.float64) # every row's sum-of-patch, i.e., sum of 12 * M rates in every row.
829
514
  sum_rates = 0 # sum of all N x M x 12 rates
830
515
 
831
516
  signal = None
@@ -1058,7 +743,7 @@ def single_test(mod, front_info, end_info, update_sum_frequency, rng):
1058
743
 
1059
744
 
1060
745
 
1061
- def run(mod, predict_runtime = False, message = ''):
746
+ def run_py(mod, predict_runtime = False, message = ''):
1062
747
  '''
1063
748
  Main function. Recursively calls single_test to run many models and then takes the average.
1064
749
 
@@ -1120,36 +805,4 @@ def run(mod, predict_runtime = False, message = ''):
1120
805
 
1121
806
 
1122
807
 
1123
- def demo_model():
1124
- '''
1125
- Returns a demo model.model object
1126
- '''
1127
-
1128
- N = 10 # Number of rows
1129
- M = 10 # Number of cols
1130
- maxtime = 300 # how long you want the model to run
1131
- record_itv = 0.1 # how often to record data.
1132
- sim_time = 1 # repeat model to reduce randomness
1133
- boundary = True # boundary condition.
1134
-
1135
- # initial population for the N x M patches.
1136
- I = [[[44, 22] for _ in range(M)] for _ in range(N)]
1137
-
1138
- # flattened payoff matrices, total resource is 0.4, cost of fighting is 0.1
1139
- X = [[[-0.1, 0.4, 0, 0.2] for _ in range(M)] for _ in range(N)]
1140
-
1141
- # patch variables
1142
- P = [[[0.5, 0.5, 200, 200, 0.001, 0.001] for _ in range(M)] for _ in range(N)]
1143
-
1144
- print_pct = 5 # print progress
1145
- seed = 36 # seed for random number generation
1146
- UV_dtype = 'float32' # data type for population
1147
- pi_dyna = 'float64' # data type for payoff
1148
-
1149
- # create a model object
1150
- 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)
1152
-
1153
- return mod
1154
-
1155
808
 
@@ -88,7 +88,7 @@ def test_var1(mod, var, values, dirs, compress_itv = None, predict_runtime = Fal
88
88
  sim2.P[i][j][PATCH_VAR_DICT[var]] = values[k]
89
89
 
90
90
  try:
91
- simulation.run(sim2, predict_runtime, message = current_var_str + ', ')
91
+ simulation.run(sim2, message = current_var_str + ', ')
92
92
  if compress_itv != None:
93
93
  sim2.compress_data(compress_itv)
94
94
  data_t.save_data(sim2, var_dirs[k], print_msg = False)
@@ -135,7 +135,7 @@ def test_var2(mod, var1, var2, values1, values2, dirs, compress_itv = None, pred
135
135
  sim2.P[i][j][PATCH_VAR_DICT[var2]] = values2[k2]
136
136
 
137
137
  try:
138
- simulation.run(sim2, predict_runtime, message = current_var_str + ', ')
138
+ simulation.run(sim2, message = current_var_str + ', ')
139
139
  if compress_itv != None:
140
140
  sim2.compress_data(compress_itv)
141
141
  data_t.save_data(sim2, var_dirs[k1][k2], print_msg = False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piegy
3
- Version: 1.1.6
3
+ Version: 2.0.1
4
4
  Summary: Payoff-Driven Stochastic Spatial Model for Evolutionary Game Theory
5
5
  Author-email: Chenning Xu <cxu7@caltech.edu>
6
6
  License: BSD 3-Clause License
@@ -8,6 +8,7 @@ src/piegy/analysis.py
8
8
  src/piegy/data_tools.py
9
9
  src/piegy/figures.py
10
10
  src/piegy/simulation.py
11
+ src/piegy/simulation_py.py
11
12
  src/piegy/test_var.py
12
13
  src/piegy/videos.py
13
14
  src/piegy.egg-info/PKG-INFO
@@ -15,6 +16,7 @@ src/piegy.egg-info/SOURCES.txt
15
16
  src/piegy.egg-info/dependency_links.txt
16
17
  src/piegy.egg-info/requires.txt
17
18
  src/piegy.egg-info/top_level.txt
19
+ src/piegy/C_core/piegyc.so
18
20
  src/piegy/tools/__init__.py
19
21
  src/piegy/tools/figure_tools.py
20
22
  src/piegy/tools/file_tools.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes