Source code for spey.interface.statistical_model

"""Statistical Model wrapper class"""
import logging
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

import numpy as np

from spey.base.backend_base import BackendBase
from spey.base.hypotest_base import HypothesisTestingBase
from spey.optimizer.core import fit
from spey.system.exceptions import (
    CombinerNotAvailable,
    MethodNotAvailable,
    UnknownCrossSection,
)
from spey.utils import ExpectationType

__all__ = ["StatisticalModel", "statistical_model_wrapper"]


def __dir__():
    return __all__


log = logging.getLogger("Spey")

# pylint: disable=W1203


[docs] class StatisticalModel(HypothesisTestingBase): r""" Statistical model base. This class wraps around the various statistical model backends available through `spey`'s plugin system. Each backend has to inherit :class:`~spey.BackendBase` which sets certain requirements on the available functionality to be used for hypothesis testing. These requirements are such as accessibility to log-likelihood, :math:`\log\mathcal{L}`, it's derivative with respect to :math:`\mu` and nuisance parameters, :math:`\partial_\theta\log\mathcal{L}`, its Hessian and Assimov data generation. Depending on availablility :class:`~spey.StatisticalModel` will take propriate action to perform requested computation. The goal of this class is to collect all different backends under same roof in order to perform combination of different likelihood recipies. Args: backend (~spey.BackendBase): Statistical model backend analysis (``Text``): Unique identifier of the statistical model. This attribue will be used for book keeping purposes. xsection (``float``, default ``np.nan``): cross section, unit is determined by the user. Cross section value is only used for computing upper limit on excluded cross-section value. ntoys (``int``, default ``1000``): Number of toy samples for hypothesis testing. (Only used for toy-based hypothesis testing) Raises: :obj:`AssertionError`: If the given backend does not inherit :class:`~spey.BackendBase` Returns: ~spey.StatisticalModel: General statistical model object that wraps around different likelihood prescriptions. """ __slots__ = ["_backend", "xsection", "analysis"] def __init__( self, backend: BackendBase, analysis: str, xsection: float = np.nan, ntoys: int = 1000, ): assert isinstance(backend, BackendBase), "Invalid backend" self._backend: BackendBase = backend self.xsection: float = xsection """Value of the cross section, unit is defined by the user.""" self.analysis: str = analysis """Unique identifier as analysis name""" super().__init__(ntoys=ntoys) def __repr__(self): calc = f"calculators={self.available_calculators}" if np.isnan(self.xsection): return ( f"StatisticalModel(analysis='{self.analysis}', backend={self.backend_type}, " + f"{calc})" ) return ( f"StatisticalModel(analysis='{self.analysis}', " f"xsection={self.xsection:.3e} [au], backend={self.backend_type}, {calc})" ) @property def backend(self) -> BackendBase: """Accessor to the backend""" return self._backend @property def backend_type(self) -> str: """Return type of the backend""" return getattr(self.backend, "name", self.backend.__class__.__name__) @property def is_asymptotic_calculator_available(self) -> bool: """Check if Asymptotic calculator is available for the backend""" return self.backend.expected_data != BackendBase.expected_data or ( self.backend.asimov_negative_loglikelihood != BackendBase.asimov_negative_loglikelihood and self.backend.minimize_asimov_negative_loglikelihood != BackendBase.minimize_asimov_negative_loglikelihood ) @property def is_toy_calculator_available(self) -> bool: """Check if Toy calculator is available for the backend""" return self.backend.get_sampler != BackendBase.get_sampler @property def is_chi_square_calculator_available(self) -> bool: """Check if chi-square calculator is available for the backend""" return True @property def available_calculators(self) -> List[str]: """ Retruns available calculator names i.e. ``'toy'``, ``'asymptotic'`` and ``'chi_square'``. """ calc = ["toy"] * self.is_toy_calculator_available calc += ["asymptotic"] * self.is_asymptotic_calculator_available calc += ["chi_square"] * self.is_chi_square_calculator_available return calc @property def is_alive(self) -> bool: """Returns True if at least one bin has non-zero signal yield.""" return self.backend.is_alive
[docs] def excluded_cross_section( self, expected: ExpectationType = ExpectationType.observed ) -> float: """ Compute excluded cross section value at 95% CL Args: 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 (default). * :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. Raises: ~spey.system.exceptions.UnknownCrossSection: If the cross-section is ``nan``. Returns: ``float``: Returns the upper limit at 95% CL on cross section value where the unit is defined by the user. """ if np.isnan(self.xsection): raise UnknownCrossSection("Cross-section value has not been initialised.") return ( self.poi_upper_limit(expected=expected, confidence_level=0.95) * self.xsection )
@property def s95exp(self) -> float: """ Compute excluded cross section value at 95% CL with :obj:`~spey.ExpectationType.apriori` expectation. See :func:`~spey.StatisticalModel.excluded_cross_section` for reference. Raises: ~spey.system.exceptions.UnknownCrossSection: If the cross-section is ``nan``. """ return self.excluded_cross_section(ExpectationType.apriori) @property def s95obs(self) -> float: """ Compute excluded cross section value at 95% CL with :obj:`~spey.ExpectationType.observed` expectation. See :func:`~spey.StatisticalModel.excluded_cross_section` for reference. Raises: ~spey.system.exceptions.UnknownCrossSection: If the cross-section is ``nan``. """ return self.excluded_cross_section(ExpectationType.observed)
[docs] def prepare_for_fit( self, data: Optional[Union[List[float], np.ndarray]] = None, expected: ExpectationType = ExpectationType.observed, allow_negative_signal: Optional[bool] = True, **kwargs, ) -> Dict: r""" Prepare backend for the optimiser. Args: data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit 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 (default). * :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. allow_negative_signal (``bool``, default ``True``): If ``True`` :math:`\hat\mu` value will be allowed to be negative. Returns: ``Dict``: Dictionary of necessary toolset for the fit. objective function, ``"func"``, use gradient boolean, ``"do_grad"`` and function to compute negative log-likelihood with given fit parameters, ``"nll"``. """ do_grad = kwargs.pop("do_grad", True) try: objective_and_grad = self.backend.get_objective_function( expected=expected, data=data, do_grad=do_grad ) except NotImplementedError: log.debug("Gradient is not available, will not be included in computation.") do_grad = False objective_and_grad = self.backend.get_objective_function( expected=expected, data=data, do_grad=do_grad ) constraints = kwargs.pop("constraints", []) if hasattr(self.backend, "constraints"): for constraint in self.backend.constraints: constraints.append(constraint) return { "func": objective_and_grad, "do_grad": do_grad, "model_configuration": self.backend.config( allow_negative_signal=allow_negative_signal ), "logpdf": self.backend.get_logpdf_func(expected=expected, data=data), "constraints": constraints, **kwargs, }
[docs] def likelihood( self, poi_test: float = 1.0, expected: ExpectationType = ExpectationType.observed, return_nll: bool = True, data: Optional[Union[List[float], np.ndarray]] = None, init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> float: r""" Compute the likelihood of the statistical model at a fixed parameter of interest Args: poi_test (``float``, default ``1.0``): parameter of interest or signal strength, :math:`\mu`. 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 (default). * :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. return_nll (``bool``, default ``True``): If ``True``, returns negative log-likelihood value. if ``False`` returns likelihood value. data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit. If ``None`` data will be set according to ``expected`` input. init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Returns: ``float``: Likelihood of the statistical model at a fixed signal strength. """ fit_opts = self.prepare_for_fit(expected=expected, data=data, **kwargs) if ( fit_opts["model_configuration"].npar == 1 and fit_opts["model_configuration"].poi_index is not None ): logpdf = fit_opts["logpdf"]([poi_test]) else: logpdf, _ = fit( **fit_opts, initial_parameters=init_pars, bounds=par_bounds, fixed_poi_value=poi_test, ) return -logpdf if return_nll else np.exp(logpdf)
[docs] def generate_asimov_data( self, expected: ExpectationType = ExpectationType.observed, test_statistic: str = "qtilde", init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> List[float]: r""" Generate Asimov data for the statistical model. This function generates a set of parameters (nuisance and poi i.e. :math:`\theta` and :math:`\mu`) with respect to ``test_statistic`` input which determines the value of :math:`\mu` i.e. if ``test_statistic="q0"`` :math:`\mu=1` and 0 for anything else. The objective function is used to optimize the statistical model to find the fit parameters for fixed poi optimisation. Then fit parameters are used to retreive the expected data through :func:`~spey.BackendBase.expected_data` function. Args: 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 (default). * :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. test_statistic (``Text``, default ``"qtilde"``): test statistics. * ``'qtilde'``: (default) performs the calculation using the alternative test statistic, :math:`\tilde{q}_{\mu}`, see eq. (62) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu_tilde`). .. warning:: Note that this assumes that :math:`\hat\mu\geq0`, hence ``allow_negative_signal`` assumed to be ``False``. If this function has been executed by user, ``spey`` assumes that this is taken care of throughout the external code consistently. Whilst computing p-values or upper limit on :math:`\mu` through ``spey`` this is taken care of automatically in the backend. * ``'q'``: performs the calculation using the test statistic :math:`q_{\mu}`, see eq. (54) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu`). * ``'q0'``: performs the calculation using the discovery test statistic, see eq. (47) of :xref:`1007.1727` :math:`q_{0}` (:func:`~spey.hypothesis_testing.test_statistics.q0`). init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Returns: ``List[float]``: Asimov data """ fit_opts = self.prepare_for_fit( expected=expected, allow_negative_signal=test_statistic in ["q", "qmu"], **kwargs, ) _, fit_pars = fit( **fit_opts, initial_parameters=init_pars, bounds=par_bounds, fixed_poi_value=1.0 if test_statistic == "q0" else 0.0, ) log.debug(f"fit parameters: {fit_pars}") return self.backend.expected_data(fit_pars)
[docs] def asimov_likelihood( self, poi_test: float = 1.0, expected: ExpectationType = ExpectationType.observed, return_nll: bool = True, test_statistics: str = "qtilde", init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> float: r""" Compute likelihood of the statistical model generated with the Asimov data. Args: poi_test (``float``, default ``1.0``): parameter of interest, :math:`\mu`. 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 (default). * :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. return_nll (``bool``, default ``True``): If ``True``, returns negative log-likelihood value. if ``False`` returns likelihood value. test_statistics (``Text``, default ``"qtilde"``): test statistics. * ``'qtilde'``: (default) performs the calculation using the alternative test statistic, :math:`\tilde{q}_{\mu}`, see eq. (62) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu_tilde`). .. warning:: Note that this assumes that :math:`\hat\mu\geq0`, hence ``allow_negative_signal`` assumed to be ``False``. If this function has been executed by user, ``spey`` assumes that this is taken care of throughout the external code consistently. Whilst computing p-values or upper limit on :math:`\mu` through ``spey`` this is taken care of automatically in the backend. * ``'q'``: performs the calculation using the test statistic :math:`q_{\mu}`, see eq. (54) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu`). * ``'q0'``: performs the calculation using the discovery test statistic, see eq. (47) of :xref:`1007.1727` :math:`q_{0}` (:func:`~spey.hypothesis_testing.test_statistics.q0`). The choice of ``test_statistics`` will effect the generation of the Asimov data where the fit is performed via :math:`\mu=1` if ``test_statistics="q0"`` and :math:`\mu=0` for others. Note that this :math:`\mu` does not correspond to the ``poi_test`` input of this function but it determines how Asimov data is generated. init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Returns: ``float``: likelihood computed for asimov data """ return self.likelihood( poi_test=poi_test, expected=expected, return_nll=return_nll, data=self.generate_asimov_data( expected=expected, test_statistic=test_statistics, init_pars=init_pars, par_bounds=par_bounds, **kwargs, ), init_pars=init_pars, par_bounds=par_bounds, **kwargs, )
[docs] def maximize_likelihood( self, return_nll: Optional[bool] = True, expected: Optional[ExpectationType] = ExpectationType.observed, allow_negative_signal: Optional[bool] = True, data: Optional[Union[List[float], np.ndarray]] = None, init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> Tuple[float, float]: r""" Find the maximum of the likelihood. Args: return_nll (``bool``, default ``True``): If ``True``, returns negative log-likelihood value. if ``False`` returns likelihood value. 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 (default). * :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. allow_negative_signal (``bool``, default ``True``): If ``True`` :math:`\hat\mu` value will be allowed to be negative. data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit. If ``None`` data will be set according to ``expected`` input. init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Returns: ``Tuple[float, float]``: :math:`\hat\mu` value and maximum value of the likelihood. """ fit_opts = self.prepare_for_fit( expected=expected, allow_negative_signal=allow_negative_signal, data=data, **kwargs, ) logpdf, fit_param = fit( **fit_opts, initial_parameters=init_pars, bounds=par_bounds ) log.debug(f"fit parameters: {fit_param}") muhat = fit_param[self.backend.config().poi_index] return muhat, -logpdf if return_nll else np.exp(logpdf)
[docs] def maximize_asimov_likelihood( self, return_nll: bool = True, expected: ExpectationType = ExpectationType.observed, test_statistics: str = "qtilde", init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> Tuple[float, float]: r""" Find the maximum of the likelihood which computed with respect to Asimov data. Args: return_nll (``bool``, default ``True``): If ``True``, returns negative log-likelihood value. if ``False`` returns likelihood value. 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 (default). * :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. test_statistics (``Text``, default ``"qtilde"``): test statistic. * ``'qtilde'``: (default) performs the calculation using the alternative test statistic, :math:`\tilde{q}_{\mu}`, see eq. (62) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu_tilde`). .. warning:: Note that this assumes that :math:`\hat\mu\geq0`, hence ``allow_negative_signal`` assumed to be ``False``. If this function has been executed by user, ``spey`` assumes that this is taken care of throughout the external code consistently. Whilst computing p-values or upper limit on :math:`\mu` through ``spey`` this is taken care of automatically in the backend. * ``'q'``: performs the calculation using the test statistic :math:`q_{\mu}`, see eq. (54) of :xref:`1007.1727` (:func:`~spey.hypothesis_testing.test_statistics.qmu`). * ``'q0'``: performs the calculation using the discovery test statistic, see eq. (47) of :xref:`1007.1727` :math:`q_{0}` (:func:`~spey.hypothesis_testing.test_statistics.q0`). init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Returns: ``Tuple[float, float]``: :math:`\hat\mu` value and maximum value of the likelihood. """ allow_negative_signal: bool = True if test_statistics in ["q", "qmu"] else False return self.maximize_likelihood( return_nll=return_nll, expected=expected, allow_negative_signal=allow_negative_signal, data=self.generate_asimov_data( expected=expected, test_statistic=test_statistics, init_pars=init_pars, par_bounds=par_bounds, **kwargs, ), init_pars=init_pars, par_bounds=par_bounds, **kwargs, )
[docs] def fixed_poi_sampler( self, poi_test: float, size: Optional[int] = None, expected: ExpectationType = ExpectationType.observed, init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> Union[np.ndarray, Callable[[int], np.ndarray]]: r""" Sample data from the statistical model with fixed parameter of interest. Args: poi_test (``float``, default ``1.0``): parameter of interest or signal strength, :math:`\mu`. size (``int``, default ``None``): sample size. If ``None`` a callable function will be returned which takes sample size as input. 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 (default). * :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. init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Raises: ~spey.system.exceptions.MethodNotAvailable: If bacend does not have sampler implementation. Returns: ``Union[np.ndarray, Callable[[int], np.ndarray]]``: Sampled data with shape of ``(size, number of bins)`` or callable function to sample from directly. """ fit_opts = self.prepare_for_fit(expected=expected, **kwargs) if ( fit_opts["model_configuration"].npar == 1 and fit_opts["model_configuration"].poi_index is not None ): fit_param = np.array( poi_test if isinstance(poi_test, (list, np.ndarray)) else [poi_test] ) else: _, fit_param = fit( **fit_opts, initial_parameters=init_pars, bounds=par_bounds, fixed_poi_value=poi_test, ) log.debug(f"fit parameters: {fit_param}") try: sampler = self.backend.get_sampler(fit_param) except NotImplementedError as exc: raise MethodNotAvailable( f"{self.backend_type} backend does not have sampling capabilities" ) from exc return sampler(size) if isinstance(size, int) else sampler
[docs] def sigma_mu_from_hessian( self, poi_test: float, expected: ExpectationType = ExpectationType.observed, init_pars: Optional[List[float]] = None, par_bounds: Optional[List[Tuple[float, float]]] = None, **kwargs, ) -> float: r""" Compute variance of :math:`\mu` from inverse Hessian. See eq. (27-28) in :xref:`1007.1727`. Args: poi_test (``float``, default ``1.0``): parameter of interest or signal strength, :math:`\mu`. 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 (default). * :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. init_pars (``List[float]``, default ``None``): initial parameters for the optimiser par_bounds (``List[Tuple[float, float]]``, default ``None``): parameter bounds for the optimiser. kwargs: keyword arguments for the optimiser. Raises: ~spey.system.exceptions.MethodNotAvailable: If bacend does not have Hessian implementation. Returns: ``float``: variance on parameter of interest. """ try: hessian_func = self.backend.get_hessian_logpdf_func(expected=expected) except NotImplementedError as exc: raise MethodNotAvailable( f"{self.backend_type} backend does not have Hessian definition." ) from exc fit_opts = self.prepare_for_fit(expected=expected, **kwargs) _ = fit_opts.pop("logpdf") _, fit_param = fit( **fit_opts, initial_parameters=init_pars, bounds=par_bounds, fixed_poi_value=poi_test, ) log.debug(f"fit parameters: {fit_param}") hessian = -1.0 * hessian_func(fit_param) log.debug(f"full hessian: {hessian}") poi_index = self.backend.config().poi_index return np.sqrt(np.linalg.inv(hessian)[poi_index, poi_index])
[docs] def combine(self, other, **kwargs): """ Combination routine between two statistical models. .. note:: This function's availability is backend dependent. Args: other (:obj:`~spey.StatisticalModel`): Statistical model to be combined with this model kwargs: backend specific arguments. Raises: :obj:`~spey.system.exceptions.CombinerNotAvailable`: If this statistical model does not have a combination routine implementation. ``AssertionError``: If the combination routine in the backend does not return a :obj:`~spey.BackendBase` object. Returns: :obj:`~spey.StatisticalModel`: Returns a new combined statistical model. """ try: combined = self.backend.combine(other.backend, **kwargs) assert isinstance(combined, BackendBase), "Invalid combination operation." return StatisticalModel( backend=combined, analysis=f"combine[{self.analysis}, {other.analysis}]" ) except NotImplementedError as err: raise CombinerNotAvailable( f"{self.backend_type} backend does not have a combination routine." ) from err
def __matmul__(self, other): """ Combination routine between two statistical models. See :func:`~spey.StatisticalModel.combine` function for details. """ return self.combine(other)
[docs] def statistical_model_wrapper( func: BackendBase, ) -> Callable[[Any], StatisticalModel]: """ Backend wrapper for :class:`~spey.StatisticalModel`. This function allows a universal integration of each backend to the :obj:`spey` environment. :func:`~spey.get_backend` function automatically wraps the backend with :func:`~spey.statistical_model_wrapper` before returning the object. Args: func (~spey.BackendBase): Desired backend to be used for statistical analysis. Raises: :obj:`AssertionError`: If the input function does not inherit :obj:`~spey.BackendBase` Returns: ``Callable[[Any], StatisticalModel]``: Wrapper that takes the following inputs * **args**: Backend specific arguments. * **analysis** (``Text``, default ``"__unknown_analysis__"``): Unique identifier of the statistical model. This attribue will be used for book keeping purposes. * **xsection** (``float``, default ``nan``): cross section, unit is determined by the user. Cross section value is only used for computing upper limit on excluded cross-section value. * **ntoys** (``int``, default ``1000``): Number of toy samples for hypothesis testing. (Only used for toy-based hypothesis testing) * **other keyword arguments**: Backend specific keyword inputs. """ @wraps(func) def wrapper( *args, analysis: str = "__unknown_analysis__", xsection: float = np.nan, ntoys: int = 1000, **kwargs, ) -> StatisticalModel: return StatisticalModel( backend=func(*args, **kwargs), analysis=analysis, xsection=xsection, ntoys=ntoys, ) docstring = ( "\n\n" + "<>" * 30 + "\n\n" + """ Optional arguments: Args: analysis (``Text``, default ``"__unknown_analysis__"``): Unique identifier of the statistical model. This attribue will be used for book keeping purposes. xsection (``float``, default ``nan``): cross section, unit is determined by the user. Cross section value is only used for computing upper limit on excluded cross-section value. ntoys (``int``, default ``1000``): Number of toy samples for hypothesis testing. (Only used for toy-based hypothesis testing) Raises: :obj:`AssertionError`: If the model input does not inherit :class:`~spey.DataBase`. Returns: ~spey.StatisticalModel: Backend wraped with statistical model interface. """ ) if wrapper.__doc__ is not None: wrapper.__doc__ += docstring else: wrapper.__doc__ = docstring return wrapper