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