ATLAS Offline Software
Loading...
Searching...
No Matches
MGC.py
Go to the documentation of this file.
1# Copyright (C) 2002-2025 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 elif param_card_input is not None and not os.access(param_card_input, os.R_OK):
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 def read_paramCard(self):
732 if os.access(self.paramCard_loc, os.R_OK):
733 mglog.info('Copying default param card from '+str(self.paramCard_loc))
734 param_card = self.paramCard_loc
735 elif os.access(self.paramCard_default_loc, os.R_OK):
736 mglog.info('Copying default param card from '+str(self.paramCard_default_loc))
737 param_card = self.paramCard_default_loc
738 else:
739 raise RuntimeError('Cannot find defualt param_card.dat or param_card_default.dat! I was looking here: %s'%self.paramCard_loc)
740
741 with open(param_card, 'r') as f:
742 card = f.read()
743
744 param_blocks = card.split('\n\n')
745
747 for block in param_blocks:
748 name = None
749 nParams = 0
750 setting = {}
751 for line in block.split('\n'):
752 if line.lower().startswith('block'):
753 if name is not None and setting != {}:
754 self.paramCardDict[name] = setting
755 nParams = 0
756 name = line.split(' ',1)[1].strip()
757 setting ={}
758 nParams+=1
759 elif line.startswith('#'):
760 continue
761 elif line.lower().startswith('decay'):
762 continue #temp while I write function to get DECAY params
763 else:
764 l = line.strip()
765 data, separator, comment = l.partition('#')
766 columns = data.split()
767 if len(columns) < 2:
768 continue
769 key, value = ' '.join(columns[:-1]), columns[-1]
770 if separator:
771 value += ' # ' + comment.strip() + ' '
772
773 setting.update({key.strip() : value})
774
775 if name is not None and setting != {}:
776 self.paramCardDict[name] = setting
777 nParams = 0
778
779 self.read_decayParams(cardloc = param_card)
780 mglog.info("Successully read param_card.dat as a dictionary paramCardDict")
781
782
783 def read_decayParams(self, cardloc = None):
784 """ The DECAY parameters are written out differently in param_card.dat compared to the other parameter blocks
785 This funciton reads in the Decay parameters and adds them to the self.paramCardDict
786 """
787
788 if cardloc is None:
789 if os.access(self.paramCard_loc, os.R_OK):
790 mglog.info('Copying default param card from '+str(self.paramCard_loc))
791 cardloc = self.paramCard_loc
792 elif os.access(self.paramCard_default_loc, os.R_OK):
793 mglog.info('Copying default param card from '+str(self.paramCard_default_loc))
794 cardloc = self.paramCard_default_loc
795 else:
796 raise RuntimeError('You did not give a card location for reading in DECAY parameters and we cannot find defualt param_card.dat or param_card_default.dat! I was looking here: %s'%self.paramCard_loc)
797
798 with open(cardloc, 'r') as f:
799 card = f.read()
800 # Break the card up by lines
801 param_lines = card.split('\n')
802 decay_params = {}
803 setting = {}
804 key = None
805 value = None
806 for line in param_lines:
807 # Get rid of and leading or trailing spaces
808 l = line.strip()
809
810 # If the Line starts with Decay
811 if l.lower().startswith('decay'):
812 decay = l[:5].strip()
813 #check to see if there is already a key and value
814 if key is not None and value is not None: # In other words, if we have already recorded a decay parameter, we want to add that to the settings
815 setting.update({key:value})
816 # Reset the key and value
817 key = None
818 value = None
819 # Record the PDG ID for the particular decay
820 key = l[5:].strip().split(' ',1)[0]
821 # We keep the entire line as the value
822 value = l
823 # Sometimes the decay parameter spans several lines, we want to make sure we get all of it.
824 # If the line is not a a new Decay parameter, it is not an empty line and we do have a key + value saved:
825 elif not l.lower().startswith('decay') and not l == '\n' and key is not None and value is not None and not l.lower().startswith('block'):
826 # Add the current line to the value (making sure we include the new line)
827 value = value + '\n' + l
828 elif l.lower().startswith('block') and len(setting) != 0: # if we reach a new block after reading in the decays then we can just stop running
829 # add the last setting before adding to a decay_params dictionary
830 setting.update({key:value})
831
832 decay_params[decay] = setting
833 # add the decay parameters to the paramCardDict
834 self.paramCardDict.update(decay_params)
835
836 mglog.info("Successfully read in Decay parameters")
837 return
838 else:
839 continue
840 # if the decay block is the last block in the card, add the last setting before adding to a decay_params dictionary
841 setting.update({key:value})
842
843 decay_params[decay] = setting
844 # add the decay parameters to the paramCardDict
845 self.paramCardDict.update(decay_params)
846
847 mglog.info("Successfully read in Decay parameters")
848
849 def modify_paramCardDict(self,params={}):
850 """ Simple function to update the paramCardDictionary that uses nested dictionaries.
851 The input params should also be a set of nested dictionaries
852 """
853
854 dict_lower = [v.lower() for v in self.paramCardDict]
855 for block in params: #for each block in the params dictionary
856 if block.strip().lower() in dict_lower:# if the block is found
857 # need to make this case-insensitive
858 for value in self.paramCardDict:
859 if block.strip().lower() == value.strip().lower():
860 name = value
861
862 for key in params[block]: #look at each key in the block sub-dictionary
863 k = key.strip()
864 if k in self.paramCardDict[name]: # if the key is in the paramCardDict block then update it
865 self.paramCardDict[name][key] = params[block][key]
866 elif len(k.split(' ',1)) > 1:
867 new_k = k.split(' ',1)[0].strip() + ' ' + k.split(' ',1)[1].strip()
868 if new_k in self.paramCardDict[name]:
869 self.paramCardDict[name][new_k] = params[block][key]
870 else:
871 mglog.warning("Looks like the parameter "+str(block)+" : "+str(key)+" isn't in the parameter card dictionary. Adding now!")
872 self.paramCardDict[name][new_k] = params[block][key]
873 else:
874 # if we can't find it we may be trying to update the parameter based on the name, not the block number
875 for value in self.paramCardDict[name]:
876 found = False
877 if '# '+str(key)+' ' in str(self.paramCardDict[str(name)][str(value)]): # look at the values for each element in the sub-dictionary
878 self.paramCardDict[name][value] = params[block][key] # if we find the key in the value, we will update that value
879 found = True
880 continue
881 if not found: # if we can't find that value, we will add a new one
882 mglog.warning("Looks like the parameter "+str(block)+" : "+str(key)+" isn't in the parameter card dictionary. Adding now!")
883 self.paramCardDict[name][key] = params[block][key]
884
885 else:# if the block is not in the paramCardDict, we will add the whole block
886 self.paramCardDict[block] = params[block]
887
888
890 """Write out paramCardDict to disk.
891 The function will copy the layout and format from the default card.
892 """
893 if self.paramCard_default_loc is None or not os.path.isfile(self.paramCard_default_loc):
894 self.paramCard_default_loc = self.paramCard_loc +'.old_to_be_deleted'
895 os.rename(self.paramCard_loc, self.paramCard_default_loc)
896
897 with open(self.paramCard_default_loc,'r') as f:
898 oldCard = f.read()
899
900 newCard = open(self.paramCard_loc,'w')
901 dict_blocks = [v.lower() for v in self.paramCardDict]
902
903 oldCard_blocks = oldCard.split('\n\n')
904
905 for block in oldCard_blocks:
906 name = None
907 nParams = []
908
909 for line in block.split('\n'):
910 l = line.strip()
911 if l.startswith('#'):
912 newCard.write(f"{line} \n")
913 elif l == '':
914 newCard.write("\n")
915 elif l.lower().startswith('block'):
916 if name is not None and len(nParams) == len(self.paramCardDict[name]):
917 name = None
918 nParams = []
919 # If we are at a new block and we have not finished writing all the params from the dictionary
920 elif name is not None and len(nParams) != len(self.paramCardDict[name]):
921 # going through each entry in the param card dictionary
922 for key in self.paramCardDict[name]:
923 # if key is in nParams, it means we have already written it
924 if key in nParams:
925 continue
926 elif key not in nParams:
927 newCard.write(f" {key} {self.paramCardDict[name][key]}\n")
928 nParams.append(key)
929
930 name = l.split(' ',1)[1].strip()
931 nParams = []
932 if name.lower() not in dict_blocks:
933 raise RuntimeError("Cannot find %s in paramCardDict"%str(name))
934 elif name not in self.paramCardDict:
935 for b in self.paramCardDict:
936 if b.lower() == name.lower():
937 name = b
938 else:
939 continue
940
941 newCard.write(f"Block {name}\n")
942 elif l.lower().startswith('decay'):
943 # just to make sure we have written everthing down from the previous section
944 if name is not None and name.lower() != 'decay':
945 if len(nParams) == len(self.paramCardDict[name]):
946 continue
947 elif len(nParams) != len(self.paramCardDict[name]):
948 # going through each entry in the param card dictionary
949 for key in self.paramCardDict[name]:
950 # if key is in nParams, it means we have already written it
951 if key in nParams:
952 continue
953 elif key not in nParams:
954 newCard.write(f" {key} {self.paramCardDict[name][key]}\n")
955 nParams.append(key)
956
957 nParams = []
958
959 name = l[:5].strip()
960
961 command = l[5:].strip()
962 ID = command.split(' ',1)[0]
963 nParams.append(ID)
964
965 newCard.write(f"{self.paramCardDict[name][ID]} \n")
966
967 elif l == '\n':
968 newCard.write(l)
969
970 else:
971 if name.lower() == 'decay':
972 continue
973 else:
974 ID = ' '.join(l.partition('#')[0].split()[:-1])
975 newCard.write(f" {ID} {self.paramCardDict[name][ID]}\n")
976 nParams.append(ID)
977
978
979 # at end of block
980 if name is not None and len(nParams) == len(self.paramCardDict[name]):
981 name = None
982 nParams = []
983 # If we are at a new block and we have not finished writing all the params from the dictionary
984 elif name is not None and len(nParams) != len(self.paramCardDict[name]):
985 # going through each entry in the param card dictionary
986 for key in self.paramCardDict[name]:
987 # if key is in nParams, it means we have already written it
988 if key in nParams:
989 continue
990 elif key not in nParams and key is not None and key.strip() != '':
991 newCard.write(f" {key} {self.paramCardDict[name][key]}\n")
992 nParams.append(key)
993 elif key is None or key.strip() == '':
994 continue
995
996 mglog.info("Finished writing paramCardDict to param_card.dat")
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:849
read_paramCard(self)
Definition MGC.py:731
__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:783
write_paramCard(self)
Definition MGC.py:889
str paramCard_default_loc
Definition MGC.py:724
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