|
| 1 | +# coding: utf-8 |
| 2 | + |
| 3 | +""" |
| 4 | +Selection modules for HH -> bbWW(qqlnu). |
| 5 | +""" |
| 6 | + |
| 7 | +from collections import defaultdict |
| 8 | +from typing import Tuple |
| 9 | + |
| 10 | +from columnflow.util import maybe_import |
| 11 | +from columnflow.selection import Selector, SelectionResult, selector |
| 12 | + |
| 13 | +from hbw.selection.common import ( |
| 14 | + jet_selection, lepton_definition, |
| 15 | + masked_sorted_indices, sl_boosted_jet_selection, vbf_jet_selection, |
| 16 | + pre_selection, post_selection, |
| 17 | +) |
| 18 | +from hbw.production.weights import event_weights_to_normalize |
| 19 | +from hbw.selection.cutflow_features import cutflow_features |
| 20 | + |
| 21 | +np = maybe_import("numpy") |
| 22 | +ak = maybe_import("awkward") |
| 23 | + |
| 24 | + |
| 25 | +@selector( |
| 26 | + uses={lepton_definition, "Electron.charge", "Muon.charge"}, |
| 27 | + produces={lepton_definition}, |
| 28 | + e_pt=None, mu_pt=None, trigger=None, |
| 29 | +) |
| 30 | +def sl_lepton_selection( |
| 31 | + self: Selector, |
| 32 | + events: ak.Array, |
| 33 | + stats: defaultdict, |
| 34 | + **kwargs, |
| 35 | +) -> Tuple[ak.Array, SelectionResult]: |
| 36 | + |
| 37 | + events, lepton_results = self[lepton_definition](events, stats, **kwargs) |
| 38 | + |
| 39 | + # tau veto |
| 40 | + lepton_results.steps["VetoTau"] = events.cutflow.n_veto_tau == 0 |
| 41 | + |
| 42 | + # number of electrons |
| 43 | + lepton_results.steps["nRecoElectron1"] = ak.num(events.Electron) >= 1 |
| 44 | + lepton_results.steps["nLooseElectron1"] = events.cutflow.n_loose_electron >= 1 |
| 45 | + lepton_results.steps["nFakeableElectron1"] = events.cutflow.n_fakeable_electron >= 1 |
| 46 | + lepton_results.steps["nTightElectron1"] = events.cutflow.n_tight_electron >= 1 |
| 47 | + |
| 48 | + # number of muons |
| 49 | + lepton_results.steps["nRecoMuon1"] = ak.num(events.Muon) >= 1 |
| 50 | + lepton_results.steps["nLooseMuon1"] = events.cutflow.n_loose_muon >= 1 |
| 51 | + lepton_results.steps["nFakeableMuon1"] = events.cutflow.n_fakeable_muon >= 1 |
| 52 | + lepton_results.steps["nTightMuon1"] = events.cutflow.n_tight_muon >= 1 |
| 53 | + |
| 54 | + # select events |
| 55 | + mu_mask_fakeable = lepton_results.x.mu_mask_fakeable |
| 56 | + e_mask_fakeable = lepton_results.x.e_mask_fakeable |
| 57 | + |
| 58 | + # NOTE: leading lepton pt could be reduced to trigger threshold + 1 |
| 59 | + leading_mu_mask = (mu_mask_fakeable) & (events.Muon.cone_pt > self.config_inst.x.mu_pt) |
| 60 | + leading_e_mask = (e_mask_fakeable) & (events.Electron.cone_pt > self.config_inst.x.e_pt) |
| 61 | + |
| 62 | + # NOTE: we might need pt > 15 for lepton SFs. Needs to be checked in Run 3. |
| 63 | + subleading_mu_mask = (mu_mask_fakeable) & (events.Muon.cone_pt > 15) |
| 64 | + subleading_e_mask = (e_mask_fakeable) & (events.Electron.cone_pt > 15) |
| 65 | + |
| 66 | + # For further analysis after Reduction, we consider all tight leptons with pt > 15 GeV |
| 67 | + lepton_results.objects["Electron"]["Electron"] = masked_sorted_indices(subleading_e_mask, events.Electron.pt) |
| 68 | + lepton_results.objects["Muon"]["Muon"] = masked_sorted_indices(subleading_mu_mask, events.Muon.pt) |
| 69 | + electron = events.Electron[subleading_e_mask] |
| 70 | + muon = events.Muon[subleading_mu_mask] |
| 71 | + |
| 72 | + # Create a temporary lepton collection |
| 73 | + lepton = ak.concatenate( |
| 74 | + [ |
| 75 | + electron * 1, |
| 76 | + muon * 1, |
| 77 | + ], |
| 78 | + axis=1, |
| 79 | + ) |
| 80 | + lepton = lepton_results.aux["lepton"] = lepton[ak.argsort(lepton.pt, axis=-1, ascending=False)] |
| 81 | + |
| 82 | + lepton_results.steps["DileptonVeto"] = ak.num(lepton, axis=1) <= 1 |
| 83 | + |
| 84 | + lepton_results.steps["Lep_e"] = e_mask = ( |
| 85 | + (ak.sum(leading_e_mask, axis=1) == 1) & |
| 86 | + (ak.sum(subleading_mu_mask, axis=1) == 0) |
| 87 | + ) |
| 88 | + lepton_results.steps["Lep_mu"] = mu_mask = ( |
| 89 | + (ak.sum(leading_mu_mask, axis=1) == 1) & |
| 90 | + (ak.sum(subleading_e_mask, axis=1) == 0) |
| 91 | + ) |
| 92 | + |
| 93 | + lepton_results.steps["Lepton"] = (e_mask | mu_mask) |
| 94 | + |
| 95 | + lepton_results.steps["Fake"] = ( |
| 96 | + lepton_results.steps.Lepton & |
| 97 | + (ak.sum(electron.is_tight, axis=1) + ak.sum(muon.is_tight, axis=1) == 0) |
| 98 | + ) |
| 99 | + lepton_results.steps["SR"] = ( |
| 100 | + lepton_results.steps.Lepton & |
| 101 | + (ak.sum(electron.is_tight, axis=1) + ak.sum(muon.is_tight, axis=1) == 1) |
| 102 | + ) |
| 103 | + |
| 104 | + for channel, trigger_columns in self.config_inst.x.trigger.items(): |
| 105 | + # apply the "or" of all triggers of this channel |
| 106 | + trigger_mask = ak.any([events.HLT[trigger_column] for trigger_column in trigger_columns], axis=0) |
| 107 | + lepton_results.steps[f"Trigger_{channel}"] = trigger_mask |
| 108 | + |
| 109 | + # ensure that Lepton channel is in agreement with trigger |
| 110 | + lepton_results.steps[f"TriggerAndLep_{channel}"] = ( |
| 111 | + lepton_results.steps[f"Trigger_{channel}"] & lepton_results.steps[f"Lep_{channel}"] |
| 112 | + ) |
| 113 | + |
| 114 | + # combine results of each individual channel |
| 115 | + lepton_results.steps["Trigger"] = ak.any([ |
| 116 | + lepton_results.steps[f"Trigger_{channel}"] |
| 117 | + for channel in self.config_inst.x.trigger.keys() |
| 118 | + ], axis=0) |
| 119 | + |
| 120 | + lepton_results.steps["TriggerAndLep"] = ak.any([ |
| 121 | + lepton_results.steps[f"TriggerAndLep_{channel}"] |
| 122 | + for channel in self.config_inst.x.trigger.keys() |
| 123 | + ], axis=0) |
| 124 | + |
| 125 | + return events, lepton_results |
| 126 | + |
| 127 | + |
| 128 | +@sl_lepton_selection.init |
| 129 | +# @call_once_on_instance() |
| 130 | +def sl_lepton_selection_init(self: Selector) -> None: |
| 131 | + # update selector steps labels |
| 132 | + self.config_inst.x.selector_step_labels = self.config_inst.x("selector_step_labels", {}) |
| 133 | + self.config_inst.x.selector_step_labels.update({ |
| 134 | + "DileptonVeto": r"$N_{lepton} \leq 1$", |
| 135 | + "Lepton": r"$N_{lepton} = 1$", |
| 136 | + "Lep_e": r"$N_{e} = 1$ and $N_{\mu} = 0$", |
| 137 | + "Lep_mu": r"$N_{\mu} = 1$ and $N_{e} = 0$", |
| 138 | + "Fake": r"$N_{lepton}^{tight} = 0$", |
| 139 | + "SR": r"$N_{lepton}^{tight} = 1$", |
| 140 | + "TriggerAndLep": "Trigger matches Lepton Channel", |
| 141 | + "VetoTau": r"$N_{\tau}^{veto} = 0$", |
| 142 | + }) |
| 143 | + |
| 144 | + year = self.config_inst.campaign.x.year |
| 145 | + |
| 146 | + # setup lepton pt and trigger requirements in the config |
| 147 | + # when the lepton selector does not define the values, resort to defaults |
| 148 | + # NOTE: this is not doing what I was intending: this allows me to share the selector info |
| 149 | + # with other tasks, but I want other selectors to be able to change these attributes... |
| 150 | + if year == 2016: |
| 151 | + self.config_inst.x.mu_pt = self.mu_pt or 25 |
| 152 | + self.config_inst.x.e_pt = self.e_pt or 27 |
| 153 | + self.config_inst.x.trigger = self.trigger or { |
| 154 | + "e": ["Ele27_WPTight_Gsf"], |
| 155 | + "mu": ["IsoMu24"], |
| 156 | + } |
| 157 | + elif year == 2017: |
| 158 | + self.config_inst.x.mu_pt = self.mu_pt or 28 |
| 159 | + self.config_inst.x.e_pt = self.e_pt or 36 |
| 160 | + self.config_inst.x.trigger = self.trigger or { |
| 161 | + "e": ["Ele35_WPTight_Gsf"], |
| 162 | + "mu": ["IsoMu27"], |
| 163 | + } |
| 164 | + elif year == 2018: |
| 165 | + self.config_inst.x.mu_pt = self.mu_pt or 25 |
| 166 | + self.config_inst.x.e_pt = self.e_pt or 33 |
| 167 | + self.config_inst.x.trigger = self.trigger or { |
| 168 | + "e": ["Ele32_WPTight_Gsf"], |
| 169 | + "mu": ["IsoMu24"], |
| 170 | + } |
| 171 | + elif year == 2022: |
| 172 | + self.config_inst.x.mu_pt = self.mu_pt or 25 |
| 173 | + self.config_inst.x.e_pt = self.e_pt or 31 |
| 174 | + self.config_inst.x.trigger = self.trigger or { |
| 175 | + "e": ["Ele30_WPTight_Gsf"], |
| 176 | + "mu": ["IsoMu24"], |
| 177 | + } |
| 178 | + else: |
| 179 | + raise Exception(f"Single lepton trigger not implemented for year {year}") |
| 180 | + |
| 181 | + # add all required trigger to the uses |
| 182 | + for trigger_columns in self.config_inst.x.trigger.values(): |
| 183 | + for column in trigger_columns: |
| 184 | + self.uses.add(f"HLT.{column}") |
| 185 | + |
| 186 | + |
| 187 | +@selector( |
| 188 | + uses={ |
| 189 | + pre_selection, post_selection, |
| 190 | + vbf_jet_selection, sl_boosted_jet_selection, |
| 191 | + jet_selection, sl_lepton_selection, |
| 192 | + }, |
| 193 | + produces={ |
| 194 | + pre_selection, post_selection, |
| 195 | + vbf_jet_selection, sl_boosted_jet_selection, |
| 196 | + jet_selection, sl_lepton_selection, |
| 197 | + }, |
| 198 | + exposed=True, |
| 199 | +) |
| 200 | +def sl( |
| 201 | + self: Selector, |
| 202 | + events: ak.Array, |
| 203 | + stats: defaultdict, |
| 204 | + **kwargs, |
| 205 | +) -> Tuple[ak.Array, SelectionResult]: |
| 206 | + # prepare events |
| 207 | + events, results = self[pre_selection](events, stats, **kwargs) |
| 208 | + |
| 209 | + # lepton selection |
| 210 | + events, lepton_results = self[sl_lepton_selection](events, stats, **kwargs) |
| 211 | + results += lepton_results |
| 212 | + |
| 213 | + # jet selection |
| 214 | + events, jet_results = self[jet_selection](events, lepton_results, stats, **kwargs) |
| 215 | + results += jet_results |
| 216 | + |
| 217 | + # boosted selection |
| 218 | + events, boosted_results = self[sl_boosted_jet_selection](events, lepton_results, jet_results, stats, **kwargs) |
| 219 | + results += boosted_results |
| 220 | + |
| 221 | + # vbf-jet selection |
| 222 | + events, vbf_jet_results = self[vbf_jet_selection](events, results, stats, **kwargs) |
| 223 | + results += vbf_jet_results |
| 224 | + |
| 225 | + results.steps["Resolved"] = (results.steps.nJet3 & results.steps.nBjet1) |
| 226 | + |
| 227 | + results.steps["ResolvedOrBoosted"] = ( |
| 228 | + (results.steps.nJet3 & results.steps.nBjet1) | results.steps.HbbJet |
| 229 | + ) |
| 230 | + |
| 231 | + # combined event selection after all steps except b-jet selection |
| 232 | + results.steps["all_but_bjet"] = ( |
| 233 | + results.steps.cleanup & |
| 234 | + (results.steps.nJet3 | results.steps.HbbJet_no_bjet) & |
| 235 | + results.steps.ll_lowmass_veto & |
| 236 | + results.steps.ll_zmass_veto & |
| 237 | + results.steps.DileptonVeto & |
| 238 | + results.steps.Lepton & |
| 239 | + results.steps.VetoTau & |
| 240 | + results.steps.Trigger & |
| 241 | + results.steps.TriggerAndLep |
| 242 | + ) |
| 243 | + |
| 244 | + # combined event selection after all steps |
| 245 | + # NOTE: we only apply the b-tagging step when no AK8 Jet is present; if some event with AK8 jet |
| 246 | + # gets categorized into the resolved category, we might need to cut again on the number of b-jets |
| 247 | + results.event = ( |
| 248 | + results.steps.all_but_bjet & |
| 249 | + ((results.steps.nJet3 & results.steps.nBjet1) | results.steps.HbbJet) |
| 250 | + ) |
| 251 | + results.steps["all"] = results.event |
| 252 | + |
| 253 | + # build categories |
| 254 | + events, results = self[post_selection](events, results, stats, **kwargs) |
| 255 | + |
| 256 | + return events, results |
| 257 | + |
| 258 | + |
| 259 | +@sl.init |
| 260 | +def sl_init(self: Selector) -> None: |
| 261 | + # define mapping from selector step to labels used in cutflow plots |
| 262 | + self.config_inst.x.selector_step_labels = self.config_inst.x("selector_step_labels", {}) |
| 263 | + self.config_inst.x.selector_step_labels.update({ |
| 264 | + "Resolved": r"$N_{jets}^{AK4} \geq 3$ and $N_{jets}^{BTag} \geq 1$", |
| 265 | + "ResolvedOrBoosted": ( |
| 266 | + r"($N_{jets}^{AK4} \geq 3$ and $N_{jets}^{BTag} \geq 1$) " |
| 267 | + r"or $N_{H \rightarrow bb}^{AK8} \geq 1$" |
| 268 | + ), |
| 269 | + }) |
| 270 | + |
| 271 | + if self.config_inst.x("do_cutflow_features", False): |
| 272 | + self.uses.add(cutflow_features) |
| 273 | + self.produces.add(cutflow_features) |
| 274 | + |
| 275 | + if not getattr(self, "dataset_inst", None) or self.dataset_inst.is_data: |
| 276 | + return |
| 277 | + |
| 278 | + self.uses.add(event_weights_to_normalize) |
| 279 | + self.produces.add(event_weights_to_normalize) |
0 commit comments