Skip to content

Commit 33dae30

Browse files
authored
Merge pull request #12 from SpeysideHEP/incomplete_patch
Incomplete signal patch treatment
2 parents 49499d9 + 125d62e commit 33dae30

File tree

8 files changed

+124
-33
lines changed

8 files changed

+124
-33
lines changed

.zenodo.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"description": "pyhf plug-in for spey package",
33
"license": "MIT",
4-
"title": "SpeysideHEP/spey-pyhf: v0.1.4",
5-
"version": "v0.1.4",
4+
"title": "SpeysideHEP/spey-pyhf: v0.1.5",
5+
"version": "v0.1.5",
66
"upload_type": "software",
77
"creators": [
88
{
@@ -29,7 +29,7 @@
2929
},
3030
{
3131
"scheme": "url",
32-
"identifier": "https://github.com/SpeysideHEP/spey-pyhf/tree/v0.1.4",
32+
"identifier": "https://github.com/SpeysideHEP/spey-pyhf/tree/v0.1.5",
3333
"relation": "isSupplementTo"
3434
},
3535
{

docs/releases/changelog-v0.1.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
* Model loading has been improved for prefit and postfit scenarios.
2222
([#10](https://github.com/SpeysideHEP/spey-pyhf/pull/10))
2323

24+
* Improve undefined channel handling in the patchset
25+
([#12](https://github.com/SpeysideHEP/spey-pyhf/pull/12))
26+
2427
## Bug fixes
2528

2629
* Bugfix in `simplify` module, where signal injector was not initiated properly.

docs/tutorials/utils.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,15 @@ we can inject signal to any channel we like
6565
````{margin}
6666
```{admonition} Attention!
6767
:class: attention
68-
Notice that the rest of the channels will be removed. If some of the channels are needed during the inference, simply remove the ones with `"op": "remove"` tag from the patch set. Patch set can be generated via `interpreter.make_patch()` function.
68+
Notice that the rest of the channels will be added without any signal yields. If some of these channels need to be removed from the patch set, they can be added to the remove list via the ``remove_channel()`` function. **Note:** This behaviour has been updated in ``v0.1.5``. In the older versions, the channels that were not declared were removed.
6969
```
7070
````
7171

7272
```{code-cell} ipython3
7373
interpreter.inject_signal('SRHMEM_mct2', [5.0, 12.0, 4.0])
7474
```
7575

76-
Notice that I only added 3 inputs since the `"SRHMEM_mct2"` region has only 3 bins. One can inject signals to as many channels as one wants, but for simplicity, we will use only one channel. Now we are ready to export this signal patch and compute the exclusion limit
76+
Notice that we only added 3 inputs since the `"SRHMEM_mct2"` region has only 3 bins. One can inject signals to as many channels as one wants, but for simplicity, we will use only one channel. Now we are ready to export this signal patch and compute the exclusion limit
7777

7878
```{code-cell} ipython3
7979
pdf_wrapper = spey.get_backend("pyhf")

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
with open("src/spey_pyhf/_version.py", mode="r", encoding="UTF-8") as f:
77
version = f.readlines()[-1].split()[-1].strip("\"'")
88

9-
requirements = ["pyhf==0.7.6", "spey>=0.1.5"]
9+
requirements = ["pyhf==0.7.6", "spey>=0.1.9"]
1010

1111
docs = [
1212
"sphinx==6.2.1",

src/spey_pyhf/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version of the spey - pyhf plugin"""
22

3-
__version__ = "0.1.4"
3+
__version__ = "0.1.5"

src/spey_pyhf/data.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
from typing import Optional, List, Tuple, Dict, Text, Union, Iterator
2-
3-
from dataclasses import dataclass
1+
import copy
2+
import json
3+
import os
44
from abc import ABC, abstractmethod
5-
import json, copy, os
6-
import numpy as np
5+
from dataclasses import dataclass
6+
from typing import Dict, Iterator, List, Optional, Text, Tuple, Union
77

8-
from spey.base import ModelConfig
8+
import numpy as np
99
from spey import ExpectationType
10+
from spey.base import ModelConfig
11+
from spey.system.exceptions import InvalidInput
1012

11-
from . import manager, WorkspaceInterpreter
13+
from . import WorkspaceInterpreter, manager
1214

1315

1416
class Base(ABC):
@@ -289,13 +291,31 @@ def __call__(
289291
)
290292

291293
if expected == ExpectationType.apriori:
292-
data = sum(
293-
(
294-
self.expected_background_yields[ch]
294+
try:
295+
data = sum(
296+
(
297+
self.expected_background_yields[ch]
298+
for ch in self._model.config.channels
299+
),
300+
[],
301+
)
302+
except KeyError as err:
303+
# provide a useful error message to guide the user to the solution
304+
missing_channels = [
305+
ch
295306
for ch in self._model.config.channels
296-
),
297-
[],
298-
)
307+
if ch not in self.expected_background_yields
308+
]
309+
raise InvalidInput(
310+
"Unable to construct expected data. "
311+
+ (len(missing_channels) > 0)
312+
* (
313+
"\nThis is likely due to missing channels in the signal patch. "
314+
+ "The missing channels are: "
315+
+ ", ".join(missing_channels)
316+
+ "\nPlease provide appropriate action for the missing channels to continue."
317+
)
318+
) from err
299319
if include_aux:
300320
data += self._model.config.auxdata
301321
else:

src/spey_pyhf/helper_functions.py

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Helper function for creating and interpreting pyhf inputs"""
2-
from typing import Dict, Iterator, List, Text, Union, Optional
2+
import logging
3+
from typing import Dict, Iterator, List, Optional, Text, Tuple, Union
34

45
__all__ = ["WorkspaceInterpreter"]
56

@@ -8,15 +9,18 @@ def __dir__():
89
return __all__
910

1011

11-
def remove_from_json(idx: int) -> Dict:
12+
log = logging.getLogger("Spey")
13+
14+
15+
def remove_from_json(idx: int) -> Dict[Text, Text]:
1216
"""
1317
Remove channel from the json file
1418
1519
Args:
1620
idx (``int``): index of the channel
1721
1822
Returns:
19-
``Dict``:
23+
``Dict[Text, Text]``:
2024
JSON patch
2125
"""
2226
return {"op": "remove", "path": f"/channels/{idx}"}
@@ -59,13 +63,19 @@ class WorkspaceInterpreter:
5963
background_only_model (``Dict``): descrioption for the background only statistical model
6064
"""
6165

62-
__slots__ = ["background_only_model", "_signal_dict", "_signal_modifiers"]
66+
__slots__ = [
67+
"background_only_model",
68+
"_signal_dict",
69+
"_signal_modifiers",
70+
"_to_remove",
71+
]
6372

6473
def __init__(self, background_only_model: Dict):
6574
self.background_only_model = background_only_model
6675
"""Background only statistical model description"""
6776
self._signal_dict = {}
6877
self._signal_modifiers = {}
78+
self._to_remove = []
6979

7080
def __getitem__(self, item):
7181
return self.background_only_model[item]
@@ -89,15 +99,29 @@ def bin_map(self) -> Dict[Text, int]:
8999
def expected_background_yields(self) -> Dict[Text, List[float]]:
90100
"""Retreive expected background yields with respect to signal injection"""
91101
yields = {}
102+
undefined_channels = []
92103
for channel in self["channels"]:
93-
if channel["name"] in self._signal_dict:
104+
if channel["name"] not in self.remove_list:
94105
yields[channel["name"]] = []
95106
for smp in channel["samples"]:
96107
if len(yields[channel["name"]]) == 0:
97108
yields[channel["name"]] = [0.0] * len(smp["data"])
98109
yields[channel["name"]] = [
99110
ch + dt for ch, dt in zip(yields[channel["name"]], smp["data"])
100111
]
112+
if channel["name"] not in self._signal_dict:
113+
undefined_channels.append(channel["name"])
114+
if len(undefined_channels) > 0:
115+
log.warning(
116+
"Some of the channels are not defined in the patch set, "
117+
"these channels will be kept in the statistical model. "
118+
)
119+
log.warning(
120+
"If these channels are meant to be removed, please indicate them in the patch set."
121+
)
122+
log.warning(
123+
"Please check the following channel(s): " + ", ".join(undefined_channels)
124+
)
101125
return yields
102126

103127
def guess_channel_type(self, channel_name: Text) -> Text:
@@ -197,8 +221,10 @@ def make_patch(self) -> List[Dict]:
197221
ich, self._signal_dict[channel], self._signal_modifiers[channel]
198222
)
199223
)
200-
else:
224+
elif channel in self._to_remove:
201225
to_remove.append(remove_from_json(ich))
226+
else:
227+
log.warning(f"Undefined channel in the patch set: {channel}")
202228

203229
to_remove.sort(key=lambda p: p["path"].split("/")[-1], reverse=True)
204230

@@ -207,12 +233,46 @@ def make_patch(self) -> List[Dict]:
207233
def reset_signal(self) -> None:
208234
"""Clear the signal map"""
209235
self._signal_dict = {}
236+
self._to_remove = []
210237

211238
def add_patch(self, signal_patch: List[Dict]) -> None:
212239
"""Inject signal patch"""
213-
self._signal_dict = self.patch_to_map(signal_patch=signal_patch)
240+
self._signal_dict, self._to_remove = self.patch_to_map(
241+
signal_patch=signal_patch, return_remove_list=True
242+
)
214243

215-
def patch_to_map(self, signal_patch: List[Dict]) -> Dict[Text, Dict]:
244+
def remove_channel(self, channel_name: Text) -> None:
245+
"""
246+
Remove channel from the likelihood
247+
248+
.. versionadded:: 0.1.5
249+
250+
Args:
251+
channel_name (``Text``): name of the channel to be removed
252+
"""
253+
if channel_name in self.channels:
254+
if channel_name not in self._to_remove:
255+
self._to_remove.append(channel_name)
256+
else:
257+
log.error(
258+
f"Channel {channel_name} does not exist in the background only model. "
259+
+ "The available channels are "
260+
+ ", ".join(list(self.channels))
261+
)
262+
263+
@property
264+
def remove_list(self) -> List[Text]:
265+
"""
266+
Channels to be removed from the model
267+
268+
.. versionadded:: 0.1.5
269+
270+
"""
271+
return self._to_remove
272+
273+
def patch_to_map(
274+
self, signal_patch: List[Dict], return_remove_list: bool = False
275+
) -> Union[Tuple[Dict[Text, Dict], List[Text]], Dict[Text, Dict]]:
216276
"""
217277
Convert JSONPatch into signal map
218278
@@ -223,20 +283,28 @@ def patch_to_map(self, signal_patch: List[Dict]) -> Dict[Text, Dict]:
223283
224284
Args:
225285
signal_patch (``List[Dict]``): JSONPatch for the signal
286+
return_remove_list (``bool``, default ``False``): Inclure channels to be removed in the output
287+
288+
.. versionadded:: 0.1.5
226289
227290
Returns:
228-
``Dict[Text, Dict]``:
229-
signal map including the data and modifiers
291+
``Tuple[Dict[Text, Dict], List[Text]]`` or ``Dict[Text, Dict]``:
292+
signal map including the data and modifiers and the list of channels to be removed.
230293
"""
231294
signal_map = {}
295+
to_remove = []
232296
for item in signal_patch:
297+
path = int(item["path"].split("/")[2])
298+
channel_name = self["channels"][path]["name"]
233299
if item["op"] == "add":
234-
path = int(item["path"].split("/")[2])
235-
channel_name = self["channels"][path]["name"]
236300
signal_map[channel_name] = {
237301
"data": item["value"]["data"],
238302
"modifiers": item["value"].get(
239303
"modifiers", _default_modifiers(poi_name=self.poi_name[0][1])
240304
),
241305
}
306+
elif item["op"] == "remove":
307+
to_remove.append(channel_name)
308+
if return_remove_list:
309+
return signal_map, to_remove
242310
return signal_map

src/spey_pyhf/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class PyhfInterface(BackendBase):
7373
"""Version of the backend"""
7474
author: Text = "SpeysideHEP"
7575
"""Author of the backend"""
76-
spey_requires: Text = ">=0.1.5,<0.2.0"
76+
spey_requires: Text = ">=0.1.9,<0.2.0"
7777
"""Spey version required for the backend"""
7878
doi: List[Text] = ["10.5281/zenodo.1169739", "10.21105/joss.02823"]
7979
"""Citable DOI for the backend"""

0 commit comments

Comments
 (0)