Package madgraph :: Package loop :: Module loop_diagram_generation
[hide private]
[frames] | no frames]

Source Code for Module madgraph.loop.loop_diagram_generation

   1  # 
   2  ################################################################################ 
   3  # Copyright (c) 2009 The MadGraph5_aMC@NLO Development team and Contributors 
   4  # 
   5  # This file is a part of the MadGraph5_aMC@NLO project, an application which  
   6  # automatically generates Feynman diagrams and matrix elements for arbitrary 
   7  # high-energy processes in the Standard Model and beyond. 
   8  # 
   9  # It is subject to the MadGraph5_aMC@NLO license which should accompany this  
  10  # distribution. 
  11  # 
  12  # For more information, visit madgraph.phys.ucl.ac.be and amcatnlo.web.cern.ch 
  13  # 
  14  ################################################################################ 
  15  """Classes for diagram generation with loop features. 
  16  """ 
  17   
  18  import array 
  19  import copy 
  20  import itertools 
  21  import logging 
  22   
  23  import madgraph.loop.loop_base_objects as loop_base_objects 
  24  import madgraph.core.base_objects as base_objects 
  25  import madgraph.core.diagram_generation as diagram_generation 
  26   
  27  from madgraph import MadGraph5Error 
  28  from madgraph import InvalidCmd 
  29  logger = logging.getLogger('madgraph.loop_diagram_generation') 
30 31 -def ldg_debug_info(msg,val):
32 # This subroutine has typically quite large DEBUG info. 33 # So even in debug mode, they are turned off by default. 34 # Remove the line below for loop diagram generation diagnostic 35 return 36 flag = "LoopGenInfo: " 37 if len(msg)>40: 38 logger.debug(flag+msg[:35]+" [...] = %s"%str(val)) 39 else: 40 logger.debug(flag+msg+''.join([' ']*(40-len(msg)))+' = %s'%str(val))
41
42 #=============================================================================== 43 # LoopAmplitude 44 #=============================================================================== 45 -class LoopAmplitude(diagram_generation.Amplitude):
46 """NLOAmplitude: process + list of diagrams (ordered) 47 Initialize with a process, then call generate_diagrams() to 48 generate the diagrams for the amplitude 49 """ 50
51 - def default_setup(self):
52 """Default values for all properties""" 53 54 # The 'diagrams' entry from the mother class is inherited but will not 55 # be used in NLOAmplitude, because it is split into the four following 56 # different categories of diagrams. 57 super(LoopAmplitude, self).default_setup() 58 self['born_diagrams'] = None 59 self['loop_diagrams'] = None 60 self['loop_UVCT_diagrams'] = base_objects.DiagramList() 61 # This is in principle equal to self['born_diagram']==[] but it can be 62 # that for some reason the born diagram can be generated but do not 63 # contribute. 64 # This will decide wether the virtual is squared against the born or 65 # itself. 66 self['has_born'] = True 67 # This where the structures obtained for this amplitudes are stored 68 self['structure_repository'] = loop_base_objects.FDStructureList() 69 70 # A list that registers what Lcut particle have already been 71 # employed in order to forbid them as loop particles in the 72 # subsequent diagram generation runs. 73 self.lcutpartemployed=[]
74
75 - def __init__(self, argument=None):
76 """Allow initialization with Process""" 77 78 if isinstance(argument, base_objects.Process): 79 super(LoopAmplitude, self).__init__() 80 self.set('process', argument) 81 self.generate_diagrams() 82 elif argument != None: 83 # call the mother routine 84 super(LoopAmplitude, self).__init__(argument) 85 else: 86 # call the mother routine 87 super(LoopAmplitude, self).__init__()
88
89 - def get_sorted_keys(self):
90 """Return diagram property names as a nicely sorted list.""" 91 92 return ['process', 'diagrams', 'has_mirror_process', 'born_diagrams', 93 'loop_diagrams','has_born', 94 'structure_repository']
95
96 - def filter(self, name, value):
97 """Filter for valid amplitude property values.""" 98 99 if name == 'diagrams': 100 if not isinstance(value, base_objects.DiagramList): 101 raise self.PhysicsObjectError, \ 102 "%s is not a valid DiagramList" % str(value) 103 for diag in value: 104 if not isinstance(diag,loop_base_objects.LoopDiagram) and \ 105 not isinstance(diag,loop_base_objects.LoopUVCTDiagram): 106 raise self.PhysicsObjectError, \ 107 "%s contains a diagram which is not an NLODiagrams." % str(value) 108 if name == 'born_diagrams': 109 if not isinstance(value, base_objects.DiagramList): 110 raise self.PhysicsObjectError, \ 111 "%s is not a valid DiagramList" % str(value) 112 for diag in value: 113 if not isinstance(diag,loop_base_objects.LoopDiagram): 114 raise self.PhysicsObjectError, \ 115 "%s contains a diagram which is not an NLODiagrams." % str(value) 116 if name == 'loop_diagrams': 117 if not isinstance(value, base_objects.DiagramList): 118 raise self.PhysicsObjectError, \ 119 "%s is not a valid DiagramList" % str(value) 120 for diag in value: 121 if not isinstance(diag,loop_base_objects.LoopDiagram): 122 raise self.PhysicsObjectError, \ 123 "%s contains a diagram which is not an NLODiagrams." % str(value) 124 if name == 'has_born': 125 if not isinstance(value, bool): 126 raise self.PhysicsObjectError, \ 127 "%s is not a valid bool" % str(value) 128 if name == 'structure_repository': 129 if not isinstance(value, loop_base_objects.FDStructureList): 130 raise self.PhysicsObjectError, \ 131 "%s is not a valid bool" % str(value) 132 133 else: 134 super(LoopAmplitude, self).filter(name, value) 135 136 return True
137
138 - def set(self, name, value):
139 """Redefine set for the particular case of diagrams""" 140 141 if name == 'diagrams': 142 if self.filter(name, value): 143 self['born_diagrams']=base_objects.DiagramList([diag for diag in value if \ 144 not isinstance(diag,loop_base_objects.LoopUVCTDiagram) and diag['type']==0]) 145 self['loop_diagrams']=base_objects.DiagramList([diag for diag in value if \ 146 not isinstance(diag,loop_base_objects.LoopUVCTDiagram) and diag['type']!=0]) 147 self['loop_UVCT_diagrams']=base_objects.DiagramList([diag for diag in value if \ 148 isinstance(diag,loop_base_objects.LoopUVCTDiagram)]) 149 150 else: 151 return super(LoopAmplitude, self).set(name, value) 152 153 return True
154
155 - def get(self, name):
156 """Redefine get for the particular case of '*_diagrams' property""" 157 158 if name == 'diagrams': 159 if self['process'] and self['loop_diagrams'] == None: 160 self.generate_diagrams() 161 return base_objects.DiagramList(self['born_diagrams']+\ 162 self['loop_diagrams']+\ 163 self['loop_UVCT_diagrams']) 164 165 if name == 'born_diagrams': 166 if self['born_diagrams'] == None: 167 # Have not yet generated born diagrams for this process 168 if self['process']['has_born']: 169 if self['process']: 170 self.generate_born_diagrams() 171 else: 172 self['born_diagrams']=base_objects.DiagramList() 173 174 return LoopAmplitude.__bases__[0].get(self, name) #return the mother routine
175 176 # Functions of the different tasks performed in generate_diagram
177 - def choose_order_config(self):
178 """ Choose the configuration of non-perturbed coupling orders to be 179 retained for all diagrams. This is used when the user did not specify 180 any order. """ 181 chosen_order_config = {} 182 min_wgt = self['born_diagrams'].get_min_order('WEIGHTED') 183 # Scan the born diagrams of minimum weight to chose a configuration 184 # of non-perturbed orders. 185 min_non_pert_order_wgt = -1 186 for diag in [d for d in self['born_diagrams'] if \ 187 d.get_order('WEIGHTED')==min_wgt]: 188 non_pert_order_wgt = min_wgt - sum([diag.get_order(order)*\ 189 self['process']['model']['order_hierarchy'][order] for order in \ 190 self['process']['perturbation_couplings']]) 191 if min_non_pert_order_wgt == -1 or \ 192 non_pert_order_wgt<min_non_pert_order_wgt: 193 chosen_order_config = self.get_non_pert_order_config(diag) 194 logger.info("Chosen coupling orders configuration: (%s)"\ 195 %self.print_config(chosen_order_config)) 196 return chosen_order_config
197
199 """If squared orders (other than WEIGHTED) are defined, then they can be 200 used for determining what is the expected upper bound for the order 201 restricting loop diagram generation.""" 202 for order, value in self['process']['squared_orders'].items(): 203 if order.upper()!='WEIGHTED' and order not in self['process']['orders']: 204 # If the bound is of type '>' we cannot say anything 205 if self['process'].get('sqorders_types')[order]=='>': 206 continue 207 # If there is no born, the min order will simply be 0 as it should. 208 bornminorder=self['born_diagrams'].get_min_order(order) 209 if value>=0: 210 self['process']['orders'][order]=value-bornminorder 211 elif self['process']['has_born']: 212 # This means the user want the leading if order=-1 or N^n 213 # Leading term if order=-n. If there is a born diag, we can 214 # infer the necessary maximum order in the loop: 215 # bornminorder+2*(n-1). 216 # If there is no born diag, then we cannot say anything. 217 self['process']['orders'][order]=bornminorder+2*(-value-1)
218
219 - def guess_loop_orders(self, user_orders):
220 """Guess the upper bound for the orders for loop diagram generation 221 based on either no squared orders or simply 'Weighted'""" 222 223 hierarchy = self['process']['model']['order_hierarchy'] 224 225 # Maximum of the hierarchy weigtht among all perturbed order 226 max_pert_wgt = max([hierarchy[order] for order in \ 227 self['process']['perturbation_couplings']]) 228 229 # In order to be sure to catch the corrections to all born diagrams that 230 # the user explicitly asked for with the amplitude orders, we take here 231 # the minimum weighted order as being the maximum between the min weighted 232 # order detected in the Born diagrams and the weight computed from the 233 # user input amplitude orders. 234 user_min_wgt = 0 235 236 # One can chose between the two behaviors below. It is debatable which 237 # one is best. The first one tries to only consider the loop which are 238 # dominant, even when the user selects the amplitude orders and the 239 # second chosen here makes sure that the user gets a correction of the 240 # desired type for all the born diagrams generated with its amplitude 241 # order specification. 242 # min_born_wgt=self['born_diagrams'].get_min_order('WEIGHTED') 243 min_born_wgt=max(self['born_diagrams'].get_min_order('WEIGHTED'), 244 sum([hierarchy[order]*val for order, val in user_orders.items() \ 245 if order!='WEIGHTED'])) 246 247 if 'WEIGHTED' not in [key.upper() for key in \ 248 self['process']['squared_orders'].keys()]: 249 # Then we guess it from the born 250 self['process']['squared_orders']['WEIGHTED']= 2*(min_born_wgt+\ 251 max_pert_wgt) 252 253 # Now we know that the remaining weighted orders which can fit in 254 # the loop diagram is (self['target_weighted_order']- 255 # min_born_weighted_order) so for each perturbed order we just have to 256 # take that number divided by its hierarchy weight to have the maximum 257 # allowed order for the loop diagram generation. Of course, 258 # we don't overwrite any order already defined by the user. 259 if self['process']['squared_orders']['WEIGHTED']>=0: 260 trgt_wgt=self['process']['squared_orders']['WEIGHTED']-min_born_wgt 261 else: 262 trgt_wgt=min_born_wgt+(-self['process']['squared_orders']['WEIGHTED']+1)*2 263 # We also need the minimum number of vertices in the born. 264 min_nvert=min([len([1 for vert in diag['vertices'] if vert['id']!=0]) \ 265 for diag in self['born_diagrams']]) 266 # And the minimum weight for the ordered declared as perturbed 267 min_pert=min([hierarchy[order] for order in \ 268 self['process']['perturbation_couplings']]) 269 270 for order, value in hierarchy.items(): 271 if order not in self['process']['orders']: 272 # The four cases below come from a study of the maximal order 273 # needed in the loop for the weighted order needed and the 274 # number of vertices available. 275 if order in self['process']['perturbation_couplings']: 276 if value!=1: 277 self['process']['orders'][order]=\ 278 int((trgt_wgt-min_nvert-2)/(value-1)) 279 else: 280 self['process']['orders'][order]=int(trgt_wgt) 281 else: 282 if value!=1: 283 self['process']['orders'][order]=\ 284 int((trgt_wgt-min_nvert-2*min_pert)/(value-1)) 285 else: 286 self['process']['orders'][order]=\ 287 int(trgt_wgt-2*min_pert) 288 # Now for the remaining orders for which the user has not set squared 289 # orders neither amplitude orders, we use the max order encountered in 290 # the born (and add 2 if this is a perturbed order). 291 # It might be that this upper bound is better than the one guessed 292 # from the hierarchy. 293 for order in self['process']['model']['coupling_orders']: 294 neworder=self['born_diagrams'].get_max_order(order) 295 if order in self['process']['perturbation_couplings']: 296 neworder+=2 297 if order not in self['process']['orders'].keys() or \ 298 neworder<self['process']['orders'][order]: 299 self['process']['orders'][order]=neworder
300
301 - def filter_from_order_config(self, diags, config, discarded_configurations):
302 """ Filter diags to select only the diagram with the non perturbed orders 303 configuration config and update discarded_configurations.Diags is the 304 name of the key attribute of this class containing the diagrams to 305 filter.""" 306 newdiagselection = base_objects.DiagramList() 307 for diag in self[diags]: 308 diag_config = self.get_non_pert_order_config(diag) 309 if diag_config == config: 310 newdiagselection.append(diag) 311 elif diag_config not in discarded_configurations: 312 discarded_configurations.append(diag_config) 313 self[diags] = newdiagselection
314 315
316 - def user_filter(self, model, structs):
317 """ User-defined user-filter. By default it is not called, but the expert 318 user can turn it on and code here is own filter. Some default examples 319 are provided here. 320 The tagging of the loop diagrams must be performed before using this 321 user loop filter""" 322 323 # By default the user filter does nothing, if you want to turn it on 324 # and edit it then remove the print statement below. 325 return 326 327 new_diag_selection = base_objects.DiagramList() 328 discarded_diags = base_objects.DiagramList() 329 i=0 330 for diag in self['loop_diagrams']: 331 if diag.get('tag')==[]: 332 raise MadGraph5Error, "Before using the user_filter, please "+\ 333 "make sure that the loop diagrams have been tagged first." 334 valid_diag = True 335 i=i+1 336 337 # if any([abs(i)!=1000021 for i in diag.get_loop_lines_pdgs()]): 338 # valid_diag=False 339 340 # Ex. 0: Chose a specific diagram number, here the 8th one for ex. 341 # if i not in [31]: 342 # valid_diag = False 343 344 # Ex. 0: Keeps only the top quark loops. 345 # if any([pdg not in [6,-6] for pdg in diag.get_loop_lines_pdgs()]): 346 # valid_diag = False 347 348 # Ex. 1: Chose the topology, i.e. number of loop line. 349 # Notice that here particles and antiparticles are not 350 # differentiated and always the particle PDG is returned. 351 # In this example, only boxes are selected. 352 # if len(diag.get_loop_lines_pdgs())>2 and \ 353 # any([i in diag.get_loop_lines_pdgs() for i in[24,-24,23]]): 354 # valid_diag=False 355 356 # Ex. 2: Use the pdgs of the particles directly attached to the loop. 357 # In this example, we forbid the Z to branch off the loop. 358 # if any([pdg not in [6,-6] for pdg in diag.get_loop_lines_pdgs()]) or \ 359 # 25 not in diag.get_pdgs_attached_to_loop(structs): 360 # valid_diag=False 361 362 # Ex. 3: Filter based on the mass of the particles running in the 363 # loop. It shows how to access the particles properties from 364 # the PDG. 365 # In this example, only massive parts. are allowed in the loop. 366 # if 'ZERO' in [model.get_particle(pdg).get('mass') for pdg in \ 367 # diag.get_loop_lines_pdgs()]: 368 # valid_diag=False 369 370 # Ex. 4: Complicated filter which gets rid of all bubble diagrams made 371 # of two vertices being the four gluon vertex and the effective 372 # glu-glu-Higgs vertex. 373 # if len(diag.get_loop_lines_pdgs())==2: 374 # bubble_lines_pdgs=[abs(diag.get('canonical_tag')[0][0]), 375 # abs(diag.get('canonical_tag')[0][0])] 376 # first_vertex_pdgs=bubble_lines_pdgs+\ 377 # [abs(structs.get_struct(struct_ID).get('binding_leg').get('id')) \ 378 # for struct_ID in diag.get('canonical_tag')[0][1]] 379 # second_vertex_pdgs=bubble_lines_pdgs+\ 380 # [abs(structs.get_struct(struct_ID).get('binding_leg').get('id')) \ 381 # for struct_ID in diag.get('canonical_tag')[1][1]] 382 # first_vertex_pdgs.sort() 383 # second_vertex_pdgs.sort() 384 # bubble_vertices=[first_vertex_pdgs,second_vertex_pdgs] 385 # bubble_vertices.sort() 386 # if bubble_vertices==[[21,21,21,21],[21,21,25]]: 387 # valid_diag=False 388 389 # If you need any more advanced function for your filter and cannot 390 # figure out how to implement them. Just contact the authors. 391 392 if valid_diag: 393 new_diag_selection.append(diag) 394 else: 395 discarded_diags.append(diag) 396 397 self['loop_diagrams'] = new_diag_selection 398 warn_msg = """ 399 The user-defined loop diagrams filter is turned on and discarded %d loops."""\ 400 %len(discarded_diags) 401 logger.warning(warn_msg)
402
404 """ Filter the loop diagrams to make sure they belong to the class 405 of coupling orders perturbed. """ 406 407 # First define what are the set of particles allowed to run in the loop. 408 allowedpart=[] 409 for part in self['process']['model']['particles']: 410 for order in self['process']['perturbation_couplings']: 411 if part.is_perturbating(order,self['process']['model']): 412 allowedpart.append(part.get_pdg_code()) 413 break 414 415 newloopselection=base_objects.DiagramList() 416 warned=False 417 warning_msg = ("Some loop diagrams contributing to this process"+\ 418 " are discarded because they are not pure (%s)-perturbation.\nMake sure"+\ 419 " you did not want to include them.")%\ 420 ('+'.join(self['process']['perturbation_couplings'])) 421 for i,diag in enumerate(self['loop_diagrams']): 422 # Now collect what are the coupling orders building the loop which 423 # are also perturbed order. 424 loop_orders=diag.get_loop_orders(self['process']['model']) 425 pert_loop_order=set(loop_orders.keys()).intersection(\ 426 set(self['process']['perturbation_couplings'])) 427 # Then make sure that the particle running in the loop for all 428 # diagrams belong to the set above. Also make sure that there is at 429 # least one coupling order building the loop which is in the list 430 # of the perturbed order. 431 valid_diag=True 432 if (diag.get_loop_line_types()-set(allowedpart))!=set() or \ 433 pert_loop_order==set([]): 434 valid_diag=False 435 if not warned: 436 logger.warning(warning_msg) 437 warned=True 438 if len([col for col in [ 439 self['process'].get('model').get_particle(pdg).get('color') \ 440 for pdg in diag.get_pdgs_attached_to_loop(\ 441 self['structure_repository'])] if col!=1])==1: 442 valid_diag=False 443 444 if valid_diag: 445 newloopselection.append(diag) 446 self['loop_diagrams']=newloopselection
447 # To monitor what are the diagrams filtered, simply comment the line 448 # directly above and uncomment the two directly below. 449 # self['loop_diagrams'] = base_objects.DiagramList( 450 # [diag for diag in self['loop_diagrams'] if diag not in newloopselection]) 451
452 - def check_factorization(self,user_orders):
453 """ Makes sure that all non perturbed orders factorize the born diagrams 454 """ 455 warning_msg = "All Born diagrams do not factorize the same sum of power(s) "+\ 456 "of the the perturbed order(s) %s.\nThis is potentially dangerous"+\ 457 " as the real-emission diagrams from aMC@NLO will not be consistent"+\ 458 " with these virtual contributions." 459 if self['process']['has_born']: 460 trgt_summed_order = sum([self['born_diagrams'][0].get_order(order) 461 for order in self['process']['perturbation_couplings']]) 462 for diag in self['born_diagrams'][1:]: 463 if sum([diag.get_order(order) for order in self['process'] 464 ['perturbation_couplings']])!=trgt_summed_order: 465 logger.warning(warning_msg%' '.join(self['process'] 466 ['perturbation_couplings'])) 467 break 468 469 warning_msg = "All born diagrams do not factorize the same power of "+\ 470 "the order %s which is not perturbed and for which you have not"+\ 471 "specified any amplitude order. \nThis is potentially dangerous"+\ 472 " as the real-emission diagrams from aMC@NLO will not be consistent"+\ 473 " with these virtual contributions." 474 if self['process']['has_born']: 475 for order in self['process']['model']['coupling_orders']: 476 if order not in self['process']['perturbation_couplings'] and \ 477 order not in user_orders.keys(): 478 order_power=self['born_diagrams'][0].get_order(order) 479 for diag in self['born_diagrams'][1:]: 480 if diag.get_order(order)!=order_power: 481 logger.warning(warning_msg%order) 482 break
483 484 # Helper function
485 - def get_non_pert_order_config(self, diagram):
486 """ Return a dictionary of all the coupling orders of this diagram which 487 are not the perturbed ones.""" 488 return dict([(order, diagram.get_order(order)) for \ 489 order in self['process']['model']['coupling_orders'] if \ 490 not order in self['process']['perturbation_couplings'] ])
491
492 - def print_config(self,config):
493 """Return a string describing the coupling order configuration""" 494 res = [] 495 for order in self['process']['model']['coupling_orders']: 496 try: 497 res.append('%s=%d'%(order,config[order])) 498 except KeyError: 499 res.append('%s=*'%order) 500 return ','.join(res)
501
502 - def generate_diagrams(self):
503 """ Generates all diagrams relevant to this Loop Process """ 504 505 # Description of the algorithm to guess the leading contribution. 506 # The summed weighted order of each diagram will be compared to 507 # 'target_weighted_order' which acts as a threshold to decide which 508 # diagram to keep. Here is an example on how MG5 sets the 509 # 'target_weighted_order'. 510 # 511 # In the sm process uu~ > dd~ [QCD, QED] with hierarchy QCD=1, QED=2 we 512 # would have at leading order contribution like 513 # (QED=4) , (QED=2, QCD=2) , (QCD=4) 514 # leading to a summed weighted order of respectively 515 # (4*2=8) , (2*2+2*1=6) , (4*1=4) 516 # at NLO in QCD and QED we would have the following possible contributions 517 # (QED=6), (QED=4,QCD=2), (QED=2,QCD=4) and (QCD=6) 518 # which translate into the following weighted orders, respectively 519 # 12, 10, 8 and 6 520 # So, now we take the largest weighted order at born level, 4, and add two 521 # times the largest weight in the hierarchy among the order for which we 522 # consider loop perturbation, in this case 2*2 wich gives us a 523 # target_weighted_order of 8. based on this we will now keep all born 524 # contributions and exclude the NLO contributions (QED=6) and (QED=4,QCD=2) 525 526 logger.debug("Generating %s "\ 527 %self['process'].nice_string().replace('Process', 'process')) 528 529 # Hierarchy and model shorthands 530 model = self['process']['model'] 531 hierarchy = model['order_hierarchy'] 532 533 # Later, we will specify the orders for the loop amplitude. 534 # It is a temporary change that will be reverted after loop diagram 535 # generation. We then back up here its value prior modification. 536 user_orders=copy.copy(self['process']['orders']) 537 # First generate the born diagram if the user asked for it 538 if self['process']['has_born']: 539 bornsuccessful = self.generate_born_diagrams() 540 ldg_debug_info("# born diagrams after first generation",\ 541 len(self['born_diagrams'])) 542 else: 543 self['born_diagrams'] = base_objects.DiagramList() 544 bornsuccessful = True 545 logger.debug("# born diagrams generation skipped by user request.") 546 547 # Make sure that all orders specified belong to the model: 548 for order in self['process']['orders'].keys()+\ 549 self['process']['squared_orders'].keys(): 550 if not order in model.get('coupling_orders') and \ 551 order != 'WEIGHTED': 552 raise InvalidCmd("Coupling order %s not found"%order +\ 553 " in any interaction of the current model %s."%model['name']) 554 555 # The decision of whether the virtual must be squared against the born or the 556 # virtual is made based on whether there are Born or not unless the user 557 # already asked for the loop squared. 558 if self['process']['has_born']: 559 self['process']['has_born'] = self['born_diagrams']!=[] 560 561 ldg_debug_info("User input born orders",self['process']['orders']) 562 ldg_debug_info("User input squared orders", 563 self['process']['squared_orders']) 564 ldg_debug_info("User input perturbation",\ 565 self['process']['perturbation_couplings']) 566 567 # Now, we can further specify the orders for the loop amplitude. 568 # Those specified by the user of course remain the same, increased by 569 # two if they are perturbed. It is a temporary change that will be 570 # reverted after loop diagram generation. 571 user_orders=copy.copy(self['process']['orders']) 572 user_squared_orders=copy.copy(self['process']['squared_orders']) 573 574 # If the user did not specify any order, we can expect him not to be an 575 # expert. So we must make sure the born all factorize the same powers of 576 # coupling orders which are not perturbed. If not we chose a configuration 577 # of non-perturbed order which has the smallest total weight and inform 578 # the user about this. It is then stored below for later filtering of 579 # the loop diagrams. 580 chosen_order_config={} 581 if self['process']['squared_orders']=={} and \ 582 self['process']['orders']=={} and self['process']['has_born']: 583 chosen_order_config = self.choose_order_config() 584 585 discarded_configurations = [] 586 # The born diagrams are now filtered according to the chose configuration 587 if chosen_order_config != {}: 588 self.filter_from_order_config('born_diagrams', \ 589 chosen_order_config,discarded_configurations) 590 591 # Before proceeding with the loop contributions, we must make sure that 592 # the born diagram generated factorize the same sum of power of the 593 # perturbed couplings. If this is not true, then it is very 594 # cumbersome to get the real radiation contribution correct and consistent 595 # with the computations of the virtuals (for now). 596 # Also, when MadLoop5 guesses the a loop amplitude order on its own, it 597 # might decide not to include some subleading loop which might be not 598 # be consistently neglected for now in the MadFKS5 so that its best to 599 # warn the user that he should enforce that target born amplitude order 600 # to any value of his choice. 601 self.check_factorization(user_orders) 602 603 # Now find an upper bound for the loop diagram generation. 604 self.guess_loop_orders_from_squared() 605 606 # If the user had not specified any fixed squared order other than 607 # WEIGHTED, we will use the guessed weighted order to assign a bound to 608 # the loop diagram order. Later we will check if the order deduced from 609 # the max order appearing in the born diagrams is a better upper bound. 610 # It will set 'WEIGHTED' to the desired value if it was not already set 611 # by the user. This is why you see the process defined with 'WEIGHTED' 612 # in the squared orders no matter the user input. Leave it like this. 613 if [k.upper() for k in self['process']['squared_orders'].keys()] in \ 614 [[],['WEIGHTED']] and self['process']['has_born']: 615 self.guess_loop_orders(user_orders) 616 617 # Finally we enforce the use of the orders specified for the born 618 # (augmented by two if perturbed) by the user, no matter what was 619 # the best guess performed above. 620 for order in user_orders.keys(): 621 if order in self['process']['perturbation_couplings']: 622 self['process']['orders'][order]=user_orders[order]+2 623 else: 624 self['process']['orders'][order]=user_orders[order] 625 if 'WEIGHTED' in user_orders.keys(): 626 self['process']['orders']['WEIGHTED']=user_orders['WEIGHTED']+\ 627 2*min([hierarchy[order] for order in \ 628 self['process']['perturbation_couplings']]) 629 630 ldg_debug_info("Orders used for loop generation",\ 631 self['process']['orders']) 632 633 # Make sure to warn the user if we already possibly excluded mixed order 634 # loops by smartly setting up the orders 635 warning_msg = ("Some loop diagrams contributing to this process might "+\ 636 "be discarded because they are not pure (%s)-perturbation.\nMake sure"+\ 637 " there are none or that you did not want to include them.")%(\ 638 ','.join(self['process']['perturbation_couplings'])) 639 640 if self['process']['has_born']: 641 for order in model['coupling_orders']: 642 if order not in self['process']['perturbation_couplings']: 643 try: 644 if self['process']['orders'][order]< \ 645 self['born_diagrams'].get_max_order(order): 646 logger.warning(warning_msg) 647 break 648 except KeyError: 649 pass 650 651 # Now we can generate the loop diagrams. 652 totloopsuccessful=self.generate_loop_diagrams() 653 self['process']['forbidden_particles']=[] 654 655 # If there is no born neither loop diagrams, return now. 656 if not self['process']['has_born'] and not self['loop_diagrams']: 657 return False 658 659 # We add here the UV renormalization contribution built in 660 # LoopUVCTDiagram. It is done before the squared order selection because 661 # it is possible that some UV-renorm. diagrams are removed as well. 662 if self['process']['has_born']: 663 self.set_Born_CT() 664 665 ldg_debug_info("#UVCTDiags generated",len(self['loop_UVCT_diagrams'])) 666 667 # Reset the orders to their original specification by the user 668 self['process']['orders'].clear() 669 self['process']['orders'].update(user_orders) 670 671 # If there was no born, we will guess the WEIGHT squared order only now, 672 # based on the minimum weighted order of the loop contributions, if it 673 # was not specified by the user. 674 if not self['process']['has_born'] and not \ 675 self['process']['squared_orders'] and hierarchy: 676 pert_order_weights=[hierarchy[order] for order in \ 677 self['process']['perturbation_couplings']] 678 self['process']['squared_orders']['WEIGHTED']=2*(\ 679 self['loop_diagrams'].get_min_order('WEIGHTED')+\ 680 max(pert_order_weights)-min(pert_order_weights)) 681 682 ldg_debug_info("Squared orders after treatment",\ 683 self['process']['squared_orders']) 684 ldg_debug_info("#Diags after diagram generation",\ 685 len(self['loop_diagrams'])) 686 687 688 # If a special non perturbed order configuration was chosen at the 689 # beginning because of the absence of order settings by the user, 690 # the corresponding filter is applied now to loop diagrams. 691 # List of discarded configurations 692 if chosen_order_config != {}: 693 self.filter_from_order_config('loop_diagrams', \ 694 chosen_order_config,discarded_configurations) 695 # # Warn about discarded configurations. 696 if discarded_configurations!=[]: 697 msg = ("The contribution%s of th%s coupling orders "+\ 698 "configuration%s %s discarded :%s")%(('s','ese','s','are','\n')\ 699 if len(discarded_configurations)>1 else ('','is','','is',' ')) 700 msg = msg + '\n'.join(['(%s)'%self.print_config(conf) for conf \ 701 in discarded_configurations]) 702 msg = msg + "\nManually set the coupling orders to "+\ 703 "generate %sthe contribution%s above."%(('any of ','s') if \ 704 len(discarded_configurations)>1 else ('','')) 705 logger.info(msg) 706 707 # The minimum of the different orders used for the selections can 708 # possibly increase, after some loop diagrams are selected out. 709 # So this check must be iterated until the number of diagrams 710 # remaining is stable. 711 # We first apply the selection rules without the negative constraint. 712 # (i.e. QCD=1 for LO contributions only) 713 regular_constraints = dict([(key,val) for (key,val) in 714 self['process']['squared_orders'].items() if val>=0]) 715 negative_constraints = dict([(key,val) for (key,val) in 716 self['process']['squared_orders'].items() if val<0]) 717 while True: 718 ndiag_remaining=len(self['loop_diagrams']+self['born_diagrams']) 719 self.check_squared_orders(regular_constraints) 720 if len(self['loop_diagrams']+self['born_diagrams'])==ndiag_remaining: 721 break 722 # And then only the negative ones 723 if negative_constraints!={}: 724 # It would be meaningless here to iterate because <order>=-X would 725 # have a different meaning every time. 726 # notice that this function will change the negative values of 727 # self['process']['squared_orders'] to their corresponding positive 728 # constraint for the present process. 729 # For example, u u~ > d d~ QCD^2=-2 becomes u u~ > d d~ QCD=2 730 # because the LO QCD contribution has QED=4, QCD=0 and the NLO one 731 # selected with -2 is QED=2, QCD=2. 732 self.check_squared_orders(negative_constraints,user_squared_orders) 733 734 ldg_debug_info("#Diags after constraints",len(self['loop_diagrams'])) 735 ldg_debug_info("#Born diagrams after constraints",len(self['born_diagrams'])) 736 ldg_debug_info("#UVCTDiags after constraints",len(self['loop_UVCT_diagrams'])) 737 738 # Now the loop diagrams are tagged and filtered for redundancy. 739 tag_selected=[] 740 loop_basis=base_objects.DiagramList() 741 for diag in self['loop_diagrams']: 742 diag.tag(self['structure_repository'],len(self['process']['legs'])+1\ 743 ,len(self['process']['legs'])+2,self['process']) 744 # Make sure not to consider wave-function renormalization, vanishing tadpoles, 745 # or redundant diagrams 746 if not diag.is_wf_correction(self['structure_repository'], \ 747 model) and not diag.is_vanishing_tadpole(model) and \ 748 diag['canonical_tag'] not in tag_selected: 749 loop_basis.append(diag) 750 tag_selected.append(diag['canonical_tag']) 751 752 self['loop_diagrams']=loop_basis 753 754 # Now select only the loops corresponding to the perturbative orders 755 # asked for. 756 self.filter_loop_for_perturbative_orders() 757 758 if len(self['loop_diagrams'])==0 and len(self['born_diagrams'])!=0: 759 raise InvalidCmd('All loop diagrams discarded by user selection.\n'+\ 760 'Consider using a tree-level generation or relaxing the coupling'+\ 761 ' order constraints.') 762 # If there is no born neither loop diagrams after filtering, return now. 763 if not self['process']['has_born'] and not self['loop_diagrams']: 764 return False 765 766 # Set the necessary UV/R2 CounterTerms for each loop diagram generated 767 self.set_LoopCT_vertices() 768 769 # Apply here some user-defined filter. 770 # For expert only, you can edit your own filter by modifying the 771 # user_filter() function which by default does nothing but in which you 772 # will find examples of common filters. 773 self.user_filter(model,self['structure_repository']) 774 775 # Now revert the squared order. This function typically adds to the 776 # squared order list the target WEIGHTED order which has been detected. 777 # This is typically not desired because if the user types in directly 778 # what it sees on the screen, it does not get back the same process. 779 # for example, u u~ > d d~ [virt=QCD] becomes 780 # u u~ > d d~ [virt=QCD] WEIGHTED=6 781 # but of course the photon-gluon s-channel Born interference is not 782 # counted in. 783 # However, if you type it in generate again with WEIGHTED=6, you will 784 # get it. 785 self['process']['squared_orders'].clear() 786 self['process']['squared_orders'].update(user_squared_orders) 787 788 # The computation below is just to report what split order are computed 789 # and which one are considered (i.e. kept using the order specifications) 790 self.print_split_order_infos() 791 792 # Give some info about the run 793 nLoopDiag = 0 794 nCT={'UV':0,'R2':0} 795 for ldiag in self['loop_UVCT_diagrams']: 796 nCT[ldiag['type'][:2]]+=len(ldiag['UVCT_couplings']) 797 for ldiag in self['loop_diagrams']: 798 nLoopDiag+=1 799 nCT['UV']+=len(ldiag.get_CT(model,'UV')) 800 nCT['R2']+=len(ldiag.get_CT(model,'R2')) 801 802 logger.info("Contributing diagrams generated: "+\ 803 "%d born, %d loop, %d R2, %d UV"%\ 804 (len(self['born_diagrams']),nLoopDiag,nCT['R2'],nCT['UV'])) 805 806 ldg_debug_info("#Diags after filtering",len(self['loop_diagrams'])) 807 ldg_debug_info("# of different structures identified",\ 808 len(self['structure_repository'])) 809 810 return (bornsuccessful or totloopsuccessful)
811
812 - def print_split_order_infos(self):
813 """This function is solely for monitoring purposes. It reports what are 814 the coupling order combination which are obtained with the diagram 815 genarated and among those which ones correspond to those selected by 816 the process definition and which ones are the extra combinations which 817 comes as a byproduct of the computation of the desired one. The typical 818 example is that if you ask for d d~ > u u~ QCD^2==2 [virt=QCD, QED], 819 you will not only get (QCD,QED)=(2,2);(2,4) which are the desired ones 820 but the code output will in principle also be able to return 821 (QCD,QED)=(4,0);(4,2);(0,4);(0,6) because they involve the same amplitudes 822 """ 823 824 hierarchy = self['process']['model']['order_hierarchy'] 825 826 sqorders_types=copy.copy(self['process'].get('sqorders_types')) 827 # The WEIGHTED order might have been automatically assigned to the 828 # squared order constraints, so we must assign it a type if not specified 829 if 'WEIGHTED' not in sqorders_types: 830 sqorders_types['WEIGHTED']='<=' 831 832 sorted_hierarchy = [order[0] for order in \ 833 sorted(hierarchy.items(), key=lambda el: el[1])] 834 835 loop_SOs = set(tuple([d.get_order(order) for order in sorted_hierarchy]) 836 for d in self['loop_diagrams']+self['loop_UVCT_diagrams']) 837 838 if self['process']['has_born']: 839 born_SOs = set(tuple([d.get_order(order) for order in \ 840 sorted_hierarchy]) for d in self['born_diagrams']) 841 else: 842 born_SOs = set([]) 843 844 born_sqSOs = set(tuple([x + y for x, y in zip(b1_SO, b2_SO)]) for b1_SO 845 in born_SOs for b2_SO in born_SOs) 846 if self['process']['has_born']: 847 ref_amps = born_SOs 848 else: 849 ref_amps = loop_SOs 850 loop_sqSOs = set(tuple([x + y for x, y in zip(b_SO, l_SO)]) for b_SO in 851 ref_amps for l_SO in loop_SOs) 852 853 # Append the corresponding WEIGHT of each contribution 854 sorted_hierarchy.append('WEIGHTED') 855 born_sqSOs = sorted([b_sqso+(sum([b*hierarchy[sorted_hierarchy[i]] for 856 i, b in enumerate(b_sqso)]),) for b_sqso in born_sqSOs], 857 key=lambda el: el[1]) 858 loop_sqSOs = sorted([l_sqso+(sum([l*hierarchy[sorted_hierarchy[i]] for 859 i, l in enumerate(l_sqso)]),) for l_sqso in loop_sqSOs], 860 key=lambda el: el[1]) 861 862 863 logger.debug("Coupling order combinations considered:"+\ 864 " (%s)"%','.join(sorted_hierarchy)) 865 866 # Now check what is left 867 born_considered = [] 868 loop_considered = [] 869 for i, sqSOList in enumerate([born_sqSOs,loop_sqSOs]): 870 considered = [] 871 extra = [] 872 for sqSO in sqSOList: 873 for sqo, constraint in self['process']['squared_orders'].items(): 874 sqo_index = sorted_hierarchy.index(sqo) 875 # Notice that I assume here that the negative coupling order 876 # constraint should have been replaced here (by its 877 # corresponding positive value). 878 if (sqorders_types[sqo]=='==' and 879 sqSO[sqo_index]!=constraint ) or \ 880 (sqorders_types[sqo] in ['=','<='] and 881 sqSO[sqo_index]>constraint) or \ 882 (sqorders_types[sqo] in ['>'] and 883 sqSO[sqo_index]<=constraint): 884 extra.append(sqSO) 885 break; 886 887 # Set the ones considered to be the complement of the omitted ones 888 considered = [sqSO for sqSO in sqSOList if sqSO not in extra] 889 890 if i==0: 891 born_considered = considered 892 name = "Born" 893 if not self['process']['has_born']: 894 logger.debug(" > No Born contributions for this process.") 895 continue 896 elif i==1: 897 loop_considered = considered 898 name = "loop" 899 900 if len(considered)==0: 901 logger.debug(" > %s : None"%name) 902 else: 903 logger.debug(" > %s : %s"%(name,' '.join(['(%s,W%d)'%( 904 ','.join(list('%d'%s for s in c[:-1])),c[-1]) 905 for c in considered]))) 906 907 if len(extra)!=0: 908 logger.debug(" > %s (not selected but available): %s"%(name,' '. 909 join(['(%s,W%d)'%(','.join(list('%d'%s for s in e[:-1])), 910 e[-1]) for e in extra]))) 911 912 # In case it is needed, the considered orders are returned 913 # (it is used by some of the unit tests) 914 return (born_considered, 915 [sqSO for sqSO in born_sqSOs if sqSO not in born_considered], 916 loop_considered, 917 [sqSO for sqSO in loop_sqSOs if sqSO not in loop_considered])
918 919
920 - def generate_born_diagrams(self):
921 """ Generates all born diagrams relevant to this NLO Process """ 922 923 bornsuccessful, self['born_diagrams'] = \ 924 super(LoopAmplitude, self).generate_diagrams(True) 925 926 return bornsuccessful
927
928 - def generate_loop_diagrams(self):
929 """ Generates all loop diagrams relevant to this NLO Process """ 930 931 # Reinitialize the loop diagram container 932 self['loop_diagrams']=base_objects.DiagramList() 933 totloopsuccessful=False 934 935 # Make sure to start with an empty l-cut particle list. 936 self.lcutpartemployed=[] 937 938 for order in self['process']['perturbation_couplings']: 939 ldg_debug_info("Perturbation coupling generated now ",order) 940 lcutPart=[particle for particle in \ 941 self['process']['model']['particles'] if \ 942 (particle.is_perturbating(order, self['process']['model']) and \ 943 particle.get_pdg_code() not in \ 944 self['process']['forbidden_particles'])] 945 # lcutPart = [lp for lp in lcutPart if abs(lp.get('pdg_code'))==1000021] 946 # print "lcutPart=",[part.get('name') for part in lcutPart] 947 for part in lcutPart: 948 if part.get_pdg_code() not in self.lcutpartemployed: 949 # First create the two L-cut particles to add to the process. 950 # Remember that in the model only the particles should be 951 # tagged as contributing to the a perturbation. Never the 952 # anti-particle. We chose here a specific orientation for 953 # the loop momentum flow, say going IN lcutone and OUT 954 # lcuttwo. We also define here the 'positive' loop fermion 955 # flow by always setting lcutone to be a particle and 956 # lcuttwo the corresponding anti-particle. 957 ldg_debug_info("Generating loop diagram with L-cut type",\ 958 part.get_name()) 959 lcutone=base_objects.Leg({'id': part.get_pdg_code(), 960 'state': True, 961 'loop_line': True}) 962 lcuttwo=base_objects.Leg({'id': part.get_anti_pdg_code(), 963 'state': True, 964 'loop_line': True}) 965 self['process'].get('legs').extend([lcutone,lcuttwo]) 966 # WARNING, it is important for the tagging to notice here 967 # that lcuttwo is the last leg in the process list of legs 968 # and will therefore carry the highest 'number' attribute as 969 # required to insure that it will never be 'propagated' to 970 # any output leg. 971 972 # We generate the diagrams now 973 loopsuccessful, lcutdiaglist = \ 974 super(LoopAmplitude, self).generate_diagrams(True) 975 976 # Now get rid of all the previously defined l-cut particles. 977 leg_to_remove=[leg for leg in self['process']['legs'] \ 978 if leg['loop_line']] 979 for leg in leg_to_remove: 980 self['process']['legs'].remove(leg) 981 982 # The correct L-cut type is specified 983 for diag in lcutdiaglist: 984 diag.set('type',part.get_pdg_code()) 985 self['loop_diagrams']+=lcutdiaglist 986 987 # Update the list of already employed L-cut particles such 988 # that we never use them again in loop particles 989 self.lcutpartemployed.append(part.get_pdg_code()) 990 self.lcutpartemployed.append(part.get_anti_pdg_code()) 991 992 ldg_debug_info("#Diags generated w/ this L-cut particle",\ 993 len(lcutdiaglist)) 994 # Accordingly update the totloopsuccessful tag 995 if loopsuccessful: 996 totloopsuccessful=True 997 998 # Reset the l-cut particle list 999 self.lcutpartemployed=[] 1000 1001 return totloopsuccessful
1002 1003
1004 - def set_Born_CT(self):
1005 """ Scan all born diagrams and add for each all the corresponding UV 1006 counterterms. It creates one LoopUVCTDiagram per born diagram and set 1007 of possible coupling_order (so that QCD and QED wavefunction corrections 1008 are not in the same LoopUVCTDiagram for example). Notice that this takes 1009 care only of the UV counterterm which factorize with the born and the 1010 other contributions like the UV mass renormalization are added in the 1011 function setLoopCTVertices""" 1012 1013 # return True 1014 # ============================================ 1015 # Including the UVtree contributions 1016 # ============================================ 1017 1018 # The following lists the UV interactions potentially giving UV counterterms 1019 # (The UVmass interactions is accounted for like the R2s) 1020 UVCTvertex_interactions = base_objects.InteractionList() 1021 for inter in self['process']['model']['interactions'].get_UV(): 1022 if inter.is_UVtree() and len(inter['particles'])>1 and \ 1023 inter.is_perturbating(self['process']['perturbation_couplings']) \ 1024 and (set(inter['orders'].keys()).intersection(\ 1025 set(self['process']['perturbation_couplings'])))!=set([]) and \ 1026 (any([set(loop_parts).intersection(set(self['process']\ 1027 ['forbidden_particles']))==set([]) for loop_parts in \ 1028 inter.get('loop_particles')]) or \ 1029 inter.get('loop_particles')==[[]]): 1030 UVCTvertex_interactions.append(inter) 1031 1032 # Temporarly give the tagging order 'UVCT_SPECIAL' to those interactions 1033 self['process']['model'].get('order_hierarchy')['UVCT_SPECIAL']=0 1034 self['process']['model'].get('coupling_orders').add('UVCT_SPECIAL') 1035 for inter in UVCTvertex_interactions: 1036 neworders=copy.copy(inter.get('orders')) 1037 neworders['UVCT_SPECIAL']=1 1038 inter.set('orders',neworders) 1039 # Refresh the model interaction dictionary while including those special 1040 # interactions 1041 self['process']['model'].actualize_dictionaries(useUVCT=True) 1042 1043 # Generate the UVCTdiagrams (born diagrams with 'UVCT_SPECIAL'=0 order 1044 # will be generated along) 1045 self['process']['orders']['UVCT_SPECIAL']=1 1046 1047 UVCTsuccessful, UVCTdiagrams = \ 1048 super(LoopAmplitude, self).generate_diagrams(True) 1049 1050 for UVCTdiag in UVCTdiagrams: 1051 if UVCTdiag.get_order('UVCT_SPECIAL')==1: 1052 newUVCTDiag = loop_base_objects.LoopUVCTDiagram({\ 1053 'vertices':copy.deepcopy(UVCTdiag['vertices'])}) 1054 UVCTinter = newUVCTDiag.get_UVCTinteraction(self['process']['model']) 1055 newUVCTDiag.set('type',UVCTinter.get('type')) 1056 # This interaction counter-term must be accounted for as many times 1057 # as they are list of loop_particles defined and allowed for by 1058 # the process. 1059 newUVCTDiag.get('UVCT_couplings').append((len([1 for loop_parts \ 1060 in UVCTinter.get('loop_particles') if set(loop_parts).intersection(\ 1061 set(self['process']['forbidden_particles']))==set([])])) if 1062 loop_parts!=[[]] else 1) 1063 self['loop_UVCT_diagrams'].append(newUVCTDiag) 1064 1065 # Remove the additional order requirement in the born orders for this 1066 # process 1067 del self['process']['orders']['UVCT_SPECIAL'] 1068 # Remove the fake order added to the selected UVCT interactions 1069 del self['process']['model'].get('order_hierarchy')['UVCT_SPECIAL'] 1070 self['process']['model'].get('coupling_orders').remove('UVCT_SPECIAL') 1071 for inter in UVCTvertex_interactions: 1072 del inter.get('orders')['UVCT_SPECIAL'] 1073 # Revert the model interaction dictionaries to default 1074 self['process']['model'].actualize_dictionaries(useUVCT=False) 1075 1076 # Set the correct orders to the loop_UVCT_diagrams 1077 for UVCTdiag in self['loop_UVCT_diagrams']: 1078 UVCTdiag.calculate_orders(self['process']['model']) 1079 1080 # ============================================ 1081 # Wavefunction renormalization 1082 # ============================================ 1083 1084 if not self['process']['has_born']: 1085 return UVCTsuccessful 1086 1087 # We now scan each born diagram, adding the necessary wavefunction 1088 # renormalizations 1089 for bornDiag in self['born_diagrams']: 1090 # This dictionary takes for keys the tuple 1091 # (('OrderName1',power1),...,('OrderNameN',powerN) representing 1092 # the power brought by the counterterm and the value is the 1093 # corresponding LoopUVCTDiagram. 1094 # The last entry is of the form ('EpsilonOrder', value) to put the 1095 # contribution of each different EpsilonOrder to different 1096 # LoopUVCTDiagrams. 1097 LoopUVCTDiagramsAdded={} 1098 for leg in self['process']['legs']: 1099 counterterm=self['process']['model'].get_particle(abs(leg['id'])).\ 1100 get('counterterm') 1101 for key, value in counterterm.items(): 1102 if key[0] in self['process']['perturbation_couplings']: 1103 for laurentOrder, CTCoupling in value.items(): 1104 # Create the order key of the UV counterterm 1105 orderKey=[(key[0],2),] 1106 orderKey.sort() 1107 orderKey.append(('EpsilonOrder',-laurentOrder)) 1108 CTCouplings=[CTCoupling for loop_parts in key[1] if 1109 set(loop_parts).intersection(set(self['process']\ 1110 ['forbidden_particles']))==set([])] 1111 if CTCouplings!=[]: 1112 try: 1113 LoopUVCTDiagramsAdded[tuple(orderKey)].get(\ 1114 'UVCT_couplings').extend(CTCouplings) 1115 except KeyError: 1116 LoopUVCTDiagramsAdded[tuple(orderKey)]=\ 1117 loop_base_objects.LoopUVCTDiagram({\ 1118 'vertices':copy.deepcopy(bornDiag['vertices']), 1119 'type':'UV'+('' if laurentOrder==0 else 1120 str(-laurentOrder)+'eps'), 1121 'UVCT_orders':{key[0]:2}, 1122 'UVCT_couplings':CTCouplings}) 1123 1124 for LoopUVCTDiagram in LoopUVCTDiagramsAdded.values(): 1125 LoopUVCTDiagram.calculate_orders(self['process']['model']) 1126 self['loop_UVCT_diagrams'].append(LoopUVCTDiagram) 1127 1128 return UVCTsuccessful
1129
1130 - def set_LoopCT_vertices(self):
1131 """ Scan each loop diagram and recognizes what are the R2/UVmass 1132 CounterTerms associated to them """ 1133 #return # debug 1134 # We first create a base dictionary with as a key (tupleA,tupleB). For 1135 # each R2/UV interaction, tuple B is the ordered tuple of the loop 1136 # particles (not anti-particles, so that the PDG is always positive!) 1137 # listed in its loop_particles attribute. Tuple A is the ordered tuple 1138 # of external particles PDGs. making up this interaction. The values of 1139 # the dictionary are a list of the interaction ID having the same key 1140 # above. 1141 CT_interactions = {} 1142 for inter in self['process']['model']['interactions']: 1143 if inter.is_UVmass() or inter.is_UVloop() or inter.is_R2() and \ 1144 len(inter['particles'])>1 and inter.is_perturbating(\ 1145 self['process']['perturbation_couplings']): 1146 # This interaction might have several possible loop particles 1147 # yielding the same CT. So we add this interaction ID 1148 # for each entry in the list loop_particles. 1149 for i, lparts in enumerate(inter['loop_particles']): 1150 keya=copy.copy(lparts) 1151 keya.sort() 1152 if inter.is_UVloop(): 1153 # If it is a CT of type UVloop, then do not specify the 1154 # keya (leave it empty) but make sure the particles 1155 # specified as loop particles are not forbidden before 1156 # adding this CT to CT_interactions 1157 if (set(self['process']['forbidden_particles']) & \ 1158 set(lparts)) != set([]): 1159 continue 1160 else: 1161 keya=[] 1162 keyb=[part.get_pdg_code() for part in inter['particles']] 1163 keyb.sort() 1164 key=(tuple(keyb),tuple(keya)) 1165 # We keep track of 'i' (i.e. the position of the 1166 # loop_particle list in the inter['loop_particles']) so 1167 # that each coupling in a vertex of type 'UVloop' is 1168 # correctly accounted for since the keya is always replaced 1169 # by an empty list since the constraint on the loop particles 1170 # is simply that there is not corresponding forbidden 1171 # particles in the process definition and not that the 1172 # actual particle content of the loop generate matches. 1173 # 1174 # This can also happen with the type 'UVmass' or 'R2' 1175 # CTvertex ex1( 1176 # type='UVmass' 1177 # [...] 1178 # loop_particles=[[[d,g],[d,g]]]) 1179 # Which is a bit silly but can happen and would mean that 1180 # we must account twice for the coupling associated to each 1181 # of these loop_particles. 1182 # One might imagine someone doing it with 1183 # loop_particles=[[[],[]]], for example, because he wanted 1184 # to get rid of the loop particle constraint for some reason. 1185 try: 1186 CT_interactions[key].append((inter['id'],i)) 1187 except KeyError: 1188 CT_interactions[key]=[(inter['id'],i),] 1189 1190 # The dictionary CTmass_added keeps track of what are the CounterTerms of 1191 # type UVmass or R2 already added and prevents us from adding them again. 1192 # For instance, the fermion boxes with four external gluons exists in 6 copies 1193 # (with different crossings of the external legs each time) and the 1194 # corresponding R2 must be added only once. The key of this dictionary 1195 # characterizing the loop is (tupleA,tupleB). Tuple A is made from the 1196 # list of the ID of the external structures attached to this loop and 1197 # tuple B from list of the pdg of the particles building this loop. 1198 1199 # Notice that when a CT of type UVmass is specified with an empty 1200 # loop_particles attribute, then it means it must be added once for each 1201 # particle with a matching topology, irrespectively of the loop content. 1202 # Whenever added, such a CT is put in the dictionary CT_added with a key 1203 # having an empty tupleB. 1204 # Finally, because CT interactions of type UVloop do specify a 1205 # loop_particles attribute, but which serves only to be filtered against 1206 # particles forbidden in the process definition, they will also be added 1207 # with an empty tupleB. 1208 CT_added = {} 1209 1210 for diag in self['loop_diagrams']: 1211 # First build the key from this loop for the CT_interaction dictionary 1212 # (Searching Key) and the key for the CT_added dictionary (tracking Key) 1213 searchingKeyA=[] 1214 # Notice that searchingKeyB below also serves as trackingKeyB 1215 searchingKeyB=[] 1216 trackingKeyA=[] 1217 for tagElement in diag['canonical_tag']: 1218 for structID in tagElement[1]: 1219 trackingKeyA.append(structID) 1220 searchingKeyA.append(self['process']['model'].get_particle(\ 1221 self['structure_repository'][structID]['binding_leg']['id']).\ 1222 get_pdg_code()) 1223 searchingKeyB.append(self['process']['model'].get_particle(\ 1224 tagElement[0]).get('pdg_code')) 1225 searchingKeyA.sort() 1226 # We do not repeat particles present many times in the loop 1227 searchingKeyB=list(set(searchingKeyB)) 1228 searchingKeyB.sort() 1229 trackingKeyA.sort() 1230 # I repeat, they are two kinds of keys: 1231 # searchingKey: 1232 # This serves to scan the CT interactions defined and then find 1233 # which ones match a given loop topology and particle. 1234 # trackingKey: 1235 # Once some CT vertices are identified to be a match for a loop, 1236 # the trackingKey is used in conjunction with the dictionary 1237 # CT_added to make sure that this CT has not already been included. 1238 1239 # Each of these two keys above, has the format 1240 # (tupleA, tupleB) 1241 # with tupleB being the loop_content and either contains the set of 1242 # loop particles PDGs of the interaction (for the searchingKey) 1243 # or of the loops already scanned (trackingKey). It can also be 1244 # empty when considering interactions of type UVmass or R2 which 1245 # have an empty loop_particle attribute or those of type UVloop. 1246 # TupleA is the set of external particle PDG (for the searchingKey) 1247 # and the unordered list of structID attached to the loop (for the 1248 # trackingKey) 1249 searchingKeySimple=(tuple(searchingKeyA),()) 1250 searchingKeyLoopPart=(tuple(searchingKeyA),tuple(searchingKeyB)) 1251 trackingKeySimple=(tuple(trackingKeyA),()) 1252 trackingKeyLoopPart=(tuple(trackingKeyA),tuple(searchingKeyB)) 1253 # Now we look for a CT which might correspond to this loop by looking 1254 # for its searchingKey in CT_interactions 1255 1256 #print "I have the following CT_interactions=",CT_interactions 1257 try: 1258 CTIDs=copy.copy(CT_interactions[searchingKeySimple]) 1259 except KeyError: 1260 CTIDs=[] 1261 try: 1262 CTIDs.extend(copy.copy(CT_interactions[searchingKeyLoopPart])) 1263 except KeyError: 1264 pass 1265 if not CTIDs: 1266 continue 1267 # We have found some CT interactions corresponding to this loop 1268 # so we must make sure we have not included them already 1269 try: 1270 usedIDs=copy.copy(CT_added[trackingKeySimple]) 1271 except KeyError: 1272 usedIDs=[] 1273 try: 1274 usedIDs.extend(copy.copy(CT_added[trackingKeyLoopPart])) 1275 except KeyError: 1276 pass 1277 1278 for CTID in CTIDs: 1279 # Make sure it has not been considered yet and that the loop 1280 # orders match 1281 if CTID not in usedIDs and diag.get_loop_orders(\ 1282 self['process']['model'])==\ 1283 self['process']['model']['interaction_dict'][CTID[0]]['orders']: 1284 # Create the amplitude vertex corresponding to this CT 1285 # and add it to the LoopDiagram treated. 1286 CTleglist = base_objects.LegList() 1287 for tagElement in diag['canonical_tag']: 1288 for structID in tagElement[1]: 1289 CTleglist.append(\ 1290 self['structure_repository'][structID]['binding_leg']) 1291 CTVertex = base_objects.Vertex({'id':CTID[0], \ 1292 'legs':CTleglist}) 1293 diag['CT_vertices'].append(CTVertex) 1294 # Now add this CT vertex to the CT_added dictionary so that 1295 # we are sure it will not be double counted 1296 if self['process']['model']['interaction_dict'][CTID[0]]\ 1297 ['loop_particles'][CTID[1]]==[] or \ 1298 self['process']['model']['interaction_dict'][CTID[0]].\ 1299 is_UVloop(): 1300 try: 1301 CT_added[trackingKeySimple].append(CTID) 1302 except KeyError: 1303 CT_added[trackingKeySimple] = [CTID, ] 1304 else: 1305 try: 1306 CT_added[trackingKeyLoopPart].append(CTID) 1307 except KeyError: 1308 CT_added[trackingKeyLoopPart] = [CTID, ]
1309
1310 - def create_diagram(self, vertexlist):
1311 """ Return a LoopDiagram created.""" 1312 return loop_base_objects.LoopDiagram({'vertices':vertexlist})
1313
1314 - def copy_leglist(self, leglist):
1315 """ Returns a DGLoopLeg list instead of the default copy_leglist 1316 defined in base_objects.Amplitude """ 1317 1318 dgloopleglist=base_objects.LegList() 1319 for leg in leglist: 1320 dgloopleglist.append(loop_base_objects.DGLoopLeg(leg)) 1321 1322 return dgloopleglist
1323
1324 - def convert_dgleg_to_leg(self, vertexdoublelist):
1325 """ Overloaded here to convert back all DGLoopLegs into Legs. """ 1326 for vertexlist in vertexdoublelist: 1327 for vertex in vertexlist: 1328 if not isinstance(vertex['legs'][0],loop_base_objects.DGLoopLeg): 1329 continue 1330 vertex['legs'][:]=[leg.convert_to_leg() for leg in \ 1331 vertex['legs']] 1332 return True
1333
1334 - def get_combined_legs(self, legs, leg_vert_ids, number, state):
1335 """Create a set of new legs from the info given.""" 1336 1337 looplegs=[leg for leg in legs if leg['loop_line']] 1338 1339 # Get rid of all vanishing tadpoles 1340 #Ease the access to the model 1341 model=self['process']['model'] 1342 exlegs=[leg for leg in looplegs if leg['depth']==0] 1343 if(len(exlegs)==2): 1344 if(any([part['mass'].lower()=='zero' for pdg,part in model.get('particle_dict').items() if pdg==abs(exlegs[0]['id'])])): 1345 return [] 1346 1347 # Correctly propagate the loopflow 1348 loopline=(len(looplegs)==1) 1349 mylegs = [] 1350 for i, (leg_id, vert_id) in enumerate(leg_vert_ids): 1351 # We can now create the set of possible merged legs. 1352 # However, we make sure that its PDG is not in the list of 1353 # L-cut particles we already explored. If it is, we simply reject 1354 # the diagram. 1355 if not loopline or not (leg_id in self.lcutpartemployed): 1356 # Reminder: The only purpose of the "depth" flag is to get rid 1357 # of (some, not all) of the wave-function renormalization 1358 # already during diagram generation. We reckognize a wf 1359 # renormalization diagram as follows: 1360 if len(legs)==2 and len(looplegs)==2: 1361 # We have candidate 1362 depths=(looplegs[0]['depth'],looplegs[1]['depth']) 1363 if (0 in depths) and (-1 not in depths) and depths!=(0,0): 1364 # Check that the PDG of the outter particle in the 1365 # wavefunction renormalization bubble is equal to the 1366 # one of the inner particle. 1367 continue 1368 1369 # If depth is not 0 because of being an external leg and not 1370 # the propagated PDG, then we set it to -1 so that from that 1371 # point we are sure the diagram will not be reckognized as a 1372 # wave-function renormalization. 1373 depth=-1 1374 # When creating a loop leg from exactly two external legs, we 1375 # set the depth to the PDG of the external non-loop line. 1376 if len(legs)==2 and loopline and (legs[0]['depth'],\ 1377 legs[1]['depth'])==(0,0): 1378 if not legs[0]['loop_line']: 1379 depth=legs[0]['id'] 1380 else: 1381 depth=legs[1]['id'] 1382 # In case of two point interactions among two same particle 1383 # we propagate the existing depth 1384 if len(legs)==1 and legs[0]['id']==leg_id: 1385 depth=legs[0]['depth'] 1386 # In all other cases we set the depth to -1 since no 1387 # wave-function renormalization diagram can arise from this 1388 # side of the diagram construction. 1389 1390 mylegs.append((loop_base_objects.DGLoopLeg({'id':leg_id, 1391 'number':number, 1392 'state':state, 1393 'from_group':True, 1394 'depth': depth, 1395 'loop_line': loopline}), 1396 vert_id)) 1397 return mylegs
1398
1399 - def get_combined_vertices(self, legs, vert_ids):
1400 """Allow for selection of vertex ids.""" 1401 1402 looplegs=[leg for leg in legs if leg['loop_line']] 1403 nonlooplegs=[leg for leg in legs if not leg['loop_line']] 1404 1405 # Get rid of all vanishing tadpoles 1406 model=self['process']['model'] 1407 exlegs=[leg for leg in looplegs if leg['depth']==0] 1408 if(len(exlegs)==2): 1409 if(any([part['mass'].lower()=='zero' for pdg,part in \ 1410 model.get('particle_dict').items() if pdg==abs(exlegs[0]['id'])])): 1411 return [] 1412 1413 1414 # Get rid of some wave-function renormalization diagrams already during 1415 # diagram generation already.In a similar manner as in get_combined_legs. 1416 if(len(legs)==3 and len(looplegs)==2): 1417 depths=(looplegs[0]['depth'],looplegs[1]['depth']) 1418 if (0 in depths) and (-1 not in depths) and depths!=(0,0): 1419 return [] 1420 1421 return vert_ids
1422 1423 # Helper function 1424
1425 - def check_squared_orders(self, sq_order_constrains, user_squared_orders=None):
1426 """ Filters the diagrams according to the constraints on the squared 1427 orders in argument and wether the process has a born or not. """ 1428 1429 diagRef=base_objects.DiagramList() 1430 AllLoopDiagrams=base_objects.DiagramList(self['loop_diagrams']+\ 1431 self['loop_UVCT_diagrams']) 1432 1433 AllBornDiagrams=base_objects.DiagramList(self['born_diagrams']) 1434 if self['process']['has_born']: 1435 diagRef=AllBornDiagrams 1436 else: 1437 diagRef=AllLoopDiagrams 1438 1439 sqorders_types=copy.copy(self['process'].get('sqorders_types')) 1440 1441 # The WEIGHTED order might have been automatically assigned to the 1442 # squared order constraints, so we must assign it a type if not specified 1443 if 'WEIGHTED' not in sqorders_types: 1444 sqorders_types['WEIGHTED']='<=' 1445 1446 if len(diagRef)==0: 1447 # If no born contributes but they were supposed to ( in the 1448 # case of self['process']['has_born']=True) then it means that 1449 # the loop cannot be squared against anything and none should 1450 # contribute either. The squared order constraints are just too 1451 # tight for anything to contribute. 1452 AllLoopDiagrams = base_objects.DiagramList() 1453 1454 1455 # Start by filtering the loop diagrams 1456 AllLoopDiagrams = AllLoopDiagrams.apply_positive_sq_orders(diagRef, 1457 sq_order_constrains, sqorders_types) 1458 # And now the Born ones if there are any 1459 if self['process']['has_born']: 1460 # We consider both the Born*Born and Born*Loop squared terms here 1461 AllBornDiagrams = AllBornDiagrams.apply_positive_sq_orders( 1462 AllLoopDiagrams+AllBornDiagrams, sq_order_constrains, sqorders_types) 1463 1464 # Now treat the negative squared order constraint (at most one) 1465 neg_orders = [(order, value) for order, value in \ 1466 sq_order_constrains.items() if value<0] 1467 if len(neg_orders)==1: 1468 neg_order, neg_value = neg_orders[0] 1469 # If there is a Born contribution, then the target order will 1470 # be computed over all Born*Born and Born*loop contributions 1471 if self['process']['has_born']: 1472 AllBornDiagrams, target_order =\ 1473 AllBornDiagrams.apply_negative_sq_order( 1474 base_objects.DiagramList(AllLoopDiagrams+AllBornDiagrams), 1475 neg_order,neg_value,sqorders_types[neg_order]) 1476 # Now we must filter the loop diagrams using to the target_order 1477 # computed above from the LO and NLO contributions 1478 AllLoopDiagrams = AllLoopDiagrams.apply_positive_sq_orders( 1479 diagRef,{neg_order:target_order}, 1480 {neg_order:sqorders_types[neg_order]}) 1481 1482 # If there is no Born, then the situation is completely analoguous 1483 # to the tree level case since it is simply Loop*Loop 1484 else: 1485 AllLoopDiagrams, target_order = \ 1486 AllLoopDiagrams.apply_negative_sq_order( 1487 diagRef,neg_order,neg_value,sqorders_types[neg_order]) 1488 1489 # Substitute the negative value to this positive one 1490 # (also in the backed up values in user_squared_orders so that 1491 # this change is permanent and we will still have access to 1492 # it at the output stage) 1493 self['process']['squared_orders'][neg_order]=target_order 1494 user_squared_orders[neg_order]=target_order 1495 1496 elif len(neg_orders)>1: 1497 raise MadGraph5Error('At most one negative squared order constraint'+\ 1498 ' can be specified, not %s.'%str(neg_orders)) 1499 1500 if self['process']['has_born']: 1501 self['born_diagrams'] = AllBornDiagrams 1502 self['loop_diagrams']=[diag for diag in AllLoopDiagrams if not \ 1503 isinstance(diag,loop_base_objects.LoopUVCTDiagram)] 1504 self['loop_UVCT_diagrams']=[diag for diag in AllLoopDiagrams if \ 1505 isinstance(diag,loop_base_objects.LoopUVCTDiagram)]
1506
1507 - def order_diagram_set(self, diag_set, split_orders):
1508 """ This is a helper function for order_diagrams_according_to_split_orders 1509 and intended to be used from LoopHelasAmplitude only""" 1510 1511 # The dictionary below has keys being the tuple (split_order<i>_values) 1512 # and values being diagram lists sharing the same split orders. 1513 diag_by_so = {} 1514 1515 for diag in diag_set: 1516 so_key = tuple([diag.get_order(order) for order in split_orders]) 1517 try: 1518 diag_by_so[so_key].append(diag) 1519 except KeyError: 1520 diag_by_so[so_key]=base_objects.DiagramList([diag,]) 1521 1522 so_keys = diag_by_so.keys() 1523 # Complete the order hierarchy by possibly missing defined order for 1524 # which we set the weight to zero by default (so that they are ignored). 1525 order_hierarchy = self.get('process').get('model').get('order_hierarchy') 1526 order_weights = copy.copy(order_hierarchy) 1527 for so in split_orders: 1528 if so not in order_hierarchy.keys(): 1529 order_weights[so]=0 1530 1531 # Now order the keys of diag_by_so by the WEIGHT of the split_orders 1532 # (and only those, the orders not included in the split_orders do not 1533 # count for this ordering as they could be mixed in any given group). 1534 so_keys = sorted(so_keys, key = lambda elem: (sum([power*order_weights[\ 1535 split_orders[i]] for i,power in enumerate(elem)]))) 1536 1537 # Now put the diagram back, ordered this time, in diag_set 1538 diag_set[:] = [] 1539 for so_key in so_keys: 1540 diag_set.extend(diag_by_so[so_key])
1541 1542
1543 - def order_diagrams_according_to_split_orders(self, split_orders):
1544 """ Reorder the loop and Born diagrams (if any) in group of diagrams 1545 sharing the same coupling orders are put together and these groups are 1546 order in decreasing WEIGHTED orders. 1547 Notice that this function is only called for now by the 1548 LoopHelasMatrixElement instances at the output stage. 1549 """ 1550 1551 # If no split order is present (unlikely since the 'corrected order' 1552 # normally is a split_order by default, then do nothing 1553 if len(split_orders)==0: 1554 return 1555 1556 self.order_diagram_set(self['born_diagrams'], split_orders) 1557 self.order_diagram_set(self['loop_diagrams'], split_orders) 1558 self.order_diagram_set(self['loop_UVCT_diagrams'], split_orders)
1559
1560 #=============================================================================== 1561 # LoopMultiProcess 1562 #=============================================================================== 1563 -class LoopMultiProcess(diagram_generation.MultiProcess):
1564 """LoopMultiProcess: MultiProcess with loop features. 1565 """ 1566 1567 @classmethod
1568 - def get_amplitude_from_proc(cls, proc):
1569 """ Return the correct amplitude type according to the characteristics 1570 of the process proc """ 1571 return LoopAmplitude({"process": proc})
1572