Repository: yt-project/yt

Issue:
Odd difference in sphere selection
I've been working on #2437 and adding redundant checks to, among other things, the sphere selection.  What I'm finding is something I don't know how to interpret.  This script demonstrates it:

```python
import numpy as np
import yt
from yt.testing import ParticleSelectionComparison

ds = yt.load_sample("snapshot_033")

dd = ds.all_data()
sp = ds.sphere('c', (0.1, 'unitary'))

r = (((dd["PartType1", "particle_position"] - sp.center)**2).sum(axis=1))**0.5
good_r = r < ds.quan(0.1, 'unitary')

ind_good = dd["PartType1", "particle_index"][good_r]
ind_bad = sp["PartType1", "particle_index"]

v_in_good_not_bad = np.lib.arraysetops.setdiff1d(ind_good, ind_bad)
v_in_bad_not_good = np.lib.arraysetops.setdiff1d(ind_bad, ind_good)
```

Note that here I am using `PartType1`, which has a smoothing length of 0.0 (i.e., it's not in `ds._sph_ptypes`).  What I see is that `v_in_good_not_bad` has a size of 1259, which is the difference between the sizes of `ind_good` and `ind_bad` (i.e., no duplicate entries).

I don't know why these produce different results.  A few things I thought might cause it -- smoothing length and periodicity -- are not issues in this particular setup, from what I can tell.

One odd thing that I found is that for this specific set of spheres, the smoothed particles give the right results, but that may be coincidence.

I'll update this ticket as I am able to track this down better.


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -106,7 +106,8 @@ def _compile(
               include_dirs=["yt/utilities/lib/",
                             "yt/utilities/lib/ewahboolarray"],
               language="c++",
-              libraries=std_libs),
+              libraries=std_libs,
+              extra_compile_args=["-std=c++11"]),
     Extension("yt.geometry.selection_routines",
               ["yt/geometry/selection_routines.pyx"],
               include_dirs=["yt/utilities/lib/"],
diff --git a/yt/geometry/particle_geometry_handler.py b/yt/geometry/particle_geometry_handler.py
--- a/yt/geometry/particle_geometry_handler.py
+++ b/yt/geometry/particle_geometry_handler.py
@@ -167,21 +167,30 @@ def _initialize_refined_index(self):
         sub_mi1 = np.zeros(max_npart, "uint64")
         sub_mi2 = np.zeros(max_npart, "uint64")
         pb = get_pbar("Initializing refined index", len(self.data_files))
+        mask_threshold = getattr(self, '_index_mask_threshold', 2)
+        count_threshold = getattr(self, '_index_count_threshold', 256)
+        mylog.debug("Using estimated thresholds of %s and %s for refinement", mask_threshold, count_threshold)
+        total_refined = 0
+        total_coarse_refined = ((mask >= 2) & (self.regions.particle_counts > count_threshold)).sum()
+        mylog.debug("This should produce roughly %s zones, for %s of the domain",
+                    total_coarse_refined, 100 * total_coarse_refined / mask.size)
         for i, data_file in enumerate(self.data_files):
+            coll = None
             pb.update(i)
             nsub_mi = 0
             for ptype, pos in self.io._yield_coordinates(data_file):
+                if pos.size == 0: continue
                 if hasattr(self.ds, '_sph_ptypes') and ptype == self.ds._sph_ptypes[0]:
                     hsml = self.io._get_smoothing_length(
                         data_file, pos.dtype, pos.shape)
                 else:
                     hsml = None
-                nsub_mi = self.regions._refined_index_data_file(
-                    pos, hsml, mask, sub_mi1, sub_mi2,
-                    data_file.file_id, nsub_mi)
-            self.regions._set_refined_index_data_file(
-                sub_mi1, sub_mi2,
-                data_file.file_id, nsub_mi)
+                nsub_mi, coll = self.regions._refined_index_data_file(
+                    coll, pos, hsml, mask, sub_mi1, sub_mi2,
+                    data_file.file_id, nsub_mi, count_threshold = count_threshold,
+                    mask_threshold = mask_threshold)
+                total_refined += nsub_mi
+            self.regions.bitmasks.append(data_file.file_id, coll)
         pb.finish()
         self.regions.find_collisions_refined()
 


Test patch:
diff --git a/yt/frontends/gadget/tests/test_outputs.py b/yt/frontends/gadget/tests/test_outputs.py
--- a/yt/frontends/gadget/tests/test_outputs.py
+++ b/yt/frontends/gadget/tests/test_outputs.py
@@ -5,7 +5,8 @@
 import tempfile
 
 import yt
-from yt.testing import requires_file
+from yt.testing import requires_file, \
+    ParticleSelectionComparison
 from yt.utilities.answer_testing.framework import \
     data_dir_load, \
     requires_ds, \
@@ -107,6 +108,73 @@ def test_multifile_read():
     assert isinstance(data_dir_load(snap_33), GadgetDataset)
     assert isinstance(data_dir_load(snap_33_dir), GadgetDataset)
 
+@requires_file(snap_33)
+def test_particle_subselection():
+    #This checks that we correctly subselect from a dataset, first by making
+    #sure we get all the particles, then by comparing manual selections against
+    #them.
+    ds = data_dir_load(snap_33)
+    psc = ParticleSelectionComparison(ds)
+
+    sp1 = ds.sphere("c", (0.1, "unitary"))
+    psc.compare_dobj_selection(sp1)
+
+    sp2 = ds.sphere("c", (0.2, "unitary"))
+    psc.compare_dobj_selection(sp2)
+
+    # Test wrapping around each axis individually: x
+    sp3_x = ds.sphere((1.0, 12.5, 12.5), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp3_x)
+
+    # Test wrapping around each axis individually: y
+    sp3_y = ds.sphere((12.5, 1.0, 12.5), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp3_y)
+
+    # Test wrapping around each axis individually: z
+    sp3_z = ds.sphere((12.5, 12.5, 1.0), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp3_z)
+
+    # Test wrapping around all three axes simultaneously on left
+    sp3_all = ds.sphere((1.0, 1.0, 1.0), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp3_all)
+
+    # Test wrapping around each axis individually on right: x
+    sp4_x = ds.sphere((24.0, 12.5, 12.5), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp4_x)
+
+    # Test wrapping around each axis individually on right: y
+    sp4_y = ds.sphere((12.5, 24.0, 12.5), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp4_y)
+
+    # Test wrapping around each axis individually on right: z
+    sp4_z = ds.sphere((12.5, 12.5, 24.0), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp4_z)
+
+    # Test wrapping around all three axes simultaneously on right
+    sp4_all = ds.sphere((24.0, 24.0, 24.0), (2.0, "code_length"))
+    psc.compare_dobj_selection(sp4_all)
+
+    sp5 = ds.sphere("c", (0.5, "unitary"))
+    psc.compare_dobj_selection(sp5)
+
+    dd = ds.all_data()
+    psc.compare_dobj_selection(dd)
+
+    reg1 = ds.r[ (0.1, 'unitary'):(0.9, 'unitary'),
+                 (0.1, 'unitary'):(0.9, 'unitary'),
+                 (0.1, 'unitary'):(0.9, 'unitary')]
+    psc.compare_dobj_selection(reg1)
+
+    reg2 = ds.r[ (0.8, 'unitary'):(0.85, 'unitary'),
+                 (0.8, 'unitary'):(0.85, 'unitary'),
+                 (0.8, 'unitary'):(0.85, 'unitary')]
+    psc.compare_dobj_selection(reg2)
+
+    reg3 = ds.r[ (0.3, 'unitary'):(0.6, 'unitary'),
+                 (0.2, 'unitary'):(0.8, 'unitary'),
+                 (0.0, 'unitary'):(0.1, 'unitary')]
+    psc.compare_dobj_selection(reg3)
+
 @requires_ds(BE_Gadget)
 def test_bigendian_field_access():
     ds = data_dir_load(BE_Gadget)
diff --git a/yt/geometry/tests/test_particle_octree.py b/yt/geometry/tests/test_particle_octree.py
--- a/yt/geometry/tests/test_particle_octree.py
+++ b/yt/geometry/tests/test_particle_octree.py
@@ -133,11 +133,12 @@ def FakeBitmap(npart, nfiles, order1, order2,
         posgen = yield_fake_decomp(decomp, npart, nfiles,
                                    left_edge, right_edge, buff=buff, 
                                    distrib=distrib)
+        coll = None
         for i, (pos, hsml) in enumerate(posgen):
-            nsub_mi = reg._refined_index_data_file(
-                pos, hsml, mask, sub_mi1, sub_mi2, i, 0)
-            reg._set_refined_index_data_file(
-                sub_mi1, sub_mi2, i, nsub_mi)
+            nsub_mi, coll = reg._refined_index_data_file(
+                coll, pos, hsml, mask, sub_mi1, sub_mi2, i,
+                0, count_threshold = 1, mask_threshold = 2)
+            reg.bitmasks.append(i, coll)
         # Save if file name provided
         if isinstance(fname, str):
             reg.save_bitmasks(fname)
@@ -175,11 +176,12 @@ def test_bitmap_no_collisions():
     sub_mi2 = np.zeros(max_npart, "uint64")
     posgen = yield_fake_decomp('sliced', npart, nfiles,
                                left_edge, right_edge)
+    coll = None
     for i, (pos, hsml) in enumerate(posgen):
-        nsub_mi = reg._refined_index_data_file(
-            pos, hsml, mask, sub_mi1, sub_mi2, i, 0)
-        reg._set_refined_index_data_file(
-            sub_mi1, sub_mi2, i, nsub_mi)
+        nsub_mi, coll = reg._refined_index_data_file(
+            coll, pos, hsml, mask, sub_mi1, sub_mi2, i,
+                0, count_threshold = 1, mask_threshold = 2)
+        reg.bitmasks.append(i, coll)
         assert_equal(reg.count_refined(i), 0)
     nr, nm = reg.find_collisions_refined()
     assert_equal(nr, 0, "%d collisions" % nr)
@@ -214,10 +216,10 @@ def test_bitmap_collisions():
     sub_mi1 = np.zeros(max_npart, "uint64")
     sub_mi2 = np.zeros(max_npart, "uint64")
     for i in range(nfiles):
-        nsub_mi = reg._refined_index_data_file(
-            pos, hsml, mask, sub_mi1, sub_mi2, i, 0)
-        reg._set_refined_index_data_file(
-            sub_mi1, sub_mi2, i, nsub_mi)
+        nsub_mi, coll = reg._refined_index_data_file(
+            None, pos, hsml, mask, sub_mi1, sub_mi2, i,
+            0, count_threshold = 1, mask_threshold = 2)
+        reg.bitmasks.append(i, coll)
         assert_equal(reg.count_refined(i), ncoll)
     nr, nm = reg.find_collisions_refined()
     assert_equal(nr, 2**(3*(order1+order2)), "%d collisions" % nr)
diff --git a/yt/testing.py b/yt/testing.py
--- a/yt/testing.py
+++ b/yt/testing.py
@@ -1223,3 +1223,49 @@ def setUp(self):
     def tearDown(self):
         os.chdir(self.curdir)
         shutil.rmtree(self.tmpdir)
+
+class ParticleSelectionComparison:
+    """
+    This is a test helper class that takes a particle dataset, caches the
+    particles it has on disk (manually reading them using lower-level IO
+    routines) and then received a data object that it compares against manually
+    running the data object's selection routines.  All supplied data objects
+    must be created from the input dataset.
+    """
+
+    def __init__(self, ds):
+        self.ds = ds
+        # Construct an index so that we get all the data_files
+        ds.index
+        particles = {}
+        # hsml is the smoothing length we use for radial selection
+        hsml = {}
+        for data_file in ds.index.data_files:
+            for ptype, pos_arr in ds.index.io._yield_coordinates(data_file):
+                particles.setdefault(ptype, []).append(pos_arr)
+                if ptype in getattr(ds, '_sph_ptypes', ()):
+                    hsml.setdefault(ptype, []).append(ds.index.io._get_smoothing_length(
+                        data_file, pos_arr.dtype, pos_arr.shape))
+        for ptype in particles:
+            particles[ptype] = np.concatenate(particles[ptype])
+            if ptype in hsml:
+                hsml[ptype] = np.concatenate(hsml[ptype])
+        self.particles = particles
+        self.hsml = hsml
+
+    def compare_dobj_selection(self, dobj):
+        for ptype in sorted(self.particles):
+            x, y, z = self.particles[ptype].T
+            # Set our radii to zero for now, I guess?
+            radii = self.hsml.get(ptype, 0.0)
+            sel_index = dobj.selector.select_points(x, y, z, radii)
+            if sel_index is None:
+                sel_pos = np.empty((0, 3))
+            else:
+                sel_pos = self.particles[ptype][sel_index, :]
+
+            obj_results = []
+            for chunk in dobj.chunks([], "io"):
+                obj_results.append(chunk[ptype, "particle_position"])
+            obj_results = np.concatenate(obj_results, axis = 0)
+            assert_equal(sel_pos, obj_results)