import logging
import os
import re
import sys
import textwrap
from typing import Any, Callable, Dict, List, Literal, Optional, Text, Tuple, Union
import numpy as np
import pkg_resources
from semantic_version import SimpleSpec, Version
from spey.base import BackendBase, ConverterBase
from spey.combiner import UnCorrStatisticsCombiner
from spey.interface.statistical_model import StatisticalModel, statistical_model_wrapper
from spey.system import logger
from spey.system.exceptions import PluginError
from spey.system.webutils import ConnectionError, check_updates, get_bibtex
from ._version import __version__
from .about import about
from .utils import ExpectationType
__all__ = [
"version",
"StatisticalModel",
"UnCorrStatisticsCombiner",
"ExpectationType",
"AvailableBackends",
"get_backend",
"get_backend_metadata",
"reset_backend_entries",
"BackendBase",
"ConverterBase",
"about",
"math",
"check_updates",
"get_backend_bibtex",
"cite",
"set_log_level",
]
def __dir__():
return __all__
logger.init(LoggerStream=sys.stdout)
log = logging.getLogger("Spey")
log.setLevel(logging.INFO)
[docs]
def set_log_level(level: Literal[0, 1, 2, 3]) -> None:
"""
Set log level for spey
Args:
level (``int``): log level
* 0: error
* 1: warning
* 2: info
* 3: debug
"""
log_dict = {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
log.setLevel(log_dict[level])
[docs]
def version() -> Text:
"""
Version of ``spey`` package
Returns:
``Text``: version in X.Y.Z format
"""
return __version__
def _get_backend_entrypoints() -> Dict[Text, pkg_resources.EntryPoint]:
"""Collect plugin entries"""
return {
entry.name: entry
for entry in pkg_resources.iter_entry_points("spey.backend.plugins")
}
_backend_entries: Dict[Text, pkg_resources.EntryPoint] = _get_backend_entrypoints()
# ! Preinitialise backends, it might be costly to scan the system everytime
[docs]
def reset_backend_entries() -> None:
"""Scan the system for backends and reset the entries"""
_backend_entries = _get_backend_entrypoints()
[docs]
def AvailableBackends() -> List[Text]: # pylint: disable=C0103
"""
Returns a list of available backends. The default backends are automatically installed
with ``spey`` package. To enable other backends, please see the relevant section
in the documentation.
.. note::
This function does not validate the backend. For backend validation please use
:func:`~spey.get_backend` function.
Returns:
``List[Text]``: list of names of available backends.
"""
return [*_backend_entries.keys()]
[docs]
def get_backend(name: Text) -> Callable[[Any], StatisticalModel]:
"""
Statistical model backend retreiver. Available backend names can be found via
:func:`~spey.AvailableBackends` function.
Args:
name (``Text``): backend identifier. This backend refers to different packages
that prescribes likelihood function.
Raises:
:obj:`~spey.system.exceptions.PluginError`: If the backend is not available
or the available backend requires different version of ``spey``.
:obj:`AssertionError`: If the backend does not have necessary metadata.
Returns:
``Callable[[Any, ...], StatisticalModel]``:
A callable function that takes backend specific arguments and two additional
keyword arguments ``analysis`` (which is a unique identifier of analysis name as :obj:`str`)
and ``xsection`` (which is cross section value with a.u.). Details about the function can be
found in :func:`~spey.statistical_model_wrapper`. This wrapper
returns a :obj:`~spey.StatisticalModel` object.
Example:
.. code-block:: python3
:linenos:
>>> import spey; import numpy as np
>>> stat_wrapper = spey.get_backend("default_pdf.uncorrelated_background")
>>> data = np.array([1])
>>> signal = np.array([0.5])
>>> background = np.array([2.])
>>> background_unc = np.array([1.1])
>>> stat_model = stat_wrapper(
... signal_yields=signal,
... background_yields=background,
... data=data,
... covariance_matrix=background_unc,
... analysis="simple_sl",
... xsection=0.123
... )
>>> stat_model.exclusion_confidence_level()
.. note::
The documentation of the ``stat_wrapper`` defined above includes the docstring
of the backend as well. Hence typing ``stat_wrapper?`` in terminal will result with
complete documentation for the :func:`~spey.statistical_model_wrapper` and
the backend it self which is in this particular example
:obj:`~spey.backends.simplifiedlikelihood_backend.interface.SimplifiedLikelihoodInterface`.
"""
backend = _backend_entries.get(name, False)
if backend:
statistical_model = backend.load()
assert hasattr(
statistical_model, "spey_requires"
), "Backend does not have `'spey_requires'` attribute."
if getattr(statistical_model, "name", False):
assert (
statistical_model.name == name
), "The identity of the statistical model is wrongly set."
else:
setattr(statistical_model, "name", name)
if Version(version()) not in SimpleSpec(statistical_model.spey_requires):
raise PluginError(
f"The backend {name}, requires spey version {statistical_model.spey_requires}. "
f"However the current spey version is {__version__}."
)
# Initialise converter base models
if ConverterBase in statistical_model.mro():
statistical_model = statistical_model()
return statistical_model_wrapper(statistical_model)
raise PluginError(
f"The backend {name} is unavailable. Available backends are "
+ ", ".join(AvailableBackends())
+ "."
)
[docs]
def get_backend_bibtex(name: Text) -> Dict[Text, List[Text]]:
"""
Retreive BibTex entry for backend plug-in if available.
The bibtext entries are retreived both from Inspire HEP, doi.org and zenodo.
If the arXiv number matches the DOI the output will include two versions
of the same reference. If backend does not include an arXiv or DOI number
it will return an empty list.
.. versionadded:: 0.1.6
Args:
name (``Text``): backend identifier. This backend refers to different packages
that prescribes likelihood function.
Returns:
``Dict[Text, List[Text]]``:
BibTex entries for the backend. Keywords include inspire, doi.org and zenodo.
.. versionchanged:: 0.1.7
In the previous version, function was returning ``List[Text]`` now it returns
a ``Dict[Text, List[Text]]`` indicating the source of BibTeX entry.
"""
# pylint: disable=import-outside-toplevel, W1203
out = {"inspire": [], "doi.org": [], "zenodo": []}
meta = get_backend_metadata(name)
try:
for arxiv_id in meta.get("arXiv", []):
tmp = get_bibtex("inspire/arxiv", arxiv_id)
if tmp != "":
out["inspire"].append(textwrap.indent(tmp, " " * 4))
else:
log.debug(f"Can not find {arxiv_id} in Inspire")
for doi in meta.get("doi", []):
tmp = get_bibtex("inspire/doi", doi)
if tmp == "":
log.debug(f"Can not find {doi} in Inspire, looking at doi.org")
tmp = get_bibtex("doi", doi)
if tmp != "":
out["doi.org"].append(tmp)
else:
log.debug(f"Can not find {doi} in doi.org")
else:
out["inspire"].append(textwrap.indent(tmp, " " * 4))
for zenodo_id in meta.get("zenodo", []):
tmp = get_bibtex("zenodo", zenodo_id)
if tmp != "":
out["zenodo"].append(textwrap.indent(tmp, " " * 4))
else:
log.debug(f"{zenodo_id} is not a valid zenodo identifier")
except ConnectionError as err:
log.error("Can not connect to the internet. Please check your connection.")
log.debug(str(err))
return out
return out
[docs]
def cite() -> List[Text]:
"""Retreive BibTex information for Spey"""
try:
arxiv = textwrap.indent(get_bibtex("inspire/arxiv", "2307.06996"), " " * 4)
zenodo = get_bibtex("zenodo", "10156353")
linker = re.search("@software{(.+?),\n", zenodo).group(1)
zenodo = textwrap.indent(zenodo.replace(linker, "spey_zenodo"), " " * 4)
return arxiv + "\n\n" + zenodo
except ConnectionError as err:
log.error("Can not connect to the internet. Please check your connection.")
log.debug(str(err))
return ""
if os.environ.get("SPEY_CHECKUPDATE", "ON").upper() != "OFF":
check_updates()