Source code for columnflow.production.cms.scale

# coding: utf-8

"""
Column production methods related to the renormalization and factorization scales.
"""

import functools

import law

from columnflow.production import Producer, producer
from columnflow.util import maybe_import, DotDict, StrEnum
from columnflow.columnar_util import set_ak_column, ak_concatenate_safe

np = maybe_import("numpy")
ak = maybe_import("awkward")


logger = law.logger.get_logger(__name__)

# helper
set_ak_column_f32 = functools.partial(set_ak_column, value_type=np.float32)


[docs] class ScaleWeightOutput(StrEnum): """ Flag to denote which type of weights to output in the :py:class:`murmuf_weights` producer. Options: - ``single``: Produces `{mur,muf}_weight{,_up,_down}`. - ``correlated``: Produces `murmuf_weight{,_up,_down}`. - ``single_correlated``: Produces `{mur,muf,murmuf}_weight{,_up,_down}`. - ``raw``: Produces all combinations `mur_{nom,up,down}_muf_{nom,up,down}`. """ single = "single" correlated = "correlated" single_correlated = "single_correlated" raw = "raw"
[docs] @producer( # used columns uses={"LHEScaleWeight"}, # which types of weights to produce (influences produced columns, defined in init) scale_weight_output=ScaleWeightOutput.single_correlated, # only run on mc mc_only=True, ) def murmuf_weights(self: Producer, events: ak.Array, **kwargs) -> ak.Array: """ Producer that reads out mur and muf uncertainties on an event-by-event basis. This producer assumes that the nominal entry is always the 5th LHEScaleWeight entry and that the nominal weight is already included in the LHEWeight. Can only be called with MC datasets. Resources: - https://cms-xpog.docs.cern.ch/autoDoc/NanoAODv15/2024/doc_TTH-Hto2G_Par-M-125_TuneCP5_13p6TeV_amcatnloFXFX-pythia8_RunIII2024Summer24NanoAODv15-150X_mcRun3_2024_realistic_v2-v2.html#LHEScaleWeight # noqa """ n_weights = ak.num(events.LHEScaleWeight, axis=1) # in rare cases, some events might have 0 weights non_zero_mask = n_weights > 0 murf_weights = events.LHEScaleWeight[non_zero_mask] # write ones in case there are no weights at all ones = np.ones(len(events), dtype=np.float32) if not ak.any(non_zero_mask): logger.warning(r"no valid 'LHEScaleWeight' vector found, saving ones for all output columns") for col in self.weight_names: events = set_ak_column_f32(events, col, ones) return events if ak.all(n_weights[non_zero_mask] == 9): indices = self.indices_9 # perform an additional check to see of the nominal value is ever != 1 murf_nominal = murf_weights[:, indices.mur_nom_muf_nom] if ak.any(murf_nominal != 1): bad_values = set(murf_nominal[murf_nominal != 1]) logger.debug( f"the nominal LHEScaleWeight is expected to be 1, but also found values {bad_values} in dataset " f"{self.dataset_inst.name}; all variations will be normalized to the nominal LHEScaleWeight and it is " "assumed that the nominal weight is already included in the LHEWeight", ) # normalize all weights by the nominal one murf_weights = murf_weights / murf_nominal elif ak.all(n_weights[non_zero_mask] == 8): # if we just have 8 weights, there is no nominal LHEScale weight and instead, initialize the nominal weights as # ones; additionally, we need to shift the last couple of weight indices down by 1 indices = self.indices_8 # additional debug log logger.debug( f"in dataset {self.dataset_inst.name} the number of LHEScaleWeights is always 8 instead of the expected 9, " "it is assumed, that the missing entry is the nominal one and all other entries are in correct order", ) else: bad_values = set(n_weights[non_zero_mask]) - {8, 9} raise Exception( f"the number of LHEScaleWeights is expected to be 8 or 9, but also found values {bad_values} in dataset " f"{self.dataset_inst.name}", ) # store output columns for weight_name in self.weight_names: weights = ones if self.scale_weight_output == ScaleWeightOutput.raw and weight_name != "mur_nom_muf_nom": weights = ones.copy() weights[non_zero_mask] = murf_weights[:, indices[weight_name]] elif self.scale_weight_output != ScaleWeightOutput.raw and not weight_name.endswith("_weight"): weights = ones.copy() weights[non_zero_mask] = murf_weights[:, indices[self.weight_map[weight_name]]] # store it events = set_ak_column_f32(events, weight_name, weights) return events
@murmuf_weights.init def murmuf_weights_init(self: Producer, **kwargs) -> None: super(murmuf_weights, self).init_func(**kwargs) # named weight indices self.indices_9 = DotDict( mur_down_muf_down=0, mur_down_muf_nom=1, mur_down_muf_up=2, mur_nom_muf_down=3, mur_nom_muf_nom=4, mur_nom_muf_up=5, mur_up_muf_down=6, mur_up_muf_nom=7, mur_up_muf_up=8, ) # named weight indices for cases where only 8 of them exist # (expecting no nominal value and all above being shifted down by one) self.indices_8 = DotDict({ key: (index if index <= self.indices_9.mur_nom_muf_nom else index - 1) for key, index in self.indices_9.items() if key != "mur_nom_muf_nom" }) # for convenience, map column names to produce to index keys self.weight_map = DotDict( mur_weight="mur_nom_muf_nom", muf_weight="mur_nom_muf_nom", murmuf_weight="mur_nom_muf_nom", mur_weight_up="mur_up_muf_nom", mur_weight_down="mur_down_muf_nom", muf_weight_up="mur_nom_muf_up", muf_weight_down="mur_nom_muf_down", murmuf_weight_up="mur_up_muf_up", murmuf_weight_down="mur_down_muf_down", ) # define produced columns if not isinstance(self.scale_weight_output, ScaleWeightOutput): self.scale_weight_output = ScaleWeightOutput(self.scale_weight_output) self.weight_names = [] if self.scale_weight_output in {ScaleWeightOutput.single, ScaleWeightOutput.single_correlated}: self.weight_names += law.util.brace_expand("{mur,muf}_weight{,_up,_down}") if self.scale_weight_output in {ScaleWeightOutput.correlated, ScaleWeightOutput.single_correlated}: self.weight_names += law.util.brace_expand("murmuf_weight{,_up,_down}") if self.scale_weight_output == ScaleWeightOutput.raw: self.weight_names += list(self.indices_9.keys()) self.produces.update(self.weight_names) murmuf_weights_raw = murmuf_weights.derive("murmuf_weights_raw", cls_dict={ "scale_weight_output": ScaleWeightOutput.raw, })
[docs] @producer( uses={murmuf_weights_raw}, produces={"murmuf_envelope_weight{,_up,_down}"}, # only run on mc mc_only=True, # which columns (produced by murmuf_weights_raw) to consider for the envelope calculation envelope_columns=[ "mur_down_muf_down", "mur_down_muf_nom", "mur_nom_muf_down", "mur_nom_muf_nom", "mur_nom_muf_up", "mur_up_muf_nom", "mur_up_muf_up", ], ) def murmuf_envelope_weights(self: Producer, events: ak.Array, **kwargs) -> ak.Array: """ Producer that determines the envelope of the mur/muf up and down variations on an event-by-event basis. See :py:class:`murmuf_weights` for details on the assumptions made about the LHEScaleWeight vector. """ # retrieve raw mur/muf weights events = self[murmuf_weights_raw](events, **kwargs) # compute the vector of weights per event murf_weights = ak_concatenate_safe([events[col][:, None] for col in self.envelope_columns], axis=1) # store columns events = set_ak_column_f32(events, "murmuf_envelope_weight", events.mur_nom_muf_nom) events = set_ak_column_f32(events, "murmuf_envelope_weight_down", ak.min(murf_weights, axis=1)) events = set_ak_column_f32(events, "murmuf_envelope_weight_up", ak.max(murf_weights, axis=1)) return events