Source code for spey.hypothesis_testing.upper_limits

"""Tools for computing upper limit on parameter of interest"""

import logging
import warnings
from functools import partial
from typing import Callable, List, Tuple, Union, Literal

import numpy as np
import scipy

from spey.hypothesis_testing.test_statistics import compute_teststatistics
from spey.utils import ExpectationType

from .asymptotic_calculator import compute_asymptotic_confidence_level

__all__ = ["find_poi_upper_limit", "find_root_limits"]


def __dir__():
    return __all__


log = logging.getLogger("Spey")

# pylint: disable=W1203,C0103


[docs] class ComputerWrapper: """ Wrapper for the computer function to track inputs and outputs Args: computer (``Callable[[float], float]``): desired function to be computed """
[docs] def __init__(self, computer: Callable[[float], float]): self.computer = computer self._results = []
def __call__(self, value: float) -> float: """Compute the input function and return its value""" self._results.append((value, self.computer(value))) return self[-1] def __getitem__(self, item: int) -> float: """Return result""" return self._results[item][1] def get_value(self, index: int) -> float: """ Get input value of the execution Args: index (``int``): index of the execution Returns: ``float``: returns the input value of the execution """ return self._results[index][0]
[docs] def find_root_limits( computer: Callable[[float], float], loc: float = 0.0, low_ini: float = 1.0, hig_ini: float = 1.0, low_bound: float = 1e-10, hig_bound: float = 1e5, ) -> Tuple[ComputerWrapper, ComputerWrapper]: """ Find upper and lower bracket limits for the root finding algorithm Args: computer (``Callable[[float], float]``): Function that we want to find the root loc (``float``, default ``0.0``): location of the root e.g. ``0.95`` for :math:`1-CL_s` value low_ini (``float``, default ``1.0``): Initial value for low bracket hig_ini (``float``, default ``1.0``): initial value for high bracket low_bound (``float``, default ``1e-10``): Stop the execution below this value hig_bound (``float``, default ``1e5``): Stop the execution above this value Returns: ``Tuple[ComputerWrapper, ComputerWrapper]``: Returns lower and upper limits for the bracketing within a computer wrapper object. """ assert callable(computer), "Invalid input. Computer must be callable." low, hig = low_ini, hig_ini low_computer = ComputerWrapper(computer) while low_computer(low) > loc: low *= 0.5 if low < low_bound: break log.debug(f"Low results: {low_computer._results}") hig_computer = ComputerWrapper(computer) while hig_computer(hig) < loc: hig *= 2.0 if hig > hig_bound: break log.debug(f"High results: {hig_computer._results}") return low_computer, hig_computer
[docs] def find_poi_upper_limit( maximum_likelihood: Tuple[float, float], logpdf: Callable[[float], float], maximum_asimov_likelihood: Tuple[float, float], asimov_logpdf: Callable[[float], float], expected: ExpectationType, confidence_level: float = 0.95, allow_negative_signal: bool = True, low_init: float = 1.0, hig_init: float = 1.0, expected_pvalue: Literal["nominal", "1sigma", "2sigma"] = "nominal", maxiter: int = 10000, ) -> Union[float, List[float]]: r""" Find upper limit for parameter of interest, :math:`\mu` Args: maximum_likelihood (``Tuple[float, float]``): Tuple including :math:`\hat\mu` and minimum negative log-likelihood. logpdf (``Callable[[float], float]``): log-likelihood as function of POI, :math:`\log\mathcal{L}(\mu)` maximum_asimov_likelihood (``Tuple[float, float]``): Tuple including :math:`\hat\mu_A` and minimum negative log-likelihood for Asimov data. asimov_logpdf (``Callable[[float], float]``): log-likelihood as function of POI, :math:`\log\mathcal{L}_A(\mu)` for Asimov data. expected (~spey.ExpectationType): Sets which values the fitting algorithm should focus and p-values to be computed. * :obj:`~spey.ExpectationType.observed`: Computes the p-values with via post-fit prescriotion which means that the experimental data will be assumed to be the truth * :obj:`~spey.ExpectationType.aposteriori`: Computes the expected p-values with via post-fit prescriotion which means that the experimental data will be assumed to be the truth. * :obj:`~spey.ExpectationType.apriori`: Computes the expected p-values with via pre-fit prescription which means that the SM will be assumed to be the truth. confidence_level (``float``, default ``0.95``): Determines the confidence level of the upper limit i.e. the value of :math:`1-CL_s`. It needs to be between ``[0,1]``. allow_negative_signal (``bool``, default ``True``): _description_ low_init (``float``, default ``None``): Lower limit for the search algorithm to start hig_init (``float``, default ``None``): Upper limit for the search algorithm to start expected_pvalue (``Text``, default ``"nominal"``): In case of :obj:`~spey.ExpectationType.aposteriori` and :obj:`~spey.ExpectationType.apriori` expectation, gives the choice to find excluded upper limit for statistical deviations as well. * ``"nominal"``: only find the upper limit for the central p-value. Returns a single value. * ``"1sigma"``: find the upper limit for central p-value and :math:`1\sigma` fluctuation from background. Returns 3 values. * ``"2sigma"``: find the upper limit for central p-value and :math:`1\sigma` and :math:`2\sigma` fluctuation from background. Returns 5 values. .. note:: For ``expected=spey.ExpectationType.observed``, ``expected_pvalue`` argument will be overwritten to ``"nominal"``. maxiter (``int``, default ``10000``): Maximum iteration limit for the optimiser. Returns: ``Union[float, List[float]]``: In case of nominal values it returns a single value for the upper limit. In case of ``expected_pvalue="1sigma"`` or ``expected_pvalue="2sigma"`` it will return a list of multiple upper limit values for fluctuations as well as the central value. The output order is :math:`-2\sigma` value, :math:`-1\sigma` value, central value, :math:`1\sigma` and :math:`2\sigma` value. """ assert expected_pvalue in [ "nominal", "1sigma", "2sigma", ], f"Unknown pvalue range {expected_pvalue}" if expected is ExpectationType.observed: expected_pvalue = "nominal" test_stat = "q" if allow_negative_signal else "qtilde" def computer(poi_test: float, pvalue_idx: int) -> float: """Compute 1 - CLs(POI) = `confidence_level`""" _, sqrt_qmuA, delta_teststat = compute_teststatistics( poi_test, maximum_likelihood, logpdf, maximum_asimov_likelihood, asimov_logpdf, test_stat, ) pvalue = list( map( lambda x: 1.0 - x, compute_asymptotic_confidence_level(sqrt_qmuA, delta_teststat, test_stat)[ 0 if expected == ExpectationType.observed else 1 ], ) ) return pvalue[pvalue_idx] - confidence_level result = [] index_range = { "nominal": [0 if expected is ExpectationType.observed else 2], "1sigma": range(1, 4), "2sigma": range(0, 5), } for pvalue_idx in index_range[expected_pvalue]: log.debug(f"Running for p-value idx: {pvalue_idx}") comp = partial(computer, pvalue_idx=pvalue_idx) # Set an upper bound for the computation hig_bound = 1e5 low, hig = find_root_limits( comp, loc=0.0, low_ini=low_init, hig_ini=hig_init, hig_bound=hig_bound ) log.debug(f"low: {low[-1]}, hig: {hig[-1]}") # Check if its possible to find roots if np.sign(low[-1]) * np.sign(hig[-1]) > 0.0: log.warning( "Can not find the roots of the function, returning `inf`" + f"\n hig, low must bracket a root f({low.get_value(-1):.5e})={low[-1]:.5e}, " + f"f({hig.get_value(-1):.5e})={hig[-1]:.5e}. " + "This is likely due to low number of signal yields." * (hig.get_value(-1) >= hig_bound), ) result.append(np.inf) continue with warnings.catch_warnings(record=True) as w: x0, r = scipy.optimize.toms748( comp, low.get_value(-1), hig.get_value(-1), k=2, xtol=2e-12, rtol=1e-4, full_output=True, maxiter=maxiter, ) log.debug("Warnings:") if log.level == logging.DEBUG: for warning in w: log.debug(f"\t{warning.message}") log.debug("<><>" * 10) if not r.converged: log.warning(f"Optimiser did not converge.\n{r}") result.append(x0) return result if len(result) > 1 else result[0]