Skip to content

Commit

Permalink
feat: ROOT parameter name compatibility module (#1439)
Browse files Browse the repository at this point in the history
* Add new module `pyhf.compat` to aid in translating to and from ROOT names
* Adds `is_scalar` and `name` attributes to paramsets
* Deprecates and removes `utils.remove_prefix`
  • Loading branch information
lukasheinrich authored May 11, 2021
1 parent bab92d0 commit 2c681a6
Show file tree
Hide file tree
Showing 21 changed files with 323 additions and 49 deletions.
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
src/pyhf/workspace.py \
src/pyhf/probability.py \
src/pyhf/patchset.py \
src/pyhf/compat.py \
src/pyhf/interpolators \
src/pyhf/infer \
src/pyhf/optimize \
Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Top-Level
set_backend
readxml
writexml
compat

Probability Distribution Functions (PDFs)
-----------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions src/pyhf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ def set_backend(backend, custom_optimizer=None, precision=None):
from .workspace import Workspace
from . import simplemodels
from . import infer
from . import compat
from .patchset import PatchSet

__all__ = [
"Model",
"PatchSet",
"Workspace",
"__version__",
"compat",
"exceptions",
"get_backend",
"infer",
Expand Down
120 changes: 120 additions & 0 deletions src/pyhf/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Compatibility functions for translating between ROOT and pyhf
"""

import re

__all__ = ["interpret_rootname", "paramset_to_rootnames"]


def __dir__():
return __all__


def paramset_to_rootnames(paramset):
"""
Generates parameter names for parameters in the set as ROOT would do.
Args:
paramset (:obj:`pyhf.paramsets.paramset`): The parameter set.
Returns:
:obj:`List[str]` or :obj:`str`: The generated parameter names
(for the non-scalar/scalar case) respectively.
Example:
pyhf parameter names and then the converted names for ROOT:
* ``"lumi"`` -> ``"Lumi"``
* unconstrained scalar parameter ``"foo"`` -> ``"foo"``
* constrained scalar parameter ``"foo"`` -> ``"alpha_foo"``
* non-scalar parameters ``"foo"`` -> ``"gamma_foo_i"``
>>> import pyhf
>>> pyhf.set_backend("numpy")
>>> model = pyhf.simplemodels.uncorrelated_background(
... signal=[12.0, 11.0], bkg=[50.0, 52.0], bkg_uncertainty=[3.0, 7.0]
... )
>>> model.config.parameters
['mu', 'uncorr_bkguncrt']
>>> pyhf.compat.paramset_to_rootnames(model.config.param_set("mu"))
'mu'
>>> pyhf.compat.paramset_to_rootnames(model.config.param_set("uncorr_bkguncrt"))
['gamma_uncorr_bkguncrt_0', 'gamma_uncorr_bkguncrt_1']
"""

if paramset.name == 'lumi':
return 'Lumi'
if paramset.is_scalar:
if paramset.constrained:
return f'alpha_{paramset.name}'
return f'{paramset.name}'
return [f'gamma_{paramset.name}_{index}' for index in range(paramset.n_parameters)]


def interpret_rootname(rootname):
"""
Interprets a ROOT-generated name as best as possible.
Possible properties of a ROOT parameter are:
* ``"constrained"``: :obj:`bool` representing if parameter is a member of a
constrained paramset.
* ``"is_scalar"``: :obj:`bool` representing if parameter is a member of a
scalar paramset.
* ``"name"``: The name of the param set.
* ``"element"``: The index in a non-scalar param set.
It is possible that some of the parameter names might not be determinable
and will then hold the string value ``"n/a"``.
Args:
rootname (:obj:`str`): The ROOT-generated name of the parameter.
Returns:
:obj:`dict`: The interpreted key-value pairs.
Example:
>>> import pyhf
>>> interpreted_name = pyhf.compat.interpret_rootname("gamma_foo_0")
>>> pyhf.compat.interpret_rootname("gamma_foo_0")
{'constrained': 'n/a', 'is_scalar': False, 'name': 'foo', 'element': 0}
>>> pyhf.compat.interpret_rootname("alpha_foo")
{'constrained': True, 'is_scalar': True, 'name': 'foo', 'element': 'n/a'}
>>> pyhf.compat.interpret_rootname("Lumi")
{'constrained': False, 'is_scalar': True, 'name': 'lumi', 'element': 'n/a'}
"""

interpretation = {
'constrained': 'n/a',
'is_scalar': 'n/a',
'name': 'n/a',
'element': 'n/a',
}
if rootname.startswith('gamma_'):
interpretation['is_scalar'] = False
match = re.search(r'^gamma_(.+)_(\d+)$', rootname)
if not match:
raise ValueError(f'confusing rootname {rootname}. Please report as a bug.')
interpretation['name'] = match.group(1)
interpretation['element'] = int(match.group(2))
else:
interpretation['is_scalar'] = True

if rootname.startswith('alpha_'):
interpretation['constrained'] = True
match = re.search(r'^alpha_(.+)$', rootname)
if not match:
raise ValueError(f'confusing rootname {rootname}. Please report as a bug.')
interpretation['name'] = match.group(1)

if not (rootname.startswith('alpha_') or rootname.startswith('gamma_')):
interpretation['constrained'] = False
interpretation['name'] = rootname

if rootname == 'Lumi':
interpretation['name'] = 'lumi'

return interpretation
1 change: 1 addition & 0 deletions src/pyhf/modifiers/histosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': 1,
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': True,
'inits': (0.0,),
'bounds': ((-5.0, 5.0),),
'fixed': False,
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/lumi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': 1,
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': True,
'op_code': cls.op_code,
'inits': None, # lumi
'bounds': None, # (0, 10*lumi)
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/normfactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': 1,
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': True,
'inits': (1.0,),
'bounds': ((0, 10),),
'fixed': False,
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/normsys.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': 1,
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': True,
'inits': (0.0,),
'bounds': ((-5.0, 5.0),),
'fixed': False,
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/shapefactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': len(sample_data),
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': False,
'inits': (1.0,) * len(sample_data),
'bounds': ((0.0, 10.0),) * len(sample_data),
'fixed': False,
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/shapesys.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': n_parameters,
'is_constrained': cls.is_constrained,
'is_shared': False,
'is_scalar': False,
'inits': (1.0,) * n_parameters,
'bounds': ((1e-10, 10.0),) * n_parameters,
'fixed': False,
Expand Down
1 change: 1 addition & 0 deletions src/pyhf/modifiers/staterror.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def required_parset(cls, sample_data, modifier_data):
'n_parameters': len(sample_data),
'is_constrained': cls.is_constrained,
'is_shared': True,
'is_scalar': False,
'inits': (1.0,) * len(sample_data),
'bounds': ((1e-10, 10.0),) * len(sample_data),
'fixed': False,
Expand Down
6 changes: 6 additions & 0 deletions src/pyhf/parameters/paramsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ def __dir__():

class paramset:
def __init__(self, **kwargs):
self.name = kwargs.pop('name')
self.n_parameters = kwargs.pop('n_parameters')
self.suggested_init = kwargs.pop('inits')
self.suggested_bounds = kwargs.pop('bounds')
self.suggested_fixed = kwargs.pop('fixed')
self.is_scalar = kwargs.pop('is_scalar')
if self.is_scalar and not (self.n_parameters == 1):
raise ValueError(
f'misconfigured parameter set {self.name}. Scalar but N>1 parameters.'
)


class unconstrained(paramset):
Expand Down
2 changes: 2 additions & 0 deletions src/pyhf/parameters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def reduce_paramsets_requirements(paramsets_requirements, paramsets_user_configs
paramset_keys = [
'paramset_type',
'n_parameters',
'is_scalar',
'inits',
'bounds',
'auxdata',
Expand Down Expand Up @@ -63,6 +64,7 @@ def reduce_paramsets_requirements(paramsets_requirements, paramsets_user_configs

combined_paramset[k] = v

combined_paramset['name'] = paramset_name
reduced_paramsets_requirements[paramset_name] = combined_paramset

return reduced_paramsets_requirements
19 changes: 8 additions & 11 deletions src/pyhf/readxml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import utils
from . import compat

import logging

Expand All @@ -7,7 +8,6 @@
import numpy as np
import tqdm
import uproot
import re

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -290,26 +290,23 @@ def process_measurements(toplvl, other_parameter_configs=None):
# might be specifying multiple parameters in the same ParamSetting
if param.text:
for param_name in param.text.strip().split(' '):
param_name = utils.remove_prefix(param_name, 'alpha_')
if param_name.startswith('gamma_') and re.search(
r'^gamma_.+_\d+$', param_name
):
param_interpretation = compat.interpret_rootname(param_name)
if not param_interpretation['is_scalar']:
raise ValueError(
f'pyhf does not support setting individual gamma parameters constant, such as for {param_name}.'
f'pyhf does not support setting non-scalar parameters ("gammas") constant, such as for {param_name}.'
)
param_name = utils.remove_prefix(param_name, 'gamma_')
# lumi will always be the first parameter
if param_name == 'Lumi':
if param_interpretation['name'] == 'lumi':
result['config']['parameters'][0].update(overall_param_obj)
else:
# pop from parameter_configs_map because we don't want to duplicate
param_obj = parameter_configs_map.pop(
param_name, {'name': param_name}
param_interpretation['name'],
{'name': param_interpretation['name']},
)
# ParamSetting will always take precedence
param_obj.update(overall_param_obj)
# add it back in to the parameter_configs_map
parameter_configs_map[param_name] = param_obj
parameter_configs_map[param_interpretation['name']] = param_obj
result['config']['parameters'].extend(parameter_configs_map.values())
results.append(result)

Expand Down
24 changes: 0 additions & 24 deletions src/pyhf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"digest",
"load_schema",
"options_from_eqdelimstring",
"remove_prefix",
"validate",
]

Expand Down Expand Up @@ -125,29 +124,6 @@ def digest(obj, algorithm='sha256'):
return hash_alg(stringified).hexdigest()


def remove_prefix(text, prefix):
"""
Remove a prefix from the beginning of the provided text.
Example:
>>> import pyhf
>>> pyhf.utils.remove_prefix("alpha_syst1", "alpha_")
'syst1'
Args:
text (:obj:`str`): A provided input to manipulate.
prefix (:obj:`str`): A prefix to remove from provided input, if it exists.
Returns:
stripped_text (:obj:`str`): Text with the prefix removed.
"""
# NB: python3.9 can be `return text.removeprefix(prefix)`
if text.startswith(prefix):
return text[len(prefix) :]
return text


def citation(oneline=False):
"""
Get the bibtex citation for pyhf
Expand Down
Loading

0 comments on commit 2c681a6

Please sign in to comment.