Repository: scikit-hep/pyhf

Issue:
Model creation from existing model - parameter names missing in model spec?
# Question

I would like to create a new `pyhf.Model` from an existing model. This seems to work in general via `pyhf.Model(model.spec)`, see the example below. The example fails when working with a custom workspace that includes parameter specifications:

```python
import pyhf

model_hepdata = pyhf.simplemodels.hepdata_like(
    signal_data=[12.0, 11.0], bkg_data=[50.0, 52.0], bkg_uncerts=[3.0, 7.0]
)

# this works
pyhf.Model(model_hepdata.spec)

# custom workspace specification
ws = {
    "channels": [
        {
            "name": "SR",
            "samples": [
                {
                    "data": [
                        10.0,
                    ],
                    "modifiers": [
                        {"data": None, "name": "mu", "type": "normfactor"},
                    ],
                    "name": "Signal",
                }
            ],
        }
    ],
    "measurements": [
        {
            "config": {
                "parameters": [{"bounds": [[0, 5]], "inits": [1], "name": "mu"}],
                "poi": "mu",
            },
            "name": "minimal_example",
        }
    ],
    "observations": [{"data": [12], "name": "SR"}],
    "version": "1.0.0",
}
workspace = pyhf.Workspace(ws)

# this works as well
model_from_ws = workspace.model()

# this does not work
pyhf.Model(model_from_ws.spec)
```

To make the last step work, the missing `"name"` needs to be added back into the specification:

```python
model_from_ws.spec["parameters"][0].update({"name": "mu"})
pyhf.Model(model_from_ws.spec)
```

Is this a bug?

And as another question, would this be the recommended way to create a modified model from an existing model? I would extract the `.spec`, modify that and feed it back into `pyhf.Model`.

# Relevant Issues and Pull Requests

none I'm aware of



Discussion:
confirmed bug -- can be fixed: https://github.com/scikit-hep/pyhf/blob/d178014019206a862d3313e0420d6c6a81504e40/src/pyhf/pdf.py#L213

we probably want to deep-copy the spec through to `ModelConfig` to be safe in any case -- but not sure why we need to `pop`. We could just `get` and not `pop`, then add a test to assert the spec remains unchanged when fed through.

Resolving patch:
diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py
--- a/src/pyhf/pdf.py
+++ b/src/pyhf/pdf.py
@@ -73,7 +73,7 @@ def _paramset_requirements_from_modelspec(spec, channel_nbins):
             raise exceptions.InvalidModel(
                 f"Multiple parameter configurations for {parameter['name']} were found."
             )
-        _paramsets_user_configs[parameter.pop('name')] = parameter
+        _paramsets_user_configs[parameter.get('name')] = parameter
 
     _reqs = reduce_paramsets_requirements(
         _paramsets_requirements, _paramsets_user_configs
@@ -546,18 +546,17 @@ def __init__(self, spec, batch_size=None, **config_kwargs):
 
         """
         self.batch_size = batch_size
-        self.spec = copy.deepcopy(spec)  # may get modified by config
+        # deep-copy "spec" as it may be modified by config
+        self.spec = copy.deepcopy(spec)
         self.schema = config_kwargs.pop('schema', 'model.json')
         self.version = config_kwargs.pop('version', None)
         # run jsonschema validation of input specification against the (provided) schema
         log.info(f"Validating spec against schema: {self.schema:s}")
         utils.validate(self.spec, self.schema, version=self.version)
         # build up our representation of the specification
-        self.config = _ModelConfig(self.spec, **config_kwargs)
+        self.config = _ModelConfig(spec, **config_kwargs)
 
-        mega_mods, _nominal_rates = _nominal_and_modifiers_from_spec(
-            self.config, self.spec
-        )
+        mega_mods, _nominal_rates = _nominal_and_modifiers_from_spec(self.config, spec)
         self.main_model = _MainModel(
             self.config,
             mega_mods=mega_mods,


Test patch:
diff --git a/tests/test_pdf.py b/tests/test_pdf.py
--- a/tests/test_pdf.py
+++ b/tests/test_pdf.py
@@ -851,3 +851,42 @@ def test_model_integration_fixed_parameters_shapesys():
     assert len(model.config.suggested_fixed()) == 5
     assert model.config.suggested_fixed() == [False, True, True, True, False]
     assert model.config.poi_index == 4
+
+
+def test_reproducible_model_spec():
+    ws = {
+        "channels": [
+            {
+                "name": "SR",
+                "samples": [
+                    {
+                        "data": [
+                            10.0,
+                        ],
+                        "modifiers": [
+                            {"data": None, "name": "mu", "type": "normfactor"},
+                        ],
+                        "name": "Signal",
+                    }
+                ],
+            }
+        ],
+        "measurements": [
+            {
+                "config": {
+                    "parameters": [{"bounds": [[0, 5]], "inits": [1], "name": "mu"}],
+                    "poi": "mu",
+                },
+                "name": "minimal_example",
+            }
+        ],
+        "observations": [{"data": [12], "name": "SR"}],
+        "version": "1.0.0",
+    }
+    workspace = pyhf.Workspace(ws)
+    model_from_ws = workspace.model()
+
+    assert model_from_ws.spec['parameters'] == [
+        {'bounds': [[0, 5]], 'inits': [1], 'name': 'mu'}
+    ]
+    assert pyhf.Model(model_from_ws.spec)