"""Abstract Methods for backend objects"""
from abc import ABC, abstractmethod
from typing import Callable, List, Optional, Text, Tuple, Union
import numpy as np
from spey.base.model_config import ModelConfig
from spey.utils import ExpectationType
__all__ = ["BackendBase"]
def __dir__():
return __all__
[docs]
class BackendBase(ABC):
"""
An abstract class construction to enforce certain behaviour on statistical model backend.
In order to perform certain computations, ``spey`` needs to have access to specific
function constructions such as precsription to form likelihood. Hence, each backend is
required to inherit :obj:`~spey.BackendBase`.
"""
@property
def is_alive(self) -> bool:
"""Returns True if at least one bin has non-zero signal yield."""
return True
[docs]
@abstractmethod
def config(
self, allow_negative_signal: bool = True, poi_upper_bound: float = 10.0
) -> ModelConfig:
r"""
Model configuration.
Args:
allow_negative_signal (``bool``, default ``True``): If ``True`` :math:`\hat\mu`
value will be allowed to be negative.
poi_upper_bound (``float``, default ``10.0``): upper bound for parameter
of interest, :math:`\mu`.
Returns:
~spey.base.model_config.ModelConfig:
Model configuration. Information regarding the position of POI in
parameter list, suggested input and bounds.
"""
[docs]
@abstractmethod
def get_logpdf_func(
self,
expected: ExpectationType = ExpectationType.observed,
data: Optional[Union[List[float], np.ndarray]] = None,
) -> Callable[[np.ndarray], float]:
r"""
Generate function to compute :math:`\log\mathcal{L}(\mu, \theta)` where :math:`\mu` is the
parameter of interest and :math:`\theta` are nuisance parameters.
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.
data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit
Returns:
``Callable[[np.ndarray], float]``:
Function that takes fit parameters (:math:`\mu` and :math:`\theta`) and computes
:math:`\log\mathcal{L}(\mu, \theta)`.
"""
[docs]
def get_objective_function(
self,
expected: ExpectationType = ExpectationType.observed,
data: Optional[Union[List[float], np.ndarray]] = None,
do_grad: bool = True,
) -> Callable[[np.ndarray], Union[float, Tuple[float, np.ndarray]]]:
r"""
Objective function is the function to perform the optimisation on. This function is
expected to be negative log-likelihood, :math:`-\log\mathcal{L}(\mu, \theta)`.
Additionally, if available it canbe bundled with the gradient of negative log-likelihood.
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.
data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit
do_grad (``bool``, default ``True``): If ``True`` return objective and its gradient
as ``tuple`` (subject to availablility) if ``False`` only returns objective function.
Returns:
``Callable[[np.ndarray], Union[float, Tuple[float, np.ndarray]]]``:
Function which takes fit parameters (:math:`\mu` and :math:`\theta`) and returns either
objective or objective and its gradient.
"""
if do_grad:
raise NotImplementedError("Gradient is not implemented by default.")
logpdf = self.get_logpdf_func(expected=expected, data=data)
return lambda pars: -logpdf(pars)
[docs]
def get_hessian_logpdf_func(
self,
expected: ExpectationType = ExpectationType.observed,
data: Optional[Union[List[float], np.ndarray]] = None,
) -> Callable[[np.ndarray], float]:
r"""
Currently Hessian of :math:`\log\mathcal{L}(\mu, \theta)` is only used to compute
variance on :math:`\mu`. This method returns a callable function which takes fit
parameters (:math:`\mu` and :math:`\theta`) and returns Hessian.
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.
data (``Union[List[float], np.ndarray]``, default ``None``): input data that to fit
Raises:
:obj:`NotImplementedError`: If the Hessian of the backend has not been implemented.
Returns:
``Callable[[np.ndarray], float]``:
Function that takes fit parameters (:math:`\mu` and :math:`\theta`) and
returns Hessian of :math:`\log\mathcal{L}(\mu, \theta)`.
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def get_sampler(self, pars: np.ndarray) -> Callable[[int], np.ndarray]:
r"""
Retreives the function to sample from.
Args:
pars (:obj:`np.ndarray`): fit parameters (:math:`\mu` and :math:`\theta`)
Raises:
:obj:`NotImplementedError`: If the sampler for the backend has not been implemented.
Returns:
``Callable[[int], np.ndarray]``:
Function that takes ``number_of_samples`` as input and draws as many samples
from the statistical model.
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def expected_data(self, pars: List[float]) -> List[float]:
r"""
Compute the expected value of the statistical model. This function is mainly used to
generate Asimov data within the package, see
:func:`~spey.StatisticalModel.generate_asimov_data`.
Args:
pars (``List[float]``): nuisance, :math:`\theta` and parameter of interest,
:math:`\mu`.
Returns:
``List[float]``:
Expected data of the statistical model
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def combine(self, other, **kwargs):
"""
A routine to combine to statistical models.
.. note::
This function is only available if the backend has a specific routine
for combination between same or other backends.
Args:
other (:obj:`~spey.BackendBase`): Statistical model object to be combined.
Raises:
``NotImplementedError``: If the backend does not have a combination scheme.
Returns:
:obj:`~spey.BackendBase`:
Create a new statistical model from combination of this and other one.
"""
raise NotImplementedError("This method does not have combination implementation.")
[docs]
def negative_loglikelihood(
self,
poi_test: float = 1.0,
expected: ExpectationType = ExpectationType.observed,
**kwargs,
) -> Tuple[float, np.ndarray]:
r"""
Backend specific method to compute negative log-likelihood for a parameter of interest
:math:`\mu`.
.. note::
Interface first calls backend specific methods to compute likelihood. If they are not
implemented, it optimizes objective function through ``spey`` interface. Either prescription
to optimizing the likelihood or objective function must be available for a backend to
be sucessfully integrated to the ``spey`` interface.
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.
kwargs: keyword arguments for the optimiser.
Raises:
:obj:`NotImplementedError`: If the method is not available for the backend.
Returns:
``Tuple[float, np.ndarray]``:
value of negative log-likelihood at POI of interest and fit parameters
(:math:`\mu` and :math:`\theta`).
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def asimov_negative_loglikelihood(
self,
poi_test: float = 1.0,
expected: ExpectationType = ExpectationType.observed,
test_statistics: Text = "qtilde",
**kwargs,
) -> Tuple[float, np.ndarray]:
r"""
Compute negative log-likelihood at fixed :math:`\mu` for Asimov data.
.. note::
Interface first calls backend specific methods to compute likelihood. If they are not
implemented, it optimizes objective function through ``spey`` interface. Either prescription
to optimizing the likelihood or objective function must be available for a backend to
be sucessfully integrated to the ``spey`` interface.
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.
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`).
kwargs: keyword arguments for the optimiser.
Raises:
:obj:`NotImplementedError`: If the method is not available for the backend.
Returns:
``Tuple[float, np.ndarray]``\:
value of negative log-likelihood at POI of interest and fit parameters
(:math:`\mu` and :math:`\theta`).
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def minimize_negative_loglikelihood(
self,
expected: ExpectationType = ExpectationType.observed,
allow_negative_signal: bool = True,
**kwargs,
) -> Tuple[float, np.ndarray]:
r"""
A backend specific method to minimize negative log-likelihood.
.. note::
Interface first calls backend specific methods to compute likelihood. If they are not
implemented, it optimizes objective function through ``spey`` interface. Either prescription
to optimizing the likelihood or objective function must be available for a backend to
be sucessfully integrated to the ``spey`` interface.
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.
allow_negative_signal (``bool``, default ``True``): If ``True`` :math:`\hat\mu`
value will be allowed to be negative.
kwargs: keyword arguments for the optimiser.
Raises:
:obj:`NotImplementedError`: If the method is not available for the backend.
Returns:
``Tuple[float, np.ndarray]``:
value of negative log-likelihood and fit parameters (:math:`\mu` and :math:`\theta`).
"""
raise NotImplementedError("This method has not been implemented")
[docs]
def minimize_asimov_negative_loglikelihood(
self,
expected: ExpectationType = ExpectationType.observed,
test_statistics: Text = "qtilde",
**kwargs,
) -> Tuple[float, np.ndarray]:
r"""
A backend specific method to minimize negative log-likelihood for Asimov data.
.. note::
Interface first calls backend specific methods to compute likelihood. If they are not
implemented, it optimizes objective function through ``spey`` interface. Either prescription
to optimizing the likelihood or objective function must be available for a backend to
be sucessfully integrated to the ``spey`` interface.
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_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`).
kwargs: keyword arguments for the optimiser.
Raises:
:obj:`NotImplementedError`: If the method is not available for the backend.
Returns:
``Tuple[float, np.ndarray]``:
value of negative log-likelihood and fit parameters (:math:`\mu` and :math:`\theta`).
"""
raise NotImplementedError("This method has not been implemented")
class ConverterBase(ABC):
"""
An abstract class construction to enforce certain behaviour on statistical model backend.
This base class is used to act as a midle function where the function can act as a converter
between two statistical models. It has to have a call function which needs to return a
:obj:`~spey.BackendBase` object.
.. note::
``ConverterBase`` object is not expected to have an ``__init__`` method that expect
arguments or keyword arguments. This function should have a ``__call__`` method that
returns :obj:`~spey.BackendBase` object.
Example:
.. code:: python3
>>> class MyStatConverter(ConverterBase):
>>> name= "example.converter"
>>> version="0.0.1"
>>> author="Tom Bombadil"
>>> spey_requires=">0.1.0"
>>> def __call__(self, stat_model: spey.StatisticalModel) -> BackendBase:
>>> # do something
>>> return UncorrelatedBackground(...)
"""
def __call__(self, *args, **kwargs) -> BackendBase:
"""
Function that compiles the :obj:`~spey.BackendBase` object.
Raises:
``NotImplementedError``: If ``__call__`` function is not implemented for the class.
Returns:
:obj:`~spey.BackendBase`:
This function should return any :obj:`~spey.BackendBase` object.
"""
raise NotImplementedError("Invalid implementation of ConverterBase object")