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