Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Raise exception if bin-wise modifier data length doesn't match sample data #1708

Merged
25 changes: 20 additions & 5 deletions src/pyhf/modifiers/histosys.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging

import pyhf
from pyhf import events
from pyhf.tensor.manager import get_backend
from pyhf import interpolators
from pyhf import events, interpolators
from pyhf.exceptions import InvalidModifier
from pyhf.parameters import ParamViewer
from pyhf.tensor.manager import get_backend

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,8 +61,8 @@ def append(self, key, channel, sample, thismod, defined_samp):
def finalize(self):
default_backend = pyhf.default_backend

for modifier in self.builder_data.values():
for sample in modifier.values():
for modifier_name, modifier in self.builder_data.items():
for sample_name, sample in modifier.items():
sample["data"]["mask"] = default_backend.concatenate(
sample["data"]["mask"]
)
Expand All @@ -75,6 +75,21 @@ def finalize(self):
sample["data"]["nom_data"] = default_backend.concatenate(
sample["data"]["nom_data"]
)
if (
not len(sample["data"]["nom_data"])
== len(sample["data"]["lo_data"])
== len(sample["data"]["hi_data"])
):
_modifier_type, _modifier_name = modifier_name.split("/")
_sample_data_len = len(sample["data"]["nom_data"])
_lo_data_len = len(sample["data"]["lo_data"])
_hi_data_len = len(sample["data"]["hi_data"])
raise InvalidModifier(
f"The '{sample_name}' sample {_modifier_type} modifier"
+ f" '{_modifier_name}' has data shape inconsistent with the sample.\n"
+ f"{sample_name} has 'data' of length {_sample_data_len} but {_modifier_name}"
+ f" has 'lo_data' of length {_lo_data_len} and 'hi_data' of length {_hi_data_len}."
)
return self.builder_data


Expand Down
17 changes: 14 additions & 3 deletions src/pyhf/modifiers/shapesys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import pyhf
from pyhf import events
from pyhf.tensor.manager import get_backend
from pyhf.exceptions import InvalidModifier
from pyhf.parameters import ParamViewer
from pyhf.tensor.manager import get_backend

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -70,8 +71,8 @@ def append(self, key, channel, sample, thismod, defined_samp):
def finalize(self):
default_backend = pyhf.default_backend

for modifier in self.builder_data.values():
for sample in modifier.values():
for modifier_name, modifier in self.builder_data.items():
for sample_name, sample in modifier.items():
sample["data"]["mask"] = default_backend.concatenate(
sample["data"]["mask"]
)
Expand All @@ -81,6 +82,16 @@ def finalize(self):
sample["data"]["nom_data"] = default_backend.concatenate(
sample["data"]["nom_data"]
)
if len(sample["data"]["nom_data"]) != len(sample["data"]["uncrt"]):
_modifier_type, _modifier_name = modifier_name.split("/")
_sample_data_len = len(sample["data"]["nom_data"])
_uncrt_len = len(sample["data"]["uncrt"])
raise InvalidModifier(
f"The '{sample_name}' sample {_modifier_type} modifier"
+ f" '{_modifier_name}' has data shape inconsistent with the sample.\n"
+ f"{sample_name} has 'data' of length {_sample_data_len} but {_modifier_name}"
+ f" has 'data' of length {_uncrt_len}."
)
return self.builder_data


Expand Down
17 changes: 14 additions & 3 deletions src/pyhf/modifiers/staterror.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import pyhf
from pyhf import events
from pyhf.tensor.manager import get_backend
from pyhf.exceptions import InvalidModifier
from pyhf.parameters import ParamViewer
from pyhf.tensor.manager import get_backend

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,8 +55,8 @@ def append(self, key, channel, sample, thismod, defined_samp):
def finalize(self):
default_backend = pyhf.default_backend

for modifier in self.builder_data.values():
for sample in modifier.values():
for modifier_name, modifier in self.builder_data.items():
for sample_name, sample in modifier.items():
sample["data"]["mask"] = default_backend.concatenate(
sample["data"]["mask"]
)
Expand All @@ -65,6 +66,16 @@ def finalize(self):
sample["data"]["nom_data"] = default_backend.concatenate(
sample["data"]["nom_data"]
)
if len(sample["data"]["nom_data"]) != len(sample["data"]["uncrt"]):
_modifier_type, _modifier_name = modifier_name.split("/")
_sample_data_len = len(sample["data"]["nom_data"])
_uncrt_len = len(sample["data"]["uncrt"])
raise InvalidModifier(
f"The '{sample_name}' sample {_modifier_type} modifier"
+ f" '{_modifier_name}' has data shape inconsistent with the sample.\n"
+ f"{sample_name} has 'data' of length {_sample_data_len} but {_modifier_name}"
+ f" has 'data' of length {_uncrt_len}."
)

for modname in self.builder_data.keys():
parname = modname.split('/')[1]
Expand Down
39 changes: 29 additions & 10 deletions tests/test_modifiers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import pyhf
import json

import numpy
import pytest
from jsonpatch import JsonPatch

modifiers_to_test = [
"histosys",
"normfactor",
"normsys",
"shapefactor",
"shapesys",
"staterror",
]
modifier_pdf_types = ["normal", None, "normal", None, "poisson", "normal"]
import pyhf


def test_shapefactor_build():
Expand Down Expand Up @@ -174,3 +169,27 @@ def test_shapesys_holes():
True,
False,
]


@pytest.mark.parametrize(
"patch_file",
[
"bad_histosys_modifier_patch.json",
"bad_shapesys_modifier_patch.json",
"bad_staterror_modifier_patch.json",
],
)
def test_invalid_bin_wise_modifier(datadir, patch_file):
"""
Test that bin-wise modifiers will raise an exception if their data shape
differs from their sample's.
"""
spec = json.load(open(datadir.join("spec.json")))

assert pyhf.Model(spec)

patch = JsonPatch.from_string(open(datadir.join(patch_file)).read())
bad_spec = patch.apply(spec)

with pytest.raises(pyhf.exceptions.InvalidModifier):
pyhf.Model(bad_spec)
24 changes: 24 additions & 0 deletions tests/test_modifiers/bad_histosys_modifier_patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"op": "add",
"path": "/channels/0/samples/1/modifiers",
"value": [
{
"name": "histosys_bad",
"type": "histosys",
"data": {
"hi_data": [
3,
6,
9
],
"lo_data": [
1,
2,
3
]
}
}
]
}
]
17 changes: 17 additions & 0 deletions tests/test_modifiers/bad_shapesys_modifier_patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"op": "add",
"path": "/channels/0/samples/1/modifiers",
"value": [
{
"name": "shapesys_bad",
"type": "shapesys",
"data": [
1,
2,
3
]
}
]
}
]
17 changes: 17 additions & 0 deletions tests/test_modifiers/bad_staterror_modifier_patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"op": "add",
"path": "/channels/0/samples/1/modifiers",
"value": [
{
"name": "staterror_bad",
"type": "staterror",
"data": [
1,
2,
3
]
}
]
}
]
35 changes: 35 additions & 0 deletions tests/test_modifiers/spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"channels": [
{
"name": "channel_1",
"samples": [
{
"name": "sample_1",
"data": [
1,
2,
3,
4
],
"modifiers": [
{
"name": "mu",
"type": "normfactor",
"data": null
}
]
},
{
"name": "sample_2",
"data": [
2,
4,
6,
8
],
"modifiers": []
}
]
}
]
}