Repository: scikit-hep/pyhf

Issue:
Fix documentation for qmu_tilde test statistic
> Looking [at the docs](https://scikit-hep.org/pyhf/_generated/pyhf.infer.test_statistics.qmu_tilde.html), it seems μ^>μ is the only time we can get q~μ=0 (whether asimov or not).

The docs don't actually match equation (16) in the paper, where the first line is $\hat{\mu} \leq \mu$.

_Originally posted by @masonproffitt in https://github.com/scikit-hep/pyhf/issues/1992#issuecomment-1241380149_

As it turns out, the docs don't match the code: https://github.com/scikit-hep/pyhf/blob/cbd68c829748fff11bf44adac2ad9e9b15068af2/src/pyhf/infer/test_statistics.py#L30-L32


Discussion:

Resolving patch:
diff --git a/src/pyhf/infer/calculators.py b/src/pyhf/infer/calculators.py
--- a/src/pyhf/infer/calculators.py
+++ b/src/pyhf/infer/calculators.py
@@ -418,8 +418,9 @@ def _false_case():
                 teststat = (qmu - qmu_A) / (2 * self.sqrtqmuA_v)
                 return teststat
 
+            # Use '<=' rather than '<' to avoid Issue #1992
             teststat = tensorlib.conditional(
-                (sqrtqmu_v < self.sqrtqmuA_v), _true_case, _false_case
+                (sqrtqmu_v <= self.sqrtqmuA_v), _true_case, _false_case
             )
         return tensorlib.astensor(teststat)
 


Test patch:
diff --git a/src/pyhf/infer/test_statistics.py b/src/pyhf/infer/test_statistics.py
--- a/src/pyhf/infer/test_statistics.py
+++ b/src/pyhf/infer/test_statistics.py
@@ -71,7 +71,7 @@ def qmu(mu, data, pdf, init_pars, par_bounds, fixed_params, return_fitted_pars=F
 
        \begin{equation}
           q_{\mu} = \left\{\begin{array}{ll}
-          -2\ln\lambda\left(\mu\right), &\hat{\mu} < \mu,\\
+          -2\ln\lambda\left(\mu\right), &\hat{\mu} \leq \mu,\\
           0, & \hat{\mu} > \mu
           \end{array}\right.
         \end{equation}
@@ -160,7 +160,7 @@ def qmu_tilde(
 
        \begin{equation}
           \tilde{q}_{\mu} = \left\{\begin{array}{ll}
-          -2\ln\tilde{\lambda}\left(\mu\right), &\hat{\mu} < \mu,\\
+          -2\ln\tilde{\lambda}\left(\mu\right), &\hat{\mu} \leq \mu,\\
           0, & \hat{\mu} > \mu
           \end{array}\right.
         \end{equation}
diff --git a/tests/test_infer.py b/tests/test_infer.py
--- a/tests/test_infer.py
+++ b/tests/test_infer.py
@@ -476,3 +476,37 @@ def test_fixed_poi(tmpdir, hypotest_args):
     pdf.config.param_set('mu').suggested_fixed = [True]
     with pytest.raises(pyhf.exceptions.InvalidModel):
         pyhf.infer.hypotest(*hypotest_args)
+
+
+def test_teststat_nan_guard():
+    # Example from Issue #1992
+    model = pyhf.simplemodels.uncorrelated_background(
+        signal=[1.0], bkg=[1.0], bkg_uncertainty=[1.0]
+    )
+    observations = [2]
+    test_poi = 0.0
+    data = observations + model.config.auxdata
+    init_pars = model.config.suggested_init()
+    par_bounds = model.config.suggested_bounds()
+    fixed_params = model.config.suggested_fixed()
+
+    test_stat = pyhf.infer.test_statistics.qmu_tilde(
+        test_poi, data, model, init_pars, par_bounds, fixed_params
+    )
+    assert test_stat == pytest.approx(0.0)
+    asymptotic_calculator = pyhf.infer.calculators.AsymptoticCalculator(
+        data, model, test_stat="qtilde"
+    )
+    # ensure not nan
+    assert ~np.isnan(asymptotic_calculator.teststatistic(test_poi))
+    assert asymptotic_calculator.teststatistic(test_poi) == pytest.approx(0.0)
+
+    # Example from Issue #529
+    model = pyhf.simplemodels.uncorrelated_background([0.005], [28.0], [5.0])
+    test_poi = 1.0
+    data = [28.0] + model.config.auxdata
+
+    test_results = pyhf.infer.hypotest(
+        test_poi, data, model, test_stat="qtilde", return_expected=True
+    )
+    assert all(~np.isnan(result) for result in test_results)