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