From afc21909705097f435fc3c1487458ffdd5df1ac0 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 18 Jul 2020 19:42:33 +0200 Subject: [PATCH] feat: support no POI (#950) * Allow POI-less models (poi_name = None) * Raise exception when used in contexts that require POI --- docs/governance/ROADMAP.rst | 2 +- src/pyhf/exceptions/__init__.py | 8 +++++ src/pyhf/infer/mle.py | 5 +++ src/pyhf/infer/test_statistics.py | 6 ++++ src/pyhf/pdf.py | 5 +-- tests/test_pdf.py | 53 +++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/docs/governance/ROADMAP.rst b/docs/governance/ROADMAP.rst index 96f3ed9001..83d009c918 100644 --- a/docs/governance/ROADMAP.rst +++ b/docs/governance/ROADMAP.rst @@ -107,7 +107,7 @@ Roadmap 2020-Q1] - |uncheck| Add "discovery" test stats (p0) (PR #520) [2019-Q4 → 2020-Q1] - |uncheck| Add better Model creation [2019-Q4 → 2020-Q1] - - |uncheck| Add background model support (Issue #514) [2019-Q4 → 2020-Q1] + - |check| Add background model support (Issues #514, #946) [2019-Q4 → 2020-Q1] - |uncheck| Develop interface for the optimizers similar to tensor/backend [2019-Q4 → 2020-Q1] - |check| Migrate to TensorFlow v2.0 (PR #541) [2019-Q4] diff --git a/src/pyhf/exceptions/__init__.py b/src/pyhf/exceptions/__init__.py index ba342c1729..659abf9627 100644 --- a/src/pyhf/exceptions/__init__.py +++ b/src/pyhf/exceptions/__init__.py @@ -50,6 +50,14 @@ class InvalidWorkspaceOperation(Exception): """InvalidWorkspaceOperation is raised when an operation on a workspace fails.""" +class UnspecifiedPOI(Exception): + """ + UnspecifiedPOI is raised when a given model does not have POI(s) defined but is used in contexts that need it. + + This can occur when e.g. trying to calculate CLs on a POI-less model. + """ + + class InvalidModel(Exception): """ InvalidModel is raised when a given model does not have the right configuration, even though it validates correctly against the schema. diff --git a/src/pyhf/infer/mle.py b/src/pyhf/infer/mle.py index d06f369421..d747c66a35 100644 --- a/src/pyhf/infer/mle.py +++ b/src/pyhf/infer/mle.py @@ -1,5 +1,6 @@ """Module for Maximum Likelihood Estimation.""" from .. import get_backend +from ..exceptions import UnspecifiedPOI def twice_nll(pars, data, pdf): @@ -76,6 +77,10 @@ def fixed_poi_fit(poi_val, data, pdf, init_pars=None, par_bounds=None, **kwargs) See optimizer API """ + if pdf.config.poi_index is None: + raise UnspecifiedPOI( + 'No POI is defined. A POI is required to fit with a fixed POI.' + ) _, opt = get_backend() init_pars = init_pars or pdf.config.suggested_init() par_bounds = par_bounds or pdf.config.suggested_bounds() diff --git a/src/pyhf/infer/test_statistics.py b/src/pyhf/infer/test_statistics.py index 42469522e1..8b767dbe51 100644 --- a/src/pyhf/infer/test_statistics.py +++ b/src/pyhf/infer/test_statistics.py @@ -1,5 +1,6 @@ from .. import get_backend from .mle import fixed_poi_fit, fit +from ..exceptions import UnspecifiedPOI def qmu(mu, data, pdf, init_pars, par_bounds): @@ -42,6 +43,11 @@ def qmu(mu, data, pdf, init_pars, par_bounds): Returns: Float: The calculated test statistic, :math:`q_{\mu}` """ + if pdf.config.poi_index is None: + raise UnspecifiedPOI( + 'No POI is defined. A POI is required for profile likelihood based test statistics.' + ) + tensorlib, optimizer = get_backend() mubhathat, fixed_poi_fit_lhood_val = fixed_poi_fit( mu, data, pdf, init_pars, par_bounds, return_fitted_val=True diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py index 03568cbdc3..e6cbc8f163 100644 --- a/src/pyhf/pdf.py +++ b/src/pyhf/pdf.py @@ -221,7 +221,6 @@ def __init__(self, spec, **config_kwargs): _required_paramsets = _paramset_requirements_from_modelspec( spec, self.channel_nbins ) - poi_name = config_kwargs.pop('poi_name', 'mu') default_modifier_settings = {'normsys': {'interpcode': 'code1'}} @@ -242,7 +241,9 @@ def __init__(self, spec, **config_kwargs): self.auxdata_order = [] self._create_and_register_paramsets(_required_paramsets) - self.set_poi(poi_name) + if poi_name is not None: + self.set_poi(poi_name) + self.npars = len(self.suggested_init()) self.nmaindata = sum(self.channel_nbins.values()) diff --git a/tests/test_pdf.py b/tests/test_pdf.py index 677e4fb7cc..5ea73b5b7b 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -5,6 +5,26 @@ import json +def test_minimum_model_spec(): + spec = { + 'channels': [ + { + 'name': 'channel', + 'samples': [ + { + 'name': 'goodsample', + 'data': [1.0], + 'modifiers': [ + {'type': 'normfactor', 'name': 'mu', 'data': None} + ], + }, + ], + } + ] + } + pyhf.Model(spec) + + def test_pdf_inputs(backend): source = { "binning": [2, -0.5, 1.5], @@ -182,6 +202,39 @@ def test_pdf_integration_staterror(backend): ) +def test_poiless_model(backend): + spec = { + 'channels': [ + { + 'name': 'channel', + 'samples': [ + { + 'name': 'goodsample', + 'data': [10.0], + 'modifiers': [ + { + 'type': 'normsys', + 'name': 'shape', + 'data': {"hi": 0.5, "lo": 1.5}, + } + ], + }, + ], + } + ] + } + model = pyhf.Model(spec, poi_name=None) + + data = [12] + model.config.auxdata + pyhf.infer.mle.fit(data, model) + + with pytest.raises(pyhf.exceptions.UnspecifiedPOI): + pyhf.infer.mle.fixed_poi_fit(1.0, data, model) + + with pytest.raises(pyhf.exceptions.UnspecifiedPOI): + pyhf.infer.hypotest(1.0, data, model) + + def test_pdf_integration_shapesys_zeros(backend): spec = { "channels": [