piegy 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piegy/__init__.py +96 -0
- piegy/__version__.py +17 -0
- piegy/analysis.py +222 -0
- piegy/data_tools.py +129 -0
- piegy/figures.py +503 -0
- piegy/model.py +1168 -0
- piegy/test_var.py +525 -0
- piegy/tools/__init__.py +15 -0
- piegy/tools/figure_tools.py +206 -0
- piegy/tools/file_tools.py +29 -0
- piegy/videos.py +322 -0
- piegy-1.0.0.dist-info/METADATA +104 -0
- piegy-1.0.0.dist-info/RECORD +16 -0
- piegy-1.0.0.dist-info/WHEEL +5 -0
- piegy-1.0.0.dist-info/licenses/LICENSE.txt +28 -0
- piegy-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
'''
|
2
|
+
Helper functions for making figures.
|
3
|
+
|
4
|
+
Functions:
|
5
|
+
- heatmap: Make a heatmap based on input data. Sets title, text ... as well
|
6
|
+
- bar: Make a barplot. Sets title, text ... as well.
|
7
|
+
- scatter: Make a scatter plot. Sets title, text ... as well.
|
8
|
+
- gen_title: Generates a title when the plot is about an interval of time.
|
9
|
+
- gen_text: Generates a text about standard deviation info.
|
10
|
+
- scale_interval: scale interval if sim's data was already reduced.
|
11
|
+
- ave_interval: Calculates average value of data over a time interval.
|
12
|
+
- ave_interval_1D: Return in a 1D format.
|
13
|
+
'''
|
14
|
+
|
15
|
+
|
16
|
+
import matplotlib.pyplot as plt
|
17
|
+
import numpy as np
|
18
|
+
import seaborn as sns
|
19
|
+
|
20
|
+
# move ax a bit left if add text
|
21
|
+
# default value is [0.125, 0.11, 0.9, 0.88]
|
22
|
+
|
23
|
+
|
24
|
+
def heatmap(data, cmap = "Greens", annot = False, fmt = '.3g', title = None, text = None):
|
25
|
+
'''
|
26
|
+
Helper function for making heatmaps.
|
27
|
+
|
28
|
+
Inputs:
|
29
|
+
data: 1D data for which you want to make a heatmap.
|
30
|
+
cmap: Color of heatmap. Uses matplotlib color maps
|
31
|
+
annot: Whether to show numbers of every block.
|
32
|
+
fmt: Number format for annotations. How many digits you want to keep.
|
33
|
+
title: The title you want to add. None means no title.
|
34
|
+
text: Adds some text in a text block at the top-right corner.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
fig: Seaborn heatmap.
|
38
|
+
'''
|
39
|
+
|
40
|
+
fig, ax = plt.subplots()
|
41
|
+
if text != None:
|
42
|
+
ax.text(0.63, 0.9, text, size = 10, linespacing = 1.5, transform = plt.gcf().transFigure)
|
43
|
+
|
44
|
+
ax = sns.heatmap(data, cmap = cmap, annot = annot, fmt = fmt)
|
45
|
+
ax.title.set_text(title)
|
46
|
+
|
47
|
+
return fig
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
def bar(data, color = "green", xlabel = None, ylabel = None, title = None, text = None):
|
52
|
+
'''
|
53
|
+
Helper Function for making barplots.
|
54
|
+
|
55
|
+
Inputs:
|
56
|
+
data: 2D data to make barplot.
|
57
|
+
color: Uses Matplotlib colors.
|
58
|
+
xlabel, y_label:
|
59
|
+
Label for axes.
|
60
|
+
title: Title for the barplot.
|
61
|
+
text: Adds some text in a text block at the top-right corner.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
fig: A Matplotlib barplot.
|
65
|
+
'''
|
66
|
+
|
67
|
+
N = np.array(data).size
|
68
|
+
xaxis = np.array([i for i in range(N)])
|
69
|
+
|
70
|
+
# make figure larger if has more data points
|
71
|
+
fig, ax = plt.subplots()
|
72
|
+
if N > 60:
|
73
|
+
fig.set_size_inches(min(N * 0.12, 9.6), 4.8)
|
74
|
+
|
75
|
+
if text != None:
|
76
|
+
ax.text(0.63, 0.9, text, size = 10, linespacing = 1.5, transform = plt.gcf().transFigure)
|
77
|
+
|
78
|
+
ax.bar(x = xaxis, height = data, color = color)
|
79
|
+
ax.set_xlabel(xlabel)
|
80
|
+
ax.set_ylabel(ylabel)
|
81
|
+
ax.title.set_text(title)
|
82
|
+
|
83
|
+
return fig
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
def scatter(X, Y, color = "orange", alpha = 0.25, xlabel = "x", ylabel = "y", title = None):
|
88
|
+
'''
|
89
|
+
Helper function for makeing scatter plots.
|
90
|
+
|
91
|
+
Inputs:
|
92
|
+
X: x-coordinates of points.
|
93
|
+
Y: y-coordinates of points.
|
94
|
+
Note color is Matplotlib colors.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
fig: A Matplotlib scatter plot.
|
98
|
+
'''
|
99
|
+
|
100
|
+
fig, ax = plt.subplots()
|
101
|
+
ax.scatter(X, Y, color = color, alpha = alpha)
|
102
|
+
|
103
|
+
ax.set_xlabel(xlabel)
|
104
|
+
ax.set_ylabel(ylabel)
|
105
|
+
ax.title.set_text(title)
|
106
|
+
|
107
|
+
return fig
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
def gen_title(title, start, end):
|
112
|
+
'''
|
113
|
+
Generate a title for plot when it's about an interval of time.
|
114
|
+
'''
|
115
|
+
title += ", " + str(round(start * 100, 1)) + " ~ " + str(round(end * 100, 1)) + "%"
|
116
|
+
return title
|
117
|
+
|
118
|
+
|
119
|
+
|
120
|
+
def gen_text(ave, std):
|
121
|
+
'''
|
122
|
+
Generate text about standard deviation info.
|
123
|
+
'''
|
124
|
+
text = "ave: " + str(round(ave, 3)) + ", std: " + str(round(std, 3))
|
125
|
+
return text
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
def ave_interval(data, start_index, end_index):
|
130
|
+
'''
|
131
|
+
Calculate average value of data over an interval. Return a 2D np.array
|
132
|
+
Assume data is 3D with shape N x M x K, then takes average on the 3rd axis.
|
133
|
+
|
134
|
+
Input:
|
135
|
+
data: 3D np.array or list. Will take average on the 3rd axis.
|
136
|
+
start_index, end_index:
|
137
|
+
over what interval to take average.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
data_ave: 2D np.array with shape N x M, contains average value of data.
|
141
|
+
'''
|
142
|
+
|
143
|
+
N = len(data)
|
144
|
+
M = len(data[0])
|
145
|
+
|
146
|
+
# plot a particular record
|
147
|
+
if start_index == end_index:
|
148
|
+
start_index = end_index - 1
|
149
|
+
|
150
|
+
data_ave = np.zeros((N, M))
|
151
|
+
|
152
|
+
for i in range(N):
|
153
|
+
for j in range(M):
|
154
|
+
for k in range(start_index, end_index):
|
155
|
+
data_ave[i][j] += data[i][j][k]
|
156
|
+
data_ave[i][j] /= (end_index - start_index)
|
157
|
+
|
158
|
+
return data_ave
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
def ave_interval_1D(data, start_index, end_index):
|
163
|
+
'''
|
164
|
+
Calculate average value of data over an interval. Return a 1D np.array.
|
165
|
+
Assume data is 3D and has shape (1 x M x K) or (N x 1 x K). Then implicitly 'compress' that 1 and takes average on the 3rd axis.
|
166
|
+
|
167
|
+
Input:
|
168
|
+
data: 3D np.array or list. One of its dimensions must have size 1. Will take average on the 3rd axis.
|
169
|
+
start_index, end_index:
|
170
|
+
over what interval to take average.
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
data_ave: 1D np.array with len N * M, contains average value of data.
|
174
|
+
'''
|
175
|
+
|
176
|
+
N = len(data)
|
177
|
+
M = len(data[0])
|
178
|
+
|
179
|
+
if start_index == end_index:
|
180
|
+
start_index = end_index - 1
|
181
|
+
|
182
|
+
data_ave = np.zeros(N * M)
|
183
|
+
|
184
|
+
for i in range(N):
|
185
|
+
for j in range(M):
|
186
|
+
for k in range(start_index, end_index):
|
187
|
+
data_ave[i * M + j] += data[i][j][k]
|
188
|
+
data_ave[i * M + j] /= (end_index - start_index)
|
189
|
+
|
190
|
+
return data_ave
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
def scale_interval(interval, compress_itv):
|
195
|
+
# scale interval if sim's data was already reduced.
|
196
|
+
if compress_itv < 1:
|
197
|
+
raise ValueError('figures.scale_interval has compress_itv < 1:', compress_itv)
|
198
|
+
|
199
|
+
interval = int(interval / compress_itv)
|
200
|
+
if interval == 0:
|
201
|
+
print('Warning: data already smoothed by an interval: sim.compress_itv =', compress_itv, 'which is coarser than your', interval)
|
202
|
+
interval = 1
|
203
|
+
|
204
|
+
return interval
|
205
|
+
|
206
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
'''
|
2
|
+
File-processing tools.
|
3
|
+
|
4
|
+
Functions:
|
5
|
+
- del_dirs: Delete everything in a directory, as well as the directory itself.
|
6
|
+
'''
|
7
|
+
|
8
|
+
import os
|
9
|
+
|
10
|
+
|
11
|
+
def del_dirs(dirs):
|
12
|
+
# Delete everything in a directory.
|
13
|
+
|
14
|
+
subdirs_list = []
|
15
|
+
|
16
|
+
for subdirs, dirs_, files in os.walk(dirs):
|
17
|
+
if subdirs not in subdirs_list:
|
18
|
+
subdirs_list.append(subdirs)
|
19
|
+
|
20
|
+
for file in files:
|
21
|
+
path = os.path.join(subdirs, file)
|
22
|
+
if os.path.isfile(path):
|
23
|
+
os.remove(path)
|
24
|
+
|
25
|
+
len_s = len(subdirs_list)
|
26
|
+
|
27
|
+
for i in range(len_s):
|
28
|
+
os.rmdir(subdirs_list[len_s - i - 1])
|
29
|
+
|
piegy/videos.py
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
'''
|
2
|
+
Make mp4 videos for simulation results.
|
3
|
+
|
4
|
+
Videos are made by:
|
5
|
+
make every frame by figures.py functions, then put frames together into a video.
|
6
|
+
|
7
|
+
Public Function:
|
8
|
+
- make_video: make video based simulation results.
|
9
|
+
|
10
|
+
Private Functions
|
11
|
+
- get_max_lim: Get the max lim (interval) over many lims, and then expand it a bit for better accommodation.
|
12
|
+
Essentially takes union of those intervals.
|
13
|
+
- video_lim: Find a large enough xlim and ylim for video.
|
14
|
+
- sort_frames: Put frames in order.
|
15
|
+
other not documented here.
|
16
|
+
|
17
|
+
'''
|
18
|
+
|
19
|
+
|
20
|
+
from . import figures
|
21
|
+
from .tools import file_tools as file_t
|
22
|
+
|
23
|
+
|
24
|
+
import matplotlib.pyplot as plt
|
25
|
+
import numpy as np
|
26
|
+
import os
|
27
|
+
import imageio.v2 as imageio
|
28
|
+
import re
|
29
|
+
from moviepy import VideoFileClip
|
30
|
+
|
31
|
+
|
32
|
+
# a list of supported figures
|
33
|
+
SUPPORTED_FIGURES = ['UV_heatmap', 'pi_heatmap', 'UV_bar', 'pi_bar', 'UV_hist', 'pi_hist', 'UV_pi']
|
34
|
+
|
35
|
+
|
36
|
+
# map function name to functios in figures.py
|
37
|
+
# functions not in this dictionary is not supported for videos.
|
38
|
+
FUNC_DICT = {'UV_heatmap': figures.UV_heatmap, 'UV_bar': figures.UV_bar, 'UV_hist': figures.UV_hist,
|
39
|
+
'pi_heatmap': figures.pi_heatmap, 'pi_bar': figures.pi_bar, 'pi_hist': figures.pi_hist, 'UV_pi': figures.UV_pi}
|
40
|
+
|
41
|
+
|
42
|
+
# Map some color maps to regular colors, used to change colors when an invalid color name is given
|
43
|
+
SNS_PLT_COLOR_DICT = {'Greens': 'green', 'Purples': 'purple', 'BuPu': 'violet', 'YlGn': 'yellowgreen'}
|
44
|
+
# Map regular colors to color maps
|
45
|
+
PLT_SNS_COLOR_DICT = {'green': 'Greens', 'purple': 'Purples', 'violet': 'BuPu', 'yellowgreen': 'YlGn'}
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
def convert_color(func_name, U_color, V_color):
|
51
|
+
'''
|
52
|
+
Converts some invalid colors.
|
53
|
+
If making heatmap videos but gave single colors, map to color maps.
|
54
|
+
If making barplot or histogram videos but gave single colors, map to Matplotlib
|
55
|
+
'''
|
56
|
+
|
57
|
+
if 'heatmap' in func_name:
|
58
|
+
# if making heatmaps but give regular colors
|
59
|
+
if U_color in PLT_SNS_COLOR_DICT.keys():
|
60
|
+
print('Making heatmaps, changed \'' + U_color + '\' to \'' + PLT_SNS_COLOR_DICT[U_color] + '\'')
|
61
|
+
U_color = PLT_SNS_COLOR_DICT[U_color]
|
62
|
+
if V_color in PLT_SNS_COLOR_DICT.keys():
|
63
|
+
print('Making heatmaps, changed \'' + V_color + '\' to \'' + PLT_SNS_COLOR_DICT[V_color] + '\'')
|
64
|
+
V_color = PLT_SNS_COLOR_DICT[V_color]
|
65
|
+
|
66
|
+
return U_color, V_color
|
67
|
+
|
68
|
+
elif 'heatmap' not in func_name:
|
69
|
+
# if making barplots or histogram
|
70
|
+
if U_color in SNS_PLT_COLOR_DICT.keys():
|
71
|
+
print('Not making heatmaps, changed \'' + U_color + '\' to \'' + SNS_PLT_COLOR_DICT[U_color] + '\'')
|
72
|
+
U_color = SNS_PLT_COLOR_DICT[U_color]
|
73
|
+
if V_color in SNS_PLT_COLOR_DICT.keys():
|
74
|
+
print('Not making heatmaps, changed \'' + V_color + '\' to \'' + SNS_PLT_COLOR_DICT[V_color] + '\'')
|
75
|
+
V_color = SNS_PLT_COLOR_DICT[V_color]
|
76
|
+
|
77
|
+
return U_color, V_color
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
def get_max_lim(lims):
|
82
|
+
'''
|
83
|
+
Get the max lim over many lims, i.e., the lowest lower bound and highest upper bound.
|
84
|
+
And then expand it a bit for better accommodation.
|
85
|
+
|
86
|
+
Input:
|
87
|
+
lim: list or np.array, has form [lim1, lim2, ...]
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
A max lim which contains all lims.
|
91
|
+
'''
|
92
|
+
|
93
|
+
lims = np.array(lims)
|
94
|
+
|
95
|
+
lim_min = np.min(lims[:, 0]) # min of min
|
96
|
+
lim_max = np.max(lims[:, 1]) # max of max
|
97
|
+
r = lim_max - lim_min
|
98
|
+
|
99
|
+
if lim_min != 0:
|
100
|
+
# negative values are reached
|
101
|
+
# extend both upper bound and lower bound
|
102
|
+
return [lim_min - r * 0.05, lim_max + r * 0.05]
|
103
|
+
else:
|
104
|
+
# only extend upper bound
|
105
|
+
return [0, lim_max + r * 0.05]
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
|
110
|
+
def frame_lim(sim, func, frames):
|
111
|
+
'''
|
112
|
+
Find a large enough xlim and ylim for frames, if not heatmaps.
|
113
|
+
|
114
|
+
Inputs:
|
115
|
+
sim: A stochastic_model.simulation object, the simulation results.
|
116
|
+
frames: How many frame to make for the video.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
xlim and ylim for U and V, 4 in total.
|
120
|
+
'''
|
121
|
+
|
122
|
+
# take 10 samples and store their lims in list
|
123
|
+
U_xlist = []
|
124
|
+
U_ylist = []
|
125
|
+
V_xlist = []
|
126
|
+
V_ylist = []
|
127
|
+
|
128
|
+
for i in range(10):
|
129
|
+
U_fig, V_fig = func(sim, start = i / 10, end = (i / 10 + 1 / frames))
|
130
|
+
|
131
|
+
U_xlist.append(U_fig.get_axes()[0].get_xlim())
|
132
|
+
U_ylist.append(U_fig.get_axes()[0].get_ylim())
|
133
|
+
V_xlist.append(V_fig.get_axes()[0].get_xlim())
|
134
|
+
V_ylist.append(V_fig.get_axes()[0].get_ylim())
|
135
|
+
|
136
|
+
plt.close(U_fig)
|
137
|
+
plt.close(V_fig)
|
138
|
+
|
139
|
+
# get the largest 'range' based on the lists
|
140
|
+
U_xlim = get_max_lim(U_xlist)
|
141
|
+
U_ylim = get_max_lim(U_ylist)
|
142
|
+
V_xlim = get_max_lim(V_xlist)
|
143
|
+
V_ylim = get_max_lim(V_ylist)
|
144
|
+
|
145
|
+
return U_xlim, U_ylim, V_xlim, V_ylim
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
def frame_heatmap_lim(sim, func, frames):
|
151
|
+
'''
|
152
|
+
Find a large enough color bar lim for frames, if heatmaps.
|
153
|
+
|
154
|
+
Inputs:
|
155
|
+
sim: A stochastic_model.simulation object, the simulation results.
|
156
|
+
frames: How many frame to make for the video.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
clim for U and V
|
160
|
+
'''
|
161
|
+
|
162
|
+
U_list = []
|
163
|
+
V_list = []
|
164
|
+
|
165
|
+
for i in range(10):
|
166
|
+
U_fig, V_fig = func(sim, start = i / 10, end = (i / 10 + 1 / frames))
|
167
|
+
|
168
|
+
U_ax = U_fig.get_axes()[0]
|
169
|
+
U_list.append(U_ax.collections[0].get_clim())
|
170
|
+
V_ax = V_fig.get_axes()[0]
|
171
|
+
V_list.append(V_ax.collections[0].get_clim())
|
172
|
+
|
173
|
+
plt.close(U_fig)
|
174
|
+
plt.close(V_fig)
|
175
|
+
|
176
|
+
U_clim = get_max_lim(U_list)
|
177
|
+
V_clim = get_max_lim(V_list)
|
178
|
+
|
179
|
+
return U_clim, V_clim
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
def sort_frames(images):
|
184
|
+
'''
|
185
|
+
Put frames in order.
|
186
|
+
|
187
|
+
Inputs:
|
188
|
+
images: A list of dirs (frame names)
|
189
|
+
'''
|
190
|
+
numeric_part, non_numeric_part = re.match(r'(\d+) (\D+)', images).groups()
|
191
|
+
return (int(numeric_part), non_numeric_part)
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
def make_mp4(dirs, frame_dirs, duration, video_name):
|
196
|
+
'''
|
197
|
+
Convert frames into a mp4 video.
|
198
|
+
|
199
|
+
Inputs:
|
200
|
+
dirs: where to store the video.
|
201
|
+
frame_dirs: where to find frames.
|
202
|
+
duration: how long the video should be.
|
203
|
+
video_name: name of the video.
|
204
|
+
'''
|
205
|
+
|
206
|
+
# png to gif
|
207
|
+
images = [img for img in os.listdir(frame_dirs) if img.endswith('.png')]
|
208
|
+
images.sort(key = sort_frames)
|
209
|
+
|
210
|
+
image_list = []
|
211
|
+
for img in images:
|
212
|
+
img_path = os.path.join(frame_dirs, img)
|
213
|
+
image_list.append(imageio.imread(img_path))
|
214
|
+
gif_dirs = dirs + '/temp.gif'
|
215
|
+
imageio.mimsave(gif_dirs, image_list, format = 'gif', duration = duration)
|
216
|
+
|
217
|
+
# gif to mp4
|
218
|
+
clip = VideoFileClip(gif_dirs)
|
219
|
+
clip.write_videofile(video_name, logger = None)
|
220
|
+
# delete gif
|
221
|
+
os.remove(gif_dirs)
|
222
|
+
|
223
|
+
|
224
|
+
|
225
|
+
def make_video(sim, func_name = 'UV_heatmap', frames = 100, speed = 1.25, dpi = 120, U_color = 'Greens', V_color = 'Purples', annot = False, fmt = '.3g', del_frames = False, dirs = 'videos'):
|
226
|
+
'''
|
227
|
+
Make a mp4 video based on simulation results.
|
228
|
+
|
229
|
+
Inputs:
|
230
|
+
- sim: a stochastic_model.simulation object, the simulation results.
|
231
|
+
- func_name: what function to use to make the frames. Should be one of the functions in figures.py
|
232
|
+
- frames: how many frames to make. Use more frames for more smooth evolutions.
|
233
|
+
- speed: how long every frame should last. Use larger number for slower video.
|
234
|
+
- dpi: dpi of frames
|
235
|
+
- U_color: color for U's videos. Color maps or regular colors, based on what function you use.
|
236
|
+
- V_color: color for V's videos.
|
237
|
+
- annot: used by heatmaps. Whether to show numbers.
|
238
|
+
- fmt: number format
|
239
|
+
- del_frames: whether to delete frames after making video.
|
240
|
+
- dirs: where to store the frames and videos.
|
241
|
+
'''
|
242
|
+
|
243
|
+
if func_name not in FUNC_DICT.keys():
|
244
|
+
raise ValueError(func_name + ' not supported for videos.')
|
245
|
+
func = FUNC_DICT[func_name]
|
246
|
+
|
247
|
+
# convert color if invalid colors are given
|
248
|
+
U_color, V_color = convert_color(func_name, U_color, V_color)
|
249
|
+
|
250
|
+
# print progress
|
251
|
+
one_progress = frames / 100
|
252
|
+
current_progress = one_progress
|
253
|
+
|
254
|
+
if 'heatmap' in func_name:
|
255
|
+
# make sure a fixed color bar for all frames
|
256
|
+
U_clim, V_clim = frame_heatmap_lim(sim, func, frames)
|
257
|
+
else:
|
258
|
+
# make sure y axis not changing if not making heatmaps
|
259
|
+
U_xlim, U_ylim, V_xlim, V_ylim = frame_lim(sim, func, frames)
|
260
|
+
|
261
|
+
|
262
|
+
U_frame_dirs = dirs + '/U-' + func_name
|
263
|
+
V_frame_dirs = dirs + '/V-' + func_name
|
264
|
+
|
265
|
+
if os.path.exists(U_frame_dirs):
|
266
|
+
file_t.del_dirs(U_frame_dirs)
|
267
|
+
os.makedirs(U_frame_dirs)
|
268
|
+
if os.path.exists(V_frame_dirs):
|
269
|
+
file_t.del_dirs(V_frame_dirs)
|
270
|
+
os.makedirs(V_frame_dirs)
|
271
|
+
|
272
|
+
|
273
|
+
#### for loop ####
|
274
|
+
|
275
|
+
for i in range(frames):
|
276
|
+
if i > current_progress:
|
277
|
+
print('making frames', round(i / frames * 100), '%', end = '\r')
|
278
|
+
current_progress += one_progress
|
279
|
+
|
280
|
+
if 'heatmap' in func_name:
|
281
|
+
U_fig, V_fig = func(sim, U_color, V_color, start = i / frames, end = (i + 1) / frames, annot = annot, fmt = fmt)
|
282
|
+
else:
|
283
|
+
U_fig, V_fig = func(sim, U_color, V_color, start = i / frames, end = (i + 1) / frames)
|
284
|
+
U_ax = U_fig.get_axes()[0]
|
285
|
+
V_ax = V_fig.get_axes()[0]
|
286
|
+
|
287
|
+
if 'heatmap' in func_name:
|
288
|
+
U_ax.collections[0].set_clim(U_clim)
|
289
|
+
V_ax.collections[0].set_clim(V_clim)
|
290
|
+
else:
|
291
|
+
# make sure y axis not changing if not heatmap and not UV_pi
|
292
|
+
U_ax.set_ylim(U_ylim)
|
293
|
+
V_ax.set_ylim(V_ylim)
|
294
|
+
if ('hist' in func_name) or (func_name == 'UV_pi'):
|
295
|
+
# need to set xlim as well for UV_pi and histograms
|
296
|
+
U_ax.set_xlim(U_xlim)
|
297
|
+
V_ax.set_xlim(V_xlim)
|
298
|
+
|
299
|
+
U_fig.savefig(U_frame_dirs + '/' + str(i) + ' U' + '.png', dpi = dpi)
|
300
|
+
V_fig.savefig(V_frame_dirs + '/' + str(i) + ' V' + '.png', dpi = dpi)
|
301
|
+
|
302
|
+
plt.close(U_fig)
|
303
|
+
plt.close(V_fig)
|
304
|
+
|
305
|
+
#### for loop ends ####
|
306
|
+
|
307
|
+
# frames done
|
308
|
+
print('making mp4... ', end = '\r')
|
309
|
+
|
310
|
+
# make videos based on frames
|
311
|
+
make_mp4(dirs, U_frame_dirs, frames * speed, dirs + '/U-' + func_name + '.mp4')
|
312
|
+
make_mp4(dirs, V_frame_dirs, frames * speed, dirs + '/V-' + func_name + '.mp4')
|
313
|
+
|
314
|
+
if del_frames:
|
315
|
+
file_t.del_dirs(U_frame_dirs)
|
316
|
+
file_t.del_dirs(V_frame_dirs)
|
317
|
+
print('video saved: ' + dirs + ', frames deleted')
|
318
|
+
else:
|
319
|
+
print('video saved: ' + dirs + ' ')
|
320
|
+
|
321
|
+
|
322
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: piegy
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Payoff-Driven Stochastic Spatial Model for Evolutionary Game Theory
|
5
|
+
Author-email: Chenning Xu <cxu7@caltech.edu>
|
6
|
+
License: BSD 3-Clause License
|
7
|
+
|
8
|
+
Copyright (c) 2025, Chenning Xu
|
9
|
+
|
10
|
+
Redistribution and use in source and binary forms, with or without
|
11
|
+
modification, are permitted provided that the following conditions are met:
|
12
|
+
|
13
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
14
|
+
list of conditions and the following disclaimer.
|
15
|
+
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
18
|
+
and/or other materials provided with the distribution.
|
19
|
+
|
20
|
+
3. Neither the name of the copyright holder nor the names of its
|
21
|
+
contributors may be used to endorse or promote products derived from
|
22
|
+
this software without specific prior written permission.
|
23
|
+
|
24
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
25
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
26
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
27
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
28
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
29
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
30
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
31
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
32
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
33
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
|
35
|
+
Project-URL: Source, https://github.com/Chenning04/piegy.git
|
36
|
+
Project-URL: Documentation, https://piegy.readthedocs.io/en/
|
37
|
+
Keywords: Game Theory,Evolutionary Game Theory,Spatial Model,Stochastic Model,Payoff Driven
|
38
|
+
Classifier: Development Status :: 4 - Beta
|
39
|
+
Classifier: Intended Audience :: Developers
|
40
|
+
Classifier: Intended Audience :: Science/Research
|
41
|
+
Classifier: Intended Audience :: Education
|
42
|
+
Classifier: License :: OSI Approved :: BSD License
|
43
|
+
Classifier: Programming Language :: Python :: 3
|
44
|
+
Classifier: Programming Language :: Python :: 3.10
|
45
|
+
Classifier: Programming Language :: Python :: 3.11
|
46
|
+
Classifier: Operating System :: OS Independent
|
47
|
+
Requires-Python: >=3.6
|
48
|
+
Description-Content-Type: text/markdown
|
49
|
+
License-File: LICENSE.txt
|
50
|
+
Requires-Dist: numpy
|
51
|
+
Requires-Dist: matplotlib
|
52
|
+
Requires-Dist: imageio>=2.37.0
|
53
|
+
Requires-Dist: moviepy>=2.1.1
|
54
|
+
Requires-Dist: seaborn>=0.13.2
|
55
|
+
Dynamic: license-file
|
56
|
+
|
57
|
+
# piegy
|
58
|
+
|
59
|
+
The package full name is: Payoff-Driven Stochastic Spatial Model for Evolutionary Game Theory
|
60
|
+
|
61
|
+
Provides a stochastic spatial model for simulating the interaction and evolution of two species in either 1D or 2D space, as well as analytic tools.
|
62
|
+
|
63
|
+
## Installation
|
64
|
+
|
65
|
+
```bash
|
66
|
+
pip install piegy
|
67
|
+
```
|
68
|
+
|
69
|
+
## Documentation and Source
|
70
|
+
|
71
|
+
See source code at: [GitHub-piegy repo](https://github.com/Chenning04/piegy.git).
|
72
|
+
The *piegy* documentation at: [piegy docs](https://piegy.readthedocs.io/en/).
|
73
|
+
|
74
|
+
## How the Model Works
|
75
|
+
|
76
|
+
Our model can be summarized as "classical game theory endowed with a spatial structure and payoff-driven migration rules". Consider two species, predators and preys (denote by *U* and *V*), in a rectangular region. We divide the region into N by M patches and simulate their interaction within a patch by classical game theory (i.e., payoff matrices and carrying capacity). Interactions across patches are simulated by payoff-driven migration rules. An individual migrates to a neighboring patch with probability weighted by payoff in the neighbors.
|
77
|
+
|
78
|
+
We use the Gillepie algorithm as the fundamental event-selection algorithm. At each time step, one event is selected and let happen; and step sizes are continuous, dependent on the current state in the space. Data are recorded every some specified time interval.
|
79
|
+
|
80
|
+
## Analytic Tools
|
81
|
+
|
82
|
+
The *piegy* package also provides a wide range of analytic and supportive tools alongside the main model, such as plotting, numerical tools, data saving & reading, etc. We also provide the *piegy.videos* module for more direct visualizations like how population distribution change over time.
|
83
|
+
|
84
|
+
## Examples
|
85
|
+
|
86
|
+
To get started, simply get our demo model and run simulation:
|
87
|
+
|
88
|
+
```python
|
89
|
+
from piegy import model, figures
|
90
|
+
|
91
|
+
sim = model.demo_model()
|
92
|
+
model.run(sim)
|
93
|
+
|
94
|
+
dynamics = figures.UV_dyna(sim)
|
95
|
+
U_hmap, V_hmap = figures.UV_heatmap(sim)
|
96
|
+
```
|
97
|
+
|
98
|
+
The figures reveal the population dynamics and steady state distribution.
|
99
|
+
|
100
|
+
|
101
|
+
## Acknowledgments
|
102
|
+
|
103
|
+
- Thanks Professor Daniel Cooney at University of Illinois Urbana-Champaign. This package is developed alongside a project with Prof. Cooney and received enormous help from him.
|
104
|
+
- Special thanks to the open-source community for making this package possible.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
piegy/__init__.py,sha256=Lrh6NegSvo6LOCXg_tBTu804eicnHpQY2zmE0FchjKE,3241
|
2
|
+
piegy/__version__.py,sha256=H58EUm6wBWYw2Q4gXk5PznfRrEry4c4zk7c--daUx_8,365
|
3
|
+
piegy/analysis.py,sha256=1cF06igQMGJGVjLiyhtgepGk8fYhzvL0orI48tOK1qY,8713
|
4
|
+
piegy/data_tools.py,sha256=dXESWglAXN4q79ZG8wtpZftJoiiaOJFU7NSaip1ZTq0,3518
|
5
|
+
piegy/figures.py,sha256=46Vg6AsWswF-juGbyndaRaChrdVuzPbpRQQd9grTWfk,17842
|
6
|
+
piegy/model.py,sha256=2Y49NDzdtog9effo9Lr_-psu5NMBF3iH9BeE8zhCHZ8,45700
|
7
|
+
piegy/test_var.py,sha256=fk_e0Hko6zNfsOx-SMnnEcsgeu6fx0rQcdfuXGceY7U,20892
|
8
|
+
piegy/videos.py,sha256=vW0F9WYDvPVJM0fHM_0Vi9Y8ZSVfJBcuCLJ2lLXeZ2U,10410
|
9
|
+
piegy/tools/__init__.py,sha256=eYOl_HJHDonYexfrmKh3koOlxvtSo46vH6jHvCEEB4k,300
|
10
|
+
piegy/tools/figure_tools.py,sha256=qhuMPZn2krEzjhjmAd6ac9QVBeCWUkSdCGOz7RASAhA,5931
|
11
|
+
piegy/tools/file_tools.py,sha256=ncxFWeHfIE-GYLQlOrahFlhBgqPyuY3R5_93fpQeCEs,630
|
12
|
+
piegy-1.0.0.dist-info/licenses/LICENSE.txt,sha256=wfzEht_CxOcfGGmg3f3at4mWJb9rTBjA51mXLl_3O3g,1498
|
13
|
+
piegy-1.0.0.dist-info/METADATA,sha256=hn5JGkA5jBMN2toZ0xPdJ-0munvYBE-BzcOMKWb-d-g,5035
|
14
|
+
piegy-1.0.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
15
|
+
piegy-1.0.0.dist-info/top_level.txt,sha256=k4QLYL8PqdqDuy95-4NZD_FVLqJDsmq67tpKkBn4vMw,6
|
16
|
+
piegy-1.0.0.dist-info/RECORD,,
|