Particle TrajectoriesΒΆ

Notebook

The particle_trajectories analysis module enables the construction of particle trajectories from a time series of datasets for a specified list of particles identified by their unique indices.

In [1]:
%matplotlib inline
import yt
import glob
from yt.analysis_modules.particle_trajectories.api import ParticleTrajectories
from yt.config import ytcfg
path = ytcfg.get("yt", "test_data_dir")
import matplotlib.pyplot as plt

First, let's start off with a FLASH dataset containing only two particles in a mutual circular orbit. We can get the list of filenames this way:

In [2]:
my_fns = glob.glob(path+"/Orbit/orbit_hdf5_chk_00[0-9][0-9]")
my_fns.sort()

And let's define a list of fields that we want to include in the trajectories. The position fields will be included by default, so let's just ask for the velocity fields:

In [3]:
fields = ["particle_velocity_x", "particle_velocity_y", "particle_velocity_z"]

There are only two particles, but for consistency's sake let's grab their indices from the dataset itself:

In [4]:
ds = yt.load(my_fns[0])
dd = ds.all_data()
indices = dd["particle_index"].astype("int")
print (indices)
[1 2] dimensionless

which is what we expected them to be. Now we're ready to create a ParticleTrajectories object:

In [5]:
trajs = ParticleTrajectories(my_fns, indices, fields=fields)

The ParticleTrajectories object trajs is essentially a dictionary-like container for the particle fields along the trajectory, and can be accessed as such:

In [6]:
print (trajs["particle_position_x"])
print (trajs["particle_position_x"].shape)
[[ 0.25        0.25131274  0.25524942  0.26175659  0.27075597  0.28215068
   0.2958247   0.31164457  0.32945507  0.3491025   0.37039668  0.39309658
   0.41697977  0.44181091  0.46731412  0.49320738  0.51918241  0.54492553
   0.57013802  0.5945386   0.61785738  0.63986883  0.66032111  0.67898584
   0.69568959  0.71026758  0.72255999  0.73244151  0.73979734  0.7445359
   0.74658248]
 [ 0.75        0.74868726  0.74475058  0.7382434   0.72924402  0.71784928
   0.70417524  0.68835534  0.67054478  0.65089727  0.62960299  0.60690298
   0.58301967  0.5581884   0.53268507  0.50679169  0.48081655  0.45507332
   0.42986072  0.40546004  0.38214117  0.36012962  0.33967725  0.32101242
   0.30430854  0.28973042  0.27743785  0.26755617  0.26020016  0.25546143
   0.25341467]] code_length
(2, 31)

Note that each field is a 2D NumPy array with the different particle indices along the first dimension and the times along the second dimension. As such, we can access them individually by indexing the field:

In [7]:
plt.plot(trajs["particle_position_x"][0].ndarray_view(), trajs["particle_position_y"][0].ndarray_view())
plt.plot(trajs["particle_position_x"][1].ndarray_view(), trajs["particle_position_y"][1].ndarray_view())
Out[7]:
[<matplotlib.lines.Line2D at 0x7f80aa76fda0>]

And we can plot the velocity fields as well:

In [8]:
plt.plot(trajs["particle_velocity_x"][0].ndarray_view(), trajs["particle_velocity_y"][0].ndarray_view())
plt.plot(trajs["particle_velocity_x"][1].ndarray_view(), trajs["particle_velocity_y"][1].ndarray_view())
Out[8]:
[<matplotlib.lines.Line2D at 0x7f80aa371f28>]

If we want to access the time along the trajectory, we use the key "particle_time":

In [9]:
plt.plot(trajs["particle_time"].ndarray_view(), trajs["particle_velocity_x"][1].ndarray_view())
plt.plot(trajs["particle_time"].ndarray_view(), trajs["particle_velocity_y"][1].ndarray_view())
Out[9]:
[<matplotlib.lines.Line2D at 0x7f80a9db7400>]

Alternatively, if we know the particle index we'd like to examine, we can get an individual trajectory corresponding to that index:

In [10]:
particle1 = trajs.trajectory_from_index(1)
plt.plot(particle1["particle_time"].ndarray_view(), particle1["particle_position_x"].ndarray_view())
plt.plot(particle1["particle_time"].ndarray_view(), particle1["particle_position_y"].ndarray_view())
Out[10]:
[<matplotlib.lines.Line2D at 0x7f80a9e576d8>]

Now let's look at a more complicated (and fun!) example. We'll use an Enzo cosmology dataset. First, we'll find the maximum density in the domain, and obtain the indices of the particles within some radius of the center. First, let's have a look at what we're getting:

In [11]:
ds = yt.load("enzo_tiny_cosmology/DD0046/DD0046")
slc = yt.SlicePlot(ds, "x", ["density","dark_matter_density"], center="max", width=(3.0, "Mpc"))
slc.show()


So far, so good--it looks like we've centered on a galaxy cluster. Let's grab all of the dark matter particles within a sphere of 0.5 Mpc (identified by "particle_type == 1"):

In [12]:
sp = ds.sphere("max", (0.5, "Mpc"))
indices = sp["particle_index"][sp["particle_type"] == 1]

Next we'll get the list of datasets we want, and create trajectories for these particles:

In [13]:
my_fns = glob.glob(path+"/enzo_tiny_cosmology/DD*/*.hierarchy")
my_fns.sort()
trajs = ParticleTrajectories(my_fns, indices)

Matplotlib can make 3D plots, so let's pick three particle trajectories at random and look at them in the volume:

In [14]:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(8.0, 8.0))
ax = fig.add_subplot(111, projection='3d')
ax.plot(trajs["particle_position_x"][100].ndarray_view(), trajs["particle_position_z"][100].ndarray_view(), 
        trajs["particle_position_z"][100].ndarray_view())
ax.plot(trajs["particle_position_x"][8].ndarray_view(), trajs["particle_position_z"][8].ndarray_view(), 
        trajs["particle_position_z"][8].ndarray_view())
ax.plot(trajs["particle_position_x"][25].ndarray_view(), trajs["particle_position_z"][25].ndarray_view(), 
        trajs["particle_position_z"][25].ndarray_view())
Out[14]:
[<mpl_toolkits.mplot3d.art3d.Line3D at 0x7f80a8087898>]

It looks like these three different particles fell into the cluster along different filaments. We can also look at their x-positions only as a function of time:

In [15]:
plt.plot(trajs["particle_time"].ndarray_view(), trajs["particle_position_x"][100].ndarray_view())
plt.plot(trajs["particle_time"].ndarray_view(), trajs["particle_position_x"][8].ndarray_view())
plt.plot(trajs["particle_time"].ndarray_view(), trajs["particle_position_x"][25].ndarray_view())
Out[15]:
[<matplotlib.lines.Line2D at 0x7f80aa095ac8>]

Suppose we wanted to know the gas density along the particle trajectory, but there wasn't a particle field corresponding to that in our dataset. Never fear! If the field exists as a grid field, yt will interpolate this field to the particle positions and add the interpolated field to the trajectory. To add such a field (or any field, including additional particle fields) we can call the add_fields method:

In [16]:
trajs.add_fields(["density"])

We also could have included "density" in our original field list. Now, plot up the gas density for each particle as a function of time:

In [17]:
plt.plot(trajs["particle_time"].ndarray_view(), trajs["density"][100].ndarray_view())
plt.plot(trajs["particle_time"].ndarray_view(), trajs["density"][8].ndarray_view())
plt.plot(trajs["particle_time"].ndarray_view(), trajs["density"][25].ndarray_view())
plt.yscale("log")

Finally, the particle trajectories can be written to disk. Two options are provided: ASCII text files with a column for each field and the time, and HDF5 files:

In [18]:
trajs.write_out("halo_trajectories") # This will write a separate file for each trajectory
trajs.write_out_h5("halo_trajectories.h5") # This will write all trajectories to a single file

(Particle_Trajectories.ipynb; Particle_Trajectories_evaluated.ipynb; Particle_Trajectories.py)