ATLAS Offline Software
Loading...
Searching...
No Matches
MGC.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3# Pythonized version of MadGraph steering executables
4# written by Zach Marshall <zach.marshall@cern.ch>
5# updates for aMC@NLO by Josh McFayden <mcfayden@cern.ch>
6# updates to LHE handling and SUSY functionality by Emma Kuwertz <ekuwertz@cern.ch>
7# Attempts to remove path-dependence of MadGraph
8# Class-based version of MadGraph Control
9# written by Kael Kemp <kael.kemp@cern.ch>
10
11import os,time,subprocess,glob
12from AthenaCommon import Logging
13from AthenaCommon.SystemOfUnits import GeV
14from MadGraphControl.MadGraphUtilsHelpers import error_check
15from MadGraphControl.MGClassParamHelpers import do_PMG_updates
16from MadGraphControl.MadGraphSystematicsUtils import convertSysCalcArguments,get_pdf_and_systematic_settings,parse_systematics_arguments,SYSTEMATICS_WEIGHT_INFO_ALTDYNSCALES,SYSTEMATICS_WEIGHT_INFO,write_systematics_arguments # noqa: F401
17
18
19mglog = Logging.logging.getLogger('MadGraphUtils')
20
21# Name of python executable
22python = 'python'
23# Magic name of gridpack directory
24MADGRAPH_GRIDPACK_LOCATION = 'madevent'
25# Name for the run (since we only have 1, just needs consistency)
26MADGRAPH_RUN_NAME = 'run_01'
27# For error handling
28MADGRAPH_CATCH_ERRORS = True
29# PDF setting (legacy module-level setting)
30MADGRAPH_PDFSETTING = None
31
32
36MADGRAPH_DEVICES = None
37
39
40 def __init__(self, process=None, plugin=None, keepJpegs=False, usePMGSettings=False, pdf_setting=None, devices=None, catch_errors=MADGRAPH_CATCH_ERRORS):
41
42 """ Generate a new process in madgraph.
43 Pass a process string.
44 Optionally request JPEGs to be kept and request for PMG settings to be used in the param card
45 Return the name of the process directory.
46 """
47 self.mglog = Logging.logging.getLogger('MadGraphUtils')
48 self.process = process
49 self.plugin = plugin
50 self.keepJpegs = keepJpegs
51 self.usePMGSettings = usePMGSettings
52 self.pdf_setting = MADGRAPH_PDFSETTING if pdf_setting is None else pdf_setting
53 self.devices = MADGRAPH_DEVICES if devices is None else devices
54 self.catch_errors = MADGRAPH_CATCH_ERRORS if catch_errors is None else catch_errors
56 self.beamEnergy = 0
57 #is_gen_from gridpack
58 self.is_gen_from_gridpack = os.access(MADGRAPH_GRIDPACK_LOCATION,os.R_OK)
59
60 # Make sure our paths are sorted
63
64
65 # Don't run if generating events from gridpack
67 self.process_dir = MADGRAPH_GRIDPACK_LOCATION
68 #Get Config card
71 #Get paramCard dictionary set up
72 self.paramCard = ParamCard(process_dir = self.process_dir)
73 #load up the run card dictionary
74 self.getRunCardDict()
75
76 return
77 else:
78 # Actually just sent the process card contents - let's make a card
79 card_loc = 'proc_card_mg5.dat'
80 mglog.info('Writing process card to '+card_loc)
81 a_card = open( card_loc , 'w' )
82 for l in process.split('\n'):
83 if 'output' not in l:
84 a_card.write(l+'\n')
85 else:
86 # Special handling for output line
87 outline = l.strip()
88 if '-nojpeg' not in l and not keepJpegs:
89 # We need to add -nojpeg somehow
90 if '#' in l:
91 outline = outline.split('#')[0]+' -nojpeg #'+outline.split('#')[1]
92 else:
93 outline = outline + ' -nojpeg'
94 # Special handling for devises
95 if self.devices is not None:
96 if self.devices.lower() in ['madevent_simd','madevent_gpu']:
97 outline = 'output '+self.devices.lower()+' '+outline.split('output')[1]
98 elif self.devices.lower() == 'max':
99 self.mglog.warning('Not fully implemented yet; setting avx')
100 outline = 'output madevent_simd '+outline.split('output')[1]
101 a_card.write(outline+'\n')
102 a_card.close()
103
104 madpath = os.environ['MADPATH']
105
106 # Check if we have a special output directory
107 process_dir = ''
108 for l in process.split('\n'):
109 # Look for an output line
110 if 'output' not in l.split('#')[0].split():
111 continue
112 # Check how many things before the options start
113 tmplist = l.split('#')[0].split(' -')[0]
114 # if two things, second is the directory
115 if len(tmplist.split())==2:
116 process_dir = tmplist.split()[1]
117 # if three things, third is the directory (second is the format)
118 elif len(tmplist.split())==3:
119 process_dir = tmplist.split()[2]
120 # See if we got a directory
121 if ''!=process_dir:
122 mglog.info('Saw that you asked for a special output directory: '+str(process_dir))
123 break
124
125 mglog.info('Started process generation at '+str(time.asctime()))
126
127 plugin_cmd = '--mode='+plugin if plugin is not None else ''
128
129
130 # Note special handling here to explicitly print the process
131 self.MADGRAPH_COMMAND_STACK += ['# All jobs should start in a clean directory']
132 self.MADGRAPH_COMMAND_STACK += ['mkdir standalone_test; cd standalone_test']
133 self.MADGRAPH_COMMAND_STACK += [' '.join([python,madpath+'/bin/mg5_aMC '+plugin_cmd+' << EOF\n'+process+'\nEOF\n'])]
134 generate = subprocess.Popen([python,madpath+'/bin/mg5_aMC',plugin_cmd,card_loc],stdin=subprocess.PIPE,stderr=subprocess.PIPE if MADGRAPH_CATCH_ERRORS else None)
135 (out,err) = generate.communicate()
136 error_check(err,generate.returncode)
137
138 mglog.info('Finished process generation at '+str(time.asctime()))
139
140 # at this point process_dir is for sure defined - it's equal to '' in the worst case
141 if process_dir == '': # no user-defined value, need to find the directory created by MadGraph5
142 for adir in sorted(glob.glob( os.getcwd()+'/*PROC*' ),reverse=True):
143 if os.access('%s/SubProcesses/subproc.mg'%adir,os.R_OK):
144 if process_dir=='':
145 process_dir = adir
146 else:
147 mglog.warning('Additional possible process directory, '+adir+' found. Had '+process_dir)
148 mglog.warning('Likely this is because you did not run from a clean directory, and this may cause errors later.')
149 else: # user-defined directory
150 if not os.access('%s/SubProcesses/subproc.mg'%process_dir,os.R_OK):
151 raise RuntimeError('No diagrams for this process in user-define dir='+str(process_dir))
152 if process_dir=='':
153 raise RuntimeError('No diagrams for this process from list: '+str(sorted(glob.glob(os.getcwd()+'/*PROC*'),reverse=True)))
154
155 self.process_dir = process_dir
156 self.get_config_cardloc()
158
159 #load up the run card dictionary
160 self.getRunCardDict()
161
162 # Initialise ParamCard class
163 self.paramCard = ParamCard(process_dir = self.process_dir)
164
165 # If requested, apply PMG default settings
166 if usePMGSettings:
167 do_PMG_updates(self.paramCard)
168
169 if not self.isNLO:
170 mglog.info('Setting default sde_strategy to old default (1)')
171 self.runCardDict['sde_strategy'] = 1
172
173 #tell MadGraph not to bother trying to create popup windows since this is running in a CLI, this will save ~50 seconds every time MadGraph is called.
174 self.configCardDict.update({'notification_center':'False'})
175
176 # Add some custom settings based on the device requests
177 if self.devices is not None:
178 if self.devices.lower()=='madevent_simd':
179 self.runCardDict['cudacpp_backend'] = 'cppauto'
180 elif self.devices.lower()=='madevent_gpu':
181 self.runCardDict['cudacpp_backend'] = 'cuda'
182 # In case we have "too new" a gcc version for the nvcc version on the node, which should be ok
183 # This patch should be temporary, but is fine while we are validating things at least
184 os.environ['ALLOW_UNSUPPORTED_COMPILER_IN_CUDA'] = 'Y'
185 elif self.devices.lower() == 'max':
186 self.mglog.warning('Not fully implemented yet; setting avx')
187 self.runCardDict['cudacpp_backend'] = 'cppauto'
188
189 # Make sure we store the resultant directory
190 self.MADGRAPH_COMMAND_STACK += ['export MGaMC_PROCESS_DIR='+os.path.basename(self.process_dir)]
191
192
193
195 # Addition for models directory
196
197 if 'PYTHONPATH' in os.environ:
198 if not any( [('Generators/madgraph/models' in x) for x in os.environ['PYTHONPATH'].split(':') ]):
199 os.environ['PYTHONPATH'] += ':/cvmfs/atlas.cern.ch/repo/sw/Generators/madgraph/models/latest'
200 self.MADGRAPH_COMMAND_STACK += ['export PYTHONPATH=${PYTHONPATH}:/cvmfs/atlas.cern.ch/repo/sw/Generators/madgraph/models/latest']
201 # Make sure that gfortran doesn't write to somewhere it shouldn't
202 if 'GFORTRAN_TMPDIR' in os.environ:
203 return
204 if 'TMPDIR' in os.environ:
205 os.environ['GFORTRAN_TMPDIR']=os.environ['TMPDIR']
206 self.MADGRAPH_COMMAND_STACK += ['export GFORTRAN_TMPDIR=${TMPDIR}']
207 return
208 if 'TMP' in os.environ:
209 os.environ['GFORTRAN_TMPDIR']=os.environ['TMP']
210 self.MADGRAPH_COMMAND_STACK += ['export GFORTRAN_TMPDIR=${TMP}']
211
212 def getRunCardDict(self,lowercase=False):
213 """Builds a dictionary from the run card.
214 This function takes in the card location and saves the contents as a dictionary object in the MGControl class.
215 """
216 run_card = self.process_dir + '/Cards/run_card.dat'
217
218 if os.access(run_card,os.R_OK):
219 mglog.info('Copying default run_card.dat from '+str(run_card))
220 else:
221 run_card = self.process_dir+'/Cards/run_card_default.dat'
222 if os.access(run_card,os.R_OK):
223 mglog.info('Copying default run_card.dat from '+str(run_card))
224 else:
225 raise RuntimeError('Cannot find default run_card.dat or run_card_default.dat! I was looking here: %s'%run_card)
226
227 card = open(run_card)
228 self.runCardDict = {} # Define the dictionary object
229 for line in iter(card):
230 if not line.strip().startswith('#'): # Ignores line commented out
231 command = line.split('!', 1)[0]
232 if '=' in command:
233 setting = command.split('=')[-1].strip() #saves the setting
234 value = '='.join(command.split('=')[:-1]).strip() #saves the value associated with the setting
235 if lowercase:
236 value = value.lower()
237 setting = setting.lower()
238 self.runCardDict[setting] = value #adds setting and value to the dictionary
239 card.close()
240
241
243 """Gets the config card location and determines if the process is LO or NLO
244 This function takes in the process diectory as an input and uses it to find the configuration.
245 Using the path to the config path, we can determine if the process will require a LO or NLO configuration.
246 """
247 self.isNLO = None
248 #Defining the possible config paths
249 lo_config_card = self.process_dir+'/Cards/me5_configuration.txt'
250 nlo_config_card = self.process_dir+'/Cards/amcatnlo_configuration.txt'
251
252 if os.access(lo_config_card,os.R_OK) and not os.access(nlo_config_card,os.R_OK): #Process is LO
253 self.config_path = lo_config_card
254 self.isNLO = False
255 elif os.access(nlo_config_card,os.R_OK) and not os.access(lo_config_card,os.R_OK): #Process is NLO
256 self.config_path = nlo_config_card
257 self.isNLO = True
258 elif os.access(nlo_config_card,os.R_OK) and os.access(lo_config_card,os.R_OK): #Process has two config cards
259 mglog.error('Found both types of config card in '+str(self.process_dir))
260 raise RuntimeError('Unable to locate configuration card')
261 else: # No config Card
262 mglog.error('No config card in '+str(self.process_dir))
263 raise RuntimeError('Unable to locate configuration card')
264
265
266 def getConfigFromPath(self, card_loc, lowercase=False):
267 """Builds a dictionary from the config card.
268 This function creates a dictionary object configCardDict from the config card.
269 Using the config card location, we copy over th settings to the dictionary.
270 Note: This function is works in the same way as self.getRunCardDict() however with small changes based on how the card is written.
271 """
272 card = open(card_loc)
273 #define the configCardDict object
275 for line in iter(card):
276 if not line.strip().startswith('#'): # Ignore lines that are commented out
277 command = line.split('!', 1)[0]
278 if '=' in command:
279 # Here is where we differ from self.getRunCardDict(), the config card has the setting to the left of the '=' and value to the right
280 value = command.split('=')[-1].strip()
281 setting = '='.join(command.split('=')[:-1]).strip()
282
283 if lowercase:
284 value = value.lower()
285 setting = setting.lower()
286 self.configCardDict[setting] = value # adds setting to the configCardDict
287 card.close()
288
289
290 def get_runArgs_info(self,runArgs):
291 """This function gets the beam energy and random seed from the runArguments
292 """
293
294 if runArgs is None:
295 raise RuntimeError('runArgs must be provided!')
296 #Get Beam Energy
297 if hasattr(runArgs,'ecmEnergy'):
298 self.beamEnergy = runArgs.ecmEnergy / 2.
299 else:
300 raise RuntimeError("No center of mass energy found in runArgs.")
301 #Get random seed
302 if hasattr(runArgs,'randomSeed'):
303 self.random_seed = runArgs.randomSeed
304 else:
305 raise RuntimeError("No random seed found in runArgs.")
306
307
308 def get_flags_info(self, flags):
309 """This function gets the beam energy and random seed from the configuration flags."""
310 if flags is None:
311 raise RuntimeError('flags must be provided!')
312
313 # Beam energy is stored in Athena units (MeV). Convert back to GeV for MadGraph run cards.
314 try:
315 self.beamEnergy = float(flags.Beam.Energy) / GeV
316 except AttributeError as e:
317 raise RuntimeError("No beam energy found in flags (expected flags.Beam.Energy).") from e
318
319 try:
320 self.random_seed = flags.Random.SeedOffset
321 except AttributeError as e:
322 raise RuntimeError("No random seed found in flags (expected flags.Random.SeedOffset).") from e
323
324
326 """Add seed and beam settings to runCardDict."""
327 # Overwrite the run-card default seed with transform seed
328 self.runCardDict['iseed'] = self.random_seed
329 if not self.isNLO:
330 self.runCardDict['python_seed'] = self.random_seed
331 if 'beamenergy' in self.runCardDict: #if the beam energy is defined in self.runCardDict
332 raise RuntimeError('Do not set beamenergy in the run card. Use flags (or runArgs during migration) instead.')
333
334 if 'ebeam1' not in self.runCardDict or self.beamEnergy != self.runCardDict['ebeam1']: # if there is no setting 'ebeam1' in self.runCardDict
335 self.runCardDict['ebeam1'] = self.beamEnergy
336 if 'ebeam2' not in self.runCardDict or self.beamEnergy != self.runCardDict['ebeam2']: #if there is no setting 'ebeam2' in self.runCardDict
337 self.runCardDict['ebeam2'] = self.beamEnergy
338
339
340 def add_runArgs(self, runArgs=None):
341 """This function adds run arguments to the self.runCardDict.
342 If the runArgs argument is left blank, the function will get the runArgs information before adding to the dictionary
343 """
344 if runArgs is not None:
345 self.get_runArgs_info(runArgs) # Use get_runArgs_info function to retrieve runArgs
346
348
349
350 def add_flags(self, flags=None):
351 """This function adds flag-derived seed and beam settings to self.runCardDict."""
352 if flags is not None:
353 self.get_flags_info(flags)
354
356
357
358 def write_runCard(self, runArgs=None, flags=None):
359 """Build a new run_card.dat from a run card dictionary.
360 This function can get a fresh run card from the runCardDict object.
361 Before writing the dictionary to the run card, we require to check a few things first
362 """
363
364 # Get seed and beam information from either runArgs or flags.
365 if flags is not None:
366 if runArgs is not None:
367 mglog.warning('Both runArgs and flags were provided to write_runCard. Using flags.')
368 self.add_flags(flags)
369 else:
370 self.add_runArgs(runArgs)
371
372 # Make sure that nevents is integer
373 if 'nevents' in self.runCardDict:
374 self.runCardDict['nevents'] = int(self.runCardDict['nevents'])
375
376 # Normalise custom_fcts early so the rewritten run_card uses the full path
377 if 'custom_fcts' in self.runCardDict and self.runCardDict['custom_fcts']:
378 raw_name = str(self.runCardDict['custom_fcts']).split()[0]
379 # Determine jobConfig directory
380 cfgdir = None
381 if flags is not None and hasattr(flags, 'Generator') and hasattr(flags.Generator, 'jobConfig') and flags.Generator.jobConfig:
382 cfgdir = flags.Generator.jobConfig[0] if isinstance(flags.Generator.jobConfig, (list, tuple)) else flags.Generator.jobConfig
383 elif runArgs is not None and hasattr(runArgs, 'jobConfig'):
384 cfgdir = runArgs.jobConfig[0] if isinstance(runArgs.jobConfig, (list, tuple)) else runArgs.jobConfig
385 elif flags is not None and 'JOBOPTSEARCHPATH' in os.environ:
386 cfgdir = os.environ['JOBOPTSEARCHPATH'].split(':')[0]
387
388 if cfgdir:
389 # Build full path and make absolute
390 full_path = os.path.join(cfgdir, raw_name)
391 self.runCardDict['custom_fcts'] = os.path.abspath(full_path)
392 mglog.info(f"Using custom function(s), specified in custom_fcts with path: {self.runCardDict['custom_fcts']}")
393 else:
394 # For internal tests, where jobConfig is not set
395 self.runCardDict['custom_fcts'] = os.path.abspath(raw_name)
396
397 # to avoid writing over the old run card, we rename the old card
398 runCard_old = self.process_dir+'/Cards/run_card.dat.old_to_be_deleted'
399 os.rename(self.process_dir+'/Cards/run_card.dat', runCard_old)
400
401 listSettings = []
402
403 # Read in old run card, we want to copy over the comments
404 # Then create a new run card in the same location as the old card
405 with open(runCard_old) as oldCard, open(self.process_dir+'/Cards/run_card.dat', 'w') as newCard:
406 for line in iter(oldCard):
407 #if the line starts with a '#' (ie. is a comment) copy it straight over
408 if line.strip().startswith('#'):
409 newCard.write(line)
410 else: #if not we want to grab the comment after the '!' as well as the associated command (before '!')
411 command= line.split('!',1)[0]
412 if len(line.split('!',1)) > 1:
413 comment= line.split('!',1)[1]
414 else:
415 comment = '\n'
416 if '=' in command:
417 setting = command.split('=')[-1].strip()
418 # Check if the setting is in the dictionary and then print with the comment and the updated value
419 if setting in self.runCardDict:
420 newCard.write( ' '+str(self.runCardDict[setting])+' = '+str(setting)+' ! '+ comment)
421 listSettings.append(str(setting))
422 else:
423 raise RuntimeError('Could not find '+str(setting)+' in the Run Card Dictionary!')
424 else:
425 newCard.write(line)
426 # Add a commented region
427 newCard.write("""#***********************************************************************
428# Any Additional settings can be added here *
429#***********************************************************************
430""")
431
432 #check that all settings have been writen
433 for setting in self.runCardDict:
434 if setting not in listSettings:
435 newCard.write( ' '+str(self.runCardDict[setting])+' = '+str(setting)+'\n')
436
437 # Check whether mcatnlo_delta is applied to setup pythia8 path
438 if 'mcatnlo_delta' in self.runCardDict:
439 if self.runCardDict['mcatnlo_delta'] == 'True':
440 self.configCardDict['pythia8_path'] = os.getenv("PY8PATH")
441 # TODO: this will require our writing out the config card again
442
443 # Tidy up after ourselves
444 mglog.info('Finished writing to run card.')
445 os.unlink(runCard_old) # delete old backup
446
448 """Build a new configuration from a config card dictionary.
449 This function can get a fresh runcard from the configCardDict object.
450 This function behaves similaraly to self.write_runCard()
451 """
452 mglog.info('Writing config card in '+self.process_dir)
453
454 #change name of old config card to avoid writing over
455 config_pathOLD = self.config_path+'.old_to_be_deleted'
456 os.rename(self.config_path, config_pathOLD) # change name of original card
457
458 # create new config card
459 newCard = open(self.config_path, 'w')
460 for setting in self.configCardDict:
461 if self.configCardDict[setting] is None: # ignore empty settings
462 continue
463 mglog.info('Writing option '+setting+' to the config card. Adding a setting to '+str(self.configCardDict[setting]))
464 newCard.write(' '+str(setting)+' = '+str(self.configCardDict[setting])+'\n') #writing config card in the format setting = value
465
466 # close file
467 newCard.close()
468
469 mglog.info('Finished writing to config card.')
470
471 os.unlink(config_pathOLD) # delete old file
472
473
474
476 """This function checks that the casing in the run card dictionary is the same as the default run card.
477 It checks if the default setting appears, with the correct casing, in the updated card
478 If it isn't in the run card, if then checks if the default setting (in lower case) appears in the lowered (updated) card
479 Assuming that any inconsistencies have just lowered the casing of the setting, the function then attempts to resolve the inconsistency
480 """
481 # Put the run card aside for the moment
482 temp_run_card = self.runCardDict
483 # Make a list with all lower case settings
484 lower_card = [key.lower() for key in self.runCardDict]
485
486 # Get the default run card to compare to
487 self.getRunCardDict()
488
489 #check for all the default settings in the default run card
490 for default_setting in self.runCardDict:
491 #if the default setting appears in the updated run card (with the same casing), we skip
492 if default_setting in temp_run_card:
493 continue
494 elif default_setting.lower() in lower_card: # If the default setting isn't in the updated run card but is in the lower case dictionary
495 mglog.warning(f"The casing in the run card seems to be wrong for {default_setting}. We will try fix this now.")
496
497 try: #want to try fixing this so we will assume that the settings has accidently been made lower-case
498 temp_run_card[default_setting] = temp_run_card[default_setting.lower()]
499 temp_run_card.pop(default_setting.lower())
500 except KeyError: #if that doesn't work we raise an error
501 self.runCardDict = temp_run_card #(just to make it easier to find the updated run card
502 raise RuntimeError("Run Card Dictionary casing is inconsistent")
503 else:
504 continue
505 # finally, lets put the run card back
506 self.runCardDict = temp_run_card
507 mglog.info('Run card casing looks good!')
508
510 """Checks the consistency of runCardDict.
511 This function should be called before writing runCardDict to disk to ensure that the run card is consistent and has appropriate settings.
512 """
514
515 # We should always use event_norm = average [AGENE-1725] otherwise Pythia cross sections are wrong
516 # Modification: average or bias is ok; sum is incorrect. Change the test to set sum to average
517 if self.runCardDict.get('event_norm',None) =='sum':
518 self.runCardDict['event_norm'] = 'average'
519 mglog.warning("setting event_norm to average, there is basically no use case where event_norm=sum is a good idea")
520
521 if not self.isNLO:
522 #Check CKKW-L setting
523 if 'ktdurham' in self.runCardDict and float(self.runCardDict['ktdurham']) > 0 and int(self.runCardDict['ickkw']) != 0:
524 log='Bad combination of settings for CKKW-L merging! ktdurham=%s and ickkw=%s.'%(self.runCardDict['ktdurham'],self.runCardDict['ickkw'])
525 mglog.error(log)
526 raise RuntimeError(log)
527
528 # Check if user is trying to use deprecated syscalc arguments with the other systematics script
529 if 'systematics_program' not in self.runCardDict or self.runCardDict['systematics_program']=='systematics': #if systematics are not set
530 syscalc_settings = ['sys_pdf', 'sys_scalefact', 'sys_alpsfact', 'sys_matchscale']
531 found_syscalc_setting = False
532 for s in syscalc_settings:
533 if s in self.runCardDict: #searches for the systematic setting in the runCard
534 mglog.warning('Using syscalc setting '+s+' with new systematics script. Systematics script is default from 2.6.2 and steered differently (https://cp3.irmp.ucl.ac.be/projects/madgraph/wiki/Systematics#Systematicspythonmodule)')
535 found_syscalc_setting = True
536 if found_syscalc_setting: #if a systematic setting was found
537 syst_arguments = convertSysCalcArguments(self.runCardDict)
538 mglog.info('Converted syscalc arguments to systematics arguments: '+syst_arguments)
539 syst_settings_update = {'systematics_arguments':syst_arguments} # save the system arguments to a dictionary
540 for s in syscalc_settings:
541 syst_settings_update[s] = None
542 self.runCardDict.update(syst_settings_update) #update the systematic settings
543
544 # Check pdf and systematics
545 mglog.info('Checking PDF and systematics settings')
546 if not self.base_fragment_setup_check(self.pdf_setting,self.runCardDict,self.isNLO): #if the base fragment has not been setup
547 # still need to set pdf and systematics
548 syst_settings = get_pdf_and_systematic_settings(self.pdf_setting,self.isNLO) # get the pdf and systemetatic settings as a dictionary
549 self.runCardDict.update(syst_settings) # update the settings in self.runCardDict
550
551 if 'systematics_arguments' in self.runCardDict:# if there are systematics set in the dictionary
552 systematics_arguments = parse_systematics_arguments(self.runCardDict['systematics_arguments'])
553 if 'weight_info' not in systematics_arguments: #if there is no event weighting information in the system arguments
554 mglog.info('Enforcing systematic weight name convention')
555 dyn = None
556 if '--dyn' in systematics_arguments or ' dyn' in systematics_arguments: #check if dynamics are set in the system arguments and sets dyn to that value.
557 if '--dyn' in systematics_arguments:
558 dyn = systematics_arguments.split('--dyn')[1]
559 if ' dyn' in systematics_arguments:
560 dyn = systematics_arguments.split(' dyn')[1]
561 dyn = dyn.replace('\'',' ').replace('=',' ').split()[0]
562 if dyn is not None and len(dyn.split(','))>1: #if there are dynamics defined, set event weights to acordingly
563 systematics_arguments['weight_info'] = SYSTEMATICS_WEIGHT_INFO_ALTDYNSCALES
564 else:
565 systematics_arguments['weight_info'] = SYSTEMATICS_WEIGHT_INFO
566 self.runCardDict['systematics_arguments'] = write_systematics_arguments(systematics_arguments)
567 # If the rocess is LO, we want to set a 'python_seed' in self.runCarDict
568 if not self.isNLO:
569 if 'python_seed' not in self.runCardDict:
570 mglog.warning('No python seed set in run_card -- adding one with same value as iseed')
571 self.runCardDict['python_seed'] = self.runCardDict['iseed'] # if there is no python_seed defined, set it to the same value as 'iseed'
572
573
574 # consistency check of 4/5 flavour shceme settings
575 FS_updates={}
576 proton_5flav = False
577 jet_5flav = False
578 with open(self.process_dir+'/Cards/proc_card_mg5.dat', 'r') as file: # This will be updated at a later point when we have added a proc_card_mg5.dat dictionary
579 content = file.readlines()
580 #we want to read int he proc_card to determine if it is a 4 or 5 flavour scheme
581 for rawline in content:
582 line = rawline.split('#')[0] #ignore commented lines
583 if line.startswith("define p"): # if we define the quarks in a proton
584 if ('b' in line.split() and 'b~' in line.split()) or ('5' in line.split() and '-5' in line.split()):
585 #if there a b and anti b-quarks defined with p we set proton 5flavour scheme to be true
586 proton_5flav = True
587 if 'j' in line.split() and jet_5flav: #if jet is defined in the proton, set proton 5 flavour to be true
588 proton_5flav = True
589 if line.startswith("define j"): # if we are defining jets
590 if ('b' in line.split() and 'b~' in line.split()) or ('5' in line.split() and '-5' in line.split()):
591 # if b and anti b-quarks are defined in jets, set jet 5 flavour scheme to be true.
592 jet_5flav = True
593 if 'p' in line.split() and proton_5flav: # if p is defined in jets and proton has been set to 5 flavour scheme then jet_5flav = True
594 jet_5flav = True
595 if proton_5flav or jet_5flav: #If either of the proton or jet have been set to the 5 flavour scheme, set asrwgtflavour dictionary entry to 5
596 FS_updates['asrwgtflavor'] = 5
597 # Before continuing, we must ensure that both proton and jet have the same colour scheme. If they are inconsistent, we assume 5 flavour scheme.
598 if not proton_5flav:
599 mglog.warning('Found 5-flavour jets but 4-flavour proton. This is inconsistent - please pick one.')
600 mglog.warning('Will proceed assuming 5-flavour scheme.')
601 if not jet_5flav:
602 mglog.warning('Found 5-flavour protons but 4-flavour jets. This is inconsistent - please pick one.')
603 mglog.warning('Will proceed assuming 5-flavour scheme.')
604 else: # otherwise set to 4 flavour scheme
605 FS_updates['asrwgtflavor'] = 4
606
607 if len(FS_updates)==0: #if we cannot determine the flavour scheme
608 mglog.warning(f'Could not identify 4- or 5-flavor scheme from process card {self.process_dir}/Cards/proc_card_mg5.dat')
609
610 #check if there is a setting in self.runCardDict for flavour scheme
611 if 'asrwgtflavor' in self.runCardDict or 'maxjetflavor' in self.runCardDict or 'pdgs_for_merging_cut' in self.runCardDict:
612 if FS_updates['asrwgtflavor'] == 5:
613 # Process card says we are in the five-flavor scheme
614 if ('asrwgtflavor' in self.runCardDict and int(self.runCardDict['asrwgtflavor']) != 5) or ('maxjetflavor' in self.runCardDict and int(self.runCardDict['maxjetflavor']) != 5) or ('pdgs_for_merging_cut' in self.runCardDict and '5' not in self.runCardDict['pdgs_for_merging_cut']):
615 # Inconsistent setting detected; warn the users and correct the settings
616 mglog.warning('b and b~ included in p and j for 5-flavor scheme but run card settings are inconsistent; adjusting run card')
617 run_card_updates = {'asrwgtflavor': 5, 'maxjetflavor': 5, 'pdgs_for_merging_cut': '1, 2, 3, 4, 5, 21'}
618 #If there is an inconsistency, update to be consistent with Process card
619 self.runCardDict.update( run_card_updates )
620 self.paramCard.modify_paramCardDict(params={'MASS': {'5': '0.000000e+00'}})
621 else:
622 mglog.debug('Consistent 5-flavor scheme setup detected.')
623
624 if FS_updates['asrwgtflavor'] == 4:
625 # Process card says we are in the four-flavor scheme
626 if ('asrwgtflavor' in self.runCardDict and int(self.runCardDict['asrwgtflavor']) != 4) or ('maxjetflavor' in self.runCardDict and int(self.runCardDict['maxjetflavor']) != 4) or ('pdgs_for_merging_cut' in self.runCardDict and '5' in self.runCardDict['pdgs_for_merging_cut']):
627 # Inconsistent setting detected; warn the users and correct the settings
628 mglog.warning('b and b~ not included in p and j (4-flavor scheme) but run card settings are inconsistent; adjusting run card')
629 run_card_updates = {'asrwgtflavor': 4, 'maxjetflavor': 4, 'pdgs_for_merging_cut': '1, 2, 3, 4, 21'}
630 #update cards to be consistent with Process Card
631 self.runCardDict.update( run_card_updates )
632 self.paramCard.modify_paramCardDict(params={'MASS': {'5': '4.700000e+00'}})
633 else:
634 mglog.debug('Consistent 4-flavor scheme setup detected.')
635 else:
636 # Flavor scheme setup is missing, adding by hand
637 if FS_updates['asrwgtflavor'] == 4:
638 # Warn the users and add the settings according to process card
639 mglog.warning('Flavor scheme setup is missing, adding by hand according to process card - b and b~ not included in p and j, 4-flavor scheme setup will be used; adjusting run card.')
640 if self.isNLO:
641 run_card_updates = {'maxjetflavor': 4}
642 else:
643 run_card_updates = {'asrwgtflavor': 4, 'maxjetflavor': 4, 'pdgs_for_merging_cut': '1, 2, 3, 4, 21'}
644
645 self.runCardDict.update( run_card_updates )
646 self.paramCard.modify_paramCardDict(params={'MASS': {'5': '4.700000e+00'}})
647 elif FS_updates['asrwgtflavor'] == 5:
648 mglog.warning('Flavor scheme setup is missing, adding by hand according to process card - b and b~ included in p and j, 5-flavor scheme setup will be used; adjusting run card.')
649 if self.isNLO:
650 run_card_updates = {'maxjetflavor': 5}
651 else:
652 run_card_updates = {'asrwgtflavor': 5, 'maxjetflavor': 5, 'pdgs_for_merging_cut': '1, 2, 3, 4, 5, 21'}
653
654
655 self.runCardDict.update( run_card_updates )
656 self.paramCard.modify_paramCardDict(params={'MASS': {'5': '0.000000e+00'}})
657
658 # Check scale consistency
659 if '91.188' not in self.runCardDict.get('scale','91.188') and self.runCardDict.get('fixed_ren_scale','f').lower() in ['f','false']:
660 mglog.error('Seems you set "scale" in the run card without setting "fixed_ren_scale" to True. Not sure what to do here, throwing an error.')
661 raise ValueError("Renormalization scale setting incorrect")
662 if ('91.188' not in self.runCardDict.get('dsqrt_q2fact1','91.188') or '91.188' not in self.runCardDict.get('dsqrt_q2fact2','91.188')) \
663 and self.runCardDict.get('fixed_fac_scale','f').lower() in ['f','false']:
664 mglog.error('Seems you set "dsqrt_q2fact1" or "dsqrt_q2fact2" in the run card without setting "fixed_fac_scale" to True. Not sure what to do here, throwing an error.')
665 raise ValueError("Factorization scale setting incorrect")
666
667 mglog.info('Finished checking run card - All OK!')
668
669
670 #==================================================================================
671 # check whether a configuration is in agreement with base fragment
672 # true if nothing needs to be done
673 # false if still needs setup
674 # error if inconsistent config
675 def base_fragment_setup_check(self,the_base_fragment,extras,isNLO):
676 # no include: allow it (with warning), as long as lhapdf is used
677 # if not (e.g. because no choice was made and the internal pdf ise used): error
678 if the_base_fragment is None:
679 mglog.warning('!!! No pdf base fragment was included in your job options. PDFs should be set with an include file. You might be unable to follow the PDF4LHC uncertainty prescription. Let\'s hope you know what you doing !!!')
680 if not extras.get('pdlabel', None) == 'lhapdf' or 'lhaid' not in extras:
681 mglog.error('!!! No pdf base fragment was included in your job options and you did not specify a LHAPDF yourself')
682 raise RuntimeError('No pdf base fragment was included in your job options and you did not specify a LHAPDF yourself')
683 return True
684 else:
685 # if setting is already exactly as it should be -- great!
686 correct_settings=get_pdf_and_systematic_settings(the_base_fragment,isNLO)
687
688 allgood=True
689 for s in correct_settings:
690 if s is None and s in extras:
691 allgood=False
692 break
693 if s not in extras or extras[s]!=correct_settings[s]:
694 allgood=False
695 break
696 if allgood:
697 return True
698 # no error but also nothing set
699 return False
700
701
702 def getCA(self, flags=None):
703 """Boilerplate code that returns a bare CA fragment.
704 To be used in MadGraphConfig.py"""
705 from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
706 from GeneratorConfig.Sequences import EvgenSequence, EvgenSequenceFactory
707
708 ca = ComponentAccumulator(EvgenSequenceFactory(EvgenSequence.Generator))
709
710 return ca
711
712
713
715 def __init__(self, param_card_input=None, param_card_backup=None, process_dir=MADGRAPH_GRIDPACK_LOCATION,output_location=None):
716
717 if param_card_input is None:
718 self.paramCard_loc = process_dir+'/Cards/param_card.dat'
719 else:
720 self.paramCard_loc = param_card_input
721
722
723
724 self.paramCard_default_loc = param_card_backup
725 self.output_location = output_location
726 self.process_dir = process_dir
727
728 #read in the paramCard and store as a dictionary
729 self.read_paramCard()
730
731 @staticmethod
732 def _block_name(line):
733 # Comments and scale qualifiers are not part of the block name.
734 # QNUMBERS blocks must remain distinct for each particle.
735 fields = line.partition('#')[0].split()
736 if fields[1].lower() == 'qnumbers':
737 return ' '.join(fields[1:3])
738 return fields[1]
739
740 def read_paramCard(self):
741 if os.access(self.paramCard_loc, os.R_OK):
742 param_card = self.paramCard_loc
743 elif self.paramCard_default_loc is not None and os.access(self.paramCard_default_loc, os.R_OK):
744 param_card = self.paramCard_default_loc
745 else:
746 raise RuntimeError('Cannot find param_card.dat or param_card_default.dat! I was looking here: %s' % self.paramCard_loc)
747
748 mglog.info('Copying default param card from ' + str(param_card))
750 name = None
751 with open(param_card) as card:
752 for line in card:
753 data, separator, comment = line.strip().partition('#')
754 columns = data.split()
755 if not columns:
756 continue
757 if columns[0].lower() == 'block':
758 name = self._block_name(line)
759 self.paramCardDict[name] = {}
760 elif columns[0].lower() == 'decay':
761 # Branching ratios belong to the decay, never to the
762 # preceding ordinary block (often YUKAWA).
763 name = None
764 elif name is not None and len(columns) >= 2:
765 key, value = ' '.join(columns[:-1]), columns[-1]
766 if separator:
767 value += ' # ' + comment.strip() + ' '
768 self.paramCardDict[name][key] = value
769
770 self.read_decayParams(cardloc=param_card)
771 mglog.info('Successfully read param_card.dat as a dictionary paramCardDict')
772
773 def read_decayParams(self, cardloc=None):
774 """Read each width together with its branching-ratio table."""
775 if cardloc is None:
776 if os.access(self.paramCard_loc, os.R_OK):
777 cardloc = self.paramCard_loc
778 elif self.paramCard_default_loc is not None and os.access(self.paramCard_default_loc, os.R_OK):
779 cardloc = self.paramCard_default_loc
780 else:
781 raise RuntimeError('Cannot find parameter card for reading DECAY parameters: %s' % self.paramCard_loc)
782
783 decays = {}
784 key = None
785 with open(cardloc) as card:
786 for line in card:
787 stripped = line.strip()
788 columns = stripped.partition('#')[0].split()
789 if not columns:
790 continue
791 if columns[0].lower() == 'decay':
792 key = columns[1]
793 decays[key] = stripped
794 elif columns[0].lower() == 'block':
795 key = None
796 elif key is not None:
797 decays[key] += '\n' + stripped
798
799 if decays:
800 self.paramCardDict['DECAY'] = decays
801 mglog.info('Successfully read in Decay parameters')
802
803 def modify_paramCardDict(self,params={}):
804 """ Simple function to update the paramCardDictionary that uses nested dictionaries.
805 The input params should also be a set of nested dictionaries
806 """
807
808 dict_lower = [v.lower() for v in self.paramCardDict]
809 for block in params: #for each block in the params dictionary
810 if block.strip().lower() in dict_lower:# if the block is found
811 # need to make this case-insensitive
812 for value in self.paramCardDict:
813 if block.strip().lower() == value.strip().lower():
814 name = value
815
816 for key in params[block]: #look at each key in the block sub-dictionary
817 k = key.strip()
818 if k in self.paramCardDict[name]: # if the key is in the paramCardDict block then update it
819 self.paramCardDict[name][key] = params[block][key]
820 elif len(k.split(' ',1)) > 1:
821 new_k = k.split(' ',1)[0].strip() + ' ' + k.split(' ',1)[1].strip()
822 if new_k in self.paramCardDict[name]:
823 self.paramCardDict[name][new_k] = params[block][key]
824 else:
825 mglog.warning("Looks like the parameter "+str(block)+" : "+str(key)+" isn't in the parameter card dictionary. Adding now!")
826 self.paramCardDict[name][new_k] = params[block][key]
827 else:
828 # if we can't find it we may be trying to update the parameter based on the name, not the block number
829 for value in self.paramCardDict[name]:
830 found = False
831 if '# '+str(key)+' ' in str(self.paramCardDict[str(name)][str(value)]): # look at the values for each element in the sub-dictionary
832 self.paramCardDict[name][value] = params[block][key] # if we find the key in the value, we will update that value
833 found = True
834 continue
835 if not found: # if we can't find that value, we will add a new one
836 mglog.warning("Looks like the parameter "+str(block)+" : "+str(key)+" isn't in the parameter card dictionary. Adding now!")
837 self.paramCardDict[name][key] = params[block][key]
838
839 else:# if the block is not in the paramCardDict, we will add the whole block
840 self.paramCardDict[block] = params[block]
841
842
843 def write_paramCard(self, output_location=None):
844 """Write the updated card, preserving section headers and comments.
845
846 Only BLOCK and DECAY headers delimit sections; empty lines do not.
847 Decay dictionary values already contain their header and branching
848 ratios, so they must not be written as ordinary key/value entries.
849 """
850 if self.paramCard_default_loc is None or not os.path.isfile(self.paramCard_default_loc):
851 self.paramCard_default_loc = self.paramCard_loc + '.old_to_be_deleted'
852 os.rename(self.paramCard_loc, self.paramCard_default_loc)
853
854 with open(self.paramCard_default_loc) as card:
855 lines = card.readlines()
856
857 block_names = {name.lower(): name for name in self.paramCardDict}
858 written = {name: set() for name in self.paramCardDict}
859 output = self.paramCard_loc if output_location is None else output_location
860 with open(output, 'w') as newCard:
861 def write_entry(name, key):
862 value = self.paramCardDict[name][key]
863 if name.lower() == 'decay':
864 newCard.write(str(value).rstrip() + '\n')
865 else:
866 newCard.write(f' {key} {value}\n')
867 written[name].add(key)
868
869 def finish_block(name):
870 if name is not None and name.lower() != 'decay':
871 for key in self.paramCardDict[name]:
872 if key not in written[name]:
873 write_entry(name, key)
874
875 name = None
876 for line in lines:
877 columns = line.partition('#')[0].split()
878 if not columns:
879 newCard.write(line.rstrip('\r\n') + '\n')
880 elif columns[0].lower() == 'block':
881 finish_block(name)
882 block = self._block_name(line)
883 if block.lower() not in block_names:
884 raise RuntimeError('Cannot find %s in paramCardDict' % block)
885 name = block_names[block.lower()]
886 newCard.write(line.rstrip() + '\n')
887 elif columns[0].lower() == 'decay':
888 finish_block(name)
889 name = block_names['decay']
890 key = columns[1]
891 if key not in written[name]:
892 write_entry(name, key)
893 elif name is None:
894 newCard.write(line.rstrip('\r\n') + '\n')
895 elif name.lower() != 'decay':
896 key = ' '.join(columns[:-1])
897 write_entry(name, key)
898 # Original branching-ratio rows have already been replaced
899 # by the complete DECAY entry above.
900 finish_block(name)
901
902 # Add newly requested blocks and decays only once, after processing
903 # all existing sections (decays need not be contiguous in SLHA).
904 for name, settings in self.paramCardDict.items():
905 missing = [key for key in settings if key not in written[name]]
906 if not missing:
907 continue
908 newCard.write('\n')
909 if name.lower() != 'decay':
910 newCard.write(f'Block {name}\n')
911 for key in missing:
912 write_entry(name, key)
913
914 mglog.info('Finished writing paramCardDict to ' + str(output))
write_configCard(self)
Definition MGC.py:447
compare_runCardCasing(self)
Definition MGC.py:475
get_config_cardloc(self)
Definition MGC.py:242
list run_card_params
Definition MGC.py:55
getCA(self, flags=None)
Definition MGC.py:702
add_flags(self, flags=None)
Definition MGC.py:350
get_runArgs_info(self, runArgs)
Definition MGC.py:290
__init__(self, process=None, plugin=None, keepJpegs=False, usePMGSettings=False, pdf_setting=None, devices=None, catch_errors=MADGRAPH_CATCH_ERRORS)
Definition MGC.py:40
_add_seed_and_beam_settings(self)
Definition MGC.py:325
write_runCard(self, runArgs=None, flags=None)
Definition MGC.py:358
setup_path_protection(self)
Definition MGC.py:194
get_flags_info(self, flags)
Definition MGC.py:308
base_fragment_setup_check(self, the_base_fragment, extras, isNLO)
Definition MGC.py:675
list MADGRAPH_COMMAND_STACK
Definition MGC.py:61
add_runArgs(self, runArgs=None)
Definition MGC.py:340
dict configCardDict
Definition MGC.py:274
run_card_consistency_check(self)
Definition MGC.py:509
getConfigFromPath(self, card_loc, lowercase=False)
Definition MGC.py:266
getRunCardDict(self, lowercase=False)
Definition MGC.py:212
modify_paramCardDict(self, params={})
Definition MGC.py:803
read_paramCard(self)
Definition MGC.py:740
write_paramCard(self, output_location=None)
Definition MGC.py:843
__init__(self, param_card_input=None, param_card_backup=None, process_dir=MADGRAPH_GRIDPACK_LOCATION, output_location=None)
Definition MGC.py:715
read_decayParams(self, cardloc=None)
Definition MGC.py:773
_block_name(line)
Definition MGC.py:732
str paramCard_default_loc
Definition MGC.py:724
STL class.
bool add(const std::string &hname, TKey *tobj)
Definition fastadd.cxx:55
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:312
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:132
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:179