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