diff --git a/.zenodo.json b/.zenodo.json index 723bfc8..1aeedd4 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,8 +1,8 @@ { "description": "smooth inference for reinterpretation studies", "license": "MIT", - "title": "SpeysideHEP/spey: v0.1.5", - "version": "v0.1.5", + "title": "SpeysideHEP/spey: v0.1.6", + "version": "v0.1.6", "upload_type": "software", "creators": [ { @@ -29,7 +29,7 @@ }, { "scheme": "url", - "identifier": "https://github.com/SpeysideHEP/spey/tree/v0.1.5", + "identifier": "https://github.com/SpeysideHEP/spey/tree/v0.1.6", "relation": "isSupplementTo" }, { diff --git a/docs/api.rst b/docs/api.rst index 376f4f5..de093c9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -16,15 +16,19 @@ Top-Level version about + check_updates ExpectationType AvailableBackends get_backend get_backend_metadata + get_backend_bibtex + cite reset_backend_entries statistical_model_wrapper helper_functions.correlation_to_covariance helper_functions.covariance_to_correlation optimizer.core.fit + set_log_level Main Classes ------------ diff --git a/docs/releases/changelog-v0.1.md b/docs/releases/changelog-v0.1.md index 0a8efe2..d5142a4 100644 --- a/docs/releases/changelog-v0.1.md +++ b/docs/releases/changelog-v0.1.md @@ -28,6 +28,15 @@ * Update clarification on text based keyword arguments. ([#30](https://github.com/SpeysideHEP/spey/pull/30)) +* Adding logging across the software and implement tools to scilence them. + ([#32](https://github.com/SpeysideHEP/spey/pull/32)) + +* Spey will automatically look for updates during initiation. + ([#32](https://github.com/SpeysideHEP/spey/pull/32)) + +* Utilities to retreive bibtex information for third-party plug-ins. + ([#32](https://github.com/SpeysideHEP/spey/pull/32)) + ## Bug Fixes * In accordance to the latest updates ```UnCorrStatisticsCombiner``` has been updated with diff --git a/setup.py b/setup.py index 54f1081..3f70e6b 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ "autograd==1.5", "semantic_version~=2.10", "tqdm>=4.64.0", + "requests>=2.31.0", ] backend_plugins = [ diff --git a/src/spey/__init__.py b/src/spey/__init__.py index a00161c..27cab96 100644 --- a/src/spey/__init__.py +++ b/src/spey/__init__.py @@ -1,4 +1,9 @@ -from typing import Any, Callable, Dict, List, Optional, Text, Tuple, Union +import logging +import os +import re +import sys +import textwrap +from typing import Any, Callable, Dict, List, Optional, Text, Tuple, Union, Literal import numpy as np import pkg_resources @@ -7,7 +12,9 @@ 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 get_bibtex, check_updates from ._version import __version__ from .about import about @@ -25,6 +32,10 @@ "BackendBase", "ConverterBase", "about", + "check_updates", + "get_backend_bibtex", + "cite", + "set_log_level", ] @@ -32,6 +43,32 @@ def __dir__(): return __all__ +logger.init(LoggerStream=sys.stdout) +log = logging.getLogger("Spey") +log.setLevel(logging.INFO) + + +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]) + + def version() -> Text: """ Version of ``spey`` package @@ -59,7 +96,7 @@ def reset_backend_entries() -> None: _backend_entries = _get_backend_entrypoints() -def AvailableBackends() -> List[Text]: +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 @@ -209,6 +246,7 @@ def get_backend_metadata(name: Text) -> Dict[Text, Any]: "spey_requires": statistical_model.spey_requires, "doi": list(getattr(statistical_model, "doi", [])), "arXiv": list(getattr(statistical_model, "arXiv", [])), + "zenodo": list(getattr(statistical_model, "zenodo", [])), } raise PluginError( @@ -216,3 +254,64 @@ def get_backend_metadata(name: Text) -> Dict[Text, Any]: + ", ".join(AvailableBackends()) + "." ) + + +def get_backend_bibtex(name: 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. + + Args: + name (``Text``): backend identifier. This backend refers to different packages + that prescribes likelihood function. + + Returns: + ``List[Text]``: + BibTex entries for the backend. + """ + # pylint: disable=import-outside-toplevel, W1203 + txt = [] + meta = get_backend_metadata(name) + + for arxiv_id in meta.get("arXiv", []): + tmp = get_bibtex("inspire/arxiv", arxiv_id) + if tmp != "": + txt.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 != "": + txt.append(tmp) + else: + log.debug(f"Can not find {doi} in doi.org") + else: + txt.append(textwrap.indent(tmp, " " * 4)) + for zenodo_id in meta.get("zenodo", []): + tmp = get_bibtex("zenodo", zenodo_id) + if tmp != "": + txt.append(textwrap.indent(tmp, " " * 4)) + else: + log.debug(f"{zenodo_id} is not a valid zenodo identifier") + + return txt + + +def cite() -> List[Text]: + """Retreive BibTex information for Spey""" + 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 + + +if os.environ.get("SPEY_CHECKUPDATE", "ON").upper() != "OFF": + check_updates() diff --git a/src/spey/_version.py b/src/spey/_version.py index acb9923..235765d 100644 --- a/src/spey/_version.py +++ b/src/spey/_version.py @@ -1,3 +1,3 @@ """Version number (major.minor.patch[-label])""" -__version__ = "0.1.5" +__version__ = "0.1.6" diff --git a/src/spey/about.py b/src/spey/about.py index f3fce8d..7576a5f 100644 --- a/src/spey/about.py +++ b/src/spey/about.py @@ -8,7 +8,7 @@ import scipy import semantic_version import tqdm -from pkg_resources import iter_entry_points +from pkg_resources import get_distribution, iter_entry_points def about() -> None: @@ -21,13 +21,7 @@ def about() -> None: ) print(f"Numpy version: {numpy.__version__}") print(f"Scipy version: {scipy.__version__}") - print( - "Autograd version: %s" - % check_output([sys.executable, "-m", "pip", "show", "autograd"]) - .decode() - .split("\n")[1] - .split(":")[1][1:] - ) + print(f"Autograd version: {get_distribution('autograd').version}") print(f"tqdm version: {tqdm.__version__}") print(f"semantic_version version: {semantic_version.__version__}") diff --git a/src/spey/backends/default_pdf/__init__.py b/src/spey/backends/default_pdf/__init__.py index 8dfdc92..a4f6051 100644 --- a/src/spey/backends/default_pdf/__init__.py +++ b/src/spey/backends/default_pdf/__init__.py @@ -1,5 +1,6 @@ """Interface for default PDF sets""" +import logging from typing import Any, Callable, Dict, List, Optional, Text, Tuple, Union from autograd import grad, hessian, jacobian @@ -17,6 +18,9 @@ from .uncertainty_synthesizer import signal_uncertainty_synthesizer # pylint: disable=E1101,E1120 +log = logging.getLogger("Spey") + +# pylint: disable=W1203 class DefaultPDFBase(BackendBase): @@ -92,7 +96,7 @@ def __init__( self.signal_uncertainty_configuration = signal_uncertainty_synthesizer( signal_yields=self.signal_yields, **signal_uncertainty_configuration, - domain=slice(len(background_yields) + 1, None) + domain=slice(len(background_yields) + 1, None), ) minimum_poi = -np.inf @@ -101,6 +105,7 @@ def __init__( self.background_yields[self.signal_yields > 0.0] / self.signal_yields[self.signal_yields > 0.0] ) + log.debug(f"Min POI set to : {minimum_poi}") self._main_model = None self._constraint_model = None @@ -243,6 +248,7 @@ def get_objective_function( self.background_yields if expected == ExpectationType.apriori else self.data ) data = current_data if data is None else data + log.debug(f"Data: {data}") def negative_loglikelihood(pars: np.ndarray) -> np.ndarray: """Compute twice negative log-likelihood""" @@ -291,6 +297,7 @@ def get_logpdf_func( self.background_yields if expected == ExpectationType.apriori else self.data ) data = current_data if data is None else data + log.debug(f"Data: {data}") return lambda pars: self.main_model.log_prob( pars, data[: len(self.data)] @@ -329,6 +336,7 @@ def get_hessian_logpdf_func( self.background_yields if expected == ExpectationType.apriori else self.data ) data = current_data if data is None else data + log.debug(f"Data: {data}") def log_prob(pars: np.ndarray) -> np.ndarray: """Compute log-probability""" diff --git a/src/spey/backends/default_pdf/third_moment.py b/src/spey/backends/default_pdf/third_moment.py index 052faa6..87e54f1 100644 --- a/src/spey/backends/default_pdf/third_moment.py +++ b/src/spey/backends/default_pdf/third_moment.py @@ -1,12 +1,13 @@ """Tools for computing third moment expansion""" import warnings from typing import Optional, Tuple, Union - +import logging import autograd.numpy as np from scipy import integrate from scipy.stats import norm -# pylint: disable=E1101,E1120 +# pylint: disable=E1101,E1120,W1203 +log = logging.getLogger("Spey") def third_moment_expansion( @@ -37,11 +38,11 @@ def third_moment_expansion( """ cov_diag = np.diag(covariance_matrix) - # ! Assertion error is removed, instead nan values will be converted to zero. - # assert np.all(8.0 * cov_diag**3 >= third_moment**2), ( - # "Given covariance matrix and diagonal terms of the third moment does not " - # + "satisfy the condition: 8 * diag(cov)**3 >= third_moment**2." - # ) + if not np.all(8.0 * cov_diag**3 >= third_moment**2): + log.warning( + r"Third moments does not satisfy the following condition: $8\Sigma_{ii}^3 \geq (m^{(3)}_i)^2$" + ) + log.warning("The values that do not satisfy this condition will be set to zero.") # arXiv:1809.05548 eq. 2.9 with warnings.catch_warnings(record=True) as w: @@ -61,12 +62,15 @@ def third_moment_expansion( category=RuntimeWarning, ) C = np.where(np.isnan(C), 0.0, C) + log.debug(f"C: {C}") # arXiv:1809.05548 eq. 2.10 B = np.sqrt(cov_diag - 2 * C**2) + log.debug(f"B: {B}") # arXiv:1809.05548 eq. 2.11 A = expectation_value - C + log.debug(f"A: {A}") # arXiv:1809.05548 eq. 2.12 eps = 1e-5 @@ -88,6 +92,7 @@ def third_moment_expansion( if i != j: corr[j, i] = corr[i, j] + log.debug(f"rho: {corr}") return A, B, C, corr return A, B, C @@ -154,5 +159,6 @@ def compute_x3BifurgatedGaussian(x: float, upper: float, lower: float) -> float: if return_integration_error: return np.array(third_moment), np.array(error) + log.debug(f"Error: {error}") return np.array(third_moment) diff --git a/src/spey/interface/functiontools.py b/src/spey/interface/functiontools.py deleted file mode 100644 index d1aec30..0000000 --- a/src/spey/interface/functiontools.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Text, Any, ClassVar - - -def get_function( - class_base: ClassVar, - function_name: Text, - default: Any = None, - exception: Exception = NotImplementedError, - **kwargs, -) -> Any: - """ - Function wrapper to be able to set a default value for the attributes that are not implemented - - :param class_base (`ClassVar`): Class basis where the function supposed to live - :param function_name (`Text`): name of the function - :param default (`Any`): default value that function needs to return. - :param exception (`Exception`, default `NotImplementedError`): Exception that should be avoided. - :param kwargs: function inputs - :return `Any`: default value or the function output - """ - try: - output = getattr(class_base, function_name, default) - if callable(output): - output = output(**kwargs) - except exception: - output = default(**kwargs) if callable(default) else default - - return output diff --git a/src/spey/interface/statistical_model.py b/src/spey/interface/statistical_model.py index 4b71967..ea414d0 100644 --- a/src/spey/interface/statistical_model.py +++ b/src/spey/interface/statistical_model.py @@ -1,6 +1,5 @@ """Statistical Model wrapper class""" - -import inspect +import logging from typing import Any, Callable, Dict, List, Optional, Text, Tuple, Union import numpy as np @@ -22,13 +21,9 @@ def __dir__(): return __all__ -def _module_check(backend: BackendBase, func_name: Union[Text, List[Text]]) -> bool: - """Check if the member function is implemented in backend or in plugin""" - func_name = [func_name] if isinstance(func_name, str) else func_name - return all( - ("spey/base/backend_base.py" not in inspect.getfile(getattr(backend, fn))) - for fn in func_name - ) +log = logging.getLogger("Spey") + +# pylint: disable=W1203 class StatisticalModel(HypothesisTestingBase): @@ -104,15 +99,17 @@ def backend_type(self) -> Text: @property def is_asymptotic_calculator_available(self) -> bool: """Check if Asymptotic calculator is available for the backend""" - return _module_check(self.backend, "expected_data") or _module_check( - self.backend, - ["asimov_negative_loglikelihood", "minimize_asimov_negative_loglikelihood"], + 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 _module_check(self.backend, "get_sampler") + return self.backend.get_sampler != BackendBase.get_sampler @property def is_chi_square_calculator_available(self) -> bool: @@ -166,8 +163,7 @@ def excluded_cross_section( raise UnknownCrossSection("Cross-section value has not been initialised.") return ( - self.poi_upper_limit(expected=expected, confidence_level=0.95) - * self.xsection + self.poi_upper_limit(expected=expected, confidence_level=0.95) * self.xsection ) @property @@ -233,6 +229,7 @@ def prepare_for_fit( 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 @@ -370,7 +367,7 @@ def generate_asimov_data( """ fit_opts = self.prepare_for_fit( expected=expected, - allow_negative_signal=True if test_statistic in ["q", "qmu"] else False, + allow_negative_signal=test_statistic in ["q", "qmu"], **kwargs, ) @@ -380,6 +377,7 @@ def generate_asimov_data( 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) @@ -510,6 +508,7 @@ def maximize_likelihood( 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) @@ -643,6 +642,7 @@ def fixed_poi_sampler( fixed_poi_value=poi_test, ) + log.debug(f"fit parameters: {fit_param}") try: sampler = self.backend.get_sampler(fit_param) except NotImplementedError as exc: @@ -704,8 +704,10 @@ def sigma_mu_from_hessian( 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]) diff --git a/src/spey/optimizer/core.py b/src/spey/optimizer/core.py index 30fb56e..a5f1cad 100644 --- a/src/spey/optimizer/core.py +++ b/src/spey/optimizer/core.py @@ -1,9 +1,16 @@ -from typing import Callable, List, Tuple, Optional, Dict +import logging +from typing import Callable, Dict, List, Optional, Tuple + import numpy as np from spey.base.model_config import ModelConfig + from .scipy_tools import minimize +# pylint: disable=W1203 + +log = logging.getLogger("Spey") + def fit( func: Callable[[np.ndarray], np.ndarray], @@ -58,6 +65,9 @@ def func(vector: np.ndarray) -> float: constraints = [] if constraints is None else constraints if fixed_poi_value is not None: init_pars[model_configuration.poi_index] = fixed_poi_value + log.debug( + f"Fixing POI index at: {model_configuration.poi_index} to value {fixed_poi_value}" + ) constraints.append( { "type": "eq", @@ -68,6 +78,7 @@ def func(vector: np.ndarray) -> float: if model_configuration.suggested_fixed is not None: for idx, isfixed in enumerate(model_configuration.suggested_fixed): if isfixed: + log.debug(f"Constraining {idx} to value {init_pars[idx]}") constraints.append( { "type": "eq", diff --git a/src/spey/optimizer/scipy_tools.py b/src/spey/optimizer/scipy_tools.py index f7494dd..777290b 100644 --- a/src/spey/optimizer/scipy_tools.py +++ b/src/spey/optimizer/scipy_tools.py @@ -1,11 +1,16 @@ """Optimisation tools based on scipy""" +import logging import warnings from typing import Callable, Dict, List, Tuple import numpy as np import scipy +# pylint: disable=W1201, W1203, R0913 + +log = logging.getLogger("Spey") + def minimize( func: Callable[[np.ndarray], float], @@ -66,10 +71,8 @@ def minimize( # Note that it is important to respect the lower bounds especially since there might # be bounds that lead to negative yields in the statistical model, e.g. Asimov data, # background + mu * signal etc. - warnings.warn( - message=opt.message + "\nspey::Expanding the bounds.", - category=RuntimeWarning, - ) + log.warning("Optimiser has not been able to satisfy all the conditions:") + log.warning(opt.message + " Expanding the bounds...") init_pars = opt.x for bdx, bound in enumerate(bounds): if bdx == poi_index and constraints is None: @@ -80,6 +83,11 @@ def minimize( break if not opt.success: - warnings.warn(message=opt.message, category=RuntimeWarning) + log.warning("Optimiser has not been able to satisfy all the conditions:") + log.warning(opt.message) + log.debug(f"func: {opt.fun}") + log.debug(f"parameters: {opt.x}") + if do_grad: + log.debug(f"gradients: {func(opt.x)[-1]}") return opt.fun, opt.x diff --git a/src/spey/system/logger.py b/src/spey/system/logger.py new file mode 100644 index 0000000..d76a987 --- /dev/null +++ b/src/spey/system/logger.py @@ -0,0 +1,52 @@ +"""Logging style for spey""" +import logging +import sys + +# pylint: disable=C0103 + + +class ColoredFormatter(logging.Formatter): + """Coloured logging formatter for spey""" + + def __init__(self, msg): + logging.Formatter.__init__(self, msg) + + def format(self, record): + if record.levelno >= 50: # FATAL + color = "\x1b[31mSpey - ERROR: " + elif record.levelno >= 40: # ERROR + color = "\x1b[31mSpey - ERROR: " + elif record.levelno >= 30: # WARNING + color = "\x1b[35mSpey - WARNING: " + elif record.levelno >= 20: # INFO + color = "\x1b[0mSpey: " + elif record.levelno >= 10: # DEBUG + color = ( + f"\x1b[36mSpey - DEBUG ({record.module}.{record.funcName}() " + f"in {record.filename}::L{record.lineno}): " + ) + else: # ANYTHING ELSE + color = "\x1b[0mSpey: " + + record.msg = color + str(record.msg) + "\x1b[0m" + return logging.Formatter.format(self, record) + + +def init(LoggerStream=sys.stdout): + """Initialise logger""" + rootLogger = logging.getLogger() + hdlr = logging.StreamHandler() + fmt = ColoredFormatter("%(message)s") + hdlr.setFormatter(fmt) + rootLogger.addHandler(hdlr) + + # we need to replace all root loggers by ma5 loggers for a proper + # interface with madgraph5 + SpeyLogger = logging.getLogger("Spey") + for hdlr in SpeyLogger.handlers: + SpeyLogger.removeHandler(hdlr) + hdlr = logging.StreamHandler(LoggerStream) + fmt = ColoredFormatter("%(message)s") + hdlr.setFormatter(fmt) + SpeyLogger.addHandler(hdlr) + SpeyLogger.propagate = False diff --git a/src/spey/system/webutils.py b/src/spey/system/webutils.py new file mode 100644 index 0000000..7256b76 --- /dev/null +++ b/src/spey/system/webutils.py @@ -0,0 +1,110 @@ +import logging +from typing import Literal, Text + +import requests +from pkg_resources import get_distribution +from semantic_version import Version + +log = logging.getLogger("Spey") + +__all__ = ["get_bibtex", "check_updates"] + + +def __dir__(): + return __all__ + + +def get_bibtex( + home: Literal["inspire/arxiv", "inspire/doi", "doi", "zenodo"], + identifier: Text, + timeout: int = 5, +) -> Text: + """ + Retreive BibTex information from InspireHEP, Zenodo or DOI.org + + Args: + home (``Text``): Home location for the identifier. + + * ``"inspire/arxiv"``: retreive information from inspire using + arXiv number. + * ``"inspire/doi"``: retreive information from inspore using + doi number. + * ``"doi"``: retreive information from doi.org + * ``"zenodo"``: retreive information from zenodo using zenodo + identifier. + + identifier (``Text``): web identifier + timeout (``int``, default ``5``): time out. + + Returns: + ``Text``: + Bibtex entry. + """ + # pylint: disable=W1203 + + home_loc = { + "inspire/arxiv": "https://inspirehep.net/api/arxiv/%s", + "inspire/doi": "https://inspirehep.net/api/doi/%s", + "doi": "https://doi.org/%s", + "zenodo": "https://zenodo.org/api/records/%s", + } + + try: + response = requests.get( + home_loc[home] % identifier, + headers={"accept": "application/x-bibtex"}, + timeout=timeout, + ) + if not 200 <= response.status_code <= 299: + log.debug(f"HTML Status Code: {response.status_code}") + return "" + response.encoding = "utf-8" + return response.text + except Exception as err: # pylint: disable=W0718 + log.debug(str(err)) + return "" + + +def check_updates() -> None: + """ + Check Spey Updates. + + Spey always checks updates when initialised. To disable this set the following + + Option if using terminal: + + .. code-block:: bash + + export SPEY_CHECKUPDATE=OFF + + Option if using python interface: + + .. code-block:: python + + import os + os.environ["SPEY_CHECKUPDATE"]="OFF" + + """ + # pylint: disable=import-outside-toplevel, W1203 + try: + response = requests.get("https://pypi.org/pypi/spey/json", timeout=1) + response.encoding = "utf-8" + pypi_info = response.json() + pypi_version = pypi_info.get("info", {}).get("version", False) + version = get_distribution("spey").version + if pypi_version: + log.debug(f"Curernt version {version}, latest version {pypi_version}.") + if Version(version) < Version(pypi_version): + log.warning( + f"An update is available. Current version of spey is {version}, " + f"available version is {pypi_version}." + ) + elif Version(version) > Version(pypi_version): + log.warning( + f"An unstable version of spey ({version}) is being used." + f" Latest stable version is {pypi_version}." + ) + + except Exception as err: # pylint: disable=W0718 + # Can not retreive updates + log.debug(str(err)) diff --git a/src/spey/utils.py b/src/spey/utils.py index af10e88..7c88272 100644 --- a/src/spey/utils.py +++ b/src/spey/utils.py @@ -2,6 +2,8 @@ __all__ = ["ExpectationType"] +# pylint: disable=C0103 + class ExpectationType(Enum): """ diff --git a/tests/test_default_pdf.py b/tests/test_default_pdf.py index eb97d23..16bd8d4 100644 --- a/tests/test_default_pdf.py +++ b/tests/test_default_pdf.py @@ -46,6 +46,9 @@ def test_correlated_background(): assert np.isclose( statistical_model.exclusion_confidence_level()[0], 0.9635100547173434 ), "CLs is wrong" + assert np.isclose( + statistical_model.sigma_mu(1.0), 0.8456469932844632 + ), "Sigma mu is wrong" def test_third_moment(): @@ -68,6 +71,9 @@ def test_third_moment(): assert np.isclose( statistical_model.poi_upper_limit(), 0.9221339770245336 ), "POI is wrong" + assert np.isclose( + statistical_model.sigma_mu(1.0), 0.854551194250324 + ), "Sigma mu is wrong" def test_effective_sigma(): @@ -90,3 +96,30 @@ def test_effective_sigma(): assert np.isclose( statistical_model.poi_upper_limit(), 1.5298573610113775 ), "POI is wrong." + assert np.isclose( + statistical_model.sigma_mu(1.0), 1.2152765953701747 + ), "Sigma mu is wrong" + + +def test_poisson(): + """tester for uncorrelated background model""" + + pdf_wrapper = spey.get_backend("default_pdf.poisson") + + data = [36, 33] + signal_yields = [12.0, 15.0] + background_yields = [50.0, 48.0] + + stat_model = pdf_wrapper( + signal_yields=signal_yields, + background_yields=background_yields, + data=data, + analysis="multi_bin", + xsection=0.123, + ) + + assert np.isclose(stat_model.poi_upper_limit(), 0.3140867496931846), "POI is wrong." + assert np.isclose( + stat_model.exclusion_confidence_level()[0], 0.9999807105228611 + ), "CLs is wrong" + assert np.isclose(stat_model.sigma_mu(1.0), 0.5573350296644078), "Sigma mu is wrong"