disk_data_analysis - Analysis and Plotting Tools for HDF5 simulation data of meshless/moving-mesh hydrodynamical disks
A Python package!
You need to have git installed. In addition, you need the NumPy and SciPy Python packages.
git clone https://github.com/djmunoz/disk_data_analysis.git cd disk_data_analysis sudo python setup.py install
That is all!
First of all, load the package and check the functions/attributes in it:
import disk_data_analysis.circumbinary as dda
dir(dda)
The most basic function in that list is get_snapshot_data
,
which can be used to give
snap = dda.get_snapshot_data('./data/snap_',0,['POS','VEL','R'])
which will only contain the requested quantities POS
(positions)
and VEL
(velocities). You can double-check the attributes of
the snap
data structure by doing
dir(snap.gas)
You can see the distribution of particles/mesh generating points by plotting the positions
import matplotlib.pyplot as plt
x = snap.gas.POS[:,0]
y = snap.gas.POS[:,1]
box = snap.header.boxsize
plt.plot(x,y,'b.',ms=2.4)
plt.xlim(0.5 * box - 2.5, 0.5 * box + 2.5)
plt.ylim(0.5 * box - 2.5, 0.5 * box + 2.5)
plt.xlabel(r'$x$',size=18)
plt.ylabel(r'$y$',size=18)
plt.show()
You can compare the mesh-generating scatter plot above with the associated Voronoi grid. Fortunately, Python has built-in Voronoi routines in 2-D.
from scipy.spatial import Voronoi, voronoi_plot_2d
ind = snap.gas.R < 3.8
points = np.array([snap.gas.POS[ind,0],snap.gas.POS[ind,1]]).T
vor = Voronoi(points)
fig = plt.figure(figsize=(4.5,4.5))
voronoi_plot_2d(vor,show_points=False,show_vertices=False)
plt.xlim(0.5 * snap.header.boxsize - 2.5, 0.5 * snap.header.boxsize + 2.5)
plt.ylim(0.5 * snap.header.boxsize - 2.5, 0.5 * snap.header.boxsize + 2.5)
plt.xlabel(r'$x$',size=18)
plt.ylabel(r'$y$',size=18)
plt.axes().set_aspect('equal')
plt.show()
Often, SPH and moving-mesh simulations of disks will be compared to simulation results obtained with polar-grid codes. In particular, comparison of azimuthally-averaged quantities is common practice. While azimuthal averaging is trivial in polar grids, it requires some tinkering when computational cells (or particles) are not placed in regular intervals in radius. One way around this is to remap the fluid's primitive variables into a structured grid of points.
import numpy as np
# We need the density and positions
snap = dda.get_snapshot_data('./data/snap_',0,['POS','RHO'])
# get a sense of the dynamical range in radius in the simulation
Rmin, Rmax = 1.0, 80.0
NR, Nphi = 200, 400
grid = dda.grid_polar(NR = NR, Nphi = Nphi,Rmin=1.0,Rmax= 80.0,scale='log')
grid.X, grid.Y = grid.X + snap.header.boxsize * 0.5, grid.Y + snap.header.boxsize * 0.5
rho_interp = dda.disk_interpolate_primitive_quantities(snap,[grid.X,grid.Y],quantities=['RHO'])[0]
# And now we can plot the density field of this structured grid
fig = plt.figure(figsize=(5,4.5))
fig.subplots_adjust(top=0.97,right=0.95,left=0.1,bottom=0.12)
ax = fig.add_subplot(111)
ax.scatter(grid.X,grid.Y,c=rho_interp ,lw=0)
ax.axis([76,84,76,84])
ax.set_xlabel(r'$x$',size=18)
ax.set_ylabel(r'$y$',size=18)
ax.set_aspect(1.0)
plt.show()
An interpolated, structured grid can be used to map AREPO snapshots into data readable by other codes like FARGO and PLUTO. But you might find other used for the polar regridding, such as computing radial profiles for diverse quantities. In such case, since AREPO (and SPH) simulation will in general have unevenly populated resolution elements, you might want to have a "nested" polar grid such as:
grid_in = dda.grid_polar(NR = 80, Nphi = 600, Rmin = 1.0, Rmax= 4.0, scale='linear')
grid_in.X, grid_in.Y = grid_in.X + snap.header.boxsize * 0.5, grid_in.Y + snap.header.boxsize * 0.5
grid_out = dda.grid_polar(NR = 140, Nphi = 300, Rmin = 4.0, Rmax= 80.0, scale='log')
grid_out.X, grid_out.Y = grid_out.X + snap.header.boxsize * 0.5, grid_out.Y + snap.header.boxsize * 0.5
X = np.append(grid_in.X,grid_out.X)
Y = np.append(grid_in.Y,grid_out.Y)
You can repeat the re-gridding step as before
rho_interp = dda.disk_interpolate_primitive_quantities(snap,[X,Y],quantities=['RHO'])[0]
fig = plt.figure(figsize=(5,4.5))
fig.subplots_adjust(top=0.97,right=0.95,left=0.1,bottom=0.12)
ax = fig.add_subplot(111)
ax.scatter(X,Y,c=rho_interp ,lw=0,s=8)
ax.axis([73,87,73,87])
ax.set_xlabel(r'$x$',size=18)
ax.set_ylabel(r'$y$',size=18)
ax.set_aspect(1.0)
plt.show()
and plot the color-coded cell locations as before
A uniform (and linear) sampling in R
and phi
allows us to create an reconstructed
image plot in polar coordinates
Rmin, Rmax = 1.0, 7.0
NR, Nphi = 300, 500
R, phi = np.meshgrid(np.arange(Rmin,Rmax,(Rmax-Rmin)/NR),\
np.linspace(0,2*np.pi-np.pi/Nphi,Nphi))
X, Y = R * np.cos(phi) + snap.header.boxsize * 0.5, R * np.sin(phi) + snap.header.boxsize * 0.5
rho_interp = dda.disk_interpolate_primitive_quantities(snap,[X,Y],quantities=['RHO'])[0]
fig = plt.figure(figsize=(8,5))
fig.subplots_adjust(top=0.97,right=0.95,left=0.1,bottom=0.12)
ax = fig.add_subplot(111)
ax.imshow(rho_interp, origin='lower',interpolation='bilinear',
extent=[R.min(),R.max(),phi.min()/np.pi,phi.max()/np.pi])
ax.set_xlabel(r'$R$',size=18)
ax.set_ylabel(r'$\phi/\pi$',size=18)
X0, X1 = ax.get_xlim()
Y0, Y1 = ax.get_ylim()
ax.set_aspect(float((X1-X0)/(Y1-Y0)/1.6))
plt.show()
Simple quantities such as "first order" primitive variables RHO
,
VELPHI
and VELR
can be evaluated over a strucured grid and plotted as a function or R
. A simple average can be used to compute the radial profile for all these quantities.
snap = dda.get_snapshot_data('./data/snap_',0,['POS','RHO','VELPHI','VELR'])
Rmin, Rmax = 1.0, 15.0
NR, Nphi = 300, 500
R, phi = np.meshgrid(np.arange(Rmin,Rmax,(Rmax-Rmin)/NR),\
np.linspace(0,2*np.pi-np.pi/Nphi,Nphi))
X, Y = R * np.cos(phi) + snap.header.boxsize * 0.5, R * np.sin(phi) + snap.header.boxsize * 0.5
rho_interp,velphi_interp,velr_interp = dda.disk_interpolate_primitive_quantities(snap,[X,Y],quantities=['RHO','VELPHI','VELR'])
Rvals = R.mean(axis=0)
sigma0 = rho_interp.mean()
sigma_av = rho_interp.mean(axis=0)
velphi_av = velphi_interp.mean(axis=0)
velr_av = velr_interp.mean(axis=0)
fig = plt.figure(figsize=(16,4))
ax = fig.add_subplot(1,3,1)
ax.plot(R,rho_interp/sigma0,'b.',ms=2.5)
ax.plot(Rvals,sigma_av/sigma0,color='darkred',lw=5.0,alpha=0.6)
ax = fig.add_subplot(1,3,2)
ax.plot(R,velphi_interp,'b.',ms=2.5)
ax.plot(Rvals,velphi_av,color='darkred',lw=5.0,alpha=0.6)
ax = fig.add_subplot(1,3,3)
ax.plot(R,velr_interp,'b.',ms=2.5)
ax.plot(Rvals,velr_av,color='darkred',lw=5.0,alpha=0.6)
plt.show()
Computing other basic quantities, such as the mass accretion profile in disks, can be non-trivial. In conservative, finite-volume codes, one can in principle store the flux of any conserved quantity across a given radial location provided: (1) the cell boundaries align with the radial coordinate (i.e., the a cylindrical grid is used to discretize the domain) and (2) the quantity under question is directly evolved in a conservative manner (i.e., by used of a Godunov-like scheme). AREPO simulations fail to meet the first requirement, since the mesh is unstructured. In such case, interpolation is left as the most straightforward tool to compute the flux of conserved quantities crossing a boundary at a specific value of radius R.
For the mass accretion rate: