Skip to content

Commit

Permalink
feat: support no POI (#950)
Browse files Browse the repository at this point in the history
* Allow POI-less models (poi_name = None)
* Raise exception when used in contexts that require POI
  • Loading branch information
lukasheinrich authored Jul 18, 2020
1 parent 0f99cc4 commit afc2190
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/governance/ROADMAP.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions src/pyhf/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/pyhf/infer/mle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module for Maximum Likelihood Estimation."""
from .. import get_backend
from ..exceptions import UnspecifiedPOI


def twice_nll(pars, data, pdf):
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions src/pyhf/infer/test_statistics.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/pyhf/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}}
Expand All @@ -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())

Expand Down
53 changes: 53 additions & 0 deletions tests/test_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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": [
Expand Down

0 comments on commit afc2190

Please sign in to comment.