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