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")))
238.7069071770326 code_length
9.94207930000004e+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:1842, in AxisAlignedSlicePlot.__init__(self, ds, normal, fields, center, width, axes_unit, origin, fontsize, field_parameters, window_size, aspect, data_source, buff_size, north_vector)
   1840     slc.get_data(fields)
   1841 validate_mesh_fields(slc, fields)
-> 1842 PWViewerMPL.__init__(
   1843     self,
   1844     slc,
   1845     bounds,
   1846     origin=origin,
   1847     fontsize=fontsize,
   1848     fields=fields,
   1849     window_size=window_size,
   1850     aspect=aspect,
   1851     buff_size=buff_size,
   1852     geometry=ds.geometry,
   1853 )
   1854 if axes_unit is None:
   1855     axes_unit = get_axes_unit(width, ds)

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:864, in PWViewerMPL.__init__(self, *args, **kwargs)
    862     self._plot_type = kwargs.pop("plot_type")
    863 self._splat_color = kwargs.pop("splat_color", None)
--> 864 PlotWindow.__init__(self, *args, **kwargs)
    866 # import type here to avoid import cycles
    867 # note that this import statement is actually crucial at runtime:
    868 # the filter methods for the present class are defined only when
    869 # fixed_resolution_filters is imported, so we need to guarantee
    870 # that it happens no later than instantiation
    871 from yt.visualization.plot_modifications import PlotCallback

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

File /var/jenkins_home/workspace/yt_docs/yt/visualization/plot_window.py:1066, in PWViewerMPL._setup_plots(self)
   1062     extenty.convert_to_units(unit_y)
   1064 extent = [*extentx, *extenty]
-> 1066 image = self.frb.get_image(f)
   1067 mask = self.frb.get_mask(f)
   1068 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:237, in CartesianCoordinateHandler.pixelize(self, dimension, data_source, field, bounds, size, antialias, periodic, return_mask)
    234     mask = np.squeeze(np.transpose(mask, (yax, xax, ax)))
    236 elif self.axis_id.get(dimension, dimension) is not None:
--> 237     buff, mask = self._ortho_pixelize(
    238         data_source, field, bounds, size, antialias, dimension, periodic
    239     )
    240 else:
    241     buff, mask = self._oblique_pixelize(
    242         data_source, field, bounds, size, antialias
    243     )

File /var/jenkins_home/workspace/yt_docs/yt/geometry/coordinates/cartesian_coordinates.py:504, in CartesianCoordinateHandler._ortho_pixelize(self, data_source, field, bounds, size, antialias, dim, periodic)
    494     buff_den = np.zeros(size, dtype="float64")
    496 for chunk in data_source.chunks([], "io"):
    497     pixelize_sph_kernel_slice(
    498         buff,
    499         mask_uint8,
    500         chunk[ptype, px_name].to("code_length"),
    501         chunk[ptype, py_name].to("code_length"),
    502         chunk[ptype, "smoothing_length"].to("code_length"),
    503         chunk[ptype, "mass"].to("code_mass"),
--> 504         chunk[ptype, "density"].to("code_density"),
    505         chunk[field].in_units(ounits),
    506         bnds,
    507         check_period=int(periodic),
    508         period=period,
    509     )
    510     if normalize:
    511         pixelize_sph_kernel_slice(
    512             buff_den,
    513             mask_uint8,
   (...)
    522             period=period,
    523         )

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/array.py:971, in unyt_array.to(self, units, equivalence, **kwargs)
    929 def to(self, units, equivalence=None, **kwargs):
    930     """
    931     Creates a copy of this array with the data converted to the
    932     supplied units, and returns it.
   (...)
    969     898755178736817.6 J
    970     """
--> 971     return self.in_units(units, equivalence=equivalence, **kwargs)

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/array.py:899, in unyt_array.in_units(self, units, equivalence, **kwargs)
    897 else:
    898     new_units = units
--> 899     (conversion_factor, offset) = self.units.get_conversion_factor(
    900         new_units, self.dtype
    901     )
    902 dsize = max(2, self.dtype.itemsize)
    903 if self.dtype.kind in ("u", "i"):

File /var/jenkins_home/workspace/yt_docs/.venv/lib/python3.11/site-packages/unyt/unit_object.py:676, in Unit.get_conversion_factor(self, other_units, dtype)
    649 def get_conversion_factor(self, other_units, dtype=None):
    650     """Get the conversion factor and offset (if any) from one unit
    651     to another
    652
   (...)
    674     (1.7999999999999998, -31.999999999999886)
    675     """
--> 676     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:921, in _get_conversion_factor(old_units, new_units, dtype)
    898 """
    899 Get the conversion factor between two units of equivalent dimensions. This
    900 is the number you multiply data by to convert from values in `old_units` to
   (...)
    918
    919 """
    920 if old_units.dimensions != new_units.dimensions:
--> 921     raise UnitConversionError(
    922         old_units, old_units.dimensions, new_units, new_units.dimensions
    923     )
    924 old_basevalue = old_units.base_value
    925 old_baseoffset = old_units.base_offset

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