ptype = fi.name[0]
                if ptype not in self.ds._sph_ptypes:
                    raise KeyError(f"{ptype} is not a SPH particle type!")
                buff = np.zeros(size, dtype="float64")
                if normalize:
                    buff_den = np.zeros(size, dtype="float64")

                pbar = tqdm(desc=f"Interpolating SPH field {field}")
                for chunk in self._data_source.chunks([field], "io"):
                    px = chunk[(ptype, "particle_position_x")].in_base("code").d
                    py = chunk[(ptype, "particle_position_y")].in_base("code").d
                    pz = chunk[(ptype, "particle_position_z")].in_base("code").d
                    hsml = chunk[(ptype, "smoothing_length")].in_base("code").d
                    mass = chunk[(ptype, "particle_mass")].in_base("code").d
                    dens = chunk[(ptype, "density")].in_base("code").d
                    field_quantity = chunk[field].d

                    pixelize_sph_kernel_arbitrary_grid(
                        buff,
                        px,
                        py,
                        pz,
                        hsml,
                        mass,
                        dens,
                        field_quantity,
                        bounds,
                        pbar=pbar,
                        check_period=is_periodic,
                        period=period,
                    )
                    if normalize:
                        pixelize_sph_kernel_arbitrary_grid(
                            buff_den,
                            px,
                            py,
                            pz,
                            hsml,
                            mass,
                            dens,
                            np.ones(dens.shape[0]),
                            bounds,
                            pbar=pbar,
                            check_period=is_periodic,
                            period=period,
                        )

                if normalize:
                    normalization_3d_utility(buff, buff_den)

                self[field] = self.ds.arr(buff, fi.units)
                pbar.close()

        if smoothing_style == "gather":
            num_neighbors = getattr(self.ds, "num_neighbors", 32)
            for field in fields:
                buff = np.zeros(size, dtype="float64")

                fields_to_get = [
                    "particle_position",
                    "density",
                    "particle_mass",
                    "smoothing_length",
                    field[1],
                ]
                all_fields = all_data(self.ds, field[0], fields_to_get, kdtree=True)

                fi = self.ds._get_field_info(field)
                interpolate_sph_grid_gather(
                    buff,
                    all_fields["particle_position"],
                    bounds,
                    all_fields["smoothing_length"],
                    all_fields["particle_mass"],
                    all_fields["density"],
                    all_fields[field[1]].in_units(fi.units),
                    self.ds.index.kdtree,
                    use_normalization=normalize,
                    num_neigh=num_neighbors,
                )

                self[field] = self.ds.arr(buff, fi.units)

    def _fill_fields(self, fields):
        fields = [f for f in fields if f not in self.field_data]
        if len(fields) == 0:
            return
        output_fields = [
            np.zeros(self.ActiveDimensions, dtype="float64") for field in fields
        ]
        domain_dims = self.ds.domain_dimensions.astype(
            "int64"
        ) * self.ds.relative_refinement(0, self.level)
        refine_by = self.ds.refine_by
        if not is_sequence(self.ds.refine_by):
            refine_by = [refine_by, refine_by, refine_by]
        refine_by = np.array(refine_by, dtype="i8")
        for chunk in parallel_objects(self._data_source.chunks(fields, "io")):
            input_fields = [chunk[field] for field in fields]
            # NOTE: This usage of "refine_by" is actually *okay*, because it's
            # being used with respect to iref, which is *already* scaled!
            fill_region(
                input_fields,
                output_fields,
                self.level,
                self.global_startindex,
                chunk.icoords,
                chunk.ires,
                domain_dims,
                refine_by,
            )
        if self.comm.size > 1:
            for i in range(len(fields)):
                output_fields[i] = self.comm.mpi_allreduce(output_fields[i], op="sum")
        for name, v in zip(fields, output_fields):
            fi = self.ds._get_field_info(*name)
            self[name] = self.ds.arr(v, fi.units)

    def _generate_container_field(self, field):
        rv = self.ds.arr(np.ones(self.ActiveDimensions, dtype="float64"), "")
        axis_name = self.ds.coordinates.axis_name
        if field == ("index", f"d{axis_name[0]}"):
            np.multiply(rv, self.dds[0], rv)
        elif field == ("index", f"d{axis_name[1]}"):
            np.multiply(rv, self.dds[1], rv)
        elif field == ("index", f"d{axis_name[2]}"):
            np.multiply(rv, self.dds[2], rv)
        elif field == ("index", axis_name[0]):
            x = np.mgrid[
                self.left_edge[0]
                + 0.5 * self.dds[0] : self.right_edge[0]
                - 0.5 * self.dds[0] : self.ActiveDimensions[0] * 1j
            ]
            np.multiply(rv, x[:, None, None], rv)
        elif field == ("index", axis_name[1]):
            y = np.mgrid[
                self.left_edge[1]
                + 0.5 * self.dds[1] : self.right_edge[1]
                - 0.5 * self.dds[1] : self.ActiveDimensions[1] * 1j
            ]
            np.multiply(rv, y[None, :, None], rv)
        elif field == ("index", axis_name[2]):
            z = np.mgrid[
                self.left_edge[2]
                + 0.5 * self.dds[2] : self.right_edge[2]
                - 0.5 * self.dds[2] : self.ActiveDimensions[2] * 1j
            ]
            np.multiply(rv, z[None, None, :], rv)
        else:
            raise KeyError(field)
        return rv

    @property
    def LeftEdge(self):
        return self.left_edge

    @property
    def RightEdge(self):
        return self.right_edge

    def deposit(self, positions, fields=None, method=None, kernel_name="cubic"):
        cls = getattr(particle_deposit, f"deposit_{method}", None)
        if cls is None:
            raise YTParticleDepositionNotImplemented(method)
        # We allocate number of zones, not number of octs. Everything
        # inside this is Fortran ordered because of the ordering in the
        # octree deposit routines, so we reverse it here to match the
        # convention there
        nvals = tuple(self.ActiveDimensions[::-1])
        # append a dummy dimension because we are only depositing onto
        # one grid
        op = cls(nvals + (1,), kernel_name)
        op.initialize()
        op.process_grid(self, positions, fields)
        # Fortran-ordered, so transpose.
        vals = op.finalize().transpose()
        # squeeze dummy dimension we appended above
        return np.squeeze(vals, axis=0)

    def write_to_gdf(self, gdf_path, fields, nprocs=1,