We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use of `del` for staterror in writexml necessary?
In PR #435 the use of `del` was added in 

https://github.com/scikit-hep/pyhf/blob/e3d879f3e4982ac629bec7bf92d78b00025e52dc/src/pyhf/writexml.py#L186-L191

I assume because later on in the PR it mentions

https://github.com/scikit-hep/pyhf/blob/e3d879f3e4982ac629bec7bf92d78b00025e52dc/tests/test_export.py#L257-L259

However, if you remove that `del` statement

```
pytest -sx tests/test_export.py -k test_export_modifier
```

still passes.

Do we still need it? @kratsg have any thoughts here?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/pyhf/writexml.py`
Content:
```
1 import logging
2 
3 from pathlib import Path
4 import shutil
5 import xml.etree.ElementTree as ET
6 import numpy as np
7 
8 import uproot
9 
10 from pyhf.mixins import _ChannelSummaryMixin
11 from pyhf.schema import path as schema_path
12 
13 _ROOT_DATA_FILE = None
14 
15 log = logging.getLogger(__name__)
16 
17 __all__ = [
18     "build_channel",
19     "build_data",
20     "build_measurement",
21     "build_modifier",
22     "build_sample",
23     "indent",
24 ]
25 
26 
27 def __dir__():
28     return __all__
29 
30 
31 # 'spec' gets passed through all functions as NormFactor is a unique case of having
32 # parameter configurations stored at the modifier-definition-spec level. This means
33 # that build_modifier() needs access to the measurements. The call stack is:
34 #
35 #      writexml
36 #          ->build_channel
37 #              ->build_sample
38 #                  ->build_modifier
39 #
40 #  Therefore, 'spec' needs to be threaded through all these calls.
41 
42 
43 def _make_hist_name(channel, sample, modifier='', prefix='hist', suffix=''):
44     middle = '_'.join(filter(lambda x: x, [channel, sample, modifier]))
45     return f"{prefix}{middle}{suffix}"
46 
47 
48 def _export_root_histogram(hist_name, data):
49     if hist_name in _ROOT_DATA_FILE:
50         raise KeyError(f"Duplicate key {hist_name} being written.")
51     _ROOT_DATA_FILE[hist_name] = uproot.to_writable(
52         (np.asarray(data), np.arange(len(data) + 1))
53     )
54 
55 
56 # https://stackoverflow.com/a/4590052
57 def indent(elem, level=0):
58     i = "\n" + level * "  "
59     if elem:
60         if not elem.text or not elem.text.strip():
61             elem.text = i + "  "
62         if not elem.tail or not elem.tail.strip():
63             elem.tail = i
64         for subelem in elem:
65             indent(subelem, level + 1)
66         if not elem.tail or not elem.tail.strip():
67             elem.tail = i
68     else:
69         if level and (not elem.tail or not elem.tail.strip()):
70             elem.tail = i
71 
72 
73 def build_measurement(measurementspec, modifiertypes):
74     """
75     Build the XML measurement specification for a given measurement adhering to defs.json/#definitions/measurement.
76 
77     Args:
78         measurementspec (:obj:`dict`): The measurements specification from a :class:`~pyhf.workspace.Workspace`.
79         modifiertypes (:obj:`dict`): A mapping from modifier name (:obj:`str`) to modifier type (:obj:`str`).
80 
81     Returns:
82         :class:`xml.etree.cElementTree.Element`: The XML measurement specification.
83 
84     """
85     # need to determine prefixes
86     prefixes = {
87         'normsys': 'alpha_',
88         'histosys': 'alpha_',
89         'shapesys': 'gamma_',
90         'staterror': 'gamma_',
91     }
92 
93     config = measurementspec['config']
94     name = measurementspec['name']
95     poi = config['poi']
96 
97     # we want to know which parameters are fixed (constant)
98     # and to additionally extract the luminosity information
99     fixed_params = []
100     lumi = 1.0
101     lumierr = 0.0
102     for parameter in config['parameters']:
103         if parameter.get('fixed', False):
104             pname = parameter['name']
105             if pname == 'lumi':
106                 fixed_params.append('Lumi')
107             else:
108                 prefix = prefixes.get(modifiertypes[pname], '')
109                 fixed_params.append(f'{prefix}{pname}')
110         # we found luminosity, so handle it
111         if parameter['name'] == 'lumi':
112             lumi = parameter['auxdata'][0]
113             lumierr = parameter['sigmas'][0]
114 
115     # define measurement
116     meas = ET.Element(
117         "Measurement",
118         Name=name,
119         Lumi=str(lumi),
120         LumiRelErr=str(lumierr),
121         ExportOnly=str(True),
122     )
123     poiel = ET.Element('POI')
124     poiel.text = poi
125     meas.append(poiel)
126 
127     # add fixed parameters (constant)
128     if fixed_params:
129         se = ET.Element('ParamSetting', Const='True')
130         se.text = ' '.join(fixed_params)
131         meas.append(se)
132     return meas
133 
134 
135 def build_modifier(spec, modifierspec, channelname, samplename, sampledata):
136     if modifierspec['name'] == 'lumi':
137         return None
138     mod_map = {
139         'histosys': 'HistoSys',
140         'staterror': 'StatError',
141         'normsys': 'OverallSys',
142         'shapesys': 'ShapeSys',
143         'normfactor': 'NormFactor',
144         'shapefactor': 'ShapeFactor',
145     }
146 
147     attrs = {'Name': modifierspec['name']}
148     if modifierspec['type'] == 'histosys':
149         attrs['HistoNameLow'] = _make_hist_name(
150             channelname, samplename, modifierspec['name'], suffix='Low'
151         )
152         attrs['HistoNameHigh'] = _make_hist_name(
153             channelname, samplename, modifierspec['name'], suffix='High'
154         )
155         _export_root_histogram(attrs['HistoNameLow'], modifierspec['data']['lo_data'])
156         _export_root_histogram(attrs['HistoNameHigh'], modifierspec['data']['hi_data'])
157     elif modifierspec['type'] == 'normsys':
158         attrs['High'] = str(modifierspec['data']['hi'])
159         attrs['Low'] = str(modifierspec['data']['lo'])
160     elif modifierspec['type'] == 'normfactor':
161         # NB: only look at first measurement for normfactor configs. In order
162         # to dump as HistFactory XML, this has to be the same for all
163         # measurements or it will not work correctly. Why?
164         #
165         # Unlike other modifiers, NormFactor has the unique circumstance of
166         # defining its parameter configurations at the modifier level inside
167         # the channel specification, instead of at the measurement level, like
168         # all of the other modifiers.
169         #
170         # However, since I strive for perfection, the "Const" attribute will
171         # never be set here, but at the per-measurement configuration instead
172         # like all other parameters. This is an acceptable compromise.
173         #
174         # Lastly, if a normfactor parameter configuration doesn't exist in the
175         # first measurement parameter configuration, then set defaults.
176         val = 1
177         low = 0
178         high = 10
179         for p in spec['measurements'][0]['config']['parameters']:
180             if p['name'] == modifierspec['name']:
181                 val = p.get('inits', [val])[0]
182                 low, high = p.get('bounds', [[low, high]])[0]
183         attrs['Val'] = str(val)
184         attrs['Low'] = str(low)
185         attrs['High'] = str(high)
186     elif modifierspec['type'] == 'staterror':
187         attrs['Activate'] = 'True'
188         attrs['HistoName'] = _make_hist_name(
189             channelname, samplename, modifierspec['name']
190         )
191         del attrs['Name']
192         # need to make this a relative uncertainty stored in ROOT file
193         _export_root_histogram(
194             attrs['HistoName'],
195             np.divide(
196                 modifierspec['data'],
197                 sampledata,
198                 out=np.zeros_like(sampledata),
199                 where=np.asarray(sampledata) != 0,
200                 dtype='float',
201             ).tolist(),
202         )
203     elif modifierspec['type'] == 'shapesys':
204         attrs['ConstraintType'] = 'Poisson'
205         attrs['HistoName'] = _make_hist_name(
206             channelname, samplename, modifierspec['name']
207         )
208         # need to make this a relative uncertainty stored in ROOT file
209         _export_root_histogram(
210             attrs['HistoName'],
211             [
212                 np.divide(
213                     a, b, out=np.zeros_like(a), where=np.asarray(b) != 0, dtype='float'
214                 )
215                 for a, b in np.array(
216                     (modifierspec['data'], sampledata), dtype="float"
217                 ).T
218             ],
219         )
220     elif modifierspec['type'] == 'shapefactor':
221         pass
222     else:
223         log.warning(
224             f"Skipping modifier {modifierspec['name']}({modifierspec['type']}) for now"
225         )
226         return None
227 
228     modifier = ET.Element(mod_map[modifierspec['type']], **attrs)
229     return modifier
230 
231 
232 def build_sample(spec, samplespec, channelname):
233     histname = _make_hist_name(channelname, samplespec['name'])
234     attrs = {
235         'Name': samplespec['name'],
236         'HistoName': histname,
237         'InputFile': _ROOT_DATA_FILE.file_path,
238         'NormalizeByTheory': 'False',
239     }
240     sample = ET.Element('Sample', **attrs)
241     for modspec in samplespec['modifiers']:
242         # if lumi modifier added for this sample, need to set NormalizeByTheory
243         if modspec['type'] == 'lumi':
244             sample.attrib.update({'NormalizeByTheory': 'True'})
245         modifier = build_modifier(
246             spec, modspec, channelname, samplespec['name'], samplespec['data']
247         )
248         if modifier is not None:
249             sample.append(modifier)
250     _export_root_histogram(histname, samplespec['data'])
251     return sample
252 
253 
254 def build_data(obsspec, channelname):
255     histname = _make_hist_name(channelname, 'data')
256     data = ET.Element('Data', HistoName=histname, InputFile=_ROOT_DATA_FILE.file_path)
257 
258     observation = next((obs for obs in obsspec if obs['name'] == channelname), None)
259     _export_root_histogram(histname, observation['data'])
260     return data
261 
262 
263 def build_channel(spec, channelspec, obsspec):
264     channel = ET.Element(
265         'Channel', Name=channelspec['name'], InputFile=_ROOT_DATA_FILE.file_path
266     )
267     if obsspec:
268         data = build_data(obsspec, channelspec['name'])
269         channel.append(data)
270     for samplespec in channelspec['samples']:
271         channel.append(build_sample(spec, samplespec, channelspec['name']))
272     return channel
273 
274 
275 def writexml(spec, specdir, data_rootdir, resultprefix):
276     global _ROOT_DATA_FILE
277 
278     shutil.copyfile(
279         schema_path.joinpath('HistFactorySchema.dtd'),
280         Path(specdir).parent.joinpath('HistFactorySchema.dtd'),
281     )
282     combination = ET.Element(
283         "Combination", OutputFilePrefix=str(Path(specdir).joinpath(resultprefix))
284     )
285 
286     with uproot.recreate(Path(data_rootdir).joinpath('data.root')) as _ROOT_DATA_FILE:
287         for channelspec in spec['channels']:
288             channelfilename = str(
289                 Path(specdir).joinpath(f'{resultprefix}_{channelspec["name"]}.xml')
290             )
291             with open(channelfilename, 'w') as channelfile:
292                 channel = build_channel(spec, channelspec, spec.get('observations'))
293                 indent(channel)
294                 channelfile.write(
295                     "<!DOCTYPE Channel SYSTEM '../HistFactorySchema.dtd'>\n\n"
296                 )
297                 channelfile.write(
298                     ET.tostring(channel, encoding='utf-8').decode('utf-8')
299                 )
300 
301             inp = ET.Element("Input")
302             inp.text = channelfilename
303             combination.append(inp)
304 
305     # need information about modifier types to get the right prefix in measurement
306     mixin = _ChannelSummaryMixin(channels=spec['channels'])
307 
308     for measurement in spec['measurements']:
309         combination.append(build_measurement(measurement, dict(mixin.modifiers)))
310     indent(combination)
311     return b"<!DOCTYPE Combination  SYSTEM 'HistFactorySchema.dtd'>\n\n" + ET.tostring(
312         combination, encoding='utf-8'
313     )
314
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/pyhf/writexml.py b/src/pyhf/writexml.py
--- a/src/pyhf/writexml.py
+++ b/src/pyhf/writexml.py
@@ -188,6 +188,7 @@
         attrs['HistoName'] = _make_hist_name(
             channelname, samplename, modifierspec['name']
         )
+        # must be deleted, HiFa XML specification does not support 'Name'
         del attrs['Name']
         # need to make this a relative uncertainty stored in ROOT file
         _export_root_histogram(

{"golden_diff": "diff --git a/src/pyhf/writexml.py b/src/pyhf/writexml.py\n--- a/src/pyhf/writexml.py\n+++ b/src/pyhf/writexml.py\n@@ -188,6 +188,7 @@\n         attrs['HistoName'] = _make_hist_name(\n             channelname, samplename, modifierspec['name']\n         )\n+        # must be deleted, HiFa XML specification does not support 'Name'\n         del attrs['Name']\n         # need to make this a relative uncertainty stored in ROOT file\n         _export_root_histogram(\n", "issue": "Use of `del` for staterror in writexml necessary?\nIn PR #435 the use of `del` was added in \r\n\r\nhttps://github.com/scikit-hep/pyhf/blob/e3d879f3e4982ac629bec7bf92d78b00025e52dc/src/pyhf/writexml.py#L186-L191\r\n\r\nI assume because later on in the PR it mentions\r\n\r\nhttps://github.com/scikit-hep/pyhf/blob/e3d879f3e4982ac629bec7bf92d78b00025e52dc/tests/test_export.py#L257-L259\r\n\r\nHowever, if you remove that `del` statement\r\n\r\n```\r\npytest -sx tests/test_export.py -k test_export_modifier\r\n```\r\n\r\nstill passes.\r\n\r\nDo we still need it? @kratsg have any thoughts here?\n", "before_files": [{"content": "import logging\n\nfrom pathlib import Path\nimport shutil\nimport xml.etree.ElementTree as ET\nimport numpy as np\n\nimport uproot\n\nfrom pyhf.mixins import _ChannelSummaryMixin\nfrom pyhf.schema import path as schema_path\n\n_ROOT_DATA_FILE = None\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\n    \"build_channel\",\n    \"build_data\",\n    \"build_measurement\",\n    \"build_modifier\",\n    \"build_sample\",\n    \"indent\",\n]\n\n\ndef __dir__():\n    return __all__\n\n\n# 'spec' gets passed through all functions as NormFactor is a unique case of having\n# parameter configurations stored at the modifier-definition-spec level. This means\n# that build_modifier() needs access to the measurements. The call stack is:\n#\n#      writexml\n#          ->build_channel\n#              ->build_sample\n#                  ->build_modifier\n#\n#  Therefore, 'spec' needs to be threaded through all these calls.\n\n\ndef _make_hist_name(channel, sample, modifier='', prefix='hist', suffix=''):\n    middle = '_'.join(filter(lambda x: x, [channel, sample, modifier]))\n    return f\"{prefix}{middle}{suffix}\"\n\n\ndef _export_root_histogram(hist_name, data):\n    if hist_name in _ROOT_DATA_FILE:\n        raise KeyError(f\"Duplicate key {hist_name} being written.\")\n    _ROOT_DATA_FILE[hist_name] = uproot.to_writable(\n        (np.asarray(data), np.arange(len(data) + 1))\n    )\n\n\n# https://stackoverflow.com/a/4590052\ndef indent(elem, level=0):\n    i = \"\\n\" + level * \"  \"\n    if elem:\n        if not elem.text or not elem.text.strip():\n            elem.text = i + \"  \"\n        if not elem.tail or not elem.tail.strip():\n            elem.tail = i\n        for subelem in elem:\n            indent(subelem, level + 1)\n        if not elem.tail or not elem.tail.strip():\n            elem.tail = i\n    else:\n        if level and (not elem.tail or not elem.tail.strip()):\n            elem.tail = i\n\n\ndef build_measurement(measurementspec, modifiertypes):\n    \"\"\"\n    Build the XML measurement specification for a given measurement adhering to defs.json/#definitions/measurement.\n\n    Args:\n        measurementspec (:obj:`dict`): The measurements specification from a :class:`~pyhf.workspace.Workspace`.\n        modifiertypes (:obj:`dict`): A mapping from modifier name (:obj:`str`) to modifier type (:obj:`str`).\n\n    Returns:\n        :class:`xml.etree.cElementTree.Element`: The XML measurement specification.\n\n    \"\"\"\n    # need to determine prefixes\n    prefixes = {\n        'normsys': 'alpha_',\n        'histosys': 'alpha_',\n        'shapesys': 'gamma_',\n        'staterror': 'gamma_',\n    }\n\n    config = measurementspec['config']\n    name = measurementspec['name']\n    poi = config['poi']\n\n    # we want to know which parameters are fixed (constant)\n    # and to additionally extract the luminosity information\n    fixed_params = []\n    lumi = 1.0\n    lumierr = 0.0\n    for parameter in config['parameters']:\n        if parameter.get('fixed', False):\n            pname = parameter['name']\n            if pname == 'lumi':\n                fixed_params.append('Lumi')\n            else:\n                prefix = prefixes.get(modifiertypes[pname], '')\n                fixed_params.append(f'{prefix}{pname}')\n        # we found luminosity, so handle it\n        if parameter['name'] == 'lumi':\n            lumi = parameter['auxdata'][0]\n            lumierr = parameter['sigmas'][0]\n\n    # define measurement\n    meas = ET.Element(\n        \"Measurement\",\n        Name=name,\n        Lumi=str(lumi),\n        LumiRelErr=str(lumierr),\n        ExportOnly=str(True),\n    )\n    poiel = ET.Element('POI')\n    poiel.text = poi\n    meas.append(poiel)\n\n    # add fixed parameters (constant)\n    if fixed_params:\n        se = ET.Element('ParamSetting', Const='True')\n        se.text = ' '.join(fixed_params)\n        meas.append(se)\n    return meas\n\n\ndef build_modifier(spec, modifierspec, channelname, samplename, sampledata):\n    if modifierspec['name'] == 'lumi':\n        return None\n    mod_map = {\n        'histosys': 'HistoSys',\n        'staterror': 'StatError',\n        'normsys': 'OverallSys',\n        'shapesys': 'ShapeSys',\n        'normfactor': 'NormFactor',\n        'shapefactor': 'ShapeFactor',\n    }\n\n    attrs = {'Name': modifierspec['name']}\n    if modifierspec['type'] == 'histosys':\n        attrs['HistoNameLow'] = _make_hist_name(\n            channelname, samplename, modifierspec['name'], suffix='Low'\n        )\n        attrs['HistoNameHigh'] = _make_hist_name(\n            channelname, samplename, modifierspec['name'], suffix='High'\n        )\n        _export_root_histogram(attrs['HistoNameLow'], modifierspec['data']['lo_data'])\n        _export_root_histogram(attrs['HistoNameHigh'], modifierspec['data']['hi_data'])\n    elif modifierspec['type'] == 'normsys':\n        attrs['High'] = str(modifierspec['data']['hi'])\n        attrs['Low'] = str(modifierspec['data']['lo'])\n    elif modifierspec['type'] == 'normfactor':\n        # NB: only look at first measurement for normfactor configs. In order\n        # to dump as HistFactory XML, this has to be the same for all\n        # measurements or it will not work correctly. Why?\n        #\n        # Unlike other modifiers, NormFactor has the unique circumstance of\n        # defining its parameter configurations at the modifier level inside\n        # the channel specification, instead of at the measurement level, like\n        # all of the other modifiers.\n        #\n        # However, since I strive for perfection, the \"Const\" attribute will\n        # never be set here, but at the per-measurement configuration instead\n        # like all other parameters. This is an acceptable compromise.\n        #\n        # Lastly, if a normfactor parameter configuration doesn't exist in the\n        # first measurement parameter configuration, then set defaults.\n        val = 1\n        low = 0\n        high = 10\n        for p in spec['measurements'][0]['config']['parameters']:\n            if p['name'] == modifierspec['name']:\n                val = p.get('inits', [val])[0]\n                low, high = p.get('bounds', [[low, high]])[0]\n        attrs['Val'] = str(val)\n        attrs['Low'] = str(low)\n        attrs['High'] = str(high)\n    elif modifierspec['type'] == 'staterror':\n        attrs['Activate'] = 'True'\n        attrs['HistoName'] = _make_hist_name(\n            channelname, samplename, modifierspec['name']\n        )\n        del attrs['Name']\n        # need to make this a relative uncertainty stored in ROOT file\n        _export_root_histogram(\n            attrs['HistoName'],\n            np.divide(\n                modifierspec['data'],\n                sampledata,\n                out=np.zeros_like(sampledata),\n                where=np.asarray(sampledata) != 0,\n                dtype='float',\n            ).tolist(),\n        )\n    elif modifierspec['type'] == 'shapesys':\n        attrs['ConstraintType'] = 'Poisson'\n        attrs['HistoName'] = _make_hist_name(\n            channelname, samplename, modifierspec['name']\n        )\n        # need to make this a relative uncertainty stored in ROOT file\n        _export_root_histogram(\n            attrs['HistoName'],\n            [\n                np.divide(\n                    a, b, out=np.zeros_like(a), where=np.asarray(b) != 0, dtype='float'\n                )\n                for a, b in np.array(\n                    (modifierspec['data'], sampledata), dtype=\"float\"\n                ).T\n            ],\n        )\n    elif modifierspec['type'] == 'shapefactor':\n        pass\n    else:\n        log.warning(\n            f\"Skipping modifier {modifierspec['name']}({modifierspec['type']}) for now\"\n        )\n        return None\n\n    modifier = ET.Element(mod_map[modifierspec['type']], **attrs)\n    return modifier\n\n\ndef build_sample(spec, samplespec, channelname):\n    histname = _make_hist_name(channelname, samplespec['name'])\n    attrs = {\n        'Name': samplespec['name'],\n        'HistoName': histname,\n        'InputFile': _ROOT_DATA_FILE.file_path,\n        'NormalizeByTheory': 'False',\n    }\n    sample = ET.Element('Sample', **attrs)\n    for modspec in samplespec['modifiers']:\n        # if lumi modifier added for this sample, need to set NormalizeByTheory\n        if modspec['type'] == 'lumi':\n            sample.attrib.update({'NormalizeByTheory': 'True'})\n        modifier = build_modifier(\n            spec, modspec, channelname, samplespec['name'], samplespec['data']\n        )\n        if modifier is not None:\n            sample.append(modifier)\n    _export_root_histogram(histname, samplespec['data'])\n    return sample\n\n\ndef build_data(obsspec, channelname):\n    histname = _make_hist_name(channelname, 'data')\n    data = ET.Element('Data', HistoName=histname, InputFile=_ROOT_DATA_FILE.file_path)\n\n    observation = next((obs for obs in obsspec if obs['name'] == channelname), None)\n    _export_root_histogram(histname, observation['data'])\n    return data\n\n\ndef build_channel(spec, channelspec, obsspec):\n    channel = ET.Element(\n        'Channel', Name=channelspec['name'], InputFile=_ROOT_DATA_FILE.file_path\n    )\n    if obsspec:\n        data = build_data(obsspec, channelspec['name'])\n        channel.append(data)\n    for samplespec in channelspec['samples']:\n        channel.append(build_sample(spec, samplespec, channelspec['name']))\n    return channel\n\n\ndef writexml(spec, specdir, data_rootdir, resultprefix):\n    global _ROOT_DATA_FILE\n\n    shutil.copyfile(\n        schema_path.joinpath('HistFactorySchema.dtd'),\n        Path(specdir).parent.joinpath('HistFactorySchema.dtd'),\n    )\n    combination = ET.Element(\n        \"Combination\", OutputFilePrefix=str(Path(specdir).joinpath(resultprefix))\n    )\n\n    with uproot.recreate(Path(data_rootdir).joinpath('data.root')) as _ROOT_DATA_FILE:\n        for channelspec in spec['channels']:\n            channelfilename = str(\n                Path(specdir).joinpath(f'{resultprefix}_{channelspec[\"name\"]}.xml')\n            )\n            with open(channelfilename, 'w') as channelfile:\n                channel = build_channel(spec, channelspec, spec.get('observations'))\n                indent(channel)\n                channelfile.write(\n                    \"<!DOCTYPE Channel SYSTEM '../HistFactorySchema.dtd'>\\n\\n\"\n                )\n                channelfile.write(\n                    ET.tostring(channel, encoding='utf-8').decode('utf-8')\n                )\n\n            inp = ET.Element(\"Input\")\n            inp.text = channelfilename\n            combination.append(inp)\n\n    # need information about modifier types to get the right prefix in measurement\n    mixin = _ChannelSummaryMixin(channels=spec['channels'])\n\n    for measurement in spec['measurements']:\n        combination.append(build_measurement(measurement, dict(mixin.modifiers)))\n    indent(combination)\n    return b\"<!DOCTYPE Combination  SYSTEM 'HistFactorySchema.dtd'>\\n\\n\" + ET.tostring(\n        combination, encoding='utf-8'\n    )\n", "path": "src/pyhf/writexml.py"}], "after_files": [{"content": "import logging\n\nfrom pathlib import Path\nimport shutil\nimport xml.etree.ElementTree as ET\nimport numpy as np\n\nimport uproot\n\nfrom pyhf.mixins import _ChannelSummaryMixin\nfrom pyhf.schema import path as schema_path\n\n_ROOT_DATA_FILE = None\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\n    \"build_channel\",\n    \"build_data\",\n    \"build_measurement\",\n    \"build_modifier\",\n    \"build_sample\",\n    \"indent\",\n]\n\n\ndef __dir__():\n    return __all__\n\n\n# 'spec' gets passed through all functions as NormFactor is a unique case of having\n# parameter configurations stored at the modifier-definition-spec level. This means\n# that build_modifier() needs access to the measurements. The call stack is:\n#\n#      writexml\n#          ->build_channel\n#              ->build_sample\n#                  ->build_modifier\n#\n#  Therefore, 'spec' needs to be threaded through all these calls.\n\n\ndef _make_hist_name(channel, sample, modifier='', prefix='hist', suffix=''):\n    middle = '_'.join(filter(lambda x: x, [channel, sample, modifier]))\n    return f\"{prefix}{middle}{suffix}\"\n\n\ndef _export_root_histogram(hist_name, data):\n    if hist_name in _ROOT_DATA_FILE:\n        raise KeyError(f\"Duplicate key {hist_name} being written.\")\n    _ROOT_DATA_FILE[hist_name] = uproot.to_writable(\n        (np.asarray(data), np.arange(len(data) + 1))\n    )\n\n\n# https://stackoverflow.com/a/4590052\ndef indent(elem, level=0):\n    i = \"\\n\" + level * \"  \"\n    if elem:\n        if not elem.text or not elem.text.strip():\n            elem.text = i + \"  \"\n        if not elem.tail or not elem.tail.strip():\n            elem.tail = i\n        for subelem in elem:\n            indent(subelem, level + 1)\n        if not elem.tail or not elem.tail.strip():\n            elem.tail = i\n    else:\n        if level and (not elem.tail or not elem.tail.strip()):\n            elem.tail = i\n\n\ndef build_measurement(measurementspec, modifiertypes):\n    \"\"\"\n    Build the XML measurement specification for a given measurement adhering to defs.json/#definitions/measurement.\n\n    Args:\n        measurementspec (:obj:`dict`): The measurements specification from a :class:`~pyhf.workspace.Workspace`.\n        modifiertypes (:obj:`dict`): A mapping from modifier name (:obj:`str`) to modifier type (:obj:`str`).\n\n    Returns:\n        :class:`xml.etree.cElementTree.Element`: The XML measurement specification.\n\n    \"\"\"\n    # need to determine prefixes\n    prefixes = {\n        'normsys': 'alpha_',\n        'histosys': 'alpha_',\n        'shapesys': 'gamma_',\n        'staterror': 'gamma_',\n    }\n\n    config = measurementspec['config']\n    name = measurementspec['name']\n    poi = config['poi']\n\n    # we want to know which parameters are fixed (constant)\n    # and to additionally extract the luminosity information\n    fixed_params = []\n    lumi = 1.0\n    lumierr = 0.0\n    for parameter in config['parameters']:\n        if parameter.get('fixed', False):\n            pname = parameter['name']\n            if pname == 'lumi':\n                fixed_params.append('Lumi')\n            else:\n                prefix = prefixes.get(modifiertypes[pname], '')\n                fixed_params.append(f'{prefix}{pname}')\n        # we found luminosity, so handle it\n        if parameter['name'] == 'lumi':\n            lumi = parameter['auxdata'][0]\n            lumierr = parameter['sigmas'][0]\n\n    # define measurement\n    meas = ET.Element(\n        \"Measurement\",\n        Name=name,\n        Lumi=str(lumi),\n        LumiRelErr=str(lumierr),\n        ExportOnly=str(True),\n    )\n    poiel = ET.Element('POI')\n    poiel.text = poi\n    meas.append(poiel)\n\n    # add fixed parameters (constant)\n    if fixed_params:\n        se = ET.Element('ParamSetting', Const='True')\n        se.text = ' '.join(fixed_params)\n        meas.append(se)\n    return meas\n\n\ndef build_modifier(spec, modifierspec, channelname, samplename, sampledata):\n    if modifierspec['name'] == 'lumi':\n        return None\n    mod_map = {\n        'histosys': 'HistoSys',\n        'staterror': 'StatError',\n        'normsys': 'OverallSys',\n        'shapesys': 'ShapeSys',\n        'normfactor': 'NormFactor',\n        'shapefactor': 'ShapeFactor',\n    }\n\n    attrs = {'Name': modifierspec['name']}\n    if modifierspec['type'] == 'histosys':\n        attrs['HistoNameLow'] = _make_hist_name(\n            channelname, samplename, modifierspec['name'], suffix='Low'\n        )\n        attrs['HistoNameHigh'] = _make_hist_name(\n            channelname, samplename, modifierspec['name'], suffix='High'\n        )\n        _export_root_histogram(attrs['HistoNameLow'], modifierspec['data']['lo_data'])\n        _export_root_histogram(attrs['HistoNameHigh'], modifierspec['data']['hi_data'])\n    elif modifierspec['type'] == 'normsys':\n        attrs['High'] = str(modifierspec['data']['hi'])\n        attrs['Low'] = str(modifierspec['data']['lo'])\n    elif modifierspec['type'] == 'normfactor':\n        # NB: only look at first measurement for normfactor configs. In order\n        # to dump as HistFactory XML, this has to be the same for all\n        # measurements or it will not work correctly. Why?\n        #\n        # Unlike other modifiers, NormFactor has the unique circumstance of\n        # defining its parameter configurations at the modifier level inside\n        # the channel specification, instead of at the measurement level, like\n        # all of the other modifiers.\n        #\n        # However, since I strive for perfection, the \"Const\" attribute will\n        # never be set here, but at the per-measurement configuration instead\n        # like all other parameters. This is an acceptable compromise.\n        #\n        # Lastly, if a normfactor parameter configuration doesn't exist in the\n        # first measurement parameter configuration, then set defaults.\n        val = 1\n        low = 0\n        high = 10\n        for p in spec['measurements'][0]['config']['parameters']:\n            if p['name'] == modifierspec['name']:\n                val = p.get('inits', [val])[0]\n                low, high = p.get('bounds', [[low, high]])[0]\n        attrs['Val'] = str(val)\n        attrs['Low'] = str(low)\n        attrs['High'] = str(high)\n    elif modifierspec['type'] == 'staterror':\n        attrs['Activate'] = 'True'\n        attrs['HistoName'] = _make_hist_name(\n            channelname, samplename, modifierspec['name']\n        )\n        # must be deleted, HiFa XML specification does not support 'Name'\n        del attrs['Name']\n        # need to make this a relative uncertainty stored in ROOT file\n        _export_root_histogram(\n            attrs['HistoName'],\n            np.divide(\n                modifierspec['data'],\n                sampledata,\n                out=np.zeros_like(sampledata),\n                where=np.asarray(sampledata) != 0,\n                dtype='float',\n            ).tolist(),\n        )\n    elif modifierspec['type'] == 'shapesys':\n        attrs['ConstraintType'] = 'Poisson'\n        attrs['HistoName'] = _make_hist_name(\n            channelname, samplename, modifierspec['name']\n        )\n        # need to make this a relative uncertainty stored in ROOT file\n        _export_root_histogram(\n            attrs['HistoName'],\n            [\n                np.divide(\n                    a, b, out=np.zeros_like(a), where=np.asarray(b) != 0, dtype='float'\n                )\n                for a, b in np.array(\n                    (modifierspec['data'], sampledata), dtype=\"float\"\n                ).T\n            ],\n        )\n    elif modifierspec['type'] == 'shapefactor':\n        pass\n    else:\n        log.warning(\n            f\"Skipping modifier {modifierspec['name']}({modifierspec['type']}) for now\"\n        )\n        return None\n\n    modifier = ET.Element(mod_map[modifierspec['type']], **attrs)\n    return modifier\n\n\ndef build_sample(spec, samplespec, channelname):\n    histname = _make_hist_name(channelname, samplespec['name'])\n    attrs = {\n        'Name': samplespec['name'],\n        'HistoName': histname,\n        'InputFile': _ROOT_DATA_FILE.file_path,\n        'NormalizeByTheory': 'False',\n    }\n    sample = ET.Element('Sample', **attrs)\n    for modspec in samplespec['modifiers']:\n        # if lumi modifier added for this sample, need to set NormalizeByTheory\n        if modspec['type'] == 'lumi':\n            sample.attrib.update({'NormalizeByTheory': 'True'})\n        modifier = build_modifier(\n            spec, modspec, channelname, samplespec['name'], samplespec['data']\n        )\n        if modifier is not None:\n            sample.append(modifier)\n    _export_root_histogram(histname, samplespec['data'])\n    return sample\n\n\ndef build_data(obsspec, channelname):\n    histname = _make_hist_name(channelname, 'data')\n    data = ET.Element('Data', HistoName=histname, InputFile=_ROOT_DATA_FILE.file_path)\n\n    observation = next((obs for obs in obsspec if obs['name'] == channelname), None)\n    _export_root_histogram(histname, observation['data'])\n    return data\n\n\ndef build_channel(spec, channelspec, obsspec):\n    channel = ET.Element(\n        'Channel', Name=channelspec['name'], InputFile=_ROOT_DATA_FILE.file_path\n    )\n    if obsspec:\n        data = build_data(obsspec, channelspec['name'])\n        channel.append(data)\n    for samplespec in channelspec['samples']:\n        channel.append(build_sample(spec, samplespec, channelspec['name']))\n    return channel\n\n\ndef writexml(spec, specdir, data_rootdir, resultprefix):\n    global _ROOT_DATA_FILE\n\n    shutil.copyfile(\n        schema_path.joinpath('HistFactorySchema.dtd'),\n        Path(specdir).parent.joinpath('HistFactorySchema.dtd'),\n    )\n    combination = ET.Element(\n        \"Combination\", OutputFilePrefix=str(Path(specdir).joinpath(resultprefix))\n    )\n\n    with uproot.recreate(Path(data_rootdir).joinpath('data.root')) as _ROOT_DATA_FILE:\n        for channelspec in spec['channels']:\n            channelfilename = str(\n                Path(specdir).joinpath(f'{resultprefix}_{channelspec[\"name\"]}.xml')\n            )\n            with open(channelfilename, 'w') as channelfile:\n                channel = build_channel(spec, channelspec, spec.get('observations'))\n                indent(channel)\n                channelfile.write(\n                    \"<!DOCTYPE Channel SYSTEM '../HistFactorySchema.dtd'>\\n\\n\"\n                )\n                channelfile.write(\n                    ET.tostring(channel, encoding='utf-8').decode('utf-8')\n                )\n\n            inp = ET.Element(\"Input\")\n            inp.text = channelfilename\n            combination.append(inp)\n\n    # need information about modifier types to get the right prefix in measurement\n    mixin = _ChannelSummaryMixin(channels=spec['channels'])\n\n    for measurement in spec['measurements']:\n        combination.append(build_measurement(measurement, dict(mixin.modifiers)))\n    indent(combination)\n    return b\"<!DOCTYPE Combination  SYSTEM 'HistFactorySchema.dtd'>\\n\\n\" + ET.tostring(\n        combination, encoding='utf-8'\n    )\n", "path": "src/pyhf/writexml.py"}]}