Analysis of SPH data

Subsnaps

When analyzing SPH data it can be useful to look at a subset of particles. For example, the simulation we have been working with has dust and gas. So far we have been plotting the total density. We may want to visualize the dust and gas separately.

To do this we make a SubSnap object. We can access these quantities using the family() method. Given that there may be sub-types of dust, using ‘dust’ returns a list by default. In this simulation there is only one dust species. We can squeeze all the dust sub-types together using the squeeze argument.

>>> gas = snap.family('gas')
>>> dust = snap.family('dust', squeeze=True)

You can access arrays on the SubSnap objects as for any Snap object.

>>> gas['mass'].sum().to('solar_mass')
0.0010000000000000005 <Unit('solar_mass')>

>>> dust['mass'].sum().to('earth_mass')
3.326810503428664 <Unit('earth_mass')>

Let’s plot the gas and dust side-by-side.

>>> subsnaps = [gas, dust]
>>> extent = (-200, 200, -200, 200)

>>> fig, axs = plt.subplots(ncols=2, figsize=(14, 5))

>>> for subsnap, ax in zip(subsnaps, axs):
...     subsnap.image(quantity='density', extent=extent, cmap='gist_heat', ax=ax)

The column density of the gas and dust.

Derived arrays

Sometimes you need new arrays on the particles that are not available in the snapshot files. Many are available in Plonk already. The following code block lists the available raw Phantom arrays on the file.

>>> list(snap._file_pointer['particles'])
['divv', 'dt', 'dustfrac', 'h', 'itype', 'tstop', 'vxyz', 'xyz']

To see all available arrays on the Snap object:

>>> snap.available_arrays()
['angular_momentum',
 'angular_velocity',
 'azimuthal_angle',
 'density',
 'dust_to_gas_ratio',
 'id',
 'kinetic_energy',
 'mass',
 'momentum',
 'polar_angle',
 'position',
 'pressure',
 'radius_cylindrical',
 'radius_spherical',
 'smoothing_length',
 'sound_speed',
 'specific_angular_momentum',
 'specific_kinetic_energy',
 'stopping_time',
 'sub_type',
 'temperature',
 'timestep',
 'type',
 'velocity',
 'velocity_divergence',
 'velocity_radial_cylindrical',
 'velocity_radial_spherical']

This is a disc simulation. You can add quantities appropriate for discs with the add_quantities() method.

>>> previous_arrays = snap.available_arrays()

>>> snap.add_quantities('disc')

# Additional available arrays
>>> set(snap.available_arrays()) - set(previous_arrays)
{'eccentricity',
 'inclination',
 'keplerian_frequency',
 'semi_major_axis',
 'stokes_number'}

You can create a new, derived array on the particles as follows.

>>> snap['rad'] = np.sqrt(snap['x'] ** 2 + snap['y'] ** 2)

>>> snap['rad']
array([8.28943225e+12, 2.00284678e+13, 1.79690392e+13, ...,
       1.81598604e+13, 1.38078875e+13, 2.08972008e+13]) <Unit('meter')>

Where, here, we have used the fact that Plonk knows that ‘x’ and ‘y’ refer to the x- and y-components of the position array.

Alternatively, you can define a function for a derived array. This makes use of the decorator add_array().

>>> @snap.add_array()
... def radius(snap):
...     radius = np.hypot(snap['x'], snap['y'])
...     return radius

>>> snap['radius']
array([8.28943225e+12, 2.00284678e+13, 1.79690392e+13, ...,
       1.81598604e+13, 1.38078875e+13, 2.08972008e+13]) <Unit('meter')>

Units

Plonk uses Pint to set arrays to physical units.

>>> snap = plonk.load_snap(filename)

>>> snap['x']
array([-3.69505001e+12, -1.63052677e+13, -7.66283930e+12, ...,
        1.39571712e+13,  9.53716176e+12,  1.21421196e+12]) <Unit('meter')>

It is easy to convert quantities to different units as required.

>>> snap['x'].to('au')
array([ -24.6998837 , -108.99398271,  -51.22291689, ...,   93.29792694,
         63.75198868,    8.11650561]) <Unit('astronomical_unit')>

Profiles

Generating a profile is a convenient method to reduce the dimensionality of the full data set. For example, we may want to see how the surface density and aspect ratio of the disc vary with radius.

To do this we use the Profile class in the analysis module.

>>> snap = plonk.load_snap(filename)

>>> snap.add_quantities('disc')

>>> prof = plonk.load_profile(snap, cmin='10 au', cmax='200 au')

>>> prof
<plonk.Profile "disc_00030.h5">

To see what profiles are loaded and what are available use the loaded_profiles() and available_profiles() methods.

>>> prof.loaded_profiles()
['number', 'radius', 'size']

>>> prof.available_profiles()
['alpha_shakura_sunyaev',
 'angular_momentum_mag',
 'angular_momentum_phi',
 'angular_momentum_theta',
 'angular_momentum_x',
 'angular_momentum_y',
 'angular_momentum_z',
 'angular_velocity',
 'aspect_ratio',
 'azimuthal_angle',
 'density',
 'disc_viscosity',
 'dust_to_gas_ratio_001',
 'eccentricity',
 'epicyclic_frequency',
 'id',
 'inclination',
 'keplerian_frequency',
 'kinetic_energy',
 'mass',
 'midplane_stokes_number_001',
 'momentum_mag',
 'momentum_x',
 'momentum_y',
 'momentum_z',
 'number',
 'polar_angle',
 'position_mag',
 'position_x',
 'position_y',
 'position_z',
 'pressure',
 'radius',
 'radius_cylindrical',
 'radius_spherical',
 'scale_height',
 'semi_major_axis',
 'size',
 'smoothing_length',
 'sound_speed',
 'specific_angular_momentum_mag',
 'specific_angular_momentum_x',
 'specific_angular_momentum_y',
 'specific_angular_momentum_z',
 'specific_kinetic_energy',
 'stokes_number_001',
 'stopping_time_001',
 'sub_type',
 'surface_density',
 'temperature',
 'timestep',
 'toomre_q',
 'type',
 'velocity_divergence',
 'velocity_mag',
 'velocity_radial_cylindrical',
 'velocity_radial_spherical',
 'velocity_x',
 'velocity_y',
 'velocity_z']

To load a profile, simply call it.

>>> prof['scale_height']
array([7.97124951e+10, 9.84227660e+10, 1.18761140e+11, 1.37555034e+11,
       1.57492383e+11, 1.79386553e+11, 1.99666627e+11, 2.20898696e+11,
       2.46311866e+11, 2.63718852e+11, 2.91092881e+11, 3.11944125e+11,
       3.35101091e+11, 3.58479038e+11, 3.86137691e+11, 4.09731915e+11,
       4.34677739e+11, 4.61230912e+11, 4.87471032e+11, 5.07589421e+11,
       5.38769961e+11, 5.64068787e+11, 5.88487300e+11, 6.15197082e+11,
       6.35591229e+11, 6.75146081e+11, 6.94786320e+11, 7.32840731e+11,
       7.65750662e+11, 7.96047221e+11, 8.26764128e+11, 8.35658042e+11,
       8.57126522e+11, 8.99037935e+11, 9.26761324e+11, 9.45798955e+11,
       9.65997944e+11, 1.01555592e+12, 1.05554201e+12, 1.07641999e+12,
       1.10797835e+12, 1.13976869e+12, 1.17181502e+12, 1.20083661e+12,
       1.23779947e+12, 1.24785058e+12, 1.28800980e+12, 1.31290341e+12,
       1.33602542e+12, 1.34618607e+12, 1.36220344e+12, 1.39412340e+12,
       1.41778445e+12, 1.46713623e+12, 1.51140364e+12, 1.54496807e+12,
       1.58327631e+12, 1.60316504e+12, 1.63374960e+12, 1.64446331e+12,
       1.66063803e+12, 1.67890856e+12, 1.68505873e+12, 1.68507230e+12,
       1.71353612e+12, 1.71314330e+12, 1.75704484e+12, 1.79183025e+12,
       1.83696336e+12, 1.88823477e+12, 1.93080810e+12, 1.98301979e+12,
       2.05279086e+12, 2.11912539e+12, 2.14224572e+12, 2.21647741e+12,
       2.27153917e+12, 2.36605186e+12, 2.38922067e+12, 2.53901104e+12,
       2.61297334e+12, 2.64782574e+12, 2.73832897e+12, 3.04654121e+12,
       3.13575612e+12, 3.37636281e+12, 3.51482502e+12, 3.69591185e+12,
       3.88308614e+12, 3.83982313e+12, 4.00147149e+12, 4.37288049e+12,
       4.35330982e+12, 4.44686164e+12, 4.47133547e+12, 4.83307604e+12,
       4.63783507e+12, 4.95119779e+12, 5.17961431e+12, 5.29308491e+12]) <Unit('meter')>

You can convert the data in the Profile object to a pandas DataFrame with the to_dataframe() method. This takes all loaded profiles and puts them into the DataFrame with units indicated in brackets.

>>> profiles = [
...    'radius',
...    'angular_momentum_phi',
...    'angular_momentum_theta',
...    'surface_density',
...    'scale_height',
... ]

>>> df = prof.to_dataframe(columns=profiles)

>>> df
      radius [m]  angular_momentum_phi [rad]  angular_momentum_theta [rad]  surface_density [kg / m ** 2]  scale_height [m]
0   1.638097e+12                   -0.019731                      0.049709                       0.486007      7.971250e+10
1   1.922333e+12                    1.914841                      0.053297                       0.630953      9.842277e+10
2   2.206569e+12                    1.293811                      0.055986                       0.808415      1.187611e+11
3   2.490805e+12                   -2.958286                      0.057931                       0.956107      1.375550e+11
4   2.775041e+12                   -1.947547                      0.059679                       1.095939      1.574924e+11
..           ...                         ...                           ...                            ...               ...
95  2.864051e+13                    3.045660                      0.168944                       0.013883      4.833076e+12
96  2.892475e+13                   -0.054956                      0.161673                       0.013246      4.637835e+12
97  2.920898e+13                   -0.217485                      0.169546                       0.011058      4.951198e+12
98  2.949322e+13                   -1.305261                      0.175302                       0.011367      5.179614e+12
99  2.977746e+13                    2.642077                      0.176867                       0.010660      5.293085e+12

[100 rows x 5 columns]

We can also plot the profiles.

>>> prof.set_units(position='au', scale_height='au', surface_density='g/cm^2')

>>> fig, axs = plt.subplots(ncols=2, figsize=(12, 5))

>>> prof.plot('radius', 'surface_density', ax=axs[0])

>>> prof.plot('radius', 'scale_height', ax=axs[1])