ATLAS Offline Software
Loading...
Searching...
No Matches
AsgAnalysisConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3# AnaAlgorithm import(s):
4import warnings
5from enum import Enum
6
7from AnalysisAlgorithmsConfig.ConfigAccumulator import (
8 DataType,
9 ExpertModeWarning,
10 GeneratorWeightWarning,
11 Run4FallbackWarning,
12)
13from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
14from AnalysisAlgorithmsConfig.ConfigSequence import groupBlocks
15from AthenaConfiguration.Enums import LHCPeriod
16
17try:
18 from AthenaCommon.Logging import logging
19except ImportError:
20 import logging
21
23 JETS = {'JET_'}
24 JER = {'JET_JER'}
25 FTAG = {'FT_'}
26 ELECTRONS = {'EG_', 'EL_'}
27 MUONS = {'MUON_'}
28 PHOTONS = {'EG_', 'PH_'}
29 TAUS = {'TAUS_'}
30 MET = {'MET_'}
31 TRACKS = {'TRK_'}
32 GENERATOR = {'GEN_'}
33 PRW = {'PRW_'}
34 EVENT = {'GEN_', 'PRW_'}
35
36class CommonServicesConfig (ConfigBlock) :
37 """the ConfigBlock for common services
38
39 The idea here is that all algorithms need some common services, and I should
40 provide configuration blocks for those. For now there is just a single
41 block, but in the future I might break out e.g. the systematics service.
42 """
43
44 def __init__ (self) :
45 super().__init__ ()
46 self.addOption ('runSystematics', None, type=bool,
47 info="whether to turn on the computation of systematic variations. "
48 "The default is to run them on MC.")
49 self.addOption ('filterSystematics', None, type=str,
50 info="a regexp string against which the systematics names will be "
51 "matched. Only positive matches are retained and used in the evaluation "
52 "of the various algorithms.")
53 self.addOption ('onlySystematicsCategories', None, type=list,
54 info="a list of strings defining categories of systematics to enable "
55 "(only recommended for studies / partial ntuple productions). Choose amongst: "
56 "`jets`, `JER`, `FTag`, `electrons`, `muons`, `photons`, `taus`, `met`, `tracks`, `generator`, `PRW`, `event`. "
57 "This option is overridden by `filterSystematics`.",
58 meta={'choices':(['jets', 'JER', 'FTag', 'electrons', 'muons', 'photons', 'taus', 'met', 'tracks', 'generator', 'PRW', 'event'], None)})
59 self.addOption ('systematicsHistogram', None , type=str,
60 info="the name of the histogram to which a list of executed "
61 "systematics will be printed. If left empty, the histogram is not written at all.")
62 self.addOption ('separateWeightSystematics', False, type=bool,
63 info="if `systematicsHistogram` is enabled, whether to create a separate "
64 "histogram holding only the names of weight-based systematics. This is useful "
65 "to help make histogramming frameworks more efficient by knowing in advance which "
66 "systematics need to recompute the observable and which don't.")
67 self.addOption ('metadataHistogram', 'metadata' , type=str,
68 info="the name of the metadata histogram which contains information about "
69 "data type, campaign, etc. If left empty, the histogram is not written at all.")
70 self.addOption ('enableExpertMode', False, type=bool,
71 info="allows CP experts and CPAlgorithm devs to use non-recommended configurations. "
72 "DO NOT USE FOR ANALYSIS.")
73 self.addOption ('streamName', None, type=str,
74 info="name of the output stream to save metadata histograms in.")
75 self.addOption ('setupONNX', False, type=bool,
76 info="creates an instance of `AthOnnx::OnnxRuntimeSvc`.")
77
78 def instanceName (self) :
79 """Return the instance name for this block"""
80 return '' # no instance name, this is a singleton
81
82 def makeAlgs (self, config) :
83
84 sysService = config.createService( 'CP::SystematicsSvc', 'SystematicsSvc' )
85
86 # Setup stream name
87 streamName = self.streamName or config.defaultHistogramStream()
88
89 # Handle all possible configuration options for systematics
90 if self.runSystematics is False:
91 runSystematics = self.runSystematics
92 elif config.noSystematics() is not None:
93 # if option not set:
94 # check to see if set in config accumulator
95 self.runSystematics = not config.noSystematics()
96 runSystematics = self.runSystematics
97 else:
98 runSystematics = True
99
100 # Now update the global configuration
101 config._noSystematics = not runSystematics
102
103 if runSystematics:
104 sysService.sigmaRecommended = 1
105 if config.dataType() is DataType.Data:
106 # Only one type of allowed systematics on data: the JER variations!
108 if self.onlySystematicsCategories is not None:
109 # Convert strings to enums and validate
110 requested_categories = set()
111 for category_str in self.onlySystematicsCategories:
112 try:
113 category_enum = SystematicsCategories[category_str.upper()]
114 requested_categories |= category_enum.value
115 except KeyError:
116 raise ValueError(f"Invalid systematics category passed to option 'onlySystematicsCategories': {category_str}. Must be one of {', '.join(category.name for category in SystematicsCategories)}")
117 # Construct regex pattern as logical-OR of category names
118 if len(requested_categories):
119 sysService.systematicsRegex = "^(?=.*(" + "|".join(sorted(requested_categories)) + ")|$).*"
120 if self.filterSystematics is not None:
121 sysService.systematicsRegex = self.filterSystematics
122 config.createService( 'CP::SelectionNameSvc', 'SelectionNameSvc')
123
124 if self.systematicsHistogram is not None:
125 # print out all systematics
126 allSysDumper = config.createAlgorithm( 'CP::SysListDumperAlg', 'SystematicsPrinter' )
127 allSysDumper.histogramName = self.systematicsHistogram
128 allSysDumper.RootStreamName = streamName
129
131 # print out only the weight systematics (for more efficient histogramming down the line)
132 weightSysDumper = config.createAlgorithm( 'CP::SysListDumperAlg', 'OnlyWeightSystematicsPrinter' )
133 weightSysDumper.histogramName = f"{self.systematicsHistogram}OnlyWeights"
134 weightSysDumper.systematicsRegex = "^(GEN_|EL_EFF_|MUON_EFF_|PH_EFF_|TAUS_TRUEHADTAU_EFF_|FT_EFF_|JET_.*JvtEfficiency_|PRW_).*"
135 weightSysDumper.RootStreamName = streamName
136
138 # add histogram with metadata
139 if not config.flags:
140 raise ValueError ("Writing out the metadata histogram requires to pass config flags")
141 metadataHistAlg = config.createAlgorithm( 'CP::MetadataHistAlg', 'MetadataHistAlg' )
142 metadataHistAlg.histogramName = self.metadataHistogram
143 metadataHistAlg.dataType = str(config.dataType().value)
144 metadataHistAlg.campaign = str(config.dataYear()) if config.dataType() is DataType.Data else str(config.campaign().value)
145 metadataHistAlg.mcChannelNumber = str(config.dsid())
146 metadataHistAlg.RootStreamName = streamName
147 if config.dataType() is DataType.Data:
148 etag = "unavailable"
149 else:
150 from AthenaConfiguration.AutoConfigFlags import GetFileMD
151 metadata = GetFileMD(config.flags.Input.Files)
152 amiTags = metadata.get("AMITag", "not found!")
153 etag = str(amiTags.split("_")[0])
154 metadataHistAlg.etag = etag
155
157 # set any expert-mode errors to be ignored instead
158 warnings.simplefilter('ignore', ExpertModeWarning)
159 # just warning users they might be doing something dangerous
160 log = logging.getLogger('CommonServices')
161 bold = "\033[1m"
162 red = "\033[91m"
163 yellow = "\033[93m"
164 reset = "\033[0m"
165 log.warning(red +r"""
166 ________ _______ ______ _____ _______ __ __ ____ _____ ______ ______ _ _ ____ _ ______ _____
167 | ____\ \ / / __ \| ____| __ \__ __| | \/ |/ __ \| __ \| ____| | ____| \ | | /\ | _ \| | | ____| __ \
168 | |__ \ V /| |__) | |__ | |__) | | | | \ / | | | | | | | |__ | |__ | \| | / \ | |_) | | | |__ | | | |
169 | __| > < | ___/| __| | _ / | | | |\/| | | | | | | | __| | __| | . ` | / /\ \ | _ <| | | __| | | | |
170 | |____ / . \| | | |____| | \ \ | | | | | | |__| | |__| | |____ | |____| |\ |/ ____ \| |_) | |____| |____| |__| |
171 |______/_/ \_\_| |______|_| \_\ |_| |_| |_|\____/|_____/|______| |______|_| \_/_/ \_\____/|______|______|_____/
172
173"""
174 +reset)
175 log.warning(f"{bold}{yellow}These settings are not recommended for analysis. Make sure you know what you're doing, or disable them with `enableExpertMode: False` in `CommonServices`.{reset}")
176
177 if self.setupONNX:
178 config.createService('AthOnnx::OnnxRuntimeSvc', 'OnnxRuntimeSvc')
179 config.setContainerMeta ('EventInfo', 'OnnxEnabled', self.setupONNX)
180
181@groupBlocks
183 seq.append(CommonServicesConfig())
184 from AsgAnalysisAlgorithms.TruthCollectionsFixerConfig import TruthCollectionsFixerBlock
185 seq.append(TruthCollectionsFixerBlock())
186
187class IOStatsBlock(ConfigBlock):
188 """Print what branches are used in analysis"""
189
190 def __init__(self):
191 super().__init__()
192 self.addOption("printOption", "Summary", type=str,
193 info='option to pass the standard ROOT printing function. Can be `Summary`, `ByEntries` or `ByBytes`.',
194 meta={'choices':(['Summary','ByEntries','ByBytes'],1)})
195
196 def instanceName (self) :
197 """Return the instance name for this block"""
198 return '' # no instance name, this is a singleton
199
200 def makeAlgs(self, config):
201 alg = config.createAlgorithm('CP::IOStatsAlg', 'IOStatsAlg')
202 alg.printOption = self.printOption
203
204
205class PileupReweightingBlock (ConfigBlock):
206 """the ConfigBlock for pileup reweighting"""
207
208 def __init__ (self) :
209 super().__init__ ()
210 self.addOption ('campaign', None, type=None,
211 info="the MC campaign for the PRW auto-configuration.")
212 self.addOption ('files', None, type=list,
213 info="the input files being processed (list of strings). "
214 "Alternative to auto-configuration.")
215 self.addOption ('useDefaultConfig', True, type=bool,
216 info="whether to use the central PRW files.")
217 self.addOption ('GRLSuffixDict', {}, type=dict,
218 info="a year-suffix dictionary to help with autoconfiguration of "
219 "GRL-specific PRW configuration, e.g. selecting 'BjetHLT'.")
220 self.addOption ('userLumicalcFiles', None, type=list,
221 info="user-provided lumicalc files (list of strings). Alternative "
222 "to auto-configuration.")
223 self.addOption ('userLumicalcFilesPerCampaign', None, type=dict,
224 info="user-provided lumicalc files (dictionary of list of strings, "
225 "with MC campaigns as the keys). Alternative to auto-configuration.")
226 self.addOption ('userPileupConfigs', None, type=list,
227 info="user-provided PRW files (list of strings). Alternative to "
228 "auto-configuration.")
229 self.addOption ('userPileupConfigsPerCampaign', None, type=dict,
230 info="user-provided PRW files (dictionary of list of strings, with "
231 "MC campaigns as the keys).")
232 self.addOption ('postfix', '', type=str,
233 info="a postfix to apply to decorations and algorithm names. "
234 "Typically not needed unless several instances of `PileupReweighting` are scheduled.")
235 self.addOption ('alternativeConfig', False, type=bool,
236 info="whether this is used as an additional alternative config for `PileupReweighting`. "
237 "Will only store the alternative pileup weight in that case.")
238 self.addOption ('unrepresentedDataWarningThreshold', 1e-4, type=float,
239 info="suppress the unrepresented-data WARNING when the unrepresented "
240 "fraction is below this value (default 0.01%). Set to 0 to always "
241 "warn.")
242 self.addOption ('writeColumnarToolVariables', False, type=bool,
243 info="whether to add `EventInfo` variables needed for running the columnar tool(s) on the output n-tuple. (EXPERIMENTAL).",
244 expertMode=True)
245 self.addOption ('writeDetailedPileupValues', False, type=bool,
246 info="whether to write the average and actual pileup values with and without data scale factor.")
247
248 def instanceName (self) :
249 """Return the instance name for this block"""
250 return self.postfix
251
252 def makeAlgs (self, config) :
253
254 from Campaigns.Utils import Campaign
255
256 log = logging.getLogger("makePileupAnalysisSequence")
257
258 eventInfoVar = [
259 ("runNumber", "unsigned"),
260 ("eventNumber", "unsigned_long"),
261 ]
262 if config.dataType() is not DataType.Data:
263 eventInfoVar.extend([
264 ("mcChannelNumber", "unsigned"),
265 ])
266 # for now still write the average and actual pileup values until the new p-tag is available
267 eventInfoVar.extend([
268 ("averageInteractionsPerCrossing", "float"),
269 ("actualInteractionsPerCrossing", "float"),
270 ])
272 # This is not strictly necessary, as the columnar users
273 # could recreate this, but it is also a single constant int,
274 # that should compress exceedingly well.
275 eventInfoVar.extend([
276 ("eventTypeBitmask", "int"),
277 ])
278
279 if config.isPhyslite() and not self.alternativeConfig:
280 # PHYSLITE already has these variables defined, just need to copy them to the output
281 log.info("Physlite does not need pileup reweighting. Variables will be copied from input instead.")
283 log.warning("writeDetailedPileupValues is set to True, but Physlite does not have these variables. They will not be written to the output.")
284
285 eventInfoVar.extend([
286 ("correctedScaledInteractionsPerCrossing", "float"),
287 ])
288 for var_name, var_type in eventInfoVar:
289 config.addOutputVar("EventInfo", var_name, var_name, noSys=True, auxType=var_type)
290
291 if config.dataType() is not DataType.Data:
292 config.addOutputVar("EventInfo", "PileupWeight_%SYS%", "weight_pileup", auxType="float")
293 if config.geometry() is LHCPeriod.Run2:
294 config.addOutputVar("EventInfo", "beamSpotWeight", "weight_beamspot", noSys=True, auxType="float")
295 return
296
297 # check files from flags
298 if self.files is None and config.flags is not None:
299 self.files = config.flags.Input.Files
300
301 campaign = self.campaign
302 # if user didn't explicitly configure campaign, let's try setting it from metadata
303 if self.campaign is None:
304 # if we used flags, campaign is auto-determined
305 if config.campaign() is not None and config.campaign() is not Campaign.Unknown:
306 campaign = config.campaign()
307 log.info("Auto-configuring campaign for PRW from flags: %s", campaign.value)
308 elif config.dataType() is DataType.Data:
309 log.info("Inferring campaign from data year: %d", config.dataYear())
310 campaign = {
311 2015: Campaign.MC20a,
312 2016: Campaign.MC20a,
313 2017: Campaign.MC20d,
314 2018: Campaign.MC20e,
315 2022: Campaign.MC23a,
316 2023: Campaign.MC23d,
317 2024: Campaign.MC23e,
318 2025: Campaign.MC23g,
319 2026: Campaign.MC23g,
320 }.get(config.dataYear(), Campaign.Unknown)
321 else:
322 # we try to determine campaign from files if above failed
323 if self.files is not None:
324 from Campaigns.Utils import getMCCampaign
325 campaign = getMCCampaign(self.files)
326 if campaign and campaign is not Campaign.Unknown:
327 log.info("Auto-configuring campaign for PRW from files: %s", campaign.value)
328 else:
329 log.info("Campaign could not be determined.")
330
331
332 toolConfigFiles = []
333 toolLumicalcFiles = []
334
335 # PRW config files should only be configured if we run on MC
336 # Run 4 not supported yet
337 if (config.dataType() is not DataType.Data and
338 config.geometry() is not LHCPeriod.Run4):
339 # check if user provides per-campaign pileup config list
340 if self.userPileupConfigs is not None and self.userPileupConfigsPerCampaign is not None:
341 raise ValueError("Both userPileupConfigs and userPileupConfigsPerCampaign specified,"
342 " use only one of the options!")
343 if self.userPileupConfigsPerCampaign is not None:
344 if not campaign:
345 raise Exception("userPileupConfigsPerCampaign requires campaign to be configured!")
346 if campaign is Campaign.Unknown:
347 raise Exception("userPileupConfigsPerCampaign used, but campaign = Unknown!")
348 try:
349 toolConfigFiles = self.userPileupConfigsPerCampaign[campaign.value][:]
350 log.info("Using user provided per-campaign PRW configuration")
351 except KeyError as e:
352 raise KeyError(f"Unconfigured campaign {e} for userPileupConfigsPerCampaign!")
353
354 elif self.userPileupConfigs is not None:
355 toolConfigFiles = self.userPileupConfigs[:]
356 log.info("Using user provided PRW configuration")
357
358 else:
359 if self.useDefaultConfig and self.files is None:
360 raise ValueError("useDefaultConfig requires files to be configured!"
361 " Either pass them as an option or use flags.")
362
363 from PileupReweighting.AutoconfigurePRW import getConfigurationFiles
364 if campaign and campaign is not Campaign.Unknown:
365 toolConfigFiles = getConfigurationFiles(campaign=campaign,
366 files=self.files,
367 useDefaultConfig=self.useDefaultConfig,
368 data_type=config.dataType(),
369 GRLSuffixDict=self.GRLSuffixDict)
371 log.info("Auto-configuring universal/default PRW config")
372 else:
373 log.info("Auto-configuring per-sample PRW config files based on input files")
374 else:
375 log.info("No campaign specified, no PRW config files configured")
376
377
378 # check if user provides per-campaign lumical config list
379 if self.userLumicalcFilesPerCampaign is not None and self.userLumicalcFiles is not None:
380 raise ValueError("Both userLumicalcFiles and userLumicalcFilesPerCampaign specified,"
381 " use only one of the options!")
382 if self.userLumicalcFilesPerCampaign is not None:
383 if not campaign:
384 raise Exception('userLumicalcFilesPerCampaign requires campaign to be configured!')
385 if campaign is Campaign.Unknown:
386 raise Exception('userLumicalcFilesPerCampaign used, but campaign = Unknown!')
387 try:
388 toolLumicalcFiles = self.userLumicalcFilesPerCampaign[campaign.value][:]
389 log.info("Using user-provided per-campaign lumicalc files")
390 except KeyError as e:
391 raise KeyError(f"Unconfigured campaign {e} for userLumicalcFilesPerCampaign!")
392 elif self.userLumicalcFiles is not None:
393 toolLumicalcFiles = self.userLumicalcFiles[:]
394 log.info("Using user-provided lumicalc files")
395 else:
396 if campaign and campaign not in {Campaign.Unknown, Campaign.PhaseII}:
397 from PileupReweighting.AutoconfigurePRW import getLumicalcFiles
398 toolLumicalcFiles = getLumicalcFiles(campaign, self.GRLSuffixDict)
399 log.info("Using auto-configured lumicalc files")
400 else:
401 log.info("No campaign specified, no lumicalc files configured for PRW")
402
403 # Set up the only algorithm of the sequence:
404 if config.geometry() is LHCPeriod.Run4:
405 warnings.warn_explicit(
406 "Pileup reweighting is not yet supported for Run 4 geometry",
407 Run4FallbackWarning, filename="", lineno=0)
408 alg = config.createAlgorithm("CP::EventDecoratorAlg", "EventDecoratorAlg")
409 alg.uint32Decorations = {
410 "RandomRunNumber": config.flags.Input.RunNumbers[0]
411 }
412
413 else:
414 alg = config.createAlgorithm("CP::PileupReweightingAlg", "PileupReweightingAlg")
415 config.addPrivateTool("pileupReweightingTool", "CP::PileupReweightingTool")
416 alg.pileupReweightingTool.ConfigFiles = toolConfigFiles
417 if not toolConfigFiles and config.dataType() is not DataType.Data:
418 log.info("No PRW config files provided. Disabling reweighting")
419 # Setting the weight decoration to the empty string disables the reweighting
420 alg.pileupWeightDecoration = ""
421 else:
422 alg.pileupWeightDecoration = f"PileupWeight{self.postfix}_%SYS%"
423 alg.pileupReweightingTool.LumiCalcFiles = toolLumicalcFiles
424 alg.pileupReweightingTool.UnrepresentedDataWarningThreshold = (
425 self.unrepresentedDataWarningThreshold)
426
427 # pile-up values
428 if not self.alternativeConfig:
430 alg.correctedScaledAverageMuDecoration = "correctedScaledAverageInteractionsPerCrossing"
431 alg.correctedScaledActualMuDecoration = "correctedScaledActualInteractionsPerCrossing"
432 alg.correctedAverageMuDecoration = "correctedUnscaledAverageInteractionsPerCrossing"
433 alg.correctedActualMuDecoration = "correctedUnscaledActualInteractionsPerCrossing"
434
435 eventInfoVar.extend([
436 ("correctedScaledAverageInteractionsPerCrossing", "float"),
437 ("correctedScaledActualInteractionsPerCrossing", "float"),
438 ("correctedUnscaledAverageInteractionsPerCrossing", "float"),
439 ("correctedUnscaledActualInteractionsPerCrossing", "float"),
440 ])
441
442 if campaign is Campaign.MC20a:
443 config.addOutputVar("EventInfo", "correctedScaledAverageInteractionsPerCrossing", "correctedScaledInteractionsPerCrossing", noSys=True, auxType="float")
444 elif campaign >= Campaign.MC20d and config.geometry() is not LHCPeriod.Run4:
445 config.addOutputVar("EventInfo", "correctedScaledActualInteractionsPerCrossing", "correctedScaledInteractionsPerCrossing", noSys=True, auxType="float")
446 else:
447 if campaign is Campaign.MC20a:
448 alg.correctedScaledAverageMuDecoration = "correctedScaledInteractionsPerCrossing"
449 elif campaign >= Campaign.MC20d:
450 alg.correctedScaledActualMuDecoration = "correctedScaledInteractionsPerCrossing"
451 eventInfoVar.extend([
452 ("correctedScaledInteractionsPerCrossing", "float"),
453 ])
454
455 if not self.alternativeConfig:
456 for var_name, var_type in eventInfoVar:
457 config.addOutputVar("EventInfo", var_name, var_name, noSys=True, auxType=var_type)
458
459 if config.dataType() is not DataType.Data and config.geometry() is LHCPeriod.Run2:
460 config.addOutputVar("EventInfo", "beamSpotWeight", "weight_beamspot", noSys=True, auxType="float")
461
462 if config.dataType() is not DataType.Data and toolConfigFiles:
463 config.addOutputVar("EventInfo", f"PileupWeight{self.postfix}_%SYS%",
464 f"weight_pileup{self.postfix}", auxType="float")
465
466
467class GeneratorAnalysisBlock (ConfigBlock):
468 """the ConfigBlock for generator algorithms"""
469
470 def __init__ (self) :
471 super().__init__ ()
472 self.addOption ('saveCutBookkeepers', True, type=bool,
473 info="whether to save the cut bookkeepers information into the "
474 "output file.")
475 self.addOption ('runNumber', None, type=int,
476 info="the MC `runNumber`. If left empty, autoconfigure from the sample metadata.")
477 self.addOption ('cutBookkeepersSystematics', None, type=bool,
478 info="whether to also save the cut bookkeepers systematics. The "
479 "default is `None` (follows the global systematics flag). Set to "
480 "`False` or `True` to override.")
481 self.addOption ('histPattern', None, type=str,
482 info="the histogram name pattern for the cut-bookkeeper histogram names.")
483 self.addOption ('streamName', None, type=str,
484 info="name of the output stream to save the cut bookkeeper in.")
485 self.addOption ('detailedPDFinfo', False, type=bool,
486 info="save the necessary information to run the LHAPDF tool offline.")
487 self.addOption ('doPDFReweighting', False, type=bool,
488 info="perform the PDF reweighting to do the PDF sensitivity studies with the existing sample, intrinsic charm PDFs as the default here. WARNING: the reweighting closure should be validated within analysis (it has been proved to be good for Madgraph, aMC@NLO, Pythia8, Herwig, and Alpgen, but not good for Sherpa and Powheg).")
489 self.addOption ('inPDFName', None, type=str, info="PDF set the input sample was produced with, for use in PDF reweighting")
490 self.addOption ('outPDFName', [
491 "CT14nnloIC/0", "CT14nnloIC/1", "CT14nnloIC/2",
492 "CT18FC/0", "CT18FC/3", "CT18FC/6", "CT18FC/9",
493 "CT18NNLO/0", "CT18XNNLO/0",
494 "NNPDF40_nnlo_pch_as_01180/0", "NNPDF40_nnlo_as_01180/0"
495 ], type=list, info="list of PDF sets to use for PDF reweighting.")
496 self.addOption ('doHFProdFracReweighting', False, type=bool,
497 info="whether to apply HF production fraction reweighting.")
498 self.addOption ('truthParticleContainer', 'TruthParticles', type=str,
499 info="the name of the truth particle container to use for HF production fraction reweighting.")
500
501 def instanceName (self) :
502 """Return the instance name for this block"""
503 return self.streamName or "DEFAULT"
504
505 def makeAlgs (self, config) :
506
507 if config.dataType() is DataType.Data:
508 # there are no generator weights in data!
509 return
510 log = logging.getLogger('GeneratorAnalysis')
511
512 # Setup stream name
513 streamName = self.streamName or config.defaultHistogramStream()
514
515 if self.runNumber is None:
516 self.runNumber = config.runNumber()
517
518 if self.saveCutBookkeepers and not self.runNumber:
519 raise ValueError ("invalid run number: " + str(self.runNumber))
520
521 # Set up the CutBookkeepers algorithm:
523 alg = config.createAlgorithm('CP::AsgCutBookkeeperAlg', 'CutBookkeeperAlg')
524 alg.RootStreamName = streamName
525 alg.runNumber = self.runNumber
526 if self.cutBookkeepersSystematics is None:
527 alg.enableSystematics = not config.noSystematics()
528 else:
529 alg.enableSystematics = self.cutBookkeepersSystematics
530 if self.histPattern:
531 alg.histPattern = self.histPattern
532 config.addPrivateTool( 'truthWeightTool', 'PMGTools::PMGTruthWeightTool' )
533
534 # Set up the weights algorithm:
535 alg = config.createAlgorithm( 'CP::PMGTruthWeightAlg', 'PMGTruthWeightAlg' )
536 config.addPrivateTool( 'truthWeightTool', 'PMGTools::PMGTruthWeightTool' )
537 alg.decoration = 'generatorWeight_%SYS%'
538 config.addOutputVar ('EventInfo', 'generatorWeight_%SYS%', 'weight_mc')
539
541 alg = config.createAlgorithm( 'CP::PDFinfoAlg', 'PDFinfoAlg', reentrant=True )
542 for var in ["PDFID1","PDFID2","PDGID1","PDGID2","Q","X1","X2","XF1","XF2"]:
543 config.addOutputVar ('EventInfo', var, 'PDFinfo_' + var, noSys=True)
544
546 generatorInfo = config.flags.Input.GeneratorsInfo
547 log.info(f"Loaded generator info: {generatorInfo}")
548
549 if not generatorInfo:
550 warnings.warn_explicit("No generator info found.", GeneratorWeightWarning, filename='', lineno=0)
551 elif isinstance(generatorInfo, dict):
552
553 unsupported_generators = {
554 "Sherpa": "PDF reweighting for Sherpa is not proven to be reliable. The reweighting closure should be validated within the analysis.",
555 "Powheg": "PDF reweighting for Powheg is not proven to be reliable. The reweighting closure should be validated within the analysis."
556 }
557
558 # Check for unsupported generators
559 for generator, message in unsupported_generators.items():
560 if generator in generatorInfo:
561 warnings.warn_explicit(
562 message,
563 GeneratorWeightWarning,
564 filename='',
565 lineno=0
566 )
567
568 alg = config.createAlgorithm( 'CP::PDFReweightAlg', 'PDFReweightAlg', reentrant=True )
569
570 if self.inPDFName is None:
571 log.error("Option inPDFName not specified, but is required for PDF reweighting. This means the PDF set the input dataset was generated with is determined as …")
572 else:
573 alg.inPDFName = self.inPDFName
574
575 alg.outPDFName = self.outPDFName
576
577 for pdf_set in self.outPDFName:
578 config.addOutputVar('EventInfo', f'PDFReweightSF_{pdf_set.replace("/", "_")}',
579 f'PDFReweightSF_{pdf_set.replace("/", "_")}', noSys=True, auxType='float')
580
582 generatorInfo = config.flags.Input.GeneratorsInfo
583 log.info(f"Loaded generator info: {generatorInfo}")
584
585 DSID = "000000"
586
587 if not generatorInfo:
588 warnings.warn_explicit(
589 "No generator info found.",
590 GeneratorWeightWarning, filename='', lineno=0)
591 DSID = "000000"
592 elif isinstance(generatorInfo, dict):
593 if "Pythia8" in generatorInfo:
594 DSID = "410470"
595 elif "Sherpa" in generatorInfo and "2.2.8" in generatorInfo["Sherpa"]:
596 DSID = "421152"
597 elif "Sherpa" in generatorInfo and "2.2.10" in generatorInfo["Sherpa"]:
598 DSID = "700122"
599 elif "Sherpa" in generatorInfo and "2.2.11" in generatorInfo["Sherpa"]:
600 warnings.warn_explicit(
601 "HF production fraction reweighting is not configured"
602 " for Sherpa 2.2.11. Using weights for Sherpa 2.2.10"
603 " instead.",
604 GeneratorWeightWarning, filename='', lineno=0)
605 DSID = "700122"
606 elif "Sherpa" in generatorInfo and "2.2.12" in generatorInfo["Sherpa"]:
607 warnings.warn_explicit(
608 "HF production fraction reweighting is not configured"
609 " for Sherpa 2.2.12. Using weights for Sherpa 2.2.10"
610 " instead.",
611 GeneratorWeightWarning, filename='', lineno=0)
612 DSID = "700122"
613 elif "Sherpa" in generatorInfo and "2.2.14" in generatorInfo["Sherpa"]:
614 warnings.warn_explicit(
615 "HF production fraction reweighting is not configured"
616 " for Sherpa 2.2.14. New weights need to be"
617 " calculated.",
618 GeneratorWeightWarning, filename='', lineno=0)
619 DSID = "000000"
620 elif "Sherpa" in generatorInfo and "2.2.1" in generatorInfo["Sherpa"]:
621 DSID = "410250"
622 elif "Herwig7" in generatorInfo and "7.1.3" in generatorInfo["Herwig7"]:
623 DSID = "411233"
624 elif "Herwig7" in generatorInfo and "7.2.1" in generatorInfo["Herwig7"]:
625 DSID = "600666"
626 elif "Herwig7" in generatorInfo and "7." in generatorInfo["Herwig7"]:
627 DSID = "410558"
628 elif "amc@NLO" in generatorInfo:
629 DSID = "410464"
630 else:
631 warnings.warn_explicit(
632 f"HF production fraction reweighting is not configured"
633 f" for this generator: {generatorInfo}."
634 f" New weights need to be calculated.",
635 GeneratorWeightWarning, filename='', lineno=0)
636 DSID = "000000"
637 else:
638 warnings.warn_explicit(
639 "Failed to determine generator from metadata",
640 GeneratorWeightWarning, filename='', lineno=0)
641 DSID = "000000"
642
643 log.info(f"Using HF production fraction weights calculated using DSID {DSID}")
644 if DSID == "000000":
645 warnings.warn_explicit(
646 "HF production fraction reweighting will return dummy"
647 " weights of 1.0",
648 GeneratorWeightWarning, filename='', lineno=0)
649
650 alg = config.createAlgorithm( 'CP::SysTruthWeightAlg', f'SysTruthWeightAlg_{streamName}' )
651 config.addPrivateTool( 'sysTruthWeightTool', 'PMGTools::PMGHFProductionFractionTool' )
652 alg.decoration = 'prodFracWeight_%SYS%'
653 alg.TruthParticleContainer = self.truthParticleContainer
654 alg.sysTruthWeightTool.ShowerGenerator = DSID
655 config.addOutputVar ('EventInfo', 'prodFracWeight_%SYS%', 'weight_HF_prod_frac')
656
657class PtEtaSelectionBlock (ConfigBlock):
658 """the ConfigBlock for a pt-eta selection"""
659
660 def __init__ (self) :
661 super().__init__ ()
662 self.addOption ('containerName', '', type=str,
663 noneAction='error',
664 info="the name of the input container.",
665 meta={'role':'containerRef'})
666 self.addOption ('selectionName', '', type=str,
667 noneAction='error',
668 info="the name of the selection to append this to. If left empty, "
669 "the cuts are applied to every "
670 "object within the container. Specifying a name (e.g. `loose`) "
671 "applies the cut only to those object who also pass that selection.",
672 meta={'role':'selection'})
673 self.addOption ('minPt', None, type=float,
674 info=r"minimum $p_\mathrm{T}$ value to cut on (in MeV).")
675 self.addOption ('maxPt', None, type=float,
676 info=r"maximum $p_\mathrm{T}$ value to cut on (in MeV).")
677 self.addOption ('minEta', None, type=float,
678 info=r"minimum $\vert\eta\vert$ value to cut on.")
679 self.addOption ('maxEta', None, type=float,
680 info=r"maximum $\vert\eta\vert$ value to cut on.")
681 self.addOption ('maxRapidity', None, type=float,
682 info="maximum rapidity value to cut on.")
683 self.addOption ('etaGapLow', None, type=float,
684 info=r"low end of the $\vert\eta\vert$ gap.")
685 self.addOption ('etaGapHigh', None, type=float,
686 info=r"high end of the $\vert\eta\vert$ gap.")
687 self.addOption ('selectionDecoration', None, type=str,
688 info="the name of the decoration to set. If `None`, will be set "
689 "to `selectPtEta` followed by the selection name.")
690 self.addOption ('useClusterEta', False, type=bool,
691 info=r"whether to use the cluster $\eta$ (`etaBE(2)`) instead of the object "
692 r"$\eta$ (for electrons and photons).")
693 self.addOption ('useDressedProperties', False, type=bool,
694 info="whether to use the dressed kinematic properties "
695 "(for truth particles only).")
696
697 def instanceName (self) :
698 """Return the instance name for this block"""
699 return self.containerName + "_" + self.selectionName
700
701 def makeAlgs (self, config) :
702
703 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'PtEtaSelectionAlg' )
704 config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
705 if self.minPt is not None :
706 alg.selectionTool.minPt = self.minPt
707 if self.maxPt is not None:
708 alg.selectionTool.maxPt = self.maxPt
709 if self.minEta is not None:
710 alg.selectionTool.minEta = self.minEta
711 if self.maxEta is not None :
712 alg.selectionTool.maxEta = self.maxEta
713 if self.maxRapidity is not None :
714 alg.selectionTool.maxRapidity = self.maxRapidity
715 if self.etaGapLow is not None:
716 alg.selectionTool.etaGapLow = self.etaGapLow
717 if self.etaGapHigh is not None:
718 alg.selectionTool.etaGapHigh = self.etaGapHigh
719 if self.selectionDecoration is None:
720 self.selectionDecoration = 'selectPtEta' + (f'_{self.selectionName}' if self.selectionName else '')
721 alg.selectionTool.useClusterEta = self.useClusterEta
722 alg.selectionTool.useDressedProperties = self.useDressedProperties
723 alg.selectionDecoration = self.selectionDecoration
724 alg.particles = config.readName (self.containerName)
725 alg.preselection = config.getPreselection (self.containerName, '')
726 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration)
727
728
729
730class ObjectCutFlowBlock (ConfigBlock):
731 """the ConfigBlock for an object cutflow"""
732
733 def __init__ (self) :
734 super().__init__ ()
735 self.addOption ('containerName', '', type=str,
736 noneAction='error',
737 info="the name of the input container.",
738 meta={'role':'containerRef'})
739 self.addOption ('selectionName', '', type=str,
740 noneAction='error',
741 info="the name of the selection to perform the cutflow for. If left empty, "
742 "the cutflow is "
743 "performed for every object within the container. Specifying a "
744 "name (e.g. `loose`) generates the cutflow only for those objects "
745 "that also pass that selection.",
746 meta={'role':'selection'})
747 self.addOption ('forceCutSequence', False, type=bool,
748 info="whether to force the cut sequence and not accept objects "
749 "if previous cuts failed.")
750 self.addOption ('streamName', None, type=str,
751 info="name of the output stream to save the cutflow histogram in.")
752
753 def instanceName (self) :
754 """Return the instance name for this block"""
755 return self.containerName + '_' + self.selectionName
756
757 def makeAlgs (self, config) :
758 streamName = self.streamName or config.defaultHistogramStream()
759
760 alg = config.createAlgorithm( 'CP::ObjectCutFlowHistAlg', 'CutFlowDumperAlg' )
761 alg.RootStreamName = streamName
762 alg.histPattern = 'cflow_' + self.containerName + "_" + self.selectionName + '_%SYS%'
763 alg.selections = config.getSelectionCutFlow (self.containerName, self.selectionName)
764 alg.input = config.readName (self.containerName)
765 alg.histTitle = "Object Cutflow: " + self.containerName + "." + self.selectionName
766 alg.forceCutSequence = self.forceCutSequence
767
768
769class EventCutFlowBlock (ConfigBlock):
770 """the ConfigBlock for an event-level cutflow"""
771
772 def __init__(self):
773 super().__init__()
774 self.addOption('selectionName', '', type=str,
775 noneAction='error',
776 info="the name of the event selection to generate cutflow histograms for. "
777 "If left blank, all selections on EventInfo will be used.",
778 meta={'role':'region'})
779 self.addOption('customSelections', [], type=None,
780 info="explicit list of selection decorations to use for the cutflow. "
781 "If provided, takes precedence over selectionName.")
782 self.addOption('cutFlowHistograms', True, type=bool,
783 info="whether to generate cutflow histograms for the selection cuts.")
784 self.addOption('cutFlowHistogramsWithSystematics', True, type=bool,
785 info="whether to generate cutflow histograms for the selection cuts "
786 "when running with systematics.")
787 self.addOption ('streamName', None, type=str,
788 info="name of the output stream to save the cut bookkeeper in.")
789
790 def instanceName(self):
791 return 'EventInfo_' + self.selectionName
792
793 def makeAlgs(self, config):
794 if not self.cutFlowHistograms:
795 return
796
797 if not config.noSystematics() and not self.cutFlowHistogramsWithSystematics:
798 return
799
800 # Setup stream name
801 streamName = self.streamName or config.defaultHistogramStream()
802
803 postfix = ('_' + self.selectionName) if self.selectionName else ''
804
805 alg = config.createAlgorithm('CP::EventCutFlowHistAlg', 'CutFlowDumperAlg')
806 alg.RootStreamName = streamName
807 alg.histPattern = 'cflow_EventInfo' + postfix + '_%SYS%'
808 alg.eventInfo = config.readName('EventInfo')
809 alg.histTitle = 'Event Cutflow: EventInfo.' + self.selectionName
810
811 if isinstance(self.customSelections, list) and len(self.customSelections) > 0:
812 # user provides a hardcoded list of selections
813 alg.selections = self.customSelections
814 elif self.selectionName:
815 # resolve selectionName to the list of cuts registered by EventSelectionConfig
816 alg.selections = config.getEventCutFlow(self.selectionName)
817 else:
818 # fallback: get all available selections from EventInfo
819 alg.selections = config.getSelectionCutFlow('EventInfo', '')
820
821 alg.selections = [sel + ',as_char' for sel in alg.selections]
822
823class OutputThinningBlock (ConfigBlock):
824 """the ConfigBlock for output thinning"""
825
826 def __init__ (self) :
827 super().__init__ ()
828 self.setBlockName('Thinning')
829 self.addOption ('containerName', '', type=str,
830 noneAction='error',
831 info="the name of the input container.",
832 meta={'role':'containerRef'})
833 self.addOption ('postfix', '', type=str,
834 info="a postfix to apply to decorations and algorithm names. "
835 "Typically not needed here.")
836 self.addOption ('selection', '', type=str,
837 info="the name of an optional selection decoration to use.",
838 meta={'role':'selection'})
839 self.addOption ('selectionName', '', type=str,
840 info="the name of the selection to append this to. If left empty, "
841 "the cuts are applied to every "
842 "object within the container. Specifying a name (e.g. `loose`) "
843 "applies the cut only to those object who also pass that selection.",
844 meta={'role':'selection'})
845 self.addOption ('outputName', None, type=str,
846 info="an optional name for the output container.",
847 meta={'role':'container'})
848 self.addOption ('deepCopy', False, type=bool,
849 info="run a deep copy of the container.")
850 self.addOption ('sortPt', False, type=bool,
851 info=r"whether to sort objects in $p_\mathrm{T}$.")
852 self.addOption ('noUniformSelection', False, type=bool,
853 info="do not run the union over all selections.")
854 self.addOption ('containerType', None, type=str,
855 info="the type of the container to thin. Only needed in AthenaMT, and only if subsequent code has a data dependency on the created container under that type.")
856
857 def instanceName (self) :
858 """Return the instance name for this block"""
859 return self.containerName + '_' + self.selectionName + self.postfix
860
861 def makeAlgs (self, config) :
862
863 postfix = self.postfix
864 if postfix != '' and postfix[0] != '_' :
865 postfix = '_' + postfix
866
867 selection = config.getFullSelection (self.containerName, self.selectionName)
868 if selection == '' :
869 selection = self.selection
870 elif self.selection != '' :
871 selection = selection + '&&' + self.selection
872
873 if selection != '' and not self.noUniformSelection :
874 alg = config.createAlgorithm( 'CP::AsgUnionSelectionAlg', 'UnionSelectionAlg')
875 alg.preselection = selection
876 alg.particles = config.readName (self.containerName)
877 alg.selectionDecoration = 'outputSelect' + postfix
878 config.addSelection (self.containerName, alg.selectionDecoration, selection)
879 selection = 'outputSelect' + postfix
880
881 alg = config.createAlgorithm( 'CP::AsgViewFromSelectionAlg', 'DeepCopyAlg' )
882 alg.input = config.readName (self.containerName)
883 if self.outputName is not None :
884 alg.output = self.outputName + '_%SYS%'
885 config.addOutputContainer (self.containerName, self.outputName)
886 else :
887 alg.output = config.copyName (self.containerName)
888 if self.containerType is not None :
889 alg.outputType = self.containerType
890 if selection != '' :
891 alg.selection = [selection]
892 else :
893 alg.selection = []
894 alg.deepCopy = self.deepCopy
895 if self.sortPt and not config.noSystematics() :
896 raise ValueError ("Sorting by pt is not supported with systematics")
897 alg.sortPt = self.sortPt
898
899
900class IFFLeptonDecorationBlock (ConfigBlock):
901 """the ConfigBlock for the IFF classification of leptons"""
902
903 def __init__ (self) :
904 super().__init__()
905 self.addOption ('containerName', '', type=str,
906 noneAction='error',
907 info="the name of the input electron or muon container.",
908 meta={'role':'containerRef'})
909 self.addOption ('separateChargeFlipElectrons', True, type=bool,
910 info="whether to consider charged-flip electrons as a separate class.")
911 self.addOption ('decoration', 'IFFClass_%SYS%', type=str,
912 info="the name of the decoration set by the IFF "
913 "`TruthClassificationTool`.")
914 # Always skip on data
915 self.setOptionValue('skipOnData', True)
916
917 def instanceName (self) :
918 """Return the instance name for this block"""
919 return self.containerName
920
921 def makeAlgs (self, config) :
922 particles = config.readName(self.containerName)
923
924 alg = config.createAlgorithm( 'CP::AsgClassificationDecorationAlg', 'IFFClassifierAlg' )
925 # the IFF classification tool
926 config.addPrivateTool( 'tool', 'TruthClassificationTool')
927 # label charge-flipped electrons as such
928 alg.tool.separateChargeFlipElectrons = self.separateChargeFlipElectrons
929 alg.decoration = self.decoration
930 alg.particles = particles
931
932 # write the decoration only once to the output
933 config.addOutputVar(self.containerName, alg.decoration, alg.decoration.split("_%SYS%")[0], noSys=True)
934
935
936class MCTCLeptonDecorationBlock (ConfigBlock):
937
938 def __init__ (self) :
939 super().__init__ ()
940
941 self.addOption ("containerName", '', type=str,
942 noneAction='error',
943 info="the input lepton container, with a possible selection, "
944 "in the format `container` or `container.selection`.",
945 meta={'role':'containerRef'})
946 self.addOption ("prefix", 'MCTC_', type=str,
947 info="the prefix of the decorations based on the MCTC "
948 "classification.")
949 # Always skip on data
950 self.setOptionValue('skipOnData', True)
951
952 def instanceName (self) :
953 """Return the instance name for this block"""
954 return self.containerName
955
956 def makeAlgs (self, config) :
957 particles, selection = config.readNameAndSelection(self.containerName)
958
959 alg = config.createAlgorithm ("CP::MCTCDecorationAlg", "MCTCDecorationAlg")
960 alg.particles = particles
961 alg.preselection = selection
962 alg.affectingSystematicsFilter = '.*'
963 config.addOutputVar (self.containerName, "MCTC_isPrompt", f"{self.prefix}isPrompt", noSys=True)
964 config.addOutputVar (self.containerName, "MCTC_fromHadron", f"{self.prefix}fromHadron", noSys=True)
965 config.addOutputVar (self.containerName, "MCTC_fromBSM", f"{self.prefix}fromBSM", noSys=True)
966 config.addOutputVar (self.containerName, "MCTC_fromTau", f"{self.prefix}fromTau", noSys=True)
967
968
969class PerEventSFBlock (ConfigBlock):
970 """the ConfigBlock for the AsgEventScaleFactorAlg"""
971
972 def __init__ (self):
973 super().__init__()
974 self.addOption('algoName', None, type=str,
975 info="unique name given to the underlying algorithm computing the "
976 "per-event scale factors.")
977 self.addOption('particles', '', type=str,
978 info="the input object container, with a possible selection, in the "
979 "format `container` or `container.selection`.",
980 meta={'role':'containerRef'})
981 self.addOption('objectSF', '', type=str,
982 info="the name of the per-object SF decoration to be used.")
983 self.addOption('eventSF', '', type=str,
984 info="the name of the per-event SF decoration.")
985
986 def instanceName (self) :
987 """Return the instance name for this block"""
988 return self.particles + '_' + self.objectSF + '_' + self.eventSF
989
990 def makeAlgs(self, config):
991 if config.dataType() is DataType.Data:
992 return
993 particles, selection = config.readNameAndSelection(self.particles)
994 alg = config.createAlgorithm('CP::AsgEventScaleFactorAlg', self.algoName if self.algoName else 'AsgEventScaleFactorAlg')
995 alg.particles = particles
996 alg.preselection = selection
997 alg.scaleFactorInputDecoration = self.objectSF
998 alg.scaleFactorOutputDecoration = self.eventSF
999
1000 config.addOutputVar('EventInfo', alg.scaleFactorOutputDecoration,
1001 alg.scaleFactorOutputDecoration.split("_%SYS%")[0])
1002
1003
1004class SelectionDecorationBlock (ConfigBlock):
1005 """the ConfigBlock to add selection decoration to a container"""
1006
1007 def __init__ (self) :
1008 super().__init__ ()
1009 # TODO: add info string
1010 self.addOption('containers', [], type=list,
1011 noneAction='error',
1012 info="")
1013
1014 def instanceName (self) :
1015 """Return the instance name for this block"""
1016 return ''
1017
1018 def makeAlgs(self, config):
1019 for container in self.containers:
1020 originContainerName = config.getOutputContainerOrigin(container)
1021 selectionNames = config.getSelectionNames(originContainerName)
1022 for selectionName in selectionNames:
1023 # skip default selection
1024 if selectionName == '':
1025 continue
1026 alg = config.createAlgorithm(
1027 'CP::AsgSelectionAlg',
1028 f'SelectionDecoration_{originContainerName}_{selectionName}')
1029 selectionDecoration = f'baselineSelection_{selectionName}_%SYS%'
1030 alg.selectionDecoration = f'{selectionDecoration},as_char'
1031 alg.particles = config.readName (originContainerName)
1032 alg.preselection = config.getFullSelection (originContainerName,
1033 selectionName)
1034 config.addOutputVar(
1035 originContainerName, selectionDecoration, selectionName)
STL class.
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:132