Repository: astropy/photutils

Issue:
DAOStarFinder to include `minsep_fwhm` ?
Hi, is it possible to include `minsep_fwhm` in the `DAOStarFinder`? It looks to me from the top level that it is a simple implementation that can be copied and pasted from the `IRAFStarFinder`, not sure if it would cause problems in other places though...

In the current `DAOStarFinder`:

https://github.com/astropy/photutils/blob/88437049ef3909e9ced1c5ed3c06076a86d17020/photutils/detection/daofinder.py#L203-L211


From `IRAFStarFinder`:

https://github.com/astropy/photutils/blob/88437049ef3909e9ced1c5ed3c06076a86d17020/photutils/detection/irafstarfinder.py#L163

and

https://github.com/astropy/photutils/blob/88437049ef3909e9ced1c5ed3c06076a86d17020/photutils/detection/irafstarfinder.py#L176-L186

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 6c62e9fe..ca2f673b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,6 +9,11 @@ General
 New Features
 ^^^^^^^^^^^^
 
+- ``photutils.detection``
+
+  - Added a ``min_separation`` keyword to ``DAOStarFinder`` and
+    ``IRAFStarFinder``. [#1663]
+
 - ``photutils.morphology``
 
   - Added a ``wcs`` keyword to ``data_properties``. [#1648]
diff --git a/photutils/detection/core.py b/photutils/detection/core.py
index b871c4f4..27ce484c 100644
--- a/photutils/detection/core.py
+++ b/photutils/detection/core.py
@@ -69,7 +69,7 @@ class StarFinderBase(metaclass=abc.ABCMeta):
         .. _starfind: https://iraf.net/irafhelp.php?val=starfind
         """
         # define a local footprint for the peak finder
-        if min_separation == 0:  # daofind
+        if min_separation == 0.0:  # DAOStarFinder
             if isinstance(kernel, np.ndarray):
                 footprint = np.ones(kernel.shape)
             else:
diff --git a/photutils/detection/daofinder.py b/photutils/detection/daofinder.py
index 6032a2ab..17afc0ad 100644
--- a/photutils/detection/daofinder.py
+++ b/photutils/detection/daofinder.py
@@ -119,11 +119,15 @@ class DAOStarFinder(StarFinderBase):
             pixel values are negative. Therefore, setting ``peakmax`` to a
             non-positive value would result in exclusion of all objects.
 
-    xycoords : `None` or Nx2 `~numpy.ndarray`
+    xycoords : `None` or Nx2 `~numpy.ndarray`, optional
         The (x, y) pixel coordinates of the approximate centroid
         positions of identified sources. If ``xycoords`` are input, the
         algorithm will skip the source-finding step.
 
+    min_separation : float, optional
+        The minimum separation (in pixels) for detected objects. Note
+        that large values may result in long run times.
+
     See Also
     --------
     IRAFStarFinder
@@ -157,7 +161,8 @@ class DAOStarFinder(StarFinderBase):
     def __init__(self, threshold, fwhm, ratio=1.0, theta=0.0,
                  sigma_radius=1.5, sharplo=0.2, sharphi=1.0, roundlo=-1.0,
                  roundhi=1.0, sky=0.0, exclude_border=False,
-                 brightest=None, peakmax=None, xycoords=None):
+                 brightest=None, peakmax=None, xycoords=None,
+                 min_separation=0.0):
 
         if not np.isscalar(threshold):
             raise TypeError('threshold must be a scalar value.')
@@ -179,6 +184,10 @@ class DAOStarFinder(StarFinderBase):
         self.brightest = self._validate_brightest(brightest)
         self.peakmax = peakmax
 
+        if min_separation < 0:
+            raise ValueError('min_separation must be >= 0')
+        self.min_separation = min_separation
+
         if xycoords is not None:
             xycoords = np.asarray(xycoords)
             if xycoords.ndim != 2 or xycoords.shape[1] != 2:
@@ -208,6 +217,7 @@ class DAOStarFinder(StarFinderBase):
         if self.xycoords is None:
             xypos = self._find_stars(convolved_data, self.kernel,
                                      self.threshold_eff, mask=mask,
+                                     min_separation=self.min_separation,
                                      exclude_border=self.exclude_border)
         else:
             xypos = self.xycoords
diff --git a/photutils/detection/irafstarfinder.py b/photutils/detection/irafstarfinder.py
index fbca422e..dd96b0bb 100644
--- a/photutils/detection/irafstarfinder.py
+++ b/photutils/detection/irafstarfinder.py
@@ -40,8 +40,10 @@ class IRAFStarFinder(StarFinderBase):
         kernel in units of pixels.
 
     minsep_fwhm : float, optional
-        The minimum separation for detected objects in units of
-        ``fwhm``.
+        The separation (in units of ``fwhm``) for detected objects. The
+        minimum separation is calculated as ``int((fwhm * minsep_fwhm) +
+        0.5)`` and is clipped to a minimum value of 2. Note that large
+        values may result in long run times.
 
     sigma_radius : float, optional
         The truncation radius of the Gaussian kernel in units of sigma
@@ -87,11 +89,17 @@ class IRAFStarFinder(StarFinderBase):
             pixel values are negative. Therefore, setting ``peakmax`` to a
             non-positive value would result in exclusion of all objects.
 
-    xycoords : `None` or Nx2 `~numpy.ndarray`
+    xycoords : `None` or Nx2 `~numpy.ndarray`, optional
         The (x, y) pixel coordinates of the approximate centroid
         positions of identified sources. If ``xycoords`` are input, the
         algorithm will skip the source-finding step.
 
+    min_separation : `None` or float, optional
+        The minimum separation (in pixels) for detected objects. If
+        `None` then ``minsep_fwhm`` will be used, otherwise this keyword
+        overrides ``minsep_fwhm``. Note that large values may result in
+        long run times.
+
     Notes
     -----
     For the convolution step, this routine sets pixels beyond the image
@@ -131,7 +139,7 @@ class IRAFStarFinder(StarFinderBase):
     def __init__(self, threshold, fwhm, sigma_radius=1.5, minsep_fwhm=2.5,
                  sharplo=0.5, sharphi=2.0, roundlo=0.0, roundhi=0.2, sky=None,
                  exclude_border=False, brightest=None, peakmax=None,
-                 xycoords=None):
+                 xycoords=None, min_separation=None):
 
         if not np.isscalar(threshold):
             raise TypeError('threshold must be a scalar value.')
@@ -160,7 +168,14 @@ class IRAFStarFinder(StarFinderBase):
 
         self.kernel = _StarFinderKernel(self.fwhm, ratio=1.0, theta=0.0,
                                         sigma_radius=self.sigma_radius)
-        self.min_separation = max(2, int((self.fwhm * self.minsep_fwhm) + 0.5))
+
+        if min_separation is not None:
+            if min_separation < 0:
+                raise ValueError('min_separation must be >= 0')
+            self.min_separation = min_separation
+        else:
+            self.min_separation = max(2, int((self.fwhm * self.minsep_fwhm)
+                                             + 0.5))
 
     @staticmethod
     def _validate_brightest(brightest):
@@ -472,8 +487,12 @@ class _IRAFStarFinderCatalog:
 
     @lazyproperty
     def roundness(self):
-        return np.sqrt(self.mu_diff**2
-                       + 4.0 * self.moments_central[:, 1, 1]**2) / self.mu_sum
+        # ignore divide-by-zero RuntimeWarning
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', RuntimeWarning)
+            return (np.sqrt(self.mu_diff**2
+                            + 4.0 * self.moments_central[:, 1, 1]**2)
+                    / self.mu_sum)
 
     @lazyproperty
     def sharpness(self):
diff --git a/photutils/detection/starfinder.py b/photutils/detection/starfinder.py
index 44aace01..f8a86266 100644
--- a/photutils/detection/starfinder.py
+++ b/photutils/detection/starfinder.py
@@ -33,7 +33,8 @@ class StarFinder(StarFinderBase):
         A 2D array of the PSF kernel.
 
     min_separation : float, optional
-        The minimum separation for detected objects in pixels.
+        The minimum separation (in pixels) for detected objects. Note
+        that large values may result in long run times.
 
     exclude_border : bool, optional
         Whether to exclude sources found within half the size of the


Test patch:
diff --git a/photutils/detection/tests/test_daofinder.py b/photutils/detection/tests/test_daofinder.py
index f4766cc7..e83133d5 100644
--- a/photutils/detection/tests/test_daofinder.py
+++ b/photutils/detection/tests/test_daofinder.py
@@ -182,3 +182,17 @@ class TestDAOStarFinder:
         xycoords = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
         with pytest.raises(ValueError):
             DAOStarFinder(threshold=10, fwhm=1.5, xycoords=xycoords)
+
+    def test_min_separation(self):
+        threshold = 5
+        fwhm = 1.0
+        finder1 = DAOStarFinder(threshold, fwhm, sigma_radius=1.5)
+        tbl1 = finder1(DATA)
+        finder2 = DAOStarFinder(threshold, fwhm, sigma_radius=1.5,
+                                min_separation=3.0)
+        tbl2 = finder2(DATA)
+        assert len(tbl1) > len(tbl2)
+
+        match = 'min_separation must be >= 0'
+        with pytest.raises(ValueError, match=match):
+            DAOStarFinder(threshold=10, fwhm=1.5, min_separation=-1.0)
diff --git a/photutils/detection/tests/test_irafstarfinder.py b/photutils/detection/tests/test_irafstarfinder.py
index 88bff451..c03c93e7 100644
--- a/photutils/detection/tests/test_irafstarfinder.py
+++ b/photutils/detection/tests/test_irafstarfinder.py
@@ -150,3 +150,22 @@ class TestIRAFStarFinder:
         xycoords = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
         with pytest.raises(ValueError):
             IRAFStarFinder(threshold=10, fwhm=1.5, xycoords=xycoords)
+
+    def test_min_separation(self):
+        threshold = 5
+        fwhm = 1.0
+        starfinder1 = IRAFStarFinder(threshold, fwhm, sigma_radius=1.5)
+        tbl1 = starfinder1(DATA)
+        starfinder2 = IRAFStarFinder(threshold, fwhm, sigma_radius=1.5,
+                                     min_separation=3.0)
+        tbl2 = starfinder2(DATA)
+        assert np.all(tbl1 == tbl2)
+
+        starfinder3 = IRAFStarFinder(threshold, fwhm, sigma_radius=1.5,
+                                     min_separation=2.0)
+        tbl3 = starfinder3(DATA)
+        assert len(tbl3) > len(tbl2)
+
+        match = 'min_separation must be >= 0'
+        with pytest.raises(ValueError, match=match):
+            IRAFStarFinder(threshold=10, fwhm=1.5, min_separation=-1.0)