Repository: astropy/photutils

Issue:
Sources that match sharplo(hi) and roundlo(hi) limits should be included
Currently star finding code filters out sources that have sharpness or roundness match one of the limits for these parameter, see https://github.com/astropy/photutils/blob/c15b004be4ecd1256f8a8db8c9c7a52e795fe3a8/photutils/detection/irafstarfinder.py#L580-L583

I believe the strict inequalities in the code above should be replaced with `>=` and `<=`.

Here is an example of a source that should be detected:

```python
In [1]: import numpy as np

In [2]: from photutils import detection

In [3]: d = np.zeros((200, 200))

In [4]: x = 121
   ...: y = 98
   ...: d[y-1:y+2, x-1:x+2] = [
   ...:     [0.1, 0.6, 0.1],
   ...:     [0.6, 0.8, 0.6],
   ...:     [0.1, 0.6, 0.1],
   ...: ]

In [5]: f = detection.IRAFStarFinder(0, 2.5, roundlo=0)

In [6]: f.find_stars(d)
WARNING: NoDetectionsWarning: Sources were found, but none pass the sharpness, roundness, or peakmax criteria [photutils.detection.irafstarfinder]

In [7]: f = detection.IRAFStarFinder(0, 2.5, roundlo=-1e-12)

In [8]: f.find_stars(d)
Out[8]:
<QTable length=1>
  id  xcentroid ycentroid        fwhm            sharpness      roundness    pa    npix   peak         flux                mag
int64  float64   float64       float64            float64        float64  float64 int64 float64      float64             float64
----- --------- --------- ------------------ ------------------ --------- ------- ----- ------- ------------------ -------------------
    1     121.0      98.0 1.5698800300206328 0.6279520120082531       0.0     0.0     9     0.8 3.5999999999999996 -1.3907562519182182
```
That is, perfectly round stars are excluded by default.

Discussion:
larrybradley: @mcara Do you recall what the old IRAF routines use?  This is a breaking change, but I suppose *could* be classified as a bug.
mcara: Sorry, I replied in the issue: https://github.com/astropy/photutils/issues/1977#issuecomment-2568689761
mcara: > This is a breaking change...

This could break some idealized tests, just like the one I am using in https://github.com/astropy/photutils/issues/1977 but in practice, what is the chance to have a perfectly round star (or a star with ellipticity precisely 2) in real observations? So, I believe this PR will have a minor effect, except for helping avoid this kind of tricks: https://github.com/spacetelescope/jwst/blob/6d0c2d50c72d32f9a16bf30264d3528bff53e36e/jwst/tweakreg/tests/test_tweakreg.py#L99 which seem unnatural given that the lower limit for roundness is 0 (it cannot be negative in the original IRAF code) yet I _have to_ set this limit to a negative value in order to include 0.


larrybradley: This also needs to be rebased due to a conflict in the changelog.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 0bbe6261..b85902d9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -51,6 +51,11 @@ API Changes
     different along the x and y edges if the kernel shape is rectangular.
     [#1957]
 
+  - Detected sources that match interval ends for sharpness, roundness, and
+    maximum peak values (``sharplo``, ``sharphi``, ``roundlo``, ``roundhi``,
+    and ``peakmax``) are now included in the returned table of detected
+    sources by ``DAOStarFinder`` and ``IRAFStarFinder``. [#1978]
+
 - ``photutils.morphology``
 
   - The ``gini`` function now returns zero instead of NaN if the
diff --git a/photutils/detection/daofinder.py b/photutils/detection/daofinder.py
index 1bdb2a3a..6c8474fd 100644
--- a/photutils/detection/daofinder.py
+++ b/photutils/detection/daofinder.py
@@ -739,14 +739,14 @@ class _DAOStarFinderCatalog:
             return None
 
         # filter based on sharpness, roundness, and peakmax
-        mask = ((newcat.sharpness > newcat.sharplo)
-                & (newcat.sharpness < newcat.sharphi)
-                & (newcat.roundness1 > newcat.roundlo)
-                & (newcat.roundness1 < newcat.roundhi)
-                & (newcat.roundness2 > newcat.roundlo)
-                & (newcat.roundness2 < newcat.roundhi))
+        mask = ((newcat.sharpness >= newcat.sharplo)
+                & (newcat.sharpness <= newcat.sharphi)
+                & (newcat.roundness1 >= newcat.roundlo)
+                & (newcat.roundness1 <= newcat.roundhi)
+                & (newcat.roundness2 >= newcat.roundlo)
+                & (newcat.roundness2 <= newcat.roundhi))
         if newcat.peakmax is not None:
-            mask &= (newcat.peak < newcat.peakmax)
+            mask &= (newcat.peak <= newcat.peakmax)
         newcat = newcat[mask]
 
         if len(newcat) == 0:
diff --git a/photutils/detection/irafstarfinder.py b/photutils/detection/irafstarfinder.py
index ebb700ce..a505a015 100644
--- a/photutils/detection/irafstarfinder.py
+++ b/photutils/detection/irafstarfinder.py
@@ -577,12 +577,12 @@ class _IRAFStarFinderCatalog:
             return None
 
         # filter based on sharpness, roundness, and peakmax
-        mask = ((newcat.sharpness > newcat.sharplo)
-                & (newcat.sharpness < newcat.sharphi)
-                & (newcat.roundness > newcat.roundlo)
-                & (newcat.roundness < newcat.roundhi))
+        mask = ((newcat.sharpness >= newcat.sharplo)
+                & (newcat.sharpness <= newcat.sharphi)
+                & (newcat.roundness >= newcat.roundlo)
+                & (newcat.roundness <= newcat.roundhi))
         if newcat.peakmax is not None:
-            mask &= (newcat.peak < newcat.peakmax)
+            mask &= (newcat.peak <= newcat.peakmax)
         newcat = newcat[mask]
 
         if len(newcat) == 0:


Test patch:
diff --git a/photutils/detection/tests/test_daofinder.py b/photutils/detection/tests/test_daofinder.py
index 570d6cb9..decba652 100644
--- a/photutils/detection/tests/test_daofinder.py
+++ b/photutils/detection/tests/test_daofinder.py
@@ -205,3 +205,30 @@ class TestDAOStarFinder:
         assert cat.isscalar
         flux = cat.flux[0]  # evaluate the flux so it can be sliced
         assert cat[0].flux == flux
+
+    def test_interval_ends_included(self):
+        # https://github.com/astropy/photutils/issues/1977
+        data = np.zeros((46, 64))
+
+        x = 33
+        y = 21
+
+        data[y - 1: y + 2, x - 1: x + 2] = [
+            [1.0, 2.0, 1.0],
+            [2.0, 1.0e20, 2.0],
+            [1.0, 2.0, 1.0],
+        ]
+
+        finder = DAOStarFinder(
+            threshold=0,
+            fwhm=2.5,
+            roundlo=0,
+            sharphi=1.407913491884342,
+            peakmax=1.0e20
+        )
+        tbl = finder.find_stars(data)
+
+        assert len(tbl) == 1
+        assert abs(tbl[0]['roundness1']) < 1.e-15
+        assert abs(tbl[0]['roundness2']) == 0.0
+        assert abs(tbl[0]['peak']) == 1.0e20
diff --git a/photutils/detection/tests/test_irafstarfinder.py b/photutils/detection/tests/test_irafstarfinder.py
index 351b6526..e62b34c4 100644
--- a/photutils/detection/tests/test_irafstarfinder.py
+++ b/photutils/detection/tests/test_irafstarfinder.py
@@ -199,3 +199,28 @@ class TestIRAFStarFinder:
         data += model3(xx, yy)
         tbl = finder(data)
         assert len(tbl) == 1
+
+    def test_interval_ends_included(self):
+        # https://github.com/astropy/photutils/issues/1977
+        data = np.zeros((46, 64))
+
+        x = 33
+        y = 21
+        data[y - 1: y + 2, x - 1: x + 2] = [
+            [0.1, 0.6, 0.1],
+            [0.6, 0.8, 0.6],
+            [0.1, 0.6, 0.1],
+        ]
+
+        finder = IRAFStarFinder(
+            threshold=0,
+            fwhm=2.5,
+            roundlo=0,
+            peakmax=0.8
+        )
+        tbl = finder.find_stars(data)
+
+        assert len(tbl) == 1
+        assert abs(tbl[0]['roundness']) == 0.0
+        assert abs(tbl[0]['pa']) == 0.0
+        assert abs(tbl[0]['peak']) == 0.8