Repository: astroML/astroML

Issue:
Minor edge case in Cminus leads to nans in output
Encountered a situation where cuml_x and cuml_y calculated by astroML.lumfunc.Cminus returns nans. This might only happen with bad luck.

Code to reproduce:
```
from astroML.lumfunc import Cminus
x, y, xmax, ymax = [10.02, 10.00], [14.97, 14.99], [10.03, 10.01], [14.98, 15.00]
Cminus(x, y, xmax, ymax)
```

Output:
`(array([0., 0.]), array([0., 0.]), array([ 0., nan]), array([ 0., nan]))`

Astrophysical context for the above input:
I would like to use the CMinus method to recover the true distributions of distance modulus (DM) and absolute magnitude (Mr), since I am limited by a flux limit of r < 25. Letting x<-DM and y<-Mr, I can calculate xmax and ymax (xmax = r_lim - y and vice versa). 

Since this example is at a peculiar case where the` x[0] > xmax[1]` this means `Nx[1] = np.sum(x[:1] < xmax[1])` to be 0, which causes `cuml_x = np.cumprod(1. + 1. / Nx)` evaluate to nan (see [source](https://github.com/astroML/astroML/blob/dbc4311fcd144cbd62dd1e32803e2b7738627f6b/astroML/lumfunc.py#L74)).

I think this only happens when, after [sorting ](https://github.com/astroML/astroML/blob/dbc4311fcd144cbd62dd1e32803e2b7738627f6b/astroML/lumfunc.py#L67), this unique case comes up where `np.sum(x[:j] < xmax[j]) == 0`, usually for very small j. Saw this while using simulated LSST data. I'm not familiar enough with Cminus to come up with a fix for this edge case, and seemed to show up most often when using astroML.lumfuc.bootstrap_Cminus.

Comments:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 9399114..0418d9d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,9 @@
+1.0.2 (unreleased)
+==================
+
+- Fix bug in ``lumfunc.Cminus`` that lead to return NaN values. [#237]
+
+
 1.0.1 (2020-09-09)
 ==================
 
diff --git a/astroML/lumfunc.py b/astroML/lumfunc.py
index 18a363e..537a385 100644
--- a/astroML/lumfunc.py
+++ b/astroML/lumfunc.py
@@ -71,10 +71,17 @@ def Cminus(x, y, xmax, ymax):
     ymax = ymax[i_sort]
 
     for j in range(1, Nall):
-        Ny[j] = np.sum(x[:j] < xmax[j])
+        # Making sure we don't divide with 0 later
+        objects = np.sum(x[:j] < xmax[j])
+        if objects:
+            Ny[j] = objects
+        else:
+            Ny[j] = np.inf
+
     Ny[0] = np.inf
     cuml_y = np.cumprod(1. + 1. / Ny)
-    Ny[0] = 0
+
+    Ny[np.isinf(Ny)] = 0
 
     # renormalize
     cuml_y *= Nall / cuml_y[-1]
@@ -87,10 +94,17 @@ def Cminus(x, y, xmax, ymax):
     ymax = ymax[i_sort]
 
     for i in range(1, Nall):
-        Nx[i] = np.sum(y[:i] < ymax[i])
+        # Making sure we don't divide with 0 later
+        objects = np.sum(y[:i] < ymax[i])
+        if objects:
+            Nx[i] = objects
+        else:
+            Nx[i] = np.inf
+
     Nx[0] = np.inf
     cuml_x = np.cumprod(1. + 1. / Nx)
-    Nx[0] = 0
+
+    Nx[np.isinf(Nx)] = 0
 
     # renormalize
     cuml_x *= Nall / cuml_x[-1]


Test patch:
diff --git a/astroML/tests/test_lumfunc.py b/astroML/tests/test_lumfunc.py
new file mode 100644
index 0000000..62ab666
--- /dev/null
+++ b/astroML/tests/test_lumfunc.py
@@ -0,0 +1,13 @@
+import numpy as np
+from astroML.lumfunc import Cminus
+
+
+def test_cminus_nans():
+    # Regression test for https://github.com/astroML/astroML/issues/234
+
+    x = [10.02, 10.00]
+    y = [14.97, 14.99]
+    xmax = [10.03, 10.01]
+    ymax = [14.98, 15.00]
+
+    assert np.isfinite(np.sum(Cminus(x, y, xmax, ymax)))