Loading Generic Particle Data

This example creates a fake in-memory particle dataset and then loads it as a yt dataset using the load_particles function.

Our “fake” dataset will be numpy arrays filled with normally distributed randoml particle positions and uniform particle masses. Since real data is often scaled, I arbitrarily multiply by 1e6 to show how to deal with scaled data.

[1]:
import numpy as np

rng = np.random.default_rng()
n_particles = 5_000_000

ppx, ppy, ppz = 1e6 * rng.normal(size=(3, n_particles))

ppm = np.ones(n_particles)

The load_particles function accepts a dictionary populated with particle data fields loaded in memory as numpy arrays or python lists:

[2]:
data = {
    ("io", "particle_position_x"): ppx,
    ("io", "particle_position_y"): ppy,
    ("io", "particle_position_z"): ppz,
    ("io", "particle_mass"): ppm,
}

To hook up with yt’s internal field system, the dictionary keys must be ‘particle_position_x’, ‘particle_position_y’, ‘particle_position_z’, and ‘particle_mass’, as well as any other particle field provided by one of the particle frontends.

The load_particles function transforms the data dictionary into an in-memory yt Dataset object, providing an interface for further analysis with yt. The example below illustrates how to load the data dictionary we created above.

[3]:
import yt
from yt.units import Msun, parsec

bbox = 1.1 * np.array(
    [[min(ppx), max(ppx)], [min(ppy), max(ppy)], [min(ppz), max(ppz)]]
)

ds = yt.load_particles(data, length_unit=1.0 * parsec, mass_unit=1e8 * Msun, bbox=bbox)

The length_unit and mass_unit are the conversion from the units used in the data dictionary to CGS. I’ve arbitrarily chosen one parsec and 10^8 Msun for this example.

The n_ref parameter controls how many particle it takes to accumulate in an oct-tree cell to trigger refinement. Larger n_ref will decrease poisson noise at the cost of resolution in the octree.

Finally, the bbox parameter is a bounding box in the units of the dataset that contains all of the particles. This is used to set the size of the base octree block.

This new dataset acts like any other yt Dataset object, and can be used to create data objects and query for yt fields.

[4]:
ad = ds.all_data()
print(ad.mean(("io", "particle_position_x")))
print(ad.sum(("io", "particle_mass")))
-582.08457330033 code_length
9.9420793e+47 g

We can project the particle mass field like so:

[5]:
prj = yt.ParticleProjectionPlot(ds, "z", ("io", "particle_mass"))
prj.set_width((8, "Mpc"))
[5]:

Finally, one can specify multiple particle types in the data directory by setting the field names to be field tuples (the default field type for particles is "io") if one is not specified:

[6]:
n_gas_particles = 1_000_000
n_star_particles = 1_000_000
n_dm_particles = 2_000_000

ppxg, ppyg, ppzg = 1e6 * rng.normal(size=(3, n_gas_particles))
ppmg = np.ones(n_gas_particles)
hsml = 10000 * np.ones(n_gas_particles)
dens = 2.0e-4 * np.ones(n_gas_particles)

ppxd, ppyd, ppzd = 1e6 * rng.normal(size=(3, n_dm_particles))
ppmd = np.ones(n_dm_particles)

ppxs, ppys, ppzs = 5e5 * rng.normal(size=(3, n_star_particles))
ppms = 0.1 * np.ones(n_star_particles)

bbox = 1.1 * np.array(
    [
        [
            min(ppxg.min(), ppxd.min(), ppxs.min()),
            max(ppxg.max(), ppxd.max(), ppxs.max()),
        ],
        [
            min(ppyg.min(), ppyd.min(), ppys.min()),
            max(ppyg.max(), ppyd.max(), ppys.max()),
        ],
        [
            min(ppzg.min(), ppzd.min(), ppzs.min()),
            max(ppzg.max(), ppzd.max(), ppzs.max()),
        ],
    ]
)

data2 = {
    ("gas", "particle_position_x"): ppxg,
    ("gas", "particle_position_y"): ppyg,
    ("gas", "particle_position_z"): ppzg,
    ("gas", "particle_mass"): ppmg,
    ("gas", "smoothing_length"): hsml,
    ("gas", "density"): dens,
    ("dm", "particle_position_x"): ppxd,
    ("dm", "particle_position_y"): ppyd,
    ("dm", "particle_position_z"): ppzd,
    ("dm", "particle_mass"): ppmd,
    ("star", "particle_position_x"): ppxs,
    ("star", "particle_position_y"): ppys,
    ("star", "particle_position_z"): ppzs,
    ("star", "particle_mass"): ppms,
}

ds2 = yt.load_particles(
    data2, length_unit=1.0 * parsec, mass_unit=1e8 * Msun, bbox=bbox
)

We now have separate "gas", "dm", and "star" particles. Since the "gas" particles have "density" and "smoothing_length" fields, they are recognized as SPH particles:

[7]:
ad = ds2.all_data()
c = np.array([ad.mean(("gas", ax)).to("code_length") for ax in "xyz"])
[8]:
slc = yt.SlicePlot(ds2, "z", ("gas", "density"), center=c)
slc.set_zlim(("gas", "density"), 1e-19, 2.0e-18)
slc.set_width((4, "Mpc"))
slc.show()
---------------------------------------------------------------------------
UnitConversionError                       Traceback (most recent call last)
Cell In[8], line 1
----> 1 slc = yt.SlicePlot(ds2, "z", ("gas", "density"), center=c)
      2 slc.set_zlim(("gas", "density"), 1e-19, 2.0e-18)
      3 slc.set_width((4, "Mpc"))

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:1856, in AxisAlignedSlicePlot.__init__(self, ds, normal, fields, center, width, axes_unit, origin, fontsize, field_parameters, window_size, aspect, data_source, buff_size, north_vector)
   1854     slc.get_data(fields)
   1855 validate_mesh_fields(slc, fields)
-> 1856 PWViewerMPL.__init__(
   1857     self,
   1858     slc,
   1859     bounds,
   1860     origin=origin,
   1861     fontsize=fontsize,
   1862     fields=fields,
   1863     window_size=window_size,
   1864     aspect=aspect,
   1865     buff_size=buff_size,
   1866     geometry=ds.geometry,
   1867 )
   1868 if axes_unit is None:
   1869     axes_unit = get_axes_unit(width, ds)

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:876, in PWViewerMPL.__init__(self, *args, **kwargs)
    874     self._plot_type = kwargs.pop("plot_type")
    875 self._splat_color = kwargs.pop("splat_color", None)
--> 876 PlotWindow.__init__(self, *args, **kwargs)
    878 # import type here to avoid import cycles
    879 # note that this import statement is actually crucial at runtime:
    880 # the filter methods for the present class are defined only when
    881 # fixed_resolution_filters is imported, so we need to guarantee
    882 # that it happens no later than instantiation
    884 self._callbacks: list[PlotCallback] = []

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:264, in PlotWindow.__init__(self, data_source, bounds, buff_size, antialias, periodic, origin, oblique, window_size, fields, fontsize, aspect, setup, geometry)
    261     self._projection = get_mpl_transform(projection)
    262     self._transform = get_mpl_transform(transform)
--> 264 self._setup_plots()
    266 for field in self.data_source._determine_fields(self.fields):
    267     finfo = self.data_source.ds._get_field_info(field)

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:1077, in PWViewerMPL._setup_plots(self)
   1073     extenty.convert_to_units(unit_y)
   1075 extent = [*extentx, *extenty]
-> 1077 image = self.frb.get_image(f)
   1078 mask = self.frb.get_mask(f)
   1079 assert mask is None or mask.dtype == bool

File /var/jenkins_home/workspace/yt_docs/yt/visualization/fixed_resolution.py:213, in FixedResolutionBuffer.get_image(self, key)
    211 def get_image(self, key, /) -> ImageArray:
    212     if not (key in self.data and self._data_valid):
--> 213         self._generate_image_and_mask(key)
    214     return self.data[key]

File /var/jenkins_home/workspace/yt_docs/yt/visualization/fixed_resolution.py:179, in FixedResolutionBuffer._generate_image_and_mask(self, item)
    176         b = float(b.in_units("code_length"))
    177     bounds.append(b)
--> 179 buff, mask = self.ds.coordinates.pixelize(
    180     self.data_source.axis,
    181     self.data_source,
    182     item,
    183     bounds,
    184     self.buff_size,
    185     int(self.antialias),
    186     return_mask=True,
    187 )
    189 buff = self._apply_filters(buff)
    191 # FIXME FIXME FIXME we shouldn't need to do this for projections
    192 # but that will require fixing data object access for particle
    193 # projections

File /var/jenkins_home/workspace/yt_docs/yt/geometry/coordinates/cartesian_coordinates.py:238, in CartesianCoordinateHandler.pixelize(self, dimension, data_source, field, bounds, size, antialias, periodic, return_mask)
    235     mask = np.squeeze(np.transpose(mask, (yax, xax, ax)))
    237 elif self.axis_id.get(dimension, dimension) is not None:
--> 238     buff, mask = self._ortho_pixelize(
    239         data_source, field, bounds, size, antialias, dimension, periodic
    240     )
    241 else:
    242     buff, mask = self._oblique_pixelize(
    243         data_source, field, bounds, size, antialias
    244     )

File /var/jenkins_home/workspace/yt_docs/yt/geometry/coordinates/cartesian_coordinates.py:551, in CartesianCoordinateHandler._ortho_pixelize(self, data_source, field, bounds, size, antialias, dim, periodic)
    541 for chunk in data_source.chunks([], "io"):
    542     hsmlname = "smoothing_length"
    543     pixelize_sph_kernel_slice(
    544         buff,
    545         mask_uint8,
    546         chunk[ptype, px_name].to("code_length").v,
    547         chunk[ptype, py_name].to("code_length").v,
    548         chunk[ptype, pz_name].to("code_length").v,
    549         chunk[ptype, hsmlname].to("code_length").v,
    550         chunk[ptype, "mass"].to("code_mass").v,
--> 551         chunk[ptype, "density"].to("code_density").v,
    552         chunk[field].in_units(ounits).v,
    553         bnds,
    554         data_source.coord.to("code_length").v,
    555         _check_period=_periodic.astype("int"),
    556         period=period3,
    557         kernel_name=kernel_name,
    558     )
    559     if normalize:
    560         pixelize_sph_kernel_slice(
    561             buff_den,
    562             mask_uint8,
   (...)    574             kernel_name=kernel_name,
    575         )

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/array.py:979, in unyt_array.to(self, units, equivalence, **kwargs)
    937 def to(self, units, equivalence=None, **kwargs):
    938     """
    939     Creates a copy of this array with the data converted to the
    940     supplied units, and returns it.
   (...)    977     898755178736817.6 J
    978     """
--> 979     return self.in_units(units, equivalence=equivalence, **kwargs)

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/array.py:907, in unyt_array.in_units(self, units, equivalence, **kwargs)
    905 else:
    906     new_units = units
--> 907     (conversion_factor, offset) = self.units.get_conversion_factor(
    908         new_units, self.dtype
    909     )
    910 dsize = max(2, self.dtype.itemsize)
    911 if self.dtype.kind in ("u", "i"):

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/unit_object.py:675, in Unit.get_conversion_factor(self, other_units, dtype)
    648 def get_conversion_factor(self, other_units, dtype=None):
    649     """Get the conversion factor and offset (if any) from one unit
    650     to another
    651
   (...)    673     (1.7999999999999998, -31.999999999999886)
    674     """
--> 675     return _get_conversion_factor(self, other_units, dtype)

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/unit_object.py:920, in _get_conversion_factor(old_units, new_units, dtype)
    897 """
    898 Get the conversion factor between two units of equivalent dimensions. This
    899 is the number you multiply data by to convert from values in `old_units` to
   (...)    917
    918 """
    919 if old_units.dimensions != new_units.dimensions:
--> 920     raise UnitConversionError(
    921         old_units, old_units.dimensions, new_units, new_units.dimensions
    922     )
    923 old_basevalue = old_units.base_value
    924 old_baseoffset = old_units.base_offset

UnitConversionError: Cannot convert between 'dimensionless' (dim '1') and 'code_density' (dim '(mass)/(length)**3').