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