Source code for pyhf.patchset

"""
pyhf patchset provides a user-friendly interface for interacting with patchsets.
"""

import logging
import jsonpatch
from pyhf import exceptions
from pyhf import utils
from pyhf import schema
from pyhf.workspace import Workspace

log = logging.getLogger(__name__)

__all__ = ["Patch", "PatchSet"]


def __dir__():
    return __all__


[docs] class Patch(jsonpatch.JsonPatch): """ A way to store a patch definition as part of a patchset (:class:`~pyhf.patchset.PatchSet`). It contains :attr:`~pyhf.patchset.Patch.metadata` about the Patch itself: * a descriptive :attr:`~pyhf.patchset.Patch.name` * a list of the :attr:`~pyhf.patchset.Patch.values` for each dimension in the phase-space the associated :class:`~pyhf.patchset.PatchSet` is defined for, see :attr:`~pyhf.patchset.PatchSet.labels` In addition to the above metadata, the Patch object behaves like the underlying :class:`jsonpatch.JsonPatch`. """
[docs] def __init__(self, spec): """ Construct a Patch. Args: spec (:obj:`jsonable`): The patch JSON specification Returns: patch (:class:`~pyhf.patchset.Patch`): The Patch instance. """ super().__init__(spec['patch']) self._metadata = spec['metadata']
@property def metadata(self): """The metadata of the patch""" return self._metadata @property def name(self): """The name of the patch""" return self.metadata['name'] @property def values(self): """The values of the associated labels for the patch""" return tuple(self.metadata['values']) def __repr__(self): """Representation of the object""" module = type(self).__module__ qualname = type(self).__qualname__ return f"<{module}.{qualname} object '{self.name}{self.values}' at {hex(id(self))}>" def __eq__(self, other): """Equality for subclass with new attributes""" if not isinstance(other, Patch): return False return ( jsonpatch.JsonPatch.__eq__(self, other) and self.metadata == other.metadata )
[docs] class PatchSet: """ A way to store a collection of patches (:class:`~pyhf.patchset.Patch`). It contains :attr:`~PatchSet.metadata` about the PatchSet itself: * a high-level :attr:`~pyhf.patchset.PatchSet.description` of what the patches represent or the analysis it is for * a list of :attr:`~pyhf.patchset.PatchSet.references` where the patchset is sourced from (e.g. hepdata) * a list of :attr:`~pyhf.patchset.PatchSet.digests` corresponding to the background-only workspace the patchset was made for * the :attr:`~pyhf.patchset.PatchSet.labels` of the dimensions of the phase-space for what the patches cover In addition to the above metadata, the PatchSet object behaves like a: * smart list allowing you to iterate over all the patches defined * smart dictionary allowing you to access a patch by the patch name or the patch values The below example shows various ways one can interact with a :class:`PatchSet` object. Example: >>> import pyhf >>> patchset = pyhf.PatchSet({ ... "metadata": { ... "references": { "hepdata": "ins1234567" }, ... "description": "example patchset", ... "digests": { "md5": "098f6bcd4621d373cade4e832627b4f6" }, ... "labels": ["x", "y"] ... }, ... "patches": [ ... { ... "metadata": { ... "name": "patch_name_for_2100x_800y", ... "values": [2100, 800] ... }, ... "patch": [ ... { ... "op": "add", ... "path": "/foo/0/bar", ... "value": { ... "foo": [1.0] ... } ... } ... ] ... } ... ], ... "version": "1.0.0" ... }) ... >>> patchset.version '1.0.0' >>> patchset.references {'hepdata': 'ins1234567'} >>> patchset.description 'example patchset' >>> patchset.digests {'md5': '098f6bcd4621d373cade4e832627b4f6'} >>> patchset.labels ['x', 'y'] >>> patchset.patches [<pyhf.patchset.Patch object 'patch_name_for_2100x_800y(2100, 800)' at 0x...>] >>> patchset['patch_name_for_2100x_800y'] <pyhf.patchset.Patch object 'patch_name_for_2100x_800y(2100, 800)' at 0x...> >>> patchset[(2100,800)] <pyhf.patchset.Patch object 'patch_name_for_2100x_800y(2100, 800)' at 0x...> >>> patchset[[2100,800]] <pyhf.patchset.Patch object 'patch_name_for_2100x_800y(2100, 800)' at 0x...> >>> patchset[2100,800] <pyhf.patchset.Patch object 'patch_name_for_2100x_800y(2100, 800)' at 0x...> >>> for patch in patchset: ... print(patch.name) ... patch_name_for_2100x_800y >>> len(patchset) 1 """
[docs] def __init__(self, spec, **config_kwargs): """ Construct a PatchSet. Args: spec (:obj:`jsonable`): The patchset JSON specification config_kwargs: Possible keyword arguments for the patchset validation Returns: patchset (:class:`~pyhf.patchset.PatchSet`): The PatchSet instance. """ self.schema = config_kwargs.pop('schema', 'patchset.json') self._version = config_kwargs.pop('version', spec.get('version', None)) # run jsonschema validation of input specification against the (provided) schema log.info(f"Validating spec against schema: {self.schema}") schema.validate(spec, self.schema, version=self._version) # set properties based on metadata self._metadata = spec['metadata'] # list of all patch objects self._patches = [] # look-up table for retrieving patch by name or values self._patches_by_key = {'name': {}, 'values': {}} # inflate all patches for patchspec in spec['patches']: patch = Patch(patchspec) if patch.name in self._patches_by_key: raise exceptions.InvalidPatchSet( f'Multiple patches were defined by name for {patch}.' ) if patch.values in self._patches_by_key: raise exceptions.InvalidPatchSet( f'Multiple patches were defined by values for {patch}.' ) if len(patch.values) != len(self.labels): raise exceptions.InvalidPatchSet( f'Incompatible number of values ({len(patch.values)} for {patch} in patchset. Expected {len(self.labels)}.' ) # all good, register patch self._patches.append(patch) # register lookup keys for the patch self._patches_by_key[patch.name] = patch self._patches_by_key[patch.values] = patch
@property def version(self): """The version of the PatchSet""" return self._version @property def metadata(self): """The metadata of the PatchSet""" return self._metadata @property def references(self): """The references in the PatchSet metadata""" return self.metadata['references'] @property def description(self): """The description in the PatchSet metadata""" return self.metadata['description'] @property def digests(self): """The digests in the PatchSet metadata""" return self.metadata['digests'] @property def labels(self): """The labels in the PatchSet metadata""" return self.metadata['labels'] @property def patches(self): """The patches in the PatchSet""" return self._patches def __repr__(self): """Representation of the object""" module = type(self).__module__ qualname = type(self).__qualname__ return f"<{module}.{qualname} object with {len(self.patches)} patch{'es' if len(self.patches) != 1 else ''} at {hex(id(self))}>" def __getitem__(self, key): """ Access the patch in the patchset by the specified key, either by name or by values. Raises: ~pyhf.exceptions.InvalidPatchLookup: if the provided patch name is not in the patchset Returns: patch (:class:`~pyhf.patchset.Patch`): The patch associated with the specified key """ # might be specified as a list, convert to hashable tuple instead for lookup if isinstance(key, list): key = tuple(key) try: return self._patches_by_key[key] except KeyError: raise exceptions.InvalidPatchLookup( f'No patch associated with "{key}" is defined in patchset.' ) def __iter__(self): """ Iterate over the defined patches in the patchset. Returns: iterable (:obj:`iter`): An iterable over the list of patches in the patchset. """ return iter(self.patches) def __len__(self): """ The number of patches in the patchset. Returns: quantity (:obj:`int`): The number of patches in the patchset. """ return len(self.patches)
[docs] def verify(self, spec): """ Verify the patchset digests against a background-only workspace specification. Verified if no exception was raised. Args: spec (:class:`~pyhf.workspace.Workspace`): The workspace specification to verify the patchset against. Raises: ~pyhf.exceptions.PatchSetVerificationError: if the patchset cannot be verified against the workspace specification Returns: None """ for hash_alg, digest in self.digests.items(): digest_calc = utils.digest(spec, algorithm=hash_alg) if not digest_calc == digest: raise exceptions.PatchSetVerificationError( f"The digest verification failed for hash algorithm '{hash_alg}'. Expected: {digest}. Got: {digest_calc}" )
[docs] def apply(self, spec, key): """ Apply the patch associated with the key to the background-only workspace specificatiom. Args: spec (:class:`~pyhf.workspace.Workspace`): The workspace specification to verify the patchset against. key (:obj:`str` or :obj:`tuple` of :obj:`int`/:obj:`float`): The key to look up the associated patch - either a name or a set of values. Raises: ~pyhf.exceptions.InvalidPatchLookup: if the provided patch name is not in the patchset ~pyhf.exceptions.PatchSetVerificationError: if the patchset cannot be verified against the workspace specification Returns: workspace (:class:`~pyhf.workspace.Workspace`): The background-only workspace with the patch applied. """ self.verify(spec) return Workspace(self[key].apply(spec))