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

n_particles = 5000000

ppx, ppy, ppz = 1e6 * np.random.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 = {
    "particle_position_x": ppx,
    "particle_position_y": ppy,
    "particle_position_z": ppz,
    "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. This example shows how to access “deposit” fields:

[4]:
ad = ds.all_data()

# This is generated with "cloud-in-cell" interpolation.
cic_density = ad["deposit", "all_cic"]

# These three are based on nearest-neighbor cell deposition
nn_density = ad["deposit", "all_density"]
nn_deposited_mass = ad["deposit", "all_mass"]
particle_count_per_cell = ad["deposit", "all_count"]
---------------------------------------------------------------------------
NeedsGridType                             Traceback (most recent call last)
File /tmp/yt/yt/data_objects/data_containers.py:287, in YTDataContainer._generate_fluid_field(self, field)
    286 try:
--> 287     finfo.check_available(gen_obj)
    288 except NeedsGridType as ngt_exception:

File /tmp/yt/yt/fields/derived_field.py:235, in DerivedField.check_available(self, data)
    234 for validator in self.validators:
--> 235     validator(data)
    236 # If we don't get an exception, we're good to go

File /tmp/yt/yt/fields/derived_field.py:620, in ValidateSpatial.__call__(self, data)
    619 if not getattr(data, "_spatial", False):
--> 620     raise NeedsGridType(self.ghost_zones, self.fields)
    621 if self.ghost_zones <= data._num_ghost_zones:

NeedsGridType: fields None require 0 ghost zones.

During handling of the above exception, another exception occurred:

YTNonIndexedDataContainer                 Traceback (most recent call last)
Cell In[4], line 4
      1 ad = ds.all_data()
      3 # This is generated with "cloud-in-cell" interpolation.
----> 4 cic_density = ad["deposit", "all_cic"]
      6 # These three are based on nearest-neighbor cell deposition
      7 nn_density = ad["deposit", "all_density"]

File /tmp/yt/yt/data_objects/data_containers.py:235, in YTDataContainer.__getitem__(self, key)
    233         return self.field_data[f]
    234     else:
--> 235         self.get_data(f)
    236 # fi.units is the unit expression string. We depend on the registry
    237 # hanging off the dataset to define this unit object.
    238 # Note that this is less succinct so that we can account for the case
    239 # when there are, for example, no elements in the object.
    240 try:

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:220, in YTSelectionContainer.get_data(self, fields)
    217     self.field_data[f].convert_to_units(finfos[f].output_units)
    219 fields_to_generate += gen_fluids + gen_particles
--> 220 self._generate_fields(fields_to_generate)
    221 for field in list(self.field_data.keys()):
    222     if field not in ofields:

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:252, in YTSelectionContainer._generate_fields(self, fields_to_generate)
    250 fi = self.ds._get_field_info(field)
    251 try:
--> 252     fd = self._generate_field(field)
    253     if hasattr(fd, "units"):
    254         fd.units.registry = self.ds.unit_registry

File /tmp/yt/yt/data_objects/data_containers.py:273, in YTDataContainer._generate_field(self, field)
    271     tr = self._generate_particle_field(field)
    272 else:
--> 273     tr = self._generate_fluid_field(field)
    274 if tr is None:
    275     raise YTCouldNotGenerateField(field, self.ds)

File /tmp/yt/yt/data_objects/data_containers.py:289, in YTDataContainer._generate_fluid_field(self, field)
    287     finfo.check_available(gen_obj)
    288 except NeedsGridType as ngt_exception:
--> 289     rv = self._generate_spatial_fluid(field, ngt_exception.ghost_zones)
    290 else:
    291     rv = finfo(gen_obj)

File /tmp/yt/yt/data_objects/data_containers.py:315, in YTDataContainer._generate_spatial_fluid(self, field, ngz)
    313 o = self._current_chunk.objs[0]
    314 if accumulate:
--> 315     rv = self.ds.arr(np.empty(o.ires.size, dtype="float64"), units)
    316     outputs.append(rv)
    317     ind = 0  # Does this work with mesh?

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:451, in YTSelectionContainer.ires(self)
    449 if self._current_chunk is None:
    450     self.index._identify_base_chunk(self)
--> 451 return self._current_chunk.ires

File /tmp/yt/yt/geometry/geometry_handler.py:272, in cacheable_property.<locals>.cacheable_func(self)
    270     return getattr(self, n)
    271 if self.data_size is None:
--> 272     tr = self._accumulate_values(n[1:])
    273 else:
    274     tr = func(self)

File /tmp/yt/yt/geometry/geometry_handler.py:308, in YTDataChunk._accumulate_values(self, method)
    306 for obj in self._fast_index or self.objs:
    307     f = getattr(obj, mname)
--> 308     arrs.append(f(self.dobj))
    309 if method == "dtcoords":
    310     arrs = [arr[0] for arr in arrs]

File /tmp/yt/yt/data_objects/index_subobjects/particle_container.py:17, in _non_indexed.<locals>._func_non_indexed(self, *args, **kwargs)
     16 def _func_non_indexed(self, *args, **kwargs):
---> 17     raise YTNonIndexedDataContainer(self)

YTNonIndexedDataContainer: The data container type (ParticleContainer) is an unindexed type. Operations such as ires, icoords, fcoords and fwidth will not work on it.
Did you just attempt to perform an off-axis operation ? Be sure to consult the latest documentation to see whether the operation you tried is actually supported for your data type.
[5]:
ds.field_list
[5]:
[('all', 'particle_mass'),
 ('all', 'particle_position_x'),
 ('all', 'particle_position_y'),
 ('all', 'particle_position_z'),
 ('io', 'particle_mass'),
 ('io', 'particle_position_x'),
 ('io', 'particle_position_y'),
 ('io', 'particle_position_z'),
 ('nbody', 'particle_mass'),
 ('nbody', 'particle_position_x'),
 ('nbody', 'particle_position_y'),
 ('nbody', 'particle_position_z')]
[6]:
ds.derived_field_list
[6]:
[('all', 'particle_mass'),
 ('all', 'particle_ones'),
 ('all', 'particle_position'),
 ('all', 'particle_position_cylindrical_radius'),
 ('all', 'particle_position_cylindrical_theta'),
 ('all', 'particle_position_cylindrical_z'),
 ('all', 'particle_position_relative_x'),
 ('all', 'particle_position_relative_y'),
 ('all', 'particle_position_relative_z'),
 ('all', 'particle_position_spherical_phi'),
 ('all', 'particle_position_spherical_radius'),
 ('all', 'particle_position_spherical_theta'),
 ('all', 'particle_position_x'),
 ('all', 'particle_position_y'),
 ('all', 'particle_position_z'),
 ('all', 'particle_radius'),
 ('all', 'relative_particle_position'),
 ('all', 'relative_particle_position_x'),
 ('all', 'relative_particle_position_y'),
 ('all', 'relative_particle_position_z'),
 ('deposit', 'all_cic'),
 ('deposit', 'all_count'),
 ('deposit', 'all_density'),
 ('deposit', 'all_mass'),
 ('deposit', 'io_cic'),
 ('deposit', 'io_count'),
 ('deposit', 'io_density'),
 ('deposit', 'io_mass'),
 ('deposit', 'nbody_cic'),
 ('deposit', 'nbody_count'),
 ('deposit', 'nbody_density'),
 ('deposit', 'nbody_mass'),
 ('gas', 'cell_volume'),
 ('gas', 'dx'),
 ('gas', 'dy'),
 ('gas', 'dz'),
 ('gas', 'path_element_x'),
 ('gas', 'path_element_y'),
 ('gas', 'path_element_z'),
 ('gas', 'volume'),
 ('gas', 'x'),
 ('gas', 'y'),
 ('gas', 'z'),
 ('index', 'cell_volume'),
 ('index', 'cylindrical_radius'),
 ('index', 'cylindrical_theta'),
 ('index', 'cylindrical_z'),
 ('index', 'dx'),
 ('index', 'dy'),
 ('index', 'dz'),
 ('index', 'grid_indices'),
 ('index', 'grid_level'),
 ('index', 'morton_index'),
 ('index', 'ones'),
 ('index', 'ones_over_dx'),
 ('index', 'path_element_x'),
 ('index', 'path_element_y'),
 ('index', 'path_element_z'),
 ('index', 'radius'),
 ('index', 'spherical_phi'),
 ('index', 'spherical_radius'),
 ('index', 'spherical_theta'),
 ('index', 'virial_radius_fraction'),
 ('index', 'volume'),
 ('index', 'x'),
 ('index', 'y'),
 ('index', 'z'),
 ('index', 'zeros'),
 ('io', 'particle_mass'),
 ('io', 'particle_ones'),
 ('io', 'particle_position'),
 ('io', 'particle_position_cylindrical_radius'),
 ('io', 'particle_position_cylindrical_theta'),
 ('io', 'particle_position_cylindrical_z'),
 ('io', 'particle_position_relative_x'),
 ('io', 'particle_position_relative_y'),
 ('io', 'particle_position_relative_z'),
 ('io', 'particle_position_spherical_phi'),
 ('io', 'particle_position_spherical_radius'),
 ('io', 'particle_position_spherical_theta'),
 ('io', 'particle_position_x'),
 ('io', 'particle_position_y'),
 ('io', 'particle_position_z'),
 ('io', 'particle_radius'),
 ('io', 'relative_particle_position'),
 ('io', 'relative_particle_position_x'),
 ('io', 'relative_particle_position_y'),
 ('io', 'relative_particle_position_z'),
 ('nbody', 'particle_mass'),
 ('nbody', 'particle_ones'),
 ('nbody', 'particle_position'),
 ('nbody', 'particle_position_cylindrical_radius'),
 ('nbody', 'particle_position_cylindrical_theta'),
 ('nbody', 'particle_position_cylindrical_z'),
 ('nbody', 'particle_position_relative_x'),
 ('nbody', 'particle_position_relative_y'),
 ('nbody', 'particle_position_relative_z'),
 ('nbody', 'particle_position_spherical_phi'),
 ('nbody', 'particle_position_spherical_radius'),
 ('nbody', 'particle_position_spherical_theta'),
 ('nbody', 'particle_position_x'),
 ('nbody', 'particle_position_y'),
 ('nbody', 'particle_position_z'),
 ('nbody', 'particle_radius'),
 ('nbody', 'relative_particle_position'),
 ('nbody', 'relative_particle_position_x'),
 ('nbody', 'relative_particle_position_y'),
 ('nbody', 'relative_particle_position_z'),
 ('stream', 'cell_volume'),
 ('stream', 'dx'),
 ('stream', 'dy'),
 ('stream', 'dz'),
 ('stream', 'path_element_x'),
 ('stream', 'path_element_y'),
 ('stream', 'path_element_z'),
 ('stream', 'volume'),
 ('stream', 'x'),
 ('stream', 'y'),
 ('stream', 'z')]
[7]:
slc = yt.SlicePlot(ds, 2, ("deposit", "all_cic"))
slc.set_width((8, "Mpc"))
---------------------------------------------------------------------------
NeedsGridType                             Traceback (most recent call last)
File /tmp/yt/yt/data_objects/data_containers.py:287, in YTDataContainer._generate_fluid_field(self, field)
    286 try:
--> 287     finfo.check_available(gen_obj)
    288 except NeedsGridType as ngt_exception:

File /tmp/yt/yt/fields/derived_field.py:235, in DerivedField.check_available(self, data)
    234 for validator in self.validators:
--> 235     validator(data)
    236 # If we don't get an exception, we're good to go

File /tmp/yt/yt/fields/derived_field.py:620, in ValidateSpatial.__call__(self, data)
    619 if not getattr(data, "_spatial", False):
--> 620     raise NeedsGridType(self.ghost_zones, self.fields)
    621 if self.ghost_zones <= data._num_ghost_zones:

NeedsGridType: fields None require 0 ghost zones.

During handling of the above exception, another exception occurred:

YTNonIndexedDataContainer                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 slc = yt.SlicePlot(ds, 2, ("deposit", "all_cic"))
      2 slc.set_width((8, "Mpc"))

File /tmp/yt/yt/visualization/plot_window.py:1839, in AxisAlignedSlicePlot.__init__(self, ds, normal, fields, center, width, axes_unit, origin, fontsize, field_parameters, window_size, aspect, data_source, buff_size, north_vector)
   1831 else:
   1832     slc = ds.slice(
   1833         axis,
   1834         center[axis],
   (...)
   1837         data_source=data_source,
   1838     )
-> 1839     slc.get_data(fields)
   1840 validate_mesh_fields(slc, fields)
   1841 PWViewerMPL.__init__(
   1842     self,
   1843     slc,
   (...)
   1851     geometry=ds.geometry,
   1852 )

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:220, in YTSelectionContainer.get_data(self, fields)
    217     self.field_data[f].convert_to_units(finfos[f].output_units)
    219 fields_to_generate += gen_fluids + gen_particles
--> 220 self._generate_fields(fields_to_generate)
    221 for field in list(self.field_data.keys()):
    222     if field not in ofields:

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:252, in YTSelectionContainer._generate_fields(self, fields_to_generate)
    250 fi = self.ds._get_field_info(field)
    251 try:
--> 252     fd = self._generate_field(field)
    253     if hasattr(fd, "units"):
    254         fd.units.registry = self.ds.unit_registry

File /tmp/yt/yt/data_objects/data_containers.py:273, in YTDataContainer._generate_field(self, field)
    271     tr = self._generate_particle_field(field)
    272 else:
--> 273     tr = self._generate_fluid_field(field)
    274 if tr is None:
    275     raise YTCouldNotGenerateField(field, self.ds)

File /tmp/yt/yt/data_objects/data_containers.py:289, in YTDataContainer._generate_fluid_field(self, field)
    287     finfo.check_available(gen_obj)
    288 except NeedsGridType as ngt_exception:
--> 289     rv = self._generate_spatial_fluid(field, ngt_exception.ghost_zones)
    290 else:
    291     rv = finfo(gen_obj)

File /tmp/yt/yt/data_objects/data_containers.py:315, in YTDataContainer._generate_spatial_fluid(self, field, ngz)
    313 o = self._current_chunk.objs[0]
    314 if accumulate:
--> 315     rv = self.ds.arr(np.empty(o.ires.size, dtype="float64"), units)
    316     outputs.append(rv)
    317     ind = 0  # Does this work with mesh?

File /tmp/yt/yt/data_objects/selection_objects/data_selection_objects.py:451, in YTSelectionContainer.ires(self)
    449 if self._current_chunk is None:
    450     self.index._identify_base_chunk(self)
--> 451 return self._current_chunk.ires

File /tmp/yt/yt/geometry/geometry_handler.py:272, in cacheable_property.<locals>.cacheable_func(self)
    270     return getattr(self, n)
    271 if self.data_size is None:
--> 272     tr = self._accumulate_values(n[1:])
    273 else:
    274     tr = func(self)

File /tmp/yt/yt/geometry/geometry_handler.py:308, in YTDataChunk._accumulate_values(self, method)
    306 for obj in self._fast_index or self.objs:
    307     f = getattr(obj, mname)
--> 308     arrs.append(f(self.dobj))
    309 if method == "dtcoords":
    310     arrs = [arr[0] for arr in arrs]

File /tmp/yt/yt/data_objects/index_subobjects/particle_container.py:17, in _non_indexed.<locals>._func_non_indexed(self, *args, **kwargs)
     16 def _func_non_indexed(self, *args, **kwargs):
---> 17     raise YTNonIndexedDataContainer(self)

YTNonIndexedDataContainer: The data container type (ParticleContainer) is an unindexed type. Operations such as ires, icoords, fcoords and fwidth will not work on it.
Did you just attempt to perform an off-axis operation ? Be sure to consult the latest documentation to see whether the operation you tried is actually supported for your data type.

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:

[8]:
n_star_particles = 1000000
n_dm_particles = 2000000

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

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

data2 = {
    ("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, n_ref=256, bbox=bbox
)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 21
      8 ppms = 0.1 * np.ones(n_star_particles)
     10 data2 = {
     11     ("dm", "particle_position_x"): ppxd,
     12     ("dm", "particle_position_y"): ppyd,
   (...)
     18     ("star", "particle_mass"): ppms,
     19 }
---> 21 ds2 = yt.load_particles(
     22     data2, length_unit=1.0 * parsec, mass_unit=1e8 * Msun, n_ref=256, bbox=bbox
     23 )

TypeError: load_particles() got an unexpected keyword argument 'n_ref'

We now have separate "dm" and "star" particles, as well as their deposited fields:

[9]:
slc = yt.SlicePlot(ds2, 2, [("deposit", "dm_cic"), ("deposit", "star_cic")])
slc.set_width((8, "Mpc"))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 slc = yt.SlicePlot(ds2, 2, [("deposit", "dm_cic"), ("deposit", "star_cic")])
      2 slc.set_width((8, "Mpc"))

NameError: name 'ds2' is not defined