Repository: scikit-hep/cabinetry

Issue:
Skip histosys modifiers for single-bin regions
Since `cabinetry` uses normalized `histosys` modifiers when constructing models, there is no need to have `histosys` modifiers for channels with a single bin: all information is contained in the correlated `normsys` modifier. The `histosys` modifiers should be skipped in the workspace construction in this case.

Comments:

Patch:
diff --git a/src/cabinetry/workspace.py b/src/cabinetry/workspace.py
index e6729ec..8521c5e 100644
--- a/src/cabinetry/workspace.py
+++ b/src/cabinetry/workspace.py
@@ -126,7 +126,9 @@ class WorkspaceBuilder:
         provides the `histosys` and `normsys` modifiers for ``pyhf`` (in `HistFactory`
         language, this corresponds to a `HistoSys` and an `OverallSys`). Symmetrization
         could happen either at this stage (this is the case currently), or somewhere
-        earlier, such as during template postprocessing.
+        earlier, such as during template postprocessing. A `histosys` modifier is not
+        created for single-bin channels, as it has no effect in this case (everything is
+        handled by the `normsys` modifier already).
 
         Args:
             region (Dict[str, Any]): region the systematic variation acts in
@@ -200,13 +202,15 @@ class WorkspaceBuilder:
         modifiers.append(norm_modifier)
 
         # add the shape part in a histosys
-        shape_modifier = {}
-        shape_modifier.update({"name": modifier_name})
-        shape_modifier.update({"type": "histosys"})
-        shape_modifier.update(
-            {"data": {"hi_data": histo_yield_up, "lo_data": histo_yield_down}}
-        )
-        modifiers.append(shape_modifier)
+        if len(histogram_nominal.yields) > 1:
+            # only relevant if there is more than one bin, otherwise there is no "shape"
+            shape_modifier = {}
+            shape_modifier.update({"name": modifier_name})
+            shape_modifier.update({"type": "histosys"})
+            shape_modifier.update(
+                {"data": {"hi_data": histo_yield_up, "lo_data": histo_yield_down}}
+            )
+            modifiers.append(shape_modifier)
         return modifiers
 
     def sys_modifiers(


Test patch:
diff --git a/tests/test_workspace.py b/tests/test_workspace.py
index 8cfa0af..f4902d5 100644
--- a/tests/test_workspace.py
+++ b/tests/test_workspace.py
@@ -136,6 +136,9 @@ def test_WorkspaceBuilder_normalization_modifier():
         # for test of symmetrization: up and nominal
         histo.Histogram.from_arrays([0, 1, 2], [26.0, 24.0], [0.1, 0.1]),
         histo.Histogram.from_arrays([0, 1, 2], [20.0, 20.0], [0.1, 0.1]),
+        # single bin for test of histosys being skipped (up and nominal)
+        histo.Histogram.from_arrays([0, 1], [26.0], [0.1]),
+        histo.Histogram.from_arrays([0, 1], [20.0], [0.1]),
     ],
 )
 def test_WorkspaceBuilder_normplusshape_modifiers(mock_histogram):
@@ -194,6 +197,12 @@ def test_WorkspaceBuilder_normplusshape_modifiers(mock_histogram):
         ((pathlib.Path("path"), region, sample, {}), {"modified": True}),
     ]
 
+    # single bin, causing histosys being skipped
+    modifiers = ws_builder.normplusshape_modifiers(region, sample, systematic)
+    assert modifiers == [
+        {"name": "mod_name", "type": "normsys", "data": {"hi": 1.3, "lo": 0.7}},
+    ]
+
 
 @mock.patch(
     "cabinetry.workspace.WorkspaceBuilder.normplusshape_modifiers",