-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvisualize.py
335 lines (291 loc) · 12.1 KB
/
visualize.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
#!/usr/bin/env python
###########################################################################
# Copyright (c) 2023 Yves Revaz & Patrick Hirling
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###########################################################################
import numpy as np
import argparse
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.pyplot as plt
import pickle as pkl
from tqdm import tqdm
from pNbody.libutil import set_ranges
from pNbody import Nbody
# We include CMasher colormaps if the package is installed (e.g. -cmap cmr.dusk)
try:
import cmasher as cmr
except ImportError:
pass
# Default colormap (custom)
clrs = np.array(
[[0, 0, 0],
[151, 0, 110],
[255, 142, 142],
[255, 220, 193],
[255, 255, 255]])
cmap1 = LinearSegmentedColormap.from_list("mycmap", clrs/255)
# ================
# Parse User input
# ================
parser = argparse.ArgumentParser(description="Extract data from SWIFT snapshot(s) and create film/image")
# Snapshot Parameters
parser.add_argument("files",nargs='+',help="Snapshot files to be imaged")
parser.add_argument("-shift",nargs='*', type=float, default=[0.0], help="Shift applied to particles, sets the origin of the image. Either a single value or 3D position (x y z)")
parser.add_argument("--openextract",action='store_true',help="Open a previous pickled extract file (in this case the first argument is interpreted as the file)")
parser.add_argument("--saveextract",action='store_true',help="Save the extracted data as a pickle file")
# Histogram Parameters
parser.add_argument("-nbins",type=int,default=400,help="Number of bins in each dimension (x,y)")
parser.add_argument("-lim",type=float,default=0.5,help="Physical limit (max position) of the histogram (-lim,lim,-lim,lim). Use -shift to set the origin")
parser.add_argument("-view",type=str,default='xy',help="Which plane to image")
parser.add_argument("-mode",type=str,default='m',help="Physical quantity to be imaged, default: 'm' (mass density)")
# Image Parameters (Color, normalization,etc) #'YlGnBu_r'
parser.add_argument("-cmap",type=str,default=None,help="Colormap (try YlGnBu_r, Magma !). Cmasher cmaps are available, e.g. cmr.dusk, cmr.ocean,...")
parser.add_argument("-cmin",type=float,default=None,help= "Minimum Physical value in the Histogram. This effectively sets the contrast of the images. Default: data minimum")
parser.add_argument("-cmax",type=float,default=None,help="Maximum physical value in the Histogram. Default: data maximum")
parser.add_argument("-interp",type=str,default='none',help="Interpolation used ('none','kaiser','gaussian',...). Default: none")
parser.add_argument("-frsp",type=float,default=0.0,help="Smoothing Factor (times rsp)")
parser.add_argument("-scale",type=str,default='log',help="Color scaling to use ('log' or 'lin'). Default is log")
# Observer Parameters
parser.add_argument("-x0",nargs=3,type=float, default = None,help="Position of Observer")
parser.add_argument("-xp",nargs=3,type=float, default = [0,0,0] ,help="Look-at point")
parser.add_argument("-alpha",type=float, default=0.0,help="Head Tilt angle")
# Figure Parameters
parser.add_argument("--notex",action='store_true')
parser.add_argument("-figheight",type=float,default=8,help="Height of figure in inches")
parser.add_argument("-figwidth",type=float,default=8,help="Width of figure in inches")
parser.add_argument("--savefilm",action='store_true',help="Save film/image")
parser.add_argument("--saveframes",action='store_true',help="Save as frames rather than film")
parser.add_argument("--nolabels",action='store_true',help="Do not display axes, ticks, labels")
parser.add_argument("--nounits",action='store_true',help="Do not display axis and time units")
# Encoding Parameters
parser.add_argument("-fps",type=float,default=24,help="Frames per second")
parser.add_argument("--headless",action='store_true',help="Flag to run on headless server, uses non-GUI Agg backend")
parser.add_argument("-bitrate",type=float,default=-1,help="Birate to use for ffmpeg")
parser.add_argument("-dpi",type=float,default=300,help="DPI if an image is saved")
# Corotation Parameters
parser.add_argument("-rcr",type=float,default=0.0,
help="Radius of the corotating origin, set to 0 for no corotation (default)")
parser.add_argument("-acr",type=float,default=0,
help="Initial angle of the corotating origin")
parser.add_argument("-vcr",type=float,default=200,
help="Circular velocity of the corotation")
parser.add_argument("--set_origin",action='store_true',
help="Set the origin of the figure to the corotating origin (default: no)")
args = parser.parse_args()
# =====================================
# Define Functions and useful variables
# =====================================
# Convenience
fnames = args.files
lim = args.lim
sh = args.shift
nbins = args.nbins
# TODO: Corotation. Should this be kept now that the view axis is possibly not normal to the xy plane ?
corot = args.rcr > 0
if corot:
omega = -args.vcr / args.rcr
x0, y0 = 0.0,0.0
if args.set_origin:
x0 = args.rcr*np.cos(args.acr)
y0 = args.rcr*np.sin(args.acr)
def corotate_pos(X,Y,t):
x = np.cos(omega*t)*X - np.sin(omega*t)*Y - x0
y = np.sin(omega*t)*X + np.cos(omega*t)*Y - y0
return x,y
# Convert 3D shift to array
if len(sh) == 1:
shift = np.array([sh[0],sh[0],sh[0]])
elif len(sh) == 3:
shift = np.array([sh[0],sh[1],sh[2]])
else:
raise ValueError("Shift can be scalar or 3-array")
# Setup Parameters for pNbody mapping
# https://obswww.unige.ch/~revaz/pNbody/rst/Display.html
# TODO: Make perspective view work
scale = args.scale
cd = None
params = {}
params['size'] = (lim,lim)
params['shape'] = (nbins,nbins)
params['rendering'] = 'map'
params['obs'] = None
params['view'] = args.view
params['persp'] = 'off'
# params['clip'] = (0,100000000)
# params['r_obs'] = 2
params['mode'] = args.mode
params['exec'] = None
params['macro'] = None
params['frsp'] = args.frsp
params['filter_name'] = None
# Setup Observer
if args.x0 is None:
params['x0'] = None
else:
params['x0'] = np.array(args.x0)
if args.xp is None:
params['xp'] = None
else:
params['xp'] = np.array(args.xp)
params['alpha'] = args.alpha
# Show Progress Bar only if multiple files
def progbar(iterable):
if len(iterable) == 1: return iterable
else: return tqdm(iterable)
# ============================================================
# Extract Data from the snapshot(s) or load a previous extract
# ============================================================
# If a list of snapshots is given, loop over files and extract image matrix
if not args.openextract:
nframes = len(fnames)
histdata = np.zeros((nframes,nbins,nbins))
times = np.empty(nframes)
print("Extracting Data...")
for i,fn in enumerate(progbar(fnames)):
nb = Nbody(fn,ftype='swift')
nb.translate(-shift)
hh = np.rot90( nb.CombiMap(params) ) # CombiMap renders a matrix of a physical quantity, e.g. mass density
histdata[i] = hh
times[i] = nb.time
print("done.")
# If requested, save extracted histogram data to pickle file
if args.saveextract:
print("Saving Extracted Data...")
infotext = "Time in Myrs, lim in kpc, 'hist' obj is a (nb_frames * nbins_x * nbins_y) array of int32"
output = {
'distunit': 'kpc',
'timeunit': 'Myr',
'lim' : lim,
'info' : infotext,
't' : times,
'hist' : histdata,
'rcr' : args.rcr,
'acr' : args.acr,
'vcr' : args.vcr,
'corot_origin' : args.set_origin
}
with open('extract.pkl','wb') as f:
pkl.dump(output,f)
# If extract is given, read image matrix and metadata
else:
if len(fnames) == 1:
print("Reading "+fnames[0]+"...")
with open(fnames[0],'rb') as f:
extract = pkl.load(f)
histdata = extract['hist']
nframes = histdata.shape[0]
lim = extract['lim']
times = extract['t']
else:
print(fnames)
raise RuntimeError("Can only open one extract")
# ====================
# Create Image or Film
# ====================
# Configure colour & normalization: by default, normalize to min,max value across ALL snapshots
if args.cmin is not None:
vmin = args.cmin
else:
vmin = np.amin(histdata)
if args.cmax is not None:
vmax = args.cmax
else:
vmax = np.amax(histdata)
if vmin >= vmax:
raise ValueError("vmin ({:n}) is >= vmax ({:n})".format(vmin,vmax))
# Configure Matplotlib
if args.headless:
import matplotlib
matplotlib.use('Agg')
if not args.notex: plt.rcParams.update({"text.usetex": True,'font.size':2*args.figheight,'font.family': 'serif'})
else: plt.rcParams.update({'font.size':15})
# Construct figure
fig, ax = plt.subplots(figsize=(args.figwidth,args.figheight))
# Units
if args.nounits:
time_unitstr = ""
space_unitstr = ""
else:
time_unitstr = " Myr"
space_unitstr = " [kpc]"
# Define imshow extent
ext = (-lim,lim,-lim,lim)
# Set axes limits and add labels if desired
ax.set_xlim(-lim,lim)
ax.set_ylim(-lim,lim)
ax.set_aspect('equal')
if args.nolabels:
ax.set_xticks([])
ax.set_yticks([])
else:
ax.set_xlabel(args.view[0] + space_unitstr)
ax.set_ylabel(args.view[1] + space_unitstr)
# Initialize Image
if args.cmap is None: cmap = cmap1
else: cmap = plt.get_cmap(args.cmap)
im = ax.imshow(np.zeros((nbins,nbins)),
interpolation = args.interp,
vmin=0,vmax=255,
extent = ext, cmap = cmap,origin='lower')
# Initialize Title
ttl = ax.text(0.01, 0.99,"$t={:.2f}$".format(times[0])+ time_unitstr,
horizontalalignment='left',
verticalalignment='top',
color = 'white',
transform = ax.transAxes)
# If a single file is passed, fill image and show
if nframes == 1:
hdat,mn_opt,mx_opt,cd_opt = set_ranges(histdata[0],scale=scale,cd=cd,mn=vmin,mx=vmax)
im.set_data(hdat)
# If multiple files are passed, make an animation
elif not args.saveframes:
# Closure function to use global image & title object with blitting
def prepare_anim(im,title):
def update(i):
# Title
title.set_text("$t={:.2f}$".format(times[i]))#+ time_unitstr)
# Update image
hdat,mn_opt,mx_opt,cd_opt = set_ranges(histdata[i],scale=scale,cd=cd,mn=vmin,mx=vmax)
im.set_data(hdat)
return im, title
return update
# Show encoding progress if --savefilm
if args.savefilm:
print("Encoding...")
itrtr = tqdm(range(nframes))
else:
itrtr = range(nframes)
# Animation
ani = FuncAnimation(fig, prepare_anim(im,ttl),frames = itrtr,
blit=True)#cache_frame_data=False)
# If multiple files are passed but a per-frame output is wished, fill image and save png for each frame
else:
for i,h in enumerate(histdata):
hdat,mn_opt,mx_opt,cd_opt = set_ranges(h,scale=scale,cd=cd,mn=vmin,mx=vmax)
im.set_data(hdat)
ttl.set_text("$t={:.2f}$".format(times[i]))#+ time_unitstr)
outfn = "frame_{:04n}.png".format(i)
fig.savefig(outfn,dpi=args.dpi,bbox_inches='tight')
# Show or save. TODO: distribute above for nicer structure
if not args.savefilm and not args.saveframes:
plt.show()
else:
if nframes == 1:
fig.savefig("image.png",dpi=args.dpi,bbox_inches='tight')
else:
ani.save('film.mp4',writer='ffmpeg',fps=args.fps,bitrate=args.bitrate)