Repository: scikit-hep/pyhf

Issue:
json2xml breaks for a simple JSON
# Description

```
{
  "channels": [
    {
      "name": "singlechannel",
      "samples": [
        {
          "name": "signal",
          "data": [
            5
          ],
          "modifiers": [
            {
              "name": "mu",
              "type": "normfactor",
              "data": null
            }
          ]
        },
        {
          "name": "background",
          "data": [
            50
          ],
          "modifiers": [
            {
              "name": "uncorr_bkguncrt",
              "type": "shapesys",
              "data": [
                6
              ]
            }
          ]
        }
      ]
    }
  ],
  "version": "1.0.0",
  "measurements": [
    {
      "name": "measurement",
      "config": {
        "poi": "mu",
        "parameters": [
          {
            "bounds": [
              [
                0,
                10
              ]
            ],
            "inits": [
              1
            ],
            "fixed": false,
            "name": "mu"
          },
          {
            "bounds": [
              [
                1e-10,
                10
              ]
            ],
            "inits": [
              1
            ],
            "fixed": false,
            "name": "uncorr_bkguncrt"
          }
        ]
      }
    }
  ],
  "observations": [
    {
      "name": "singlechannel",
      "data": [
        50
      ]
    }
  ]
}
```


Discussion:
Easy fix: https://github.com/scikit-hep/pyhf/blob/7c8fe4376e8e2766237c07202700cc7286de59c0/src/pyhf/writexml.py#L196-L204

Make sure a/b are float like (include the zeros_like(a)) as well. This is just because there's a conversion from int to float as part of the division and that's being stored back into something that's int-like.
See numpy/numpy#14843
> Make sure a/b are float like (include the zeros_like(a)) as well. This is just because there's a conversion from int to float as part of the division and that's being stored back into something that's int-like.

Yeah, as mentioned, manually converting this workspace to have floats allows for it to succeed

```
$ pyhf json2xml --output-dir example_output_float issue_1177_float.json
```

where the `int` version results in the reported

```pytb
$ pyhf json2xml --output-dir example_output issue_1177.json 
Traceback (most recent call last):
  File "/home/feickert/.venvs/pyhf-CPU/bin/pyhf", line 33, in <module>
    sys.exit(load_entry_point('pyhf', 'console_scripts', 'pyhf')())
  File "/home/feickert/.venvs/pyhf-CPU/lib/python3.7/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/home/feickert/.venvs/pyhf-CPU/lib/python3.7/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/home/feickert/.venvs/pyhf-CPU/lib/python3.7/site-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/feickert/.venvs/pyhf-CPU/lib/python3.7/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/feickert/.venvs/pyhf-CPU/lib/python3.7/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/cli/rootio.py", line 90, in json2xml
    resultprefix,
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/writexml.py", line 279, in writexml
    channel = build_channel(spec, channelspec, spec.get('observations'))
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/writexml.py", line 256, in build_channel
    channel.append(build_sample(spec, samplespec, channelspec['name']))
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/writexml.py", line 231, in build_sample
    spec, modspec, channelname, samplespec['name'], samplespec['data']
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/writexml.py", line 202, in build_modifier
    for a, b in np.array((modifierspec['data'], sampledata)).T
  File "/home/feickert/Code/GitHub/pyhf/src/pyhf/writexml.py", line 202, in <listcomp>
    for a, b in np.array((modifierspec['data'], sampledata)).T
TypeError: No loop matching the specified signature and casting was found for ufunc true_divide
```

but just changing

```diff
-         for a, b in np.array((modifierspec['data'], sampledata)).T 
+         for a, b in np.array((modifierspec['data'], sampledata), dtype="float").T
```

allows for

```
$ pyhf json2xml --output-dir example_output issue_1177.json
```
to pass with the same results
```
$ diff example_output/FitConfig.xml example_output_float/FitConfig.xml 
3,4c3,4
< <Combination OutputFilePrefix="example_output/config/FitConfig">
<   <Input>example_output/config/FitConfig_singlechannel.xml</Input>
---
> <Combination OutputFilePrefix="example_output_float/config/FitConfig">
>   <Input>example_output_float/config/FitConfig_singlechannel.xml</Input>
```

I'll open up a PR now.

Resolving patch:
diff --git a/src/pyhf/writexml.py b/src/pyhf/writexml.py
--- a/src/pyhf/writexml.py
+++ b/src/pyhf/writexml.py
@@ -199,7 +199,9 @@ def build_modifier(spec, modifierspec, channelname, samplename, sampledata):
                 np.divide(
                     a, b, out=np.zeros_like(a), where=np.asarray(b) != 0, dtype='float'
                 )
-                for a, b in np.array((modifierspec['data'], sampledata)).T
+                for a, b in np.array(
+                    (modifierspec['data'], sampledata), dtype="float"
+                ).T
             ],
         )
     elif modifierspec['type'] == 'shapefactor':


Test patch:
diff --git a/tests/test_export.py b/tests/test_export.py
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -390,3 +390,15 @@ def test_export_data(mocker):
     assert data.attrib['HistoName']
     assert data.attrib['InputFile']
     assert pyhf.writexml._ROOT_DATA_FILE.__setitem__.called
+
+
+def test_integer_data(datadir, mocker):
+    """
+    Test that a spec with only integer data will be written correctly
+    """
+    spec = json.load(open(datadir.join("workspace_integer_data.json")))
+    channel_spec = spec["channels"][0]
+    mocker.patch("pyhf.writexml._ROOT_DATA_FILE")
+
+    channel = pyhf.writexml.build_channel(spec, channel_spec, {})
+    assert channel
diff --git a/tests/test_export/workspace_integer_data.json b/tests/test_export/workspace_integer_data.json
new file mode 100644
--- /dev/null
+++ b/tests/test_export/workspace_integer_data.json
@@ -0,0 +1,82 @@
+{
+    "channels": [
+        {
+            "name": "singlechannel",
+            "samples": [
+                {
+                    "data": [
+                        5
+                    ],
+                    "modifiers": [
+                        {
+                            "data": null,
+                            "name": "mu",
+                            "type": "normfactor"
+                        }
+                    ],
+                    "name": "signal"
+                },
+                {
+                    "data": [
+                        50
+                    ],
+                    "modifiers": [
+                        {
+                            "data": [
+                                6
+                            ],
+                            "name": "uncorr_bkguncrt",
+                            "type": "shapesys"
+                        }
+                    ],
+                    "name": "background"
+                }
+            ]
+        }
+    ],
+    "measurements": [
+        {
+            "config": {
+                "parameters": [
+                    {
+                        "bounds": [
+                            [
+                                0,
+                                10
+                            ]
+                        ],
+                        "fixed": false,
+                        "inits": [
+                            1
+                        ],
+                        "name": "mu"
+                    },
+                    {
+                        "bounds": [
+                            [
+                                1e-10,
+                                10
+                            ]
+                        ],
+                        "fixed": false,
+                        "inits": [
+                            1
+                        ],
+                        "name": "uncorr_bkguncrt"
+                    }
+                ],
+                "poi": "mu"
+            },
+            "name": "measurement"
+        }
+    ],
+    "observations": [
+        {
+            "data": [
+                50
+            ],
+            "name": "singlechannel"
+        }
+    ],
+    "version": "1.0.0"
+}