Package madgraph :: Package core :: Module helas_objects
[hide private]
[frames] | no frames]

Source Code for Module madgraph.core.helas_objects

   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   
  16  """Definitions of objects used to generate language-independent Helas 
  17  calls: HelasWavefunction, HelasAmplitude, HelasDiagram for the 
  18  generation of wavefunctions and amplitudes, HelasMatrixElement and 
  19  HelasMultiProcess for generation of complete matrix elements for 
  20  single and multiple processes; and HelasModel, which is the 
  21  language-independent base class for the language-specific classes for 
  22  writing Helas calls, found in the iolibs directory""" 
  23   
  24  import array 
  25  import copy 
  26  import collections 
  27  import logging 
  28  import itertools 
  29  import math 
  30   
  31  import aloha 
  32   
  33  import madgraph.core.base_objects as base_objects 
  34  import madgraph.core.diagram_generation as diagram_generation 
  35  import madgraph.core.color_amp as color_amp 
  36  import madgraph.loop.loop_diagram_generation as loop_diagram_generation 
  37  import madgraph.loop.loop_color_amp as loop_color_amp 
  38  import madgraph.core.color_algebra as color 
  39  import madgraph.various.misc as misc 
  40   
  41  from madgraph import InvalidCmd, MadGraph5Error 
  42   
  43  #=============================================================================== 
  44  #  
  45  #=============================================================================== 
  46   
  47  logger = logging.getLogger('madgraph.helas_objects') 
48 49 #=============================================================================== 50 # DiagramTag class to identify matrix elements 51 #=============================================================================== 52 53 -class IdentifyMETag(diagram_generation.DiagramTag):
54 """DiagramTag daughter class to identify processes with identical 55 matrix elements. Need to compare leg number, color, lorentz, 56 coupling, state, spin, self_antipart, mass, width, color, decay 57 and is_part. 58 59 Note that we also need to check that the processes agree on 60 has_mirror_process, process id, and 61 identical_particle_factor. Don't allow combining decay chains. 62 63 We also don't want to combine processes with different possibly 64 onshell s-channel propagators (i.e., with non-zero width and 65 onshell=True or None) since we want the right propagator written 66 in the event file in the end. This is done by the vertex.""" 67 68 # dec_number is used to separate between decay chains. 69 # This is needed since we don't want to merge different decays, 70 # in order to get the right factor for identical/non-identical particles 71 dec_number = 1 72 73 @classmethod
74 - def create_tag(cls, amplitude, identical_particle_factor = 0):
75 """Create a tag which identifies identical matrix elements""" 76 process = amplitude.get('process') 77 ninitial = process.get_ninitial() 78 model = process.get('model') 79 dc = 0 80 if process.get('is_decay_chain'): 81 dc = cls.dec_number 82 cls.dec_number += 1 83 if not identical_particle_factor: 84 identical_particle_factor = process.identical_particle_factor() 85 if process.get('perturbation_couplings') and \ 86 process.get('NLO_mode') not in ['virt', 'loop','noborn']: 87 sorted_tags = sorted([IdentifyMETagFKS(d, model, ninitial) for d in \ 88 amplitude.get('diagrams')]) 89 elif process.get('NLO_mode')=='noborn': 90 # For loop-induced processes, make sure to create the Tag based on 91 # the contracted diagram 92 sorted_tags = sorted([cls(d.get_contracted_loop_diagram(model, 93 amplitude.get('structure_repository')), model, ninitial) for d in \ 94 amplitude.get('diagrams')]) 95 else: 96 sorted_tags = sorted([cls(d, model, ninitial) for d in \ 97 amplitude.get('diagrams')]) 98 99 # Do not use this for loop diagrams as for now the HelasMultiProcess 100 # always contain only exactly one loop amplitude. 101 if sorted_tags and not isinstance(amplitude, \ 102 loop_diagram_generation.LoopAmplitude): 103 # Need to keep track of relative permutations for all diagrams, 104 # to make sure we indeed have different matrix elements, 105 # and not just identical diagrams disregarding particle order. 106 # However, identical particles should be treated symmetrically. 107 exts = sorted_tags[0].get_external_numbers() 108 comp_dict = IdentifyMETag.prepare_comp_dict(process, exts) 109 perms = [array.array('H', 110 sum([comp_dict[n] for n in p.get_external_numbers()], [])) 111 for p in sorted_tags[1:]] 112 else: 113 perms = [] 114 115 return [amplitude.get('has_mirror_process'), 116 process.get('id'), 117 process.get('is_decay_chain'), 118 identical_particle_factor, 119 dc, 120 perms, 121 sorted_tags]
122 123 @staticmethod
124 - def prepare_comp_dict(process, numbers):
125 """Prepare a dictionary from leg number to [positions] in such 126 a way that identical particles are treated in a symmetric way""" 127 128 # Crate dictionary from leg number to position 129 start_perm_dict = dict([(n,i) for (i,n) in enumerate(numbers)]) 130 131 # Ids for process legs 132 legs = [l.get('id') for l in sorted(process.get_legs_with_decays())] 133 if process.get('is_decay_chain'): 134 legs.insert(0,process.get('legs')[0].get('id')) 135 ninitial = len([l for l in process.get('legs') if \ 136 not l.get('state')]) 137 # Dictionary from leg id to position for final-state legs 138 id_num_dict = {} 139 for n in start_perm_dict.keys(): 140 if n > ninitial: 141 id_num_dict.setdefault(legs[n-1], []).append(\ 142 start_perm_dict[n]) 143 # Make each entry in start_perm_dict independent of position of 144 # identical particles by including all positions 145 for n in start_perm_dict.keys(): 146 if n <= ninitial: 147 # Just turn it into a list 148 start_perm_dict[n] = [start_perm_dict[n]] 149 else: 150 # Turn into list of positions for identical particles 151 start_perm_dict[n] = sorted(id_num_dict[legs[n-1]]) 152 153 return start_perm_dict
154 155 @staticmethod 174 175 @staticmethod
176 - def vertex_id_from_vertex(vertex, last_vertex, model, ninitial):
177 """Returns the info needed to identify matrix elements: 178 interaction color, lorentz, coupling, and wavefunction spin, 179 self_antipart, mass, width, color, decay and is_part, plus PDG 180 code if possible onshell s-channel prop. Note that is_part 181 and PDG code needs to be flipped if we move the final vertex around.""" 182 183 if vertex.get('id') in [0,-1]: 184 return ((vertex.get('id'),),) 185 186 if vertex.get('id') == -2: 187 ret_list = vertex.get('loop_tag') 188 else: 189 inter = model.get_interaction(vertex.get('id')) 190 coup_keys = sorted(inter.get('couplings').keys()) 191 ret_list = tuple([(key, inter.get('couplings')[key]) for key in \ 192 coup_keys] + \ 193 [str(c) for c in inter.get('color')] + \ 194 inter.get('lorentz')+sorted(inter.get('orders'))) 195 196 if last_vertex: 197 return ((ret_list,),) 198 else: 199 part = model.get_particle(vertex.get('legs')[-1].get('id')) 200 # If we have possibly onshell s-channel particles with 201 # identical properties but different PDG code, split the 202 # processes to ensure that we write the correct resonance 203 # in the event file 204 s_pdg = vertex.get_s_channel_id(model, ninitial) 205 if s_pdg and (part.get('width').lower() == 'zero' or \ 206 vertex.get('legs')[-1].get('onshell') == False): 207 s_pdg = 0 208 return (((part.get('spin'), part.get('color'), 209 part.get('self_antipart'), 210 part.get('mass'), part.get('width'), s_pdg), 211 ret_list),)
212 213 @staticmethod
214 - def flip_vertex(new_vertex, old_vertex, links):
215 """Move the wavefunction part of vertex id appropriately""" 216 217 if len(new_vertex[0]) == 1 and len(old_vertex[0]) == 2: 218 # We go from a last link to next-to-last link - add propagator info 219 return ((old_vertex[0][0],new_vertex[0][0]),) 220 elif len(new_vertex[0]) == 2 and len(old_vertex[0]) == 1: 221 # We go from next-to-last link to last link - remove propagator info 222 return ((new_vertex[0][1],),) 223 # We should not get here 224 assert(False)
225
226 227 -class IdentifyMETagFKS(IdentifyMETag):
228 """on top of what IdentifyMETag, the diagram tags also have the charge 229 difference along the fermionic flow in them for initial state legs.""" 230
231 - def __init__(self, diagram, model = None, ninitial = 2):
232 self.flow_charge_diff = diagram.get_flow_charge_diff(model) 233 super(IdentifyMETagFKS, self).__init__(diagram, model, ninitial)
234
235 - def __eq__(self, other):
236 return super(IdentifyMETag, self).__eq__(other) and \ 237 self.flow_charge_diff == other.flow_charge_diff
238
239 240 -class IdentifyMETagMadSpin(IdentifyMETag):
241 """Should ensure that the splitting is the same with and without decay 242 So we want to combine processes with different possibly 243 onshell s-channel propagators. This was done by the vertex. 244 """ 245 246 @staticmethod
247 - def vertex_id_from_vertex(vertex, last_vertex, model, ninitial):
248 """Returns the info needed to identify matrix elements: 249 interaction color, lorentz, coupling, and wavefunction spin, 250 self_antipart, mass, width, color, decay and is_part. 251 BUT NOT PDG code for possible onshell s-channel prop.""" 252 if last_vertex: 253 return IdentifyMETag.vertex_id_from_vertex(vertex, last_vertex, model, ninitial) 254 import random 255 data = IdentifyMETag.vertex_id_from_vertex(vertex, last_vertex, model, ninitial) 256 ((spin, color, selfanti, mass, width, pdg), ret_list) = data 257 return ((spin, color, selfanti, mass, width, random.random()), ret_list)
258
259 260 #=============================================================================== 261 # DiagramTag class to create canonical order configs 262 #=============================================================================== 263 264 -class CanonicalConfigTag(diagram_generation.DiagramTag):
265 """DiagramTag daughter class to create canonical order of 266 config. Need to compare leg number, mass, width, and color. 267 Also implement find s- and t-channels from the tag. 268 Warning! The sorting in this tag must be identical to that of 269 IdentifySGConfigTag in diagram_symmetry.py (apart from leg number) 270 to make sure symmetry works!""" 271 272
273 - def get_s_and_t_channels(self, ninitial, model, new_pdg, max_final_leg = 2):
274 """Get s and t channels from the tag, as two lists of vertices 275 ordered from the outermost s-channel and in/down towards the highest 276 number initial state leg. 277 Algorithm: Start from the final tag. Check for final leg number for 278 all links and move in the direction towards leg 2 (or 1, if 1 and 2 279 are in the same direction). 280 """ 281 282 final_leg = min(ninitial, max_final_leg) 283 284 # Look for final leg numbers in all links 285 done = [l for l in self.tag.links if \ 286 l.end_link and l.links[0][1][0] == final_leg] 287 while not done: 288 # Identify the chain closest to final_leg 289 right_num = -1 290 for num, link in enumerate(self.tag.links): 291 if len(link.vertex_id) == 3 and \ 292 link.vertex_id[1][-1] == final_leg: 293 right_num = num 294 if right_num == -1: 295 # We need to look for leg number 1 instead 296 for num, link in enumerate(self.tag.links): 297 if len(link.vertex_id) == 3 and \ 298 link.vertex_id[1][-1] == 1: 299 right_num = num 300 if right_num == -1: 301 # This should never happen 302 raise diagram_generation.DiagramTag.DiagramTagError, \ 303 "Error in CanonicalConfigTag, no link with number 1 or 2." 304 305 # Now move one step in the direction of right_link 306 right_link = self.tag.links[right_num] 307 # Create a new link corresponding to moving one step 308 new_links = list(self.tag.links[:right_num]) + \ 309 list(self.tag.links[right_num + 1:]) 310 311 new_link = diagram_generation.DiagramTagChainLink(\ 312 new_links, 313 self.flip_vertex(\ 314 self.tag.vertex_id, 315 right_link.vertex_id, 316 new_links)) 317 318 # Create a new final vertex in the direction of the right_link 319 other_links = list(right_link.links) + [new_link] 320 other_link = diagram_generation.DiagramTagChainLink(\ 321 other_links, 322 self.flip_vertex(\ 323 right_link.vertex_id, 324 self.tag.vertex_id, 325 other_links)) 326 327 self.tag = other_link 328 done = [l for l in self.tag.links if \ 329 l.end_link and l.links[0][1][0] == final_leg] 330 331 # Construct a diagram from the resulting tag 332 diagram = self.diagram_from_tag(model) 333 334 # Go through the vertices and add them to s-channel or t-channel 335 schannels = base_objects.VertexList() 336 tchannels = base_objects.VertexList() 337 338 for vert in diagram.get('vertices')[:-1]: 339 if vert.get('legs')[-1].get('number') > ninitial: 340 schannels.append(vert) 341 else: 342 tchannels.append(vert) 343 344 # Need to make sure leg number 2 is always last in last vertex 345 lastvertex = diagram.get('vertices')[-1] 346 legs = lastvertex.get('legs') 347 leg2 = [l.get('number') for l in legs].index(final_leg) 348 legs.append(legs.pop(leg2)) 349 if ninitial == 2: 350 # Last vertex always counts as t-channel 351 tchannels.append(lastvertex) 352 else: 353 legs[-1].set('id', 354 model.get_particle(legs[-1].get('id')).get_anti_pdg_code()) 355 schannels.append(lastvertex) 356 357 # Split up multiparticle vertices using fake s-channel propagators 358 multischannels = [(i, v) for (i, v) in enumerate(schannels) \ 359 if len(v.get('legs')) > 3] 360 multitchannels = [(i, v) for (i, v) in enumerate(tchannels) \ 361 if len(v.get('legs')) > 3] 362 363 increase = 0 364 for channel in multischannels + multitchannels: 365 newschannels = [] 366 vertex = channel[1] 367 while len(vertex.get('legs')) > 3: 368 # Pop the first two legs and create a new 369 # s-channel from them 370 popped_legs = \ 371 base_objects.LegList([vertex.get('legs').pop(0) \ 372 for i in [0,1]]) 373 popped_legs.append(base_objects.Leg({\ 374 'id': new_pdg, 375 'number': min([l.get('number') for l in popped_legs]), 376 'state': True, 377 'onshell': None})) 378 379 new_vertex = base_objects.Vertex({ 380 'id': vertex.get('id'), 381 'legs': popped_legs}) 382 383 # Insert the new s-channel before this vertex 384 if channel in multischannels: 385 schannels.insert(channel[0]+increase, new_vertex) 386 # Account for previous insertions 387 increase += 1 388 else: 389 schannels.append(new_vertex) 390 legs = vertex.get('legs') 391 # Insert the new s-channel into vertex 392 legs.insert(0, copy.copy(popped_legs[-1])) 393 # Renumber resulting leg according to minimum leg number 394 legs[-1].set('number', min([l.get('number') for l in legs[:-1]])) 395 396 # Finally go through all vertices, sort the legs and replace 397 # leg number with propagator number -1, -2, ... 398 number_dict = {} 399 nprop = 0 400 for vertex in schannels + tchannels: 401 # Sort the legs 402 legs = vertex.get('legs')[:-1] 403 if vertex in schannels: 404 legs.sort(lambda l1, l2: l2.get('number') - \ 405 l1.get('number')) 406 else: 407 legs.sort(lambda l1, l2: l1.get('number') - \ 408 l2.get('number')) 409 for ileg,leg in enumerate(legs): 410 newleg = copy.copy(leg) 411 try: 412 newleg.set('number', number_dict[leg.get('number')]) 413 except KeyError: 414 pass 415 else: 416 legs[ileg] = newleg 417 nprop = nprop - 1 418 last_leg = copy.copy(vertex.get('legs')[-1]) 419 number_dict[last_leg.get('number')] = nprop 420 last_leg.set('number', nprop) 421 legs.append(last_leg) 422 vertex.set('legs', base_objects.LegList(legs)) 423 424 return schannels, tchannels
425 426 @staticmethod 443 444 @staticmethod
445 - def vertex_id_from_vertex(vertex, last_vertex, model, ninitial):
446 """Returns the info needed to identify configs: 447 interaction color, mass, width. Also provide propagator PDG code. 448 The third element of the tuple vertex_id serves to store potential 449 necessary information regarding the shrunk loop.""" 450 451 if isinstance(vertex,base_objects.ContractedVertex): 452 inter = None 453 # I don't add here the 'loop_tag' because it is heavy and in principle 454 # not necessary. It can however be added in the future if proven 455 # necessary. 456 loop_info = {'PDGs':vertex.get('PDGs'), 457 'loop_orders':vertex.get('loop_orders')} 458 else: 459 # Not that it is going to be used here, but it might be eventually 460 inter = model.get_interaction(vertex.get('id')) 461 loop_info = {} 462 463 if last_vertex: 464 return ((0,), 465 (vertex.get('id'), 466 min([l.get('number') for l in vertex.get('legs')])), 467 loop_info) 468 else: 469 part = model.get_particle(vertex.get('legs')[-1].get('id')) 470 return ((part.get('color'), 471 part.get('mass'), part.get('width')), 472 (vertex.get('id'), 473 vertex.get('legs')[-1].get('onshell'), 474 vertex.get('legs')[-1].get('number')), 475 loop_info)
476 477 @staticmethod
478 - def flip_vertex(new_vertex, old_vertex, links):
479 """Move the wavefunction part of propagator id appropriately""" 480 481 # Find leg numbers for new vertex 482 min_number = min([l.vertex_id[1][-1] for l in links if not l.end_link]\ 483 + [l.links[0][1][0] for l in links if l.end_link]) 484 485 if len(new_vertex[0]) == 1 and len(old_vertex[0]) > 1: 486 # We go from a last link to next-to-last link 487 return (old_vertex[0], 488 (new_vertex[1][0], old_vertex[1][1], min_number), new_vertex[2]) 489 elif len(new_vertex[0]) > 1 and len(old_vertex[0]) == 1: 490 # We go from next-to-last link to last link - remove propagator info 491 return (old_vertex[0], (new_vertex[1][0], min_number), new_vertex[2]) 492 493 # We should not get here 494 raise diagram_generation.DiagramTag.DiagramTagError, \ 495 "Error in CanonicalConfigTag, wrong setup of vertices in link."
496 497 @staticmethod 510 511 @classmethod 530 531 @staticmethod
532 - def id_from_vertex_id(vertex_id):
533 """Return the numerical vertex id from a link.vertex_id""" 534 535 return vertex_id[1][0]
536
537 538 #=============================================================================== 539 # HelasWavefunction 540 #=============================================================================== 541 -class HelasWavefunction(base_objects.PhysicsObject):
542 """HelasWavefunction object, has the information necessary for 543 writing a call to a HELAS wavefunction routine: the PDG number, 544 all relevant particle information, a list of mother wavefunctions, 545 interaction id, all relevant interaction information, fermion flow 546 state, wavefunction number 547 """ 548 549 supported_analytical_info = ['wavefunction_rank','interaction_rank'] 550 551 552 @staticmethod
553 - def spin_to_size(spin):
554 """ Returns the size of a wavefunction (i.e. number of element) carrying 555 a particle with spin 'spin' """ 556 557 sizes = {1:1,2:4,3:4,4:16,5:16} 558 try: 559 return sizes[abs(spin)] 560 except KeyError: 561 raise MadGraph5Error, "L-cut particle has spin %d which is not supported."%spin
562
563 - def default_setup(self):
564 """Default values for all properties""" 565 566 # Properties related to the particle propagator 567 # For an electron, would have the following values 568 # pdg_code = 11 569 # name = 'e-' 570 # antiname = 'e+' 571 # spin = '1' defined as 2 x spin + 1 572 # color = '1' 1= singlet, 3 = triplet, 8=octet 573 # mass = 'zero' 574 # width = 'zero' 575 # is_part = 'true' Particle not antiparticle 576 # self_antipart='false' gluon, photo, h, or majorana would be true 577 self['particle'] = base_objects.Particle() 578 self['antiparticle'] = base_objects.Particle() 579 self['is_part'] = True 580 # Properties related to the interaction generating the propagator 581 # For an e- produced from an e+e-A vertex would have the following 582 # proporties: 583 # interaction_id = the id of the interaction in the model 584 # pdg_codes = the pdg_codes property of the interaction, [11, -11, 22] 585 # inter_color = the 'color' property of the interaction: [] 586 # lorentz = the 'lorentz' property of the interaction: ('') 587 # couplings = the coupling names from the interaction: {(0,0):'MGVX12'} 588 self['interaction_id'] = 0 589 self['pdg_codes'] = [] 590 self['orders'] = {} 591 self['inter_color'] = None 592 self['lorentz'] = [] 593 self['coupling'] = ['none'] 594 # The color index used in this wavefunction 595 self['color_key'] = 0 596 # Properties relating to the leg/vertex 597 # state = initial/final (for external bosons), 598 # intermediate (for intermediate bosons), 599 # incoming/outgoing (for fermions) 600 # leg_state = initial/final for initial/final legs 601 # intermediate for non-external wavefunctions 602 # number_external = the 'number' property of the corresponding Leg, 603 # corresponds to the number of the first external 604 # particle contributing to this leg 605 # fermionflow = 1 fermions have +-1 for flow (bosons always +1), 606 # -1 is used only if there is a fermion flow clash 607 # due to a Majorana particle 608 # is_loop = logical true if this function builds a loop or belong 609 # to an external structure. 610 self['state'] = 'initial' 611 self['leg_state'] = True 612 self['mothers'] = HelasWavefunctionList() 613 self['number_external'] = 0 614 self['number'] = 0 615 self['me_id'] = 0 616 self['fermionflow'] = 1 617 self['is_loop'] = False 618 # Some analytical information about the interaction and wavefunction 619 # can be cached here in the form of a dictionary. 620 # An example of a key of this dictionary is 'rank' which is used in the 621 # open loop method and stores the rank of the polynomial describing the 622 # loop numerator up to this loop wavefunction. 623 # See the class variable 'supported_analytical_info' for the list of 624 # supporter analytical information 625 self['analytic_info'] = {} 626 # The lcut_size stores the size of the L-Cut wavefunction at the origin 627 # of the loopwavefunction. 628 self['lcut_size']=None 629 # The decay flag is used in processes with defined decay chains, 630 # to indicate that this wavefunction has a decay defined 631 self['decay'] = False 632 # The onshell flag is used in processes with defined decay 633 # chains, to indicate that this wavefunction is decayed and 634 # should be onshell (True), as well as for forbidden s-channels (False). 635 # Default is None 636 self['onshell'] = None 637 # conjugate_indices is a list [1,2,...] with fermion lines 638 # that need conjugates. Default is "None" 639 self['conjugate_indices'] = None
640 641 # Customized constructor
642 - def __init__(self, *arguments):
643 """Allow generating a HelasWavefunction from a Leg 644 """ 645 646 if len(arguments) > 2: 647 if isinstance(arguments[0], base_objects.Leg) and \ 648 isinstance(arguments[1], int) and \ 649 isinstance(arguments[2], base_objects.Model): 650 super(HelasWavefunction, self).__init__() 651 leg = arguments[0] 652 interaction_id = arguments[1] 653 model = arguments[2] 654 # decay_ids is the pdg codes for particles with decay 655 # chains defined 656 decay_ids = [] 657 if len(arguments) > 3: 658 decay_ids = arguments[3] 659 self.set('particle', leg.get('id'), model) 660 self.set('number_external', leg.get('number')) 661 self.set('number', leg.get('number')) 662 self.set('is_loop', leg.get('loop_line')) 663 self.set('state', {False: 'initial', True: 'final'}[leg.get('state')]) 664 if leg.get('onshell') == False: 665 # Denotes forbidden s-channel 666 self.set('onshell', leg.get('onshell')) 667 self.set('leg_state', leg.get('state')) 668 # Need to set 'decay' to True for particles which will be 669 # decayed later, in order to not combine such processes 670 # although they might have identical matrix elements before 671 # the decay is applied 672 if self['state'] == 'final' and self.get('pdg_code') in decay_ids: 673 self.set('decay', True) 674 675 # Set fermion flow state. Initial particle and final 676 # antiparticle are incoming, and vice versa for 677 # outgoing 678 if self.is_fermion(): 679 if leg.get('state') == False and \ 680 self.get('is_part') or \ 681 leg.get('state') == True and \ 682 not self.get('is_part'): 683 self.set('state', 'incoming') 684 else: 685 self.set('state', 'outgoing') 686 self.set('interaction_id', interaction_id, model) 687 elif arguments: 688 super(HelasWavefunction, self).__init__(arguments[0]) 689 else: 690 super(HelasWavefunction, self).__init__()
691
692 - def filter(self, name, value):
693 """Filter for valid wavefunction property values.""" 694 695 if name in ['particle', 'antiparticle']: 696 if not isinstance(value, base_objects.Particle): 697 raise self.PhysicsObjectError, \ 698 "%s tag %s is not a particle" % (name, repr(value)) 699 700 if name == 'is_part': 701 if not isinstance(value, bool): 702 raise self.PhysicsObjectError, \ 703 "%s tag %s is not a boolean" % (name, repr(value)) 704 705 if name == 'interaction_id': 706 if not isinstance(value, int): 707 raise self.PhysicsObjectError, \ 708 "%s is not a valid integer " % str(value) + \ 709 " for wavefunction interaction id" 710 711 if name == 'pdg_codes': 712 #Should be a list of strings 713 if not isinstance(value, list): 714 raise self.PhysicsObjectError, \ 715 "%s is not a valid list of integers" % str(value) 716 for mystr in value: 717 if not isinstance(mystr, int): 718 raise self.PhysicsObjectError, \ 719 "%s is not a valid integer" % str(mystr) 720 721 if name == 'orders': 722 #Should be a dict with valid order names ask keys and int as values 723 if not isinstance(value, dict): 724 raise self.PhysicsObjectError, \ 725 "%s is not a valid dict for coupling orders" % \ 726 str(value) 727 for order in value.keys(): 728 if not isinstance(order, str): 729 raise self.PhysicsObjectError, \ 730 "%s is not a valid string" % str(order) 731 if not isinstance(value[order], int): 732 raise self.PhysicsObjectError, \ 733 "%s is not a valid integer" % str(value[order]) 734 735 736 if name == 'inter_color': 737 # Should be None or a color string 738 if value and not isinstance(value, color.ColorString): 739 raise self.PhysicsObjectError, \ 740 "%s is not a valid Color String" % str(value) 741 742 if name == 'lorentz': 743 #Should be a list of string 744 if not isinstance(value, list): 745 raise self.PhysicsObjectError, \ 746 "%s is not a valid list" % str(value) 747 for name in value: 748 if not isinstance(name, str): 749 raise self.PhysicsObjectError, \ 750 "%s doesn't contain only string" % str(value) 751 752 if name == 'coupling': 753 #Should be a list of string 754 if not isinstance(value, list): 755 raise self.PhysicsObjectError, \ 756 "%s is not a valid coupling string" % str(value) 757 for name in value: 758 if not isinstance(name, str): 759 raise self.PhysicsObjectError, \ 760 "%s doesn't contain only string" % str(value) 761 if len(value) == 0: 762 raise self.PhysicsObjectError, \ 763 "%s should have at least one value" % str(value) 764 765 if name == 'color_key': 766 if value and not isinstance(value, int): 767 raise self.PhysicsObjectError, \ 768 "%s is not a valid integer" % str(value) 769 770 if name == 'state': 771 if not isinstance(value, str): 772 raise self.PhysicsObjectError, \ 773 "%s is not a valid string for wavefunction state" % \ 774 str(value) 775 if value not in ['incoming', 'outgoing', 776 'intermediate', 'initial', 'final']: 777 raise self.PhysicsObjectError, \ 778 "%s is not a valid wavefunction " % str(value) + \ 779 "state (incoming|outgoing|intermediate)" 780 if name == 'leg_state': 781 if value not in [False, True]: 782 raise self.PhysicsObjectError, \ 783 "%s is not a valid wavefunction " % str(value) + \ 784 "state (incoming|outgoing|intermediate)" 785 if name in ['fermionflow']: 786 if not isinstance(value, int): 787 raise self.PhysicsObjectError, \ 788 "%s is not a valid integer" % str(value) 789 if not value in [-1, 1]: 790 raise self.PhysicsObjectError, \ 791 "%s is not a valid sign (must be -1 or 1)" % str(value) 792 793 if name in ['number_external', 'number']: 794 if not isinstance(value, int): 795 raise self.PhysicsObjectError, \ 796 "%s is not a valid integer" % str(value) + \ 797 " for wavefunction number" 798 799 if name == 'mothers': 800 if not isinstance(value, HelasWavefunctionList): 801 raise self.PhysicsObjectError, \ 802 "%s is not a valid list of mothers for wavefunction" % \ 803 str(value) 804 805 if name in ['decay']: 806 if not isinstance(value, bool): 807 raise self.PhysicsObjectError, \ 808 "%s is not a valid bool" % str(value) + \ 809 " for decay" 810 811 if name in ['onshell']: 812 if not isinstance(value, bool): 813 raise self.PhysicsObjectError, \ 814 "%s is not a valid bool" % str(value) + \ 815 " for onshell" 816 817 if name in ['is_loop']: 818 if not isinstance(value, bool): 819 raise self.PhysicsObjectError, \ 820 "%s is not a valid bool" % str(value) + \ 821 " for is_loop" 822 823 if name == 'conjugate_indices': 824 if not isinstance(value, tuple) and value != None: 825 raise self.PhysicsObjectError, \ 826 "%s is not a valid tuple" % str(value) + \ 827 " for conjugate_indices" 828 829 if name == 'rank': 830 if not isinstance(value, int) and value != None: 831 raise self.PhysicsObjectError, \ 832 "%s is not a valid int" % str(value) + \ 833 " for the rank" 834 835 if name == 'lcut_size': 836 if not isinstance(value, int) and value != None: 837 raise self.PhysicsObjectError, \ 838 "%s is not a valid int" % str(value) + \ 839 " for the lcut_size" 840 841 return True
842 843 # Enhanced get function, where we can directly call the properties of the particle
844 - def get(self, name):
845 """When calling any property related to the particle, 846 automatically call the corresponding property of the particle.""" 847 848 # Set conjugate_indices if it's not already set 849 if name == 'conjugate_indices' and self[name] == None: 850 self['conjugate_indices'] = self.get_conjugate_index() 851 852 if name == 'lcut_size' and self[name] == None: 853 self['lcut_size'] = self.get_lcut_size() 854 855 if name in ['spin', 'mass', 'width', 'self_antipart']: 856 return self['particle'].get(name) 857 elif name == 'pdg_code': 858 return self['particle'].get_pdg_code() 859 elif name == 'color': 860 return self['particle'].get_color() 861 elif name == 'name': 862 return self['particle'].get_name() 863 elif name == 'antiname': 864 return self['particle'].get_anti_name() 865 elif name == 'me_id': 866 out = super(HelasWavefunction, self).get(name) 867 if out: 868 return out 869 else: 870 return super(HelasWavefunction, self).get('number') 871 else: 872 return super(HelasWavefunction, self).get(name)
873 874 875 # Enhanced set function, where we can append a model
876 - def set(self, *arguments):
877 """When setting interaction_id, if model is given (in tuple), 878 set all other interaction properties. When setting pdg_code, 879 if model is given, set all other particle properties.""" 880 881 assert len(arguments) >1, "Too few arguments for set" 882 883 name = arguments[0] 884 value = arguments[1] 885 886 if len(arguments) > 2 and \ 887 isinstance(value, int) and \ 888 isinstance(arguments[2], base_objects.Model): 889 model = arguments[2] 890 if name == 'interaction_id': 891 self.set('interaction_id', value) 892 if value > 0: 893 inter = model.get('interaction_dict')[value] 894 self.set('pdg_codes', 895 [part.get_pdg_code() for part in \ 896 inter.get('particles')]) 897 self.set('orders', inter.get('orders')) 898 # Note that the following values might change, if 899 # the relevant color/lorentz/coupling is not index 0 900 if inter.get('color'): 901 self.set('inter_color', inter.get('color')[0]) 902 if inter.get('lorentz'): 903 self.set('lorentz', [inter.get('lorentz')[0]]) 904 if inter.get('couplings'): 905 self.set('coupling', [inter.get('couplings').values()[0]]) 906 return True 907 elif name == 'particle': 908 self.set('particle', model.get('particle_dict')[value]) 909 self.set('is_part', self['particle'].get('is_part')) 910 if self['particle'].get('self_antipart'): 911 self.set('antiparticle', self['particle']) 912 else: 913 self.set('antiparticle', model.get('particle_dict')[-value]) 914 return True 915 else: 916 raise self.PhysicsObjectError, \ 917 "%s not allowed name for 3-argument set", name 918 else: 919 return super(HelasWavefunction, self).set(name, value)
920
921 - def get_sorted_keys(self):
922 """Return particle property names as a nicely sorted list.""" 923 924 return ['particle', 'antiparticle', 'is_part', 925 'interaction_id', 'pdg_codes', 'orders', 'inter_color', 926 'lorentz', 'coupling', 'color_key', 'state', 'number_external', 927 'number', 'fermionflow', 'mothers', 'is_loop']
928 929 # Helper functions 930
931 - def flip_part_antipart(self):
932 """Flip between particle and antiparticle.""" 933 part = self.get('particle') 934 self.set('particle', self.get('antiparticle')) 935 self.set('antiparticle', part)
936
937 - def is_anticommutating_ghost(self):
938 """ Return True if the particle of this wavefunction is a ghost""" 939 return self.get('particle').get('ghost')
940
941 - def is_fermion(self):
942 return self.get('spin') % 2 == 0
943
944 - def is_boson(self):
945 return not self.is_fermion()
946
947 - def is_majorana(self):
948 return self.is_fermion() and self.get('self_antipart')
949
950 - def get_analytic_info(self, info, alohaModel=None):
951 """ Returns a given analytic information about this loop wavefunction or 952 its characterizing interaction. The list of available information is in 953 the HelasWavefunction class variable 'supported_analytical_info'. An 954 example of analytic information is the 'interaction_rank', corresponding 955 to the power of the loop momentum q brought by the interaction 956 and propagator from which this loop wavefunction originates. This 957 is done in a general way by having aloha analyzing the lorentz structure 958 used. 959 Notice that if one knows that this analytic information has already been 960 computed before (for example because a call to compute_analytic_information 961 has been performed before, then alohaModel is not required since 962 the result can be recycled.""" 963 # This function makes no sense if not called for a loop interaction. 964 # At least for now 965 assert(self.get('is_loop')) 966 # Check that the required information is supported 967 assert(info in self.supported_analytical_info) 968 969 # Try to recycle the information 970 try: 971 return self['analytic_info'][info] 972 except KeyError: 973 # It then need be computed and for this, an alohaModel is necessary 974 if alohaModel is None: 975 raise MadGraph5Error,"The analytic information %s has"%info+\ 976 " not been computed yet for this wavefunction and an"+\ 977 " alohaModel was not specified, so that the information"+\ 978 " cannot be retrieved." 979 980 result = None 981 982 if info=="interaction_rank" and len(self['mothers'])==0: 983 # It is of course zero for an external particle 984 result = 0 985 986 elif info=="interaction_rank": 987 # To get advanced analytic information about the interaction, aloha 988 # has to work as if in the optimized output, namely treat individually 989 # the loop momentum from the rest of the contributions. 990 # The 'L' tag is therefore always followed by an integer representing 991 # the place of the loop wavefunction in the mothers. 992 # For this, optimized_output is set to True below, no matter what. 993 aloha_info = self.get_aloha_info(True) 994 # aloha_info[0] is the tuple of all lorent structures for this lwf, 995 # aloha_info[1] are the tags and aloha_info[2] is the outgoing number. 996 max_rank = max([ alohaModel.get_info('rank', lorentz, 997 aloha_info[2], aloha_info[1], cached=True) 998 for lorentz in aloha_info[0] ]) 999 result = max_rank 1000 1001 elif info=="wavefunction_rank": 1002 # wavefunction_rank is the sum of all the interaction rank 1003 # from the history of open-loop call. 1004 loop_mothers=[wf for wf in self['mothers'] if wf['is_loop']] 1005 if len(loop_mothers)==0: 1006 # It is an external loop wavefunction 1007 result = 0 1008 elif len(loop_mothers)==1: 1009 result=loop_mothers[0].get_analytic_info('wavefunction_rank', 1010 alohaModel) 1011 result = result+self.get_analytic_info('interaction_rank', 1012 alohaModel) 1013 else: 1014 raise MadGraph5Error, "A loop wavefunction has more than one loop"+\ 1015 " mothers." 1016 1017 # Now cache the resulting analytic info 1018 self['analytic_info'][info] = result 1019 1020 return result
1021
1022 - def compute_analytic_information(self, alohaModel):
1023 """ Make sure that all analytic pieces of information about this 1024 wavefunction are computed so that they can be recycled later, typically 1025 without the need of specifying an alohaModel.""" 1026 1027 for analytic_info in self.supported_analytical_info: 1028 self.get_analytic_info(analytic_info, alohaModel)
1029
1030 - def to_array(self):
1031 """Generate an array with the information needed to uniquely 1032 determine if a wavefunction has been used before: interaction 1033 id and mother wavefunction numbers.""" 1034 1035 # Identification based on interaction id 1036 array_rep = array.array('i', [self['interaction_id']]) 1037 # Need the coupling key, to distinguish between 1038 # wavefunctions from the same interaction but different 1039 # color structures 1040 array_rep.append(self['color_key']) 1041 # Also need to specify if it is a loop wf 1042 array_rep.append(int(self['is_loop'])) 1043 1044 # Finally, the mother numbers 1045 array_rep.extend([mother['number'] for \ 1046 mother in self['mothers']]) 1047 1048 return array_rep
1049
1050 - def get_pdg_code(self):
1051 """Generate the corresponding pdg_code for an outgoing particle, 1052 taking into account fermion flow, for mother wavefunctions""" 1053 1054 return self.get('pdg_code')
1055
1056 - def get_anti_pdg_code(self):
1057 """Generate the corresponding pdg_code for an incoming particle, 1058 taking into account fermion flow, for mother wavefunctions""" 1059 1060 if self.get('self_antipart'): 1061 #This is its own antiparticle e.g. gluon 1062 return self.get('pdg_code') 1063 1064 return - self.get('pdg_code')
1065
1066 - def set_scalar_coupling_sign(self, model):
1067 """Check if we need to add a minus sign due to non-identical 1068 bosons in HVS type couplings""" 1069 1070 inter = model.get('interaction_dict')[self.get('interaction_id')] 1071 if [p.get('spin') for p in \ 1072 inter.get('particles')] == [3, 1, 1]: 1073 particles = inter.get('particles') 1074 # lambda p1, p2: p1.get('spin') - p2.get('spin')) 1075 if particles[1].get_pdg_code() != particles[2].get_pdg_code() \ 1076 and self.get('pdg_code') == \ 1077 particles[1].get_anti_pdg_code()\ 1078 and not self.get('coupling')[0].startswith('-'): 1079 # We need a minus sign in front of the coupling 1080 self.set('coupling', ['-%s'%c for c in self.get('coupling')])
1081
1083 """For octet Majorana fermions, need an extra minus sign in 1084 the FVI (and FSI?) wavefunction in UFO models.""" 1085 1086 # Add minus sign to coupling of color octet Majorana 1087 # particles to g for FVI vertex 1088 if self.get('color') == 8 and \ 1089 self.get_spin_state_number() == -2 and \ 1090 self.get('self_antipart') and \ 1091 [m.get('color') for m in self.get('mothers')] == [8, 8] and \ 1092 not self.get('coupling')[0].startswith('-'): 1093 self.set('coupling', ['-%s' % c for c in self.get('coupling')])
1094
1095 - def set_state_and_particle(self, model):
1096 """Set incoming/outgoing state according to mother states and 1097 Lorentz structure of the interaction, and set PDG code 1098 according to the particles in the interaction""" 1099 1100 assert isinstance(model, base_objects.Model), \ 1101 "%s is not a valid model for call to set_state_and_particle" \ 1102 % repr(model) 1103 1104 # leg_state is final, unless there is exactly one initial 1105 # state particle involved in the combination -> t-channel 1106 if len(filter(lambda mother: mother.get('leg_state') == False, 1107 self.get('mothers'))) == 1: 1108 leg_state = False 1109 else: 1110 leg_state = True 1111 self.set('leg_state', leg_state) 1112 1113 # Start by setting the state of the wavefunction 1114 if self.is_boson(): 1115 # For boson, set state to intermediate 1116 self.set('state', 'intermediate') 1117 else: 1118 # For fermion, set state to same as other fermion (in the 1119 # right way) 1120 mother = self.find_mother_fermion() 1121 1122 if self.get('self_antipart'): 1123 self.set('state', mother.get_with_flow('state')) 1124 self.set('is_part', mother.get_with_flow('is_part')) 1125 else: 1126 self.set('state', mother.get('state')) 1127 self.set('fermionflow', mother.get('fermionflow')) 1128 # Check that the state is compatible with particle/antiparticle 1129 if self.get('is_part') and self.get('state') == 'incoming' or \ 1130 not self.get('is_part') and self.get('state') == 'outgoing': 1131 self.set('state', {'incoming':'outgoing', 1132 'outgoing':'incoming'}[self.get('state')]) 1133 self.set('fermionflow', -self.get('fermionflow')) 1134 return True
1135
1136 - def check_and_fix_fermion_flow(self, 1137 wavefunctions, 1138 diagram_wavefunctions, 1139 external_wavefunctions, 1140 wf_number):
1141 """Check for clashing fermion flow (N(incoming) != 1142 N(outgoing)) in mothers. This can happen when there is a 1143 Majorana particle in the diagram, which can flip the fermion 1144 flow. This is detected either by a wavefunctions or an 1145 amplitude, with 2 fermion mothers with same state. 1146 1147 In this case, we need to follow the fermion lines of the 1148 mother wavefunctions until we find the outermost Majorana 1149 fermion. For all fermions along the line up to (but not 1150 including) the Majorana fermion, we need to flip incoming <-> 1151 outgoing and particle id. For all fermions after the Majorana 1152 fermion, we need to flip the fermionflow property (1 <-> -1). 1153 1154 The reason for this is that in the Helas calls, we need to 1155 keep track of where the actual fermion flow clash happens 1156 (i.e., at the outermost Majorana), as well as having the 1157 correct fermion flow for all particles along the fermion line. 1158 1159 This is done by the mothers using 1160 HelasWavefunctionList.check_and_fix_fermion_flow, which in 1161 turn calls the recursive function 1162 check_majorana_and_flip_flow to trace the fermion lines. 1163 """ 1164 1165 # Use the HelasWavefunctionList helper function 1166 # Have to keep track of wavefunction number, since we might 1167 # need to add new wavefunctions. 1168 self.set('mothers', self.get('mothers').sort_by_pdg_codes(\ 1169 self.get('pdg_codes'), self.get_anti_pdg_code())[0]) 1170 1171 wf_number = self.get('mothers').\ 1172 check_and_fix_fermion_flow(wavefunctions, 1173 diagram_wavefunctions, 1174 external_wavefunctions, 1175 self, 1176 wf_number) 1177 1178 return self, wf_number
1179
1180 - def check_majorana_and_flip_flow(self, found_majorana, 1181 wavefunctions, 1182 diagram_wavefunctions, 1183 external_wavefunctions, 1184 wf_number, force_flip_flow=False, 1185 number_to_wavefunctions=[]):
1186 """Recursive function. Check for Majorana fermion. If found, 1187 continue down to external leg, then flip all the fermion flows 1188 on the way back up, in the correct way: 1189 Only flip fermionflow after the last Majorana fermion; for 1190 wavefunctions before the last Majorana fermion, instead flip 1191 particle identities and state. Return the new (or old) 1192 wavefunction, and the present wavefunction number. 1193 1194 Arguments: 1195 found_majorana: boolean 1196 wavefunctions: HelasWavefunctionList with previously 1197 defined wavefunctions 1198 diagram_wavefunctions: HelasWavefunctionList with the wavefunctions 1199 already defined in this diagram 1200 external_wavefunctions: dictionary from legnumber to external wf 1201 wf_number: The present wavefunction number 1202 """ 1203 1204 if not found_majorana: 1205 found_majorana = self.get('self_antipart') 1206 1207 new_wf = self 1208 flip_flow = False 1209 flip_sign = False 1210 1211 # Stop recursion at the external leg 1212 mothers = copy.copy(self.get('mothers')) 1213 if not mothers: 1214 if force_flip_flow: 1215 flip_flow = True 1216 elif not self.get('self_antipart'): 1217 flip_flow = found_majorana 1218 else: 1219 flip_sign = found_majorana 1220 else: 1221 # Follow fermion flow up through tree 1222 fermion_mother = self.find_mother_fermion() 1223 1224 if fermion_mother.get_with_flow('state') != \ 1225 self.get_with_flow('state'): 1226 new_mother = fermion_mother 1227 else: 1228 # Perform recursion by calling on mother 1229 new_mother, wf_number = fermion_mother.\ 1230 check_majorana_and_flip_flow(\ 1231 found_majorana, 1232 wavefunctions, 1233 diagram_wavefunctions, 1234 external_wavefunctions, 1235 wf_number, 1236 force_flip_flow) 1237 1238 # If this is Majorana and mother has different fermion 1239 # flow, we should flip the particle id and flow state. 1240 # Otherwise, if mother has different fermion flow, flip 1241 # flow 1242 flip_sign = new_mother.get_with_flow('state') != \ 1243 self.get_with_flow('state') and \ 1244 self.get('self_antipart') 1245 flip_flow = new_mother.get_with_flow('state') != \ 1246 self.get_with_flow('state') and \ 1247 not self.get('self_antipart') 1248 1249 # Replace old mother with new mother 1250 mothers[mothers.index(fermion_mother)] = new_mother 1251 1252 # Flip sign if needed 1253 if flip_flow or flip_sign: 1254 if self in wavefunctions: 1255 # Need to create a new copy, since we don't want to change 1256 # the wavefunction for previous diagrams 1257 new_wf = copy.copy(self) 1258 # Update wavefunction number 1259 wf_number = wf_number + 1 1260 new_wf.set('number', wf_number) 1261 try: 1262 # In call from insert_decay, we want to replace 1263 # also identical wavefunctions in the same diagram 1264 old_wf_index = diagram_wavefunctions.index(self) 1265 old_wf = diagram_wavefunctions[old_wf_index] 1266 if self.get('number') == old_wf.get('number'): 1267 # The wavefunction and old_wf are the same - 1268 # need to reset wf_number and new_wf number 1269 wf_number -= 1 1270 new_wf.set('number', old_wf.get('number')) 1271 diagram_wavefunctions[old_wf_index] = new_wf 1272 except ValueError: 1273 # Make sure that new_wf comes before any wavefunction 1274 # which has it as mother 1275 if len(self['mothers']) == 0: 1276 #insert at the beginning 1277 if diagram_wavefunctions: 1278 wf_nb = diagram_wavefunctions[0].get('number') 1279 for w in diagram_wavefunctions: 1280 w.set('number', w.get('number') + 1) 1281 new_wf.set('number', wf_nb) 1282 diagram_wavefunctions.insert(0, new_wf) 1283 else: 1284 diagram_wavefunctions.insert(0, new_wf) 1285 else: 1286 for i, wf in enumerate(diagram_wavefunctions): 1287 if self in wf.get('mothers'): 1288 # Update wf numbers 1289 new_wf.set('number', wf.get('number')) 1290 for w in diagram_wavefunctions[i:]: 1291 w.set('number', w.get('number') + 1) 1292 # Insert wavefunction 1293 diagram_wavefunctions.insert(i, new_wf) 1294 break 1295 else: 1296 # For loop processes, care is needed since 1297 # some loop wavefunctions in the diag_wfs might have 1298 # the new_wf in their mother, so we want to place 1299 # new_wf as early as possible in the list. 1300 # We first look if any mother of the wavefunction 1301 # we want to add appears in the diagram_wavefunctions 1302 # list. If it doesn't, max_mother_index is -1. 1303 # If it does, then max_mother_index is the maximum 1304 # index in diagram_wavefunctions of those of the 1305 # mothers present in this list. 1306 max_mother_index = max([-1]+ 1307 [diagram_wavefunctions.index(wf) for wf in 1308 mothers if wf in diagram_wavefunctions]) 1309 1310 # We want to insert this new_wf as early as 1311 # possible in the diagram_wavefunctions list so that 1312 # we are guaranteed that it will be placed *before* 1313 # wavefunctions that have new_wf as a mother. 1314 # We therefore place it at max_mother_index+1. 1315 if max_mother_index<len(diagram_wavefunctions)-1: 1316 new_wf.set('number',diagram_wavefunctions[ 1317 max_mother_index+1].get('number')) 1318 for wf in diagram_wavefunctions[max_mother_index+1:]: 1319 wf.set('number',wf.get('number')+1) 1320 diagram_wavefunctions.insert(max_mother_index+1, 1321 new_wf) 1322 1323 # Set new mothers 1324 new_wf.set('mothers', mothers) 1325 1326 # Now flip flow or sign 1327 if flip_flow: 1328 # Flip fermion flow 1329 new_wf.set('fermionflow', -new_wf.get('fermionflow')) 1330 1331 if flip_sign: 1332 # Flip state and particle identity 1333 # (to keep particle identity * flow state) 1334 new_wf.set('state', filter(lambda state: \ 1335 state != new_wf.get('state'), 1336 ['incoming', 'outgoing'])[0]) 1337 new_wf.set('is_part', not new_wf.get('is_part')) 1338 try: 1339 # Use the copy in wavefunctions instead. 1340 # Remove this copy from diagram_wavefunctions 1341 new_wf_number = new_wf.get('number') 1342 new_wf = wavefunctions[wavefunctions.index(new_wf)] 1343 diagram_wf_numbers = [w.get('number') for w in \ 1344 diagram_wavefunctions] 1345 index = diagram_wf_numbers.index(new_wf_number) 1346 diagram_wavefunctions.pop(index) 1347 # We need to decrease the wf number for later 1348 # diagram wavefunctions 1349 for wf in diagram_wavefunctions: 1350 if wf.get('number') > new_wf_number: 1351 wf.set('number', wf.get('number') - 1) 1352 # Since we reuse the old wavefunction, reset wf_number 1353 wf_number = wf_number - 1 1354 1355 # Need to replace wavefunction in number_to_wavefunctions 1356 # (in case this wavefunction is in another of the dicts) 1357 for n_to_wf_dict in number_to_wavefunctions: 1358 if new_wf in n_to_wf_dict.values(): 1359 for key in n_to_wf_dict.keys(): 1360 if n_to_wf_dict[key] == new_wf: 1361 n_to_wf_dict[key] = new_wf 1362 1363 if self.get('is_loop'): 1364 # fix a bug for the g g > go go g [virt=QCD] 1365 # when there is a wf which is replaced, we need to propagate 1366 # the change in all wavefunction of that diagrams which could 1367 # have this replaced wavefunction in their mothers. This 1368 # plays the role of the 'number_to_wavefunction' dictionary 1369 # used for tree level. 1370 for wf in diagram_wavefunctions: 1371 for i,mother_wf in enumerate(wf.get('mothers')): 1372 if mother_wf.get('number')==new_wf_number: 1373 wf.get('mothers')[i]=new_wf 1374 1375 except ValueError: 1376 pass 1377 1378 # Return the new (or old) wavefunction, and the new 1379 # wavefunction number 1380 return new_wf, wf_number
1381
1382 - def get_fermion_order(self):
1383 """Recursive function to get a list of fermion numbers 1384 corresponding to the order of fermions along fermion lines 1385 connected to this wavefunction, in the form [n1,n2,...] for a 1386 boson, and [N,[n1,n2,...]] for a fermion line""" 1387 1388 # End recursion if external wavefunction 1389 if not self.get('mothers'): 1390 if self.is_fermion(): 1391 return [self.get('number_external'), []] 1392 else: 1393 return [] 1394 1395 # Pick out fermion mother 1396 fermion_mother = None 1397 if self.is_fermion(): 1398 fermion_mother = self.find_mother_fermion() 1399 1400 other_fermions = [wf for wf in self.get('mothers') if \ 1401 wf.is_fermion() and wf != fermion_mother] 1402 1403 # Pick out bosons 1404 bosons = filter(lambda wf: wf.is_boson(), self.get('mothers')) 1405 1406 fermion_number_list = [] 1407 1408 if self.is_fermion(): 1409 # Fermions return the result N from their mother 1410 # and the list from bosons, so [N,[n1,n2,...]] 1411 mother_list = fermion_mother.get_fermion_order() 1412 fermion_number_list.extend(mother_list[1]) 1413 1414 # If there are fermion line pairs, append them as 1415 # [NI,NO,n1,n2,...] 1416 fermion_numbers = [f.get_fermion_order() for f in other_fermions] 1417 for iferm in range(0, len(fermion_numbers), 2): 1418 fermion_number_list.append(fermion_numbers[iferm][0]) 1419 fermion_number_list.append(fermion_numbers[iferm+1][0]) 1420 fermion_number_list.extend(fermion_numbers[iferm][1]) 1421 fermion_number_list.extend(fermion_numbers[iferm+1][1]) 1422 1423 for boson in bosons: 1424 # Bosons return a list [n1,n2,...] 1425 fermion_number_list.extend(boson.get_fermion_order()) 1426 1427 if self.is_fermion(): 1428 return [mother_list[0], fermion_number_list] 1429 1430 return fermion_number_list
1431
1432 - def needs_hermitian_conjugate(self):
1433 """Returns true if any of the mothers have negative 1434 fermionflow""" 1435 1436 return self.get('conjugate_indices') != ()
1437
1438 - def get_with_flow(self, name):
1439 """Generate the is_part and state needed for writing out 1440 wavefunctions, taking into account the fermion flow""" 1441 1442 if self.get('fermionflow') > 0: 1443 # Just return (spin, state) 1444 return self.get(name) 1445 1446 # If fermionflow is -1, need to flip particle identity and state 1447 if name == 'is_part': 1448 return not self.get('is_part') 1449 if name == 'state': 1450 return filter(lambda state: state != self.get('state'), 1451 ['incoming', 'outgoing'])[0] 1452 return self.get(name)
1453
1455 """ Returns a dictionary for formatting this external wavefunction 1456 helas call """ 1457 1458 if self['mothers']: 1459 raise MadGraph5Error, "This function should be called only for"+\ 1460 " external wavefunctions." 1461 return_dict = {} 1462 if self.get('is_loop'): 1463 return_dict['conjugate'] = ('C' if self.needs_hermitian_conjugate() \ 1464 else '') 1465 return_dict['lcutspinletter'] = self.get_lcutspinletter() 1466 return_dict['number'] = self.get('number') 1467 return_dict['me_id'] = self.get('me_id') 1468 return_dict['number_external'] = self.get('number_external') 1469 return_dict['mass'] = self.get('mass') 1470 if self.is_boson(): 1471 return_dict['state_id'] = (-1) ** (self.get('state') == 'initial') 1472 else: 1473 return_dict['state_id'] = -(-1) ** self.get_with_flow('is_part') 1474 return_dict['number_external'] = self.get('number_external') 1475 1476 return return_dict
1477
1478 - def get_helas_call_dict(self, index=1, OptimizedOutput=False, 1479 specifyHel=True,**opt):
1480 """ return a dictionary to be used for formatting 1481 HELAS call. The argument index sets the flipping while optimized output 1482 changes the wavefunction specification in the arguments.""" 1483 1484 if index == 1: 1485 flip = 0 1486 else: 1487 flip = 1 1488 1489 output = {} 1490 if self.get('is_loop') and OptimizedOutput: 1491 output['vertex_rank']=self.get_analytic_info('interaction_rank') 1492 output['lcut_size']=self.get('lcut_size') 1493 output['out_size']=self.spin_to_size(self.get('spin')) 1494 1495 loop_mother_found=False 1496 for ind, mother in enumerate(self.get('mothers')): 1497 # temporary START 1498 # This temporary modification is only because aloha has the convention 1499 # of putting the loop polynomial mother wavefunction first in the 1500 # list of argument of the helas call. I this convention is changed 1501 # to be the 'natural' order in the interaction, this would not be 1502 # needed anymore. 1503 if OptimizedOutput and self.get('is_loop'): 1504 if mother.get('is_loop'): 1505 i=0 1506 else: 1507 if loop_mother_found: 1508 i=ind 1509 else: 1510 i=ind+1 1511 else: 1512 i=ind 1513 # temporary END 1514 nb = mother.get('me_id') - flip 1515 output[str(i)] = nb 1516 if not OptimizedOutput: 1517 if mother.get('is_loop'): 1518 output['WF%d'%i] = 'L(1,%d)'%nb 1519 else: 1520 output['WF%d'%i] = '(1,WE(%d)'%nb 1521 else: 1522 if mother.get('is_loop'): 1523 output['loop_mother_number']=nb 1524 output['loop_mother_rank']=\ 1525 mother.get_analytic_info('wavefunction_rank') 1526 output['in_size']=self.spin_to_size(mother.get('spin')) 1527 output['WF%d'%i] = 'PL(0,%d)'%nb 1528 loop_mother_found=True 1529 else: 1530 output['WF%d'%i] = 'W(1,%d'%nb 1531 if not mother.get('is_loop'): 1532 if specifyHel: 1533 output['WF%d'%i]=output['WF%d'%i]+',H)' 1534 else: 1535 output['WF%d'%i]=output['WF%d'%i]+')' 1536 1537 #fixed argument 1538 for i, coup in enumerate(self.get_with_flow('coupling')): 1539 # We do not include the - sign in front of the coupling of loop 1540 # wavefunctions (only the loop ones, the tree ones are treated normally) 1541 # in the non optimized output because this sign was already applied to 1542 # the coupling passed in argument when calling the loop amplitude. 1543 if not OptimizedOutput and self.get('is_loop'): 1544 output['coup%d'%i] = coup[1:] if coup.startswith('-') else coup 1545 else: 1546 output['coup%d'%i] = coup 1547 1548 output['out'] = self.get('me_id') - flip 1549 output['M'] = self.get('mass') 1550 output['W'] = self.get('width') 1551 output['propa'] = self.get('particle').get('propagator') 1552 if output['propa'] not in ['', None]: 1553 output['propa'] = 'P%s' % output['propa'] 1554 # optimization 1555 if aloha.complex_mass: 1556 if (self.get('width') == 'ZERO' or self.get('mass') == 'ZERO'): 1557 #print self.get('width'), self.get('mass') 1558 output['CM'] = '%s' % self.get('mass') 1559 else: 1560 output['CM'] ='CMASS_%s' % self.get('mass') 1561 output.update(opt) 1562 return output
1563
1564 - def get_spin_state_number(self, flip=False):
1565 """Returns the number corresponding to the spin state, with a 1566 minus sign for incoming fermions. For flip=True, this 1567 spin_state_number is suited for find the index in the interaction 1568 of a MOTHER wavefunction. """ 1569 1570 state_number = {'incoming':-1 if not flip else 1, 1571 'outgoing': 1 if not flip else -1, 1572 'intermediate': 1, 'initial': 1, 'final': 1} 1573 return self.get('fermionflow') * \ 1574 state_number[self.get('state')] * \ 1575 self.get('spin')
1576
1577 - def find_mother_fermion(self):
1578 """Return the fermion mother which is fermion flow connected to 1579 this fermion""" 1580 1581 if not self.is_fermion(): 1582 return None 1583 1584 part_number = self.find_outgoing_number() 1585 mother_number = (part_number-1)//2*2 1586 1587 return HelasMatrixElement.sorted_mothers(self)[mother_number]
1588
1589 - def find_outgoing_number(self):
1590 "Return the position of the resulting particles in the interactions" 1591 # First shot: just the index in the interaction 1592 1593 if self.get('interaction_id') == 0: 1594 return 0 1595 1596 return self.find_leg_index(self.get_anti_pdg_code(),\ 1597 self.get_spin_state_number())
1598
1599 - def find_leg_index(self, pdg_code, spin_state):
1600 """ Find the place in the interaction list of the given particle with 1601 pdg 'pdg_code' and spin 'spin_stat'. For interactions with several identical particles (or 1602 fermion pairs) the outgoing index is always the first occurence. 1603 """ 1604 wf_indices = self.get('pdg_codes') 1605 wf_index = wf_indices.index(pdg_code) 1606 1607 # If fermion, then we need to correct for I/O status 1608 if spin_state % 2 == 0: 1609 if wf_index % 2 == 0 and spin_state < 0: 1610 # Outgoing particle at even slot -> increase by 1 1611 wf_index += 1 1612 elif wf_index % 2 == 1 and spin_state > 0: 1613 # Incoming particle at odd slot -> decrease by 1 1614 wf_index -= 1 1615 return wf_index + 1
1616
1617 - def get_call_key(self):
1618 """Generate the (spin, number, C-state) tuple used as key for 1619 the helas call dictionaries in HelasModel""" 1620 1621 res = [] 1622 for mother in self.get('mothers'): 1623 res.append(mother.get_spin_state_number()) 1624 1625 # Sort according to spin and flow direction 1626 res.sort() 1627 res.append(self.get_spin_state_number()) 1628 res.append(self.find_outgoing_number()) 1629 1630 if self['is_loop']: 1631 res.append(self.get_loop_index()) 1632 if not self.get('mothers'): 1633 res.append(self.get('is_part')) 1634 1635 # Check if we need to append a charge conjugation flag 1636 if self.needs_hermitian_conjugate(): 1637 res.append(self.get('conjugate_indices')) 1638 1639 return (tuple(res), tuple(self.get('lorentz')))
1640
1641 - def get_base_vertices(self, wf_dict, vx_list = [], optimization = 1):
1642 """Recursive method to get a base_objects.VertexList 1643 corresponding to this wavefunction and its mothers.""" 1644 1645 vertices = base_objects.VertexList() 1646 1647 mothers = self.get('mothers') 1648 1649 if not mothers: 1650 return vertices 1651 1652 # Add vertices for all mothers 1653 for mother in mothers: 1654 # This is where recursion happens 1655 vertices.extend(mother.get_base_vertices(\ 1656 wf_dict, vx_list,optimization)) 1657 1658 vertex = self.get_base_vertex(wf_dict, vx_list, optimization) 1659 1660 try: 1661 index = vx_list.index(vertex) 1662 vertex = vx_list[index] 1663 except ValueError: 1664 pass 1665 1666 vertices.append(vertex) 1667 1668 return vertices
1669
1670 - def get_base_vertex(self, wf_dict, vx_list = [], optimization = 1):
1671 """Get a base_objects.Vertex corresponding to this 1672 wavefunction.""" 1673 1674 # Generate last vertex 1675 legs = base_objects.LegList() 1676 1677 # We use the onshell flag to indicate whether this outgoing 1678 # leg corresponds to a decaying (onshell) particle, forbidden 1679 # s-channel, or regular 1680 try: 1681 if self.get('is_loop'): 1682 # Loop wavefunction should always be redefined 1683 raise KeyError 1684 lastleg = wf_dict[(self.get('number'),self.get('onshell'))] 1685 except KeyError: 1686 lastleg = base_objects.Leg({ 1687 'id': self.get_pdg_code(), 1688 'number': self.get('number_external'), 1689 'state': self.get('leg_state'), 1690 'onshell': self.get('onshell'), 1691 'loop_line':self.get('is_loop') 1692 }) 1693 1694 if optimization != 0 and not self.get('is_loop'): 1695 wf_dict[(self.get('number'),self.get('onshell'))] = lastleg 1696 1697 for mother in self.get('mothers'): 1698 try: 1699 if mother.get('is_loop'): 1700 # Loop wavefunction should always be redefined 1701 raise KeyError 1702 leg = wf_dict[(mother.get('number'),False)] 1703 except KeyError: 1704 leg = base_objects.Leg({ 1705 'id': mother.get_pdg_code(), 1706 'number': mother.get('number_external'), 1707 'state': mother.get('leg_state'), 1708 'onshell': None, 1709 'loop_line':mother.get('is_loop'), 1710 'onshell': None 1711 }) 1712 if optimization != 0 and not mother.get('is_loop'): 1713 wf_dict[(mother.get('number'),False)] = leg 1714 legs.append(leg) 1715 1716 legs.append(lastleg) 1717 1718 vertex = base_objects.Vertex({ 1719 'id': self.get('interaction_id'), 1720 'legs': legs}) 1721 1722 return vertex
1723
1724 - def get_color_indices(self):
1725 """Recursive method to get the color indices corresponding to 1726 this wavefunction and its mothers.""" 1727 1728 if not self.get('mothers'): 1729 return [] 1730 1731 color_indices = [] 1732 1733 # Add color indices for all mothers 1734 for mother in self.get('mothers'): 1735 # This is where recursion happens 1736 color_indices.extend(mother.get_color_indices()) 1737 # Add this wf's color index 1738 color_indices.append(self.get('color_key')) 1739 1740 return color_indices
1741
1742 - def get_aloha_info(self, optimized_output=True):
1743 """Returns the tuple (lorentz_name, tag, outgoing_number) providing 1744 the necessary information to compute_subset of create_aloha to write 1745 out the HELAS-like routines.""" 1746 1747 # In principle this function should not be called for the case below, 1748 # or if it does it should handle specifically the None returned value. 1749 if self.get('interaction_id') in [0,-1]: 1750 return None 1751 1752 tags = ['C%s' % w for w in self.get_conjugate_index()] 1753 if self.get('is_loop'): 1754 if not optimized_output: 1755 tags.append('L') 1756 else: 1757 tags.append('L%d'%self.get_loop_index()) 1758 1759 if self.get('particle').get('propagator') not in ['', None]: 1760 tags.append('P%s' % str(self.get('particle').get('propagator'))) 1761 1762 return (tuple(self.get('lorentz')),tuple(tags),self.find_outgoing_number())
1763
1764 - def get_lcutspinletter(self):
1765 """Returns S,V or F depending on the spin of the mother loop particle. 1766 Return '' otherwise.""" 1767 1768 if self['is_loop'] and not self.get('mothers'): 1769 if self.get('spin') == 1: 1770 if self.get('particle').get('is_part'): 1771 return 'S' 1772 else: 1773 return 'AS' 1774 if self.get('spin') == 2: 1775 if self.get('particle').get('is_part'): 1776 return 'F' 1777 else: 1778 return 'AF' 1779 if self.get('spin') == 3: 1780 return 'V' 1781 else: 1782 raise MadGraph5Error,'L-cut particle type not supported' 1783 else: 1784 return ''
1785
1786 - def get_s_and_t_channels(self, ninitial, mother_leg, reverse_t_ch = False):
1787 """Returns two lists of vertices corresponding to the s- and 1788 t-channels that can be traced from this wavefunction, ordered 1789 from the outermost s-channel and in/down towards the highest 1790 (if not reverse_t_ch) or lowest (if reverse_t_ch) number initial 1791 state leg. mother_leg corresponds to self but with 1792 correct leg number = min(final state mothers).""" 1793 1794 schannels = base_objects.VertexList() 1795 tchannels = base_objects.VertexList() 1796 1797 mother_leg = copy.copy(mother_leg) 1798 1799 (startleg, finalleg) = (1,2) 1800 if reverse_t_ch: (startleg, finalleg) = (2,1) 1801 1802 # Add vertices for all s-channel mothers 1803 final_mothers = filter(lambda wf: wf.get('number_external') > ninitial, 1804 self.get('mothers')) 1805 1806 for mother in final_mothers: 1807 schannels.extend(mother.get_base_vertices({}, optimization = 0)) 1808 1809 # Extract initial state mothers 1810 init_mothers = filter(lambda wf: wf.get('number_external') <= ninitial, 1811 self.get('mothers')) 1812 1813 assert len(init_mothers) < 3 , \ 1814 "get_s_and_t_channels can only handle up to 2 initial states" 1815 1816 if len(init_mothers) == 1: 1817 # This is an s-channel or t-channel leg, or the initial 1818 # leg of a decay process. Add vertex and continue stepping 1819 # down towards external initial state 1820 legs = base_objects.LegList() 1821 mothers = final_mothers + init_mothers 1822 1823 for mother in mothers: 1824 legs.append(base_objects.Leg({ 1825 'id': mother.get_pdg_code(), 1826 'number': mother.get('number_external'), 1827 'state': mother.get('leg_state'), 1828 'onshell': mother.get('onshell') 1829 })) 1830 1831 if init_mothers[0].get('number_external') == startleg and \ 1832 not init_mothers[0].get('leg_state') and ninitial > 1: 1833 # If this is t-channel going towards external leg 1, 1834 # mother_leg is resulting wf 1835 legs.append(mother_leg) 1836 else: 1837 # For decay processes or if init_mother is an s-channel leg 1838 # or we are going towards external leg 2, mother_leg 1839 # is one of the mothers (placed next-to-last) 1840 legs.insert(-1, mother_leg) 1841 # Need to switch direction of the resulting s-channel 1842 legs[-1].set('id', init_mothers[0].get_anti_pdg_code()) 1843 1844 # Renumber resulting leg according to minimum leg number 1845 legs[-1].set('number', min([l.get('number') for l in legs[:-1]])) 1846 1847 vertex = base_objects.Vertex({ 1848 'id': self.get('interaction_id'), 1849 'legs': legs}) 1850 1851 # Add s- and t-channels from init_mother 1852 new_mother_leg = legs[-1] 1853 if init_mothers[0].get('number_external') == startleg and \ 1854 not init_mothers[0].get('leg_state') and \ 1855 ninitial > 1: 1856 # Mother of next vertex is init_mothers[0] 1857 # (next-to-last in legs) 1858 new_mother_leg = legs[-2] 1859 1860 mother_s, tchannels = \ 1861 init_mothers[0].get_s_and_t_channels(ninitial, 1862 new_mother_leg, 1863 reverse_t_ch) 1864 if ninitial == 1 or init_mothers[0].get('leg_state') == True: 1865 # This vertex is s-channel 1866 schannels.append(vertex) 1867 elif init_mothers[0].get('number_external') == startleg: 1868 # If init_mothers is going towards external leg 1, add 1869 # to t-channels, at end 1870 tchannels.append(vertex) 1871 else: 1872 # If init_mothers is going towards external leg 2, add to 1873 # t-channels, at start 1874 tchannels.insert(0, vertex) 1875 1876 schannels.extend(mother_s) 1877 1878 elif len(init_mothers) == 2: 1879 # This is a t-channel junction. Start with the leg going 1880 # towards external particle 1, and then do external 1881 # particle 2 1882 init_mothers1 = filter(lambda wf: wf.get('number_external') == \ 1883 startleg, 1884 init_mothers)[0] 1885 init_mothers2 = filter(lambda wf: wf.get('number_external') == \ 1886 finalleg, 1887 init_mothers)[0] 1888 1889 # Create vertex 1890 legs = base_objects.LegList() 1891 for mother in final_mothers + [init_mothers1, init_mothers2]: 1892 legs.append(base_objects.Leg({ 1893 'id': mother.get_pdg_code(), 1894 'number': mother.get('number_external'), 1895 'state': mother.get('leg_state'), 1896 'onshell': mother.get('onshell') 1897 })) 1898 legs.insert(0, mother_leg) 1899 1900 # Renumber resulting leg according to minimum leg number 1901 legs[-1].set('number', min([l.get('number') for l in legs[:-1]])) 1902 1903 vertex = base_objects.Vertex({ 1904 'id': self.get('interaction_id'), 1905 'legs': legs}) 1906 1907 # Add s- and t-channels going down towards leg 1 1908 mother_s, tchannels = \ 1909 init_mothers1.get_s_and_t_channels(ninitial, legs[-2], 1910 reverse_t_ch) 1911 schannels.extend(mother_s) 1912 1913 # Add vertex 1914 tchannels.append(vertex) 1915 1916 # Add s- and t-channels going down towards leg 2 1917 mother_s, mother_t = \ 1918 init_mothers2.get_s_and_t_channels(ninitial, legs[-1], 1919 reverse_t_ch) 1920 schannels.extend(mother_s) 1921 tchannels.extend(mother_t) 1922 1923 # Sort s-channels according to number 1924 schannels.sort(lambda x1,x2: x2.get('legs')[-1].get('number') - \ 1925 x1.get('legs')[-1].get('number')) 1926 1927 return schannels, tchannels
1928
1930 """ Return a set containing the ids of all the non-loop outter-most 1931 external legs attached to the loop at the interaction point of this 1932 loop wavefunction """ 1933 1934 if not self.get('mothers'): 1935 return set([self.get('number_external'),]) 1936 1937 res=set([]) 1938 for wf in self.get('mothers'): 1939 if not wf['is_loop']: 1940 res=res.union(wf.get_struct_external_leg_ids()) 1941 return res
1942 #
1943 - def get_loop_index(self):
1944 """Return the index of the wavefunction in the mothers which is the 1945 loop one""" 1946 1947 if not self.get('mothers'): 1948 return 0 1949 1950 try: 1951 loop_wf_index=\ 1952 [wf['is_loop'] for wf in self.get('mothers')].index(True) 1953 except ValueError: 1954 raise MadGraph5Error, "The loop wavefunctions should have exactly"+\ 1955 " one loop wavefunction mother." 1956 1957 if self.find_outgoing_number()-1<=loop_wf_index: 1958 # If the incoming loop leg is placed after the outgoing one we 1959 # need to increment once more its index in the interaction list ( 1960 # because the outgoing loop leg is not part of the mother wf list) 1961 return loop_wf_index+2 1962 else: 1963 # Basic increment of +1 because aloha counts particles in the 1964 # interaction starting at 1. 1965 return loop_wf_index+1
1966
1967 - def get_lcut_size(self):
1968 """ Return the size (i.e number of elements) of the L-Cut wavefunction 1969 this loop wavefunction originates from. """ 1970 1971 if not self['is_loop']: 1972 return 0 1973 1974 # Obtain the L-cut wavefunction this loop wavefunction comes from. 1975 # (I'm using two variable instead of one in order to have only one call 1976 # to get_loop_mother()) 1977 last_loop_wf=self 1978 last_loop_wf_loop_mother=last_loop_wf.get_loop_mother() 1979 while last_loop_wf_loop_mother: 1980 last_loop_wf=last_loop_wf_loop_mother 1981 last_loop_wf_loop_mother=last_loop_wf_loop_mother.get_loop_mother() 1982 1983 # Translate its spin into a wavefunction size. 1984 return self.spin_to_size(last_loop_wf.get('spin'))
1985
1986 - def get_loop_mother(self):
1987 """ Return the mother of type 'loop', if any. """ 1988 1989 if not self.get('mothers'): 1990 return None 1991 loop_wfs=[wf for wf in self.get('mothers') if wf['is_loop']] 1992 if loop_wfs: 1993 if len(loop_wfs)==1: 1994 return loop_wfs[0] 1995 else: 1996 raise MadGraph5Error, "The loop wavefunction must have either"+\ 1997 " no mothers, or exactly one mother with type 'loop'." 1998 else: 1999 return None
2000
2001 - def get_conjugate_index(self):
2002 """Return the index of the particle that should be conjugated.""" 2003 2004 if not any([(wf.get('fermionflow') < 0 or wf.is_majorana()) for wf in \ 2005 self.get('mothers')]) and \ 2006 (not self.get('interaction_id') or \ 2007 self.get('fermionflow') >= 0): 2008 return () 2009 2010 # Pick out first sorted mothers, then fermions 2011 mothers, self_index = \ 2012 self.get('mothers').sort_by_pdg_codes(self.get('pdg_codes'), 2013 self.get_anti_pdg_code()) 2014 fermions = HelasWavefunctionList([wf for wf in mothers if wf.is_fermion()]) 2015 2016 # Insert this wavefunction in list (in the right place) 2017 if self.is_fermion(): 2018 me = copy.copy(self) 2019 # Flip incoming/outgoing to make me equivalent to mother 2020 # as needed by majorana_conjugates 2021 me.set('state', [state for state in ['incoming', 'outgoing'] \ 2022 if state != me.get('state')][0]) 2023 fermions.insert(self_index, me) 2024 2025 # Initialize indices with indices due to Majoranas with wrong order 2026 indices = fermions.majorana_conjugates() 2027 2028 # Check for fermions with negative fermion flow 2029 for i in range(0,len(fermions), 2): 2030 if fermions[i].get('fermionflow') < 0 or \ 2031 fermions[i+1].get('fermionflow') < 0: 2032 indices.append(i/2 + 1) 2033 2034 return tuple(sorted(indices))
2035
2036 - def get_vertex_leg_numbers(self, 2037 veto_inter_id=base_objects.Vertex.ID_to_veto_for_multichanneling, 2038 max_n_loop=0):
2039 """Get a list of the number of legs in vertices in this diagram""" 2040 2041 if not self.get('mothers'): 2042 return [] 2043 2044 if max_n_loop == 0: 2045 max_n_loop = base_objects.Vertex.max_n_loop_for_multichanneling 2046 2047 vertex_leg_numbers = [len(self.get('mothers')) + 1] if \ 2048 (self.get('interaction_id') not in veto_inter_id) or\ 2049 (self.get('interaction_id')==-2 and len(self.get('mothers'))+1 > 2050 max_n_loop) else [] 2051 for mother in self.get('mothers'): 2052 vertex_leg_numbers.extend(mother.get_vertex_leg_numbers( 2053 veto_inter_id = veto_inter_id)) 2054 2055 return vertex_leg_numbers
2056 2057 # Overloaded operators 2058
2059 - def __eq__(self, other):
2060 """Overloading the equality operator, to make comparison easy 2061 when checking if wavefunction is already written, or when 2062 checking for identical processes. Note that the number for 2063 this wavefunction, the pdg code, and the interaction id are 2064 irrelevant, while the numbers for the mothers are important. 2065 """ 2066 2067 if not isinstance(other, HelasWavefunction): 2068 return False 2069 2070 # Check relevant directly defined properties 2071 if self['number_external'] != other['number_external'] or \ 2072 self['fermionflow'] != other['fermionflow'] or \ 2073 self['color_key'] != other['color_key'] or \ 2074 self['lorentz'] != other['lorentz'] or \ 2075 self['coupling'] != other['coupling'] or \ 2076 self['state'] != other['state'] or \ 2077 self['onshell'] != other['onshell'] or \ 2078 self.get('spin') != other.get('spin') or \ 2079 self.get('self_antipart') != other.get('self_antipart') or \ 2080 self.get('mass') != other.get('mass') or \ 2081 self.get('width') != other.get('width') or \ 2082 self.get('color') != other.get('color') or \ 2083 self['decay'] != other['decay'] or \ 2084 self['decay'] and self['particle'] != other['particle']: 2085 return False 2086 2087 # Check that mothers have the same numbers (only relevant info) 2088 return sorted([mother['number'] for mother in self['mothers']]) == \ 2089 sorted([mother['number'] for mother in other['mothers']])
2090
2091 - def __ne__(self, other):
2092 """Overloading the nonequality operator, to make comparison easy""" 2093 return not self.__eq__(other)
2094 2095 #=============================================================================== 2096 # Start of the legacy of obsolete functions of the HelasWavefunction class. 2097 #=============================================================================== 2098
2100 """ Returns the power of the loop momentum q brought by the interaction 2101 and propagator from which this loop wavefunction originates. This 2102 is done in a SM ad-hoc way, but it should be promoted to be general in 2103 the future, by reading the lorentz structure of the interaction. 2104 This function is now rendered obsolete by the use of the function 2105 get_analytical_info. It is however kept for legacy.""" 2106 rank=0 2107 # First add the propagator power for a fermion of spin 1/2. 2108 # For the bosons, it is assumed to be in Feynman gauge so that the 2109 # propagator does not bring in any power of the loop momentum. 2110 if self.get('spin')==2: 2111 rank=rank+1 2112 2113 # Treat in an ad-hoc way the higgs effective theory 2114 spin_cols = [(self.get('spin'),abs(self.get('color')))]+\ 2115 [(w.get('spin'),abs(w.get('color'))) for w in self.get('mothers')] 2116 # HGG effective vertex 2117 if sorted(spin_cols) == sorted([(1,1),(3,8),(3,8)]): 2118 return rank+2 2119 # HGGG effective vertex 2120 if sorted(spin_cols) == sorted([(1,1),(3,8),(3,8),(3,8)]): 2121 return rank+1 2122 # HGGGG effective vertex 2123 if sorted(spin_cols) == sorted([(1,1),(3,8),(3,8),(3,8),(3,8)]): 2124 return rank 2125 2126 # Now add a possible power of the loop momentum depending on the 2127 # vertex creating this loop wavefunction. For now we don't read the 2128 # lorentz structure but just use an SM ad-hoc rule that only 2129 # the feynman rules for a three point vertex with only bosons bring 2130 # in one power of q. 2131 if self.is_boson() and len([w for w in self.get('mothers') \ 2132 if w.is_boson()])==2: 2133 rank=rank+1 2134 return rank
2135
2136 #=============================================================================== 2137 # End of the legacy of obsolete functions of the HelasWavefunction class. 2138 #=============================================================================== 2139 2140 #=============================================================================== 2141 # HelasWavefunctionList 2142 #=============================================================================== 2143 -class HelasWavefunctionList(base_objects.PhysicsObjectList):
2144 """List of HelasWavefunction objects. This class has the routine 2145 check_and_fix_fermion_flow, which checks for fermion flow clashes 2146 among the mothers of an amplitude or wavefunction. 2147 """ 2148
2149 - def is_valid_element(self, obj):
2150 """Test if object obj is a valid HelasWavefunction for the list.""" 2151 2152 return isinstance(obj, HelasWavefunction)
2153 2154 # Helper functions 2155
2156 - def to_array(self):
2157 return array.array('i', [w['number'] for w in self])
2158
2159 - def check_and_fix_fermion_flow(self, 2160 wavefunctions, 2161 diagram_wavefunctions, 2162 external_wavefunctions, 2163 my_wf, 2164 wf_number, 2165 force_flip_flow=False, 2166 number_to_wavefunctions=[]):
2167 2168 """Check for clashing fermion flow (N(incoming) != 2169 N(outgoing)). If found, we need to trace back through the 2170 mother structure (only looking at fermions), until we find a 2171 Majorana fermion. Then flip fermion flow along this line all 2172 the way from the initial clash to the external fermion (in the 2173 right way, see check_majorana_and_flip_flow), and consider an 2174 incoming particle with fermionflow -1 as outgoing (and vice 2175 versa). Continue until we have N(incoming) = N(outgoing). 2176 2177 Since the wavefunction number might get updated, return new 2178 wavefunction number. 2179 """ 2180 2181 # Clash is defined by whether any of the fermion lines are clashing 2182 fermion_mother = None 2183 2184 # Keep track of clashing fermion wavefunctions 2185 clashes = [] 2186 2187 # First check the fermion mother on the same fermion line 2188 if my_wf and my_wf.is_fermion(): 2189 fermion_mother = my_wf.find_mother_fermion() 2190 if my_wf.get_with_flow('state') != \ 2191 fermion_mother.get_with_flow('state'): 2192 clashes.append([fermion_mother]) 2193 2194 # Now check all other fermions 2195 other_fermions = [w for w in self if \ 2196 w.is_fermion() and w != fermion_mother] 2197 2198 for iferm in range(0, len(other_fermions), 2): 2199 if other_fermions[iferm].get_with_flow('state') == \ 2200 other_fermions[iferm+1].get_with_flow('state'): 2201 clashes.append([other_fermions[iferm], 2202 other_fermions[iferm+1]]) 2203 2204 if not clashes: 2205 return wf_number 2206 2207 # If any of the mothers have negative fermionflow, we need to 2208 # take this mother first. 2209 for clash in clashes: 2210 neg_fermionflow_mothers = [m for m in clash if \ 2211 m.get('fermionflow') < 0] 2212 2213 if not neg_fermionflow_mothers: 2214 neg_fermionflow_mothers = clash 2215 2216 for mother in neg_fermionflow_mothers: 2217 2218 # Call recursive function to check for Majorana fermions 2219 # and flip fermionflow if found 2220 2221 found_majorana = False 2222 state_before = mother.get_with_flow('state') 2223 new_mother, wf_number = mother.check_majorana_and_flip_flow(\ 2224 found_majorana, 2225 wavefunctions, 2226 diagram_wavefunctions, 2227 external_wavefunctions, 2228 wf_number, 2229 force_flip_flow, 2230 number_to_wavefunctions) 2231 2232 if new_mother.get_with_flow('state') == state_before: 2233 # Fermion flow was not flipped, try next mother 2234 continue 2235 2236 # Replace old mother with new mother 2237 mother_index = self.index(mother) 2238 self[self.index(mother)] = new_mother 2239 clash_index = clash.index(mother) 2240 clash[clash.index(mother)] = new_mother 2241 2242 # Fermion flow was flipped, abort loop 2243 break 2244 2245 if len(clash) == 1 and clash[0].get_with_flow('state') != \ 2246 my_wf.get_with_flow('state') or \ 2247 len(clash) == 2 and clash[0].get_with_flow('state') == \ 2248 clash[1].get_with_flow('state'): 2249 # No Majorana fermion in any relevant legs - try again, 2250 # but simply use the first relevant leg 2251 force_flip_flow = True 2252 wf_number = self.check_and_fix_fermion_flow(\ 2253 wavefunctions, 2254 diagram_wavefunctions, 2255 external_wavefunctions, 2256 my_wf, 2257 wf_number, 2258 force_flip_flow, 2259 number_to_wavefunctions) 2260 # Already ran for all clashes, abort loop 2261 break 2262 2263 return wf_number
2264
2265 - def insert_own_mothers(self):
2266 """Recursively go through a wavefunction list and insert the 2267 mothers of all wavefunctions, return the result. 2268 Assumes that all wavefunctions have unique numbers.""" 2269 2270 res = copy.copy(self) 2271 # Recursively build up res 2272 for wf in self: 2273 index = res.index(wf) 2274 res = res[:index] + wf.get('mothers').insert_own_mothers() \ 2275 + res[index:] 2276 2277 # Make sure no wavefunctions occur twice, by removing doublets 2278 # from the back 2279 i = len(res) - 1 2280 while res[:i]: 2281 if res[i].get('number') in [w.get('number') for w in res[:i]]: 2282 res.pop(i) 2283 i = i - 1 2284 2285 return res
2286
2287 - def sort_by_pdg_codes(self, pdg_codes, my_pdg_code = 0):
2288 """Sort this HelasWavefunctionList according to the cyclic 2289 order of the pdg codes given. my_pdg_code is the pdg code of 2290 the daughter wavefunction (or 0 if daughter is amplitude).""" 2291 2292 if not pdg_codes: 2293 return self, 0 2294 2295 pdg_codes = copy.copy(pdg_codes) 2296 2297 # Remove the argument wavefunction code from pdg_codes 2298 2299 my_index = -1 2300 if my_pdg_code: 2301 # Remember index of my code 2302 my_index = pdg_codes.index(my_pdg_code) 2303 pdg_codes.pop(my_index) 2304 2305 mothers = copy.copy(self) 2306 # Sort according to interaction pdg codes 2307 2308 mother_codes = [ wf.get_pdg_code() for wf \ 2309 in mothers ] 2310 if pdg_codes == mother_codes: 2311 # Already sorted - skip sort below 2312 return mothers, my_index 2313 2314 sorted_mothers = [] 2315 for i, code in enumerate(pdg_codes): 2316 index = mother_codes.index(code) 2317 mother_codes.pop(index) 2318 mother = mothers.pop(index) 2319 sorted_mothers.append(mother) 2320 2321 if mothers: 2322 raise base_objects.PhysicsObject.PhysicsObjectError 2323 2324 return HelasWavefunctionList(sorted_mothers), my_index
2325
2326 - def majorana_conjugates(self):
2327 """Returns a list [1,2,...] of fermion lines that need 2328 conjugate wfs due to wrong order of I/O Majorana particles 2329 compared to interaction order (or empty list if no Majorana 2330 particles). This is crucial if the Lorentz structure depends 2331 on the direction of the Majorana particles, as in MSSM with 2332 goldstinos.""" 2333 2334 if len([m for m in self if m.is_majorana()]) < 2: 2335 return [] 2336 2337 conjugates = [] 2338 2339 # Check if the order for Majorana fermions is correct 2340 for i in range(0, len(self), 2): 2341 if self[i].is_majorana() and self[i+1].is_majorana() \ 2342 and self[i].get_pdg_code() != \ 2343 self[i+1].get_pdg_code(): 2344 # Check if mother I/O order is correct (IO) 2345 if self[i].get_spin_state_number() > 0 and \ 2346 self[i + 1].get_spin_state_number() < 0: 2347 # Order is wrong, we need a conjugate here 2348 conjugates.append(True) 2349 else: 2350 conjugates.append(False) 2351 elif self[i].is_fermion(): 2352 # For non-Majorana case, always False 2353 conjugates.append(False) 2354 2355 # Return list 1,2,... for which indices are needed 2356 conjugates = [i+1 for (i,c) in enumerate(conjugates) if c] 2357 2358 return conjugates
2359 2360
2361 - def check_wavefunction_numbers_order(self, applyChanges=False, raiseError=True):
2362 """ This function only serves as an internal consistency check to 2363 make sure that when setting the 'wavefunctions' attribute of the 2364 diagram, their order is consistent, in the sense that all mothers 2365 of any given wavefunction appear before that wavefunction. 2366 This function returns True if there was no change and the original 2367 wavefunction list was consistent and False otherwise. 2368 The option 'applyChanges' controls whether the function should substitute 2369 the original list (self) with the new corrected one. For now, this function 2370 is only used for self-consistency checks and the changes are not applied.""" 2371 2372 if len(self)<2: 2373 return True 2374 2375 def RaiseError(): 2376 raise self.PhysicsObjectListError, \ 2377 "This wavefunction list does not have a consistent wavefunction ordering."+\ 2378 "\n Wf numbers: %s"%str([wf['number'] for wf in diag_wfs])+\ 2379 "\n Wf mothers: %s"%str([[mother['number'] for mother in wf['mothers']] \ 2380 for wf in diag_wfs])
2381 2382 # We want to work on a local copy of the wavefunction list attribute 2383 diag_wfs = copy.copy(self) 2384 2385 # We want to keep the original wf numbering (but beware that this 2386 # implies changing the 'number' attribute of some wf if this function 2387 # was used for actual reordering and not just self-consistency check) 2388 wfNumbers = [wf['number'] for wf in self] 2389 2390 exitLoop=False 2391 while not exitLoop: 2392 for i, wf in enumerate(diag_wfs): 2393 if i==len(diag_wfs)-1: 2394 exitLoop=True 2395 break 2396 found=False 2397 # Look at all subsequent wfs in the list placed after wf at 2398 # index i. None of them should have wf as its mother 2399 for w in diag_wfs[i+1:]: 2400 if w['number'] in [mwf['number'] for mwf in wf.get('mothers')]: 2401 # There is an inconsisent order so we must move this 2402 # mother w *before* wf which is placed at i. 2403 diag_wfs.remove(w) 2404 diag_wfs.insert(i,w) 2405 found=True 2406 if raiseError: RaiseError() 2407 if not applyChanges: 2408 return False 2409 break 2410 if found: 2411 break 2412 2413 if diag_wfs!=self: 2414 # After this, diag_wfs is the properly re-ordered and 2415 # consistent list that should be used, where each mother appear 2416 # before its daughter 2417 for i,wf in enumerate(diag_wfs): 2418 wf.set('number', wfNumbers[i]) 2419 2420 # Replace this wavefunction list by corrected one 2421 del self[:] 2422 self.extend(diag_wfs) 2423 2424 # The original list was inconsistent, so it returns False. 2425 return False 2426 2427 # The original list was consistent, so it returns True 2428 return True
2429 2430 @staticmethod
2431 - def extract_wavefunctions(mothers):
2432 """Recursively extract the wavefunctions from mothers of mothers""" 2433 2434 wavefunctions = copy.copy(mothers) 2435 for wf in mothers: 2436 wavefunctions.extend(HelasWavefunctionList.\ 2437 extract_wavefunctions(wf.get('mothers'))) 2438 2439 return wavefunctions
2440
2441 #=============================================================================== 2442 # HelasAmplitude 2443 #=============================================================================== 2444 -class HelasAmplitude(base_objects.PhysicsObject):
2445 """HelasAmplitude object, has the information necessary for 2446 writing a call to a HELAS amplitude routine:a list of mother wavefunctions, 2447 interaction id, amplitude number 2448 """ 2449
2450 - def default_setup(self):
2451 """Default values for all properties""" 2452 2453 # Properties related to the interaction generating the propagator 2454 self['interaction_id'] = 0 2455 # Base for born amplitude, the 'type' argument for the CT-vertices 2456 # and 'loop' for the HelasAmplitudes in a LoopHelasAmplitude. 2457 self['type'] = 'base' 2458 self['pdg_codes'] = [] 2459 self['orders'] = {} 2460 self['inter_color'] = None 2461 self['lorentz'] = [] 2462 self['coupling'] = ['none'] 2463 # The Lorentz and color index used in this amplitude 2464 self['color_key'] = 0 2465 # Properties relating to the vertex 2466 self['number'] = 0 2467 self['fermionfactor'] = 0 2468 self['color_indices'] = [] 2469 self['mothers'] = HelasWavefunctionList() 2470 # conjugate_indices is a list [1,2,...] with fermion lines 2471 # that need conjugates. Default is "None" 2472 self['conjugate_indices'] = None
2473 2474 # Customized constructor
2475 - def __init__(self, *arguments):
2476 """Allow generating a HelasAmplitude from a Vertex 2477 """ 2478 2479 if len(arguments) > 1: 2480 if isinstance(arguments[0], base_objects.Vertex) and \ 2481 isinstance(arguments[1], base_objects.Model): 2482 super(HelasAmplitude, self).__init__() 2483 self.set('interaction_id', 2484 arguments[0].get('id'), arguments[1]) 2485 elif arguments: 2486 super(HelasAmplitude, self).__init__(arguments[0]) 2487 else: 2488 super(HelasAmplitude, self).__init__()
2489
2490 - def filter(self, name, value):
2491 """Filter for valid property values.""" 2492 2493 if name == 'interaction_id': 2494 if not isinstance(value, int): 2495 raise self.PhysicsObjectError, \ 2496 "%s is not a valid integer for interaction id" % \ 2497 str(value) 2498 2499 if name == 'pdg_codes': 2500 #Should be a list of integers 2501 if not isinstance(value, list): 2502 raise self.PhysicsObjectError, \ 2503 "%s is not a valid list of integers" % str(value) 2504 for mystr in value: 2505 if not isinstance(mystr, int): 2506 raise self.PhysicsObjectError, \ 2507 "%s is not a valid integer" % str(mystr) 2508 2509 if name == 'orders': 2510 #Should be a dict with valid order names ask keys and int as values 2511 if not isinstance(value, dict): 2512 raise self.PhysicsObjectError, \ 2513 "%s is not a valid dict for coupling orders" % \ 2514 str(value) 2515 for order in value.keys(): 2516 if not isinstance(order, str): 2517 raise self.PhysicsObjectError, \ 2518 "%s is not a valid string" % str(order) 2519 if not isinstance(value[order], int): 2520 raise self.PhysicsObjectError, \ 2521 "%s is not a valid integer" % str(value[order]) 2522 2523 if name == 'inter_color': 2524 # Should be None or a color string 2525 if value and not isinstance(value, color.ColorString): 2526 raise self.PhysicsObjectError, \ 2527 "%s is not a valid Color String" % str(value) 2528 2529 if name == 'lorentz': 2530 #Should be a list of string 2531 if not isinstance(value, list): 2532 raise self.PhysicsObjectError, \ 2533 "%s is not a valid list of string" % str(value) 2534 for name in value: 2535 if not isinstance(name, str): 2536 raise self.PhysicsObjectError, \ 2537 "%s doesn't contain only string" % str(value) 2538 2539 if name == 'coupling': 2540 #Should be a list of string 2541 if not isinstance(value, list): 2542 raise self.PhysicsObjectError, \ 2543 "%s is not a valid coupling (list of string)" % str(value) 2544 2545 for name in value: 2546 if not isinstance(name, str): 2547 raise self.PhysicsObjectError, \ 2548 "%s doesn't contain only string" % str(value) 2549 if not len(value): 2550 raise self.PhysicsObjectError, \ 2551 'coupling should have at least one value' 2552 2553 if name == 'color_key': 2554 if value and not isinstance(value, int): 2555 raise self.PhysicsObjectError, \ 2556 "%s is not a valid integer" % str(value) 2557 2558 if name == 'number': 2559 if not isinstance(value, int): 2560 raise self.PhysicsObjectError, \ 2561 "%s is not a valid integer for amplitude number" % \ 2562 str(value) 2563 2564 if name == 'fermionfactor': 2565 if not isinstance(value, int): 2566 raise self.PhysicsObjectError, \ 2567 "%s is not a valid integer for fermionfactor" % \ 2568 str(value) 2569 if not value in [-1, 0, 1]: 2570 raise self.PhysicsObjectError, \ 2571 "%s is not a valid fermion factor (-1, 0 or 1)" % \ 2572 str(value) 2573 2574 if name == 'color_indices': 2575 #Should be a list of integers 2576 if not isinstance(value, list): 2577 raise self.PhysicsObjectError, \ 2578 "%s is not a valid list of integers" % str(value) 2579 for mystr in value: 2580 if not isinstance(mystr, int): 2581 raise self.PhysicsObjectError, \ 2582 "%s is not a valid integer" % str(mystr) 2583 2584 if name == 'mothers': 2585 if not isinstance(value, HelasWavefunctionList): 2586 raise self.PhysicsObjectError, \ 2587 "%s is not a valid list of mothers for amplitude" % \ 2588 str(value) 2589 2590 if name == 'conjugate_indices': 2591 if not isinstance(value, tuple) and value != None: 2592 raise self.PhysicsObjectError, \ 2593 "%s is not a valid tuple" % str(value) + \ 2594 " for conjugate_indices" 2595 2596 return True
2597
2598 - def __str__(self):
2599 """ practicle way to represent an HelasAmplitude""" 2600 2601 mystr = '{\n' 2602 for prop in self.get_sorted_keys(): 2603 if isinstance(self[prop], str): 2604 mystr = mystr + ' \'' + prop + '\': \'' + \ 2605 self[prop] + '\',\n' 2606 elif isinstance(self[prop], float): 2607 mystr = mystr + ' \'' + prop + '\': %.2f,\n' % self[prop] 2608 elif isinstance(self[prop], int): 2609 mystr = mystr + ' \'' + prop + '\': %s,\n' % self[prop] 2610 elif prop != 'mothers': 2611 mystr = mystr + ' \'' + prop + '\': ' + \ 2612 str(self[prop]) + ',\n' 2613 else: 2614 info = [m.get('pdg_code') for m in self['mothers']] 2615 mystr += ' \'%s\': %s,\n' % (prop, info) 2616 2617 mystr = mystr.rstrip(',\n') 2618 mystr = mystr + '\n}' 2619 2620 return mystr
2621 2622 # Enhanced get function
2623 - def get(self, name):
2624 """Get the value of the property name.""" 2625 2626 if name == 'fermionfactor' and not self[name]: 2627 self.calculate_fermionfactor() 2628 2629 # Set conjugate_indices if it's not already set 2630 if name == 'conjugate_indices' and self[name] == None: 2631 self['conjugate_indices'] = self.get_conjugate_index() 2632 2633 return super(HelasAmplitude, self).get(name)
2634 2635 # Enhanced set function, where we can append a model 2636
2637 - def set(self, *arguments):
2638 """When setting interaction_id, if model is given (in tuple), 2639 set all other interaction properties. When setting pdg_code, 2640 if model is given, set all other particle properties.""" 2641 2642 assert len(arguments) > 1, "Too few arguments for set" 2643 2644 name = arguments[0] 2645 value = arguments[1] 2646 2647 if len(arguments) > 2 and \ 2648 isinstance(value, int) and \ 2649 isinstance(arguments[2], base_objects.Model): 2650 if name == 'interaction_id': 2651 self.set('interaction_id', value) 2652 if value > 0: 2653 inter = arguments[2].get('interaction_dict')[value] 2654 self.set('pdg_codes', 2655 [part.get_pdg_code() for part in \ 2656 inter.get('particles')]) 2657 self.set('orders', inter.get('orders')) 2658 # Note that the following values might change, if 2659 # the relevant color/lorentz/coupling is not index 0 2660 if inter.get('type'): 2661 self.set('type', inter.get('type')) 2662 if inter.get('color'): 2663 self.set('inter_color', inter.get('color')[0]) 2664 if inter.get('lorentz'): 2665 self.set('lorentz', [inter.get('lorentz')[0]]) 2666 if inter.get('couplings'): 2667 self.set('coupling', [inter.get('couplings').values()[0]]) 2668 return True 2669 else: 2670 raise self.PhysicsObjectError, \ 2671 "%s not allowed name for 3-argument set", name 2672 else: 2673 return super(HelasAmplitude, self).set(name, value)
2674
2675 - def get_sorted_keys(self):
2676 """Return particle property names as a nicely sorted list.""" 2677 2678 return ['interaction_id', 'pdg_codes', 'orders', 'inter_color', 2679 'lorentz', 'coupling', 'color_key', 'number', 'color_indices', 2680 'fermionfactor', 'mothers']
2681 2682 # Helper functions 2683
2684 - def check_and_fix_fermion_flow(self, 2685 wavefunctions, 2686 diagram_wavefunctions, 2687 external_wavefunctions, 2688 wf_number):
2689 """Check for clashing fermion flow (N(incoming) != 2690 N(outgoing)) in mothers. For documentation, check 2691 HelasWavefunction.check_and_fix_fermion_flow. 2692 """ 2693 2694 self.set('mothers', self.get('mothers').sort_by_pdg_codes(\ 2695 self.get('pdg_codes'), 0)[0]) 2696 2697 return self.get('mothers').check_and_fix_fermion_flow(\ 2698 wavefunctions, 2699 diagram_wavefunctions, 2700 external_wavefunctions, 2701 None, 2702 wf_number)
2703 2704
2705 - def needs_hermitian_conjugate(self):
2706 """Returns true if any of the mothers have negative 2707 fermionflow""" 2708 2709 return self.get('conjugate_indices') != ()
2710
2711 - def get_epsilon_order(self):
2712 """Based on the type of the amplitude, determines to which epsilon 2713 order it contributes""" 2714 2715 if '1eps' in self['type']: 2716 return 1 2717 elif '2eps' in self['type']: 2718 return 2 2719 else: 2720 return 0
2721
2722 - def get_call_key(self):
2723 """Generate the (spin, state) tuples used as key for the helas call 2724 dictionaries in HelasModel""" 2725 2726 res = [] 2727 for mother in self.get('mothers'): 2728 res.append(mother.get_spin_state_number()) 2729 2730 # Sort according to spin and flow direction 2731 res.sort() 2732 2733 # The call is different depending on the type of vertex. 2734 # For example, base would give AMP(%d), R2 would give AMPL(0,%d) 2735 # and a single pole UV counter-term would give AMPL(1,%d). 2736 # Also for loop amplitudes, one must have the tag 'loop' 2737 if self['type']!='base': 2738 res.append(self['type']) 2739 2740 # Check if we need to append a charge conjugation flag 2741 if self.needs_hermitian_conjugate(): 2742 res.append(self.get('conjugate_indices')) 2743 2744 return (tuple(res), tuple(self.get('lorentz')))
2745
2746 - def calculate_fermionfactor(self):
2747 """Calculate the fermion factor for the diagram corresponding 2748 to this amplitude""" 2749 2750 # Pick out fermion mothers 2751 fermions = [wf for wf in self.get('mothers') if wf.is_fermion()] 2752 assert len(fermions) % 2 == 0 2753 2754 # Pick out bosons 2755 bosons = filter(lambda wf: wf.is_boson(), self.get('mothers')) 2756 2757 fermion_number_list = [] 2758 2759 # If there are fermion line pairs, append them as 2760 # [NI,NO,n1,n2,...] 2761 fermion_numbers = [f.get_fermion_order() for f in fermions] 2762 2763 # Apply the right sign correction for anti-commutating ghost loops 2764 if self.get('type')=='loop': 2765 # Fetch the second l-cut wavefunctions 2766 lcuf_wf_2=[m for m in self.get('mothers') if m['is_loop'] and \ 2767 len(m.get('mothers'))==0][0] 2768 ghost_factor = -1 if lcuf_wf_2.is_anticommutating_ghost() else 1 2769 else: 2770 # no ghost at tree level 2771 ghost_factor = 1 2772 2773 fermion_loop_factor = 1 2774 2775 # Now put together the fermion line merging in this amplitude 2776 if self.get('type')=='loop' and len(fermion_numbers)>0: 2777 # Remember that the amplitude closing the loop is always a 2-point 2778 # "fake interaction" attached on the second l-cut wavefunction. 2779 # So len(fermion_numbers) is either be 0 or 2. 2780 lcut_wf2_number = lcuf_wf_2.get('number_external') 2781 assert len(fermion_numbers)==2, "Incorrect number of fermions"+\ 2782 " (%d) for the amp. closing the loop."%len(fermion_numbers) 2783 # Fetch the first l-cut wavefunctions 2784 lcuf_wf_1=[m for m in self.get('mothers') if m['is_loop'] and \ 2785 len(m.get('mothers'))>0][0] 2786 while len(lcuf_wf_1.get('mothers'))>0: 2787 lcuf_wf_1 = lcuf_wf_1.get_loop_mother() 2788 lcut_wf1_number = lcuf_wf_1.get('number_external') 2789 2790 # We must now close the loop fermion flow, if there is any. 2791 # This means merging the two lists representing the fermion flow of 2792 # each of the two l-cut fermions into one. Example for the process 2793 # g g > go go [virt=QCD] in the MSSM. 2794 # Loop diagram 21 has the fermion_number_list 2795 # [[3, [5, 4]], [6, []]] 2796 # and 22 has 2797 # [[6, []], [4, [3, 5]]] 2798 # Which should be merged into [3,4] both times 2799 2800 # Here, iferm_to_replace is the position of the fermion line 2801 # pairing which is *not* [6,[]] in the above example. 2802 iferm_to_replace = (fermion_numbers.index([lcut_wf2_number,[]])+1)%2 2803 2804 if fermion_numbers[iferm_to_replace][0]==lcut_wf1_number: 2805 # We have a closed loop fermion flow here, so we must simply 2806 # add a minus sign (irrespectively of whether the closed loop 2807 # fermion flow goes clockwise or counter-clockwise) and not 2808 # consider the fermion loop line in the fermion connection list. 2809 fermion_number_list.extend(fermion_numbers[iferm_to_replace][1]) 2810 fermion_loop_factor = -1 2811 else: 2812 # The fermion flow escape the loop in this case. 2813 fermion_number_list = \ 2814 copy.copy(fermion_numbers[iferm_to_replace][1]) 2815 # We must find to which external fermion the lcut_wf1 is 2816 # connected (i.e. 5 being connected to 3(resp. 4) in the example 2817 # of diagram 22 (resp. 21) above) 2818 i_connected_fermion = fermion_number_list.index(lcut_wf1_number) 2819 fermion_number_list[i_connected_fermion] = \ 2820 fermion_numbers[iferm_to_replace][0] 2821 else: 2822 for iferm in range(0, len(fermion_numbers), 2): 2823 fermion_number_list.append(fermion_numbers[iferm][0]) 2824 fermion_number_list.append(fermion_numbers[iferm+1][0]) 2825 fermion_number_list.extend(fermion_numbers[iferm][1]) 2826 fermion_number_list.extend(fermion_numbers[iferm+1][1]) 2827 2828 2829 # Bosons are treated in the same way for a bosonic loop than for tree 2830 # level kind of amplitudes. 2831 for boson in bosons: 2832 # Bosons return a list [n1,n2,...] 2833 fermion_number_list.extend(boson.get_fermion_order()) 2834 2835 # if not hasattr(HelasAmplitude,"counter"): 2836 # HelasAmplitude.counter=1 2837 # print "MMMMME" 2838 # save1 = copy.deepcopy(fermion_number_list) 2839 # save2 = copy.deepcopy(fermion_number_list2) 2840 # save3 = copy.deepcopy(fermion_number_list) 2841 # save4 = copy.deepcopy(fermion_number_list2) 2842 # if HelasAmplitude.counter<500000 and self.get('type')=='loop' and \ 2843 # HelasAmplitude.sign_flips_to_order(save1)*HelasAmplitude.sign_flips_to_order(save2)==-1: 2844 # print "Before %i=%s"%(HelasAmplitude.counter,str(fermion_numbers_save)) 2845 # print "FOOOOR %i=%s"%(HelasAmplitude.counter,str(fermion_number_list)) 2846 # print "NEW %i=%s"%(HelasAmplitude.counter,str(fermion_number_list2)) 2847 # print "Relative sign =%d"%(HelasAmplitude.sign_flips_to_order(save3)*HelasAmplitude.sign_flips_to_order(save4)) 2848 # HelasAmplitude.counter=self.counter+1 2849 2850 #fermion_number_list = fermion_number_list2 2851 2852 fermion_factor = HelasAmplitude.sign_flips_to_order(fermion_number_list) 2853 2854 self['fermionfactor'] = fermion_factor*ghost_factor*fermion_loop_factor
2855 # print "foooor %i ="%HelasAmplitude.counter, fermion_factor, self.get('type') 2856 2857 @staticmethod
2858 - def sign_flips_to_order(fermions):
2859 """Gives the sign corresponding to the number of flips needed 2860 to place the fermion numbers in order""" 2861 2862 # Perform bubble sort on the fermions, and keep track of 2863 # the number of flips that are needed 2864 2865 nflips = 0 2866 2867 for i in range(len(fermions) - 1): 2868 for j in range(i + 1, len(fermions)): 2869 if fermions[j] < fermions[i]: 2870 fermions[i], fermions[j] = fermions[j], fermions[i] 2871 nflips = nflips + 1 2872 2873 return (-1) ** nflips
2874
2875 - def get_aloha_info(self, optimized_output=True):
2876 """Returns the tuple (lorentz_name, tag, outgoing_number) providing 2877 the necessary information to compute_subset of create_aloha to write 2878 out the HELAS-like routines.""" 2879 2880 # In principle this function should not be called for the case below, 2881 # or if it does it should handle specifically the None returned value. 2882 if self.get('interaction_id') in [0,-1]: 2883 return None 2884 2885 tags = ['C%s' % w for w in self.get_conjugate_index()] 2886 2887 return (tuple(self.get('lorentz')),tuple(tags),self.find_outgoing_number())
2888
2889 - def get_base_diagram(self, wf_dict, vx_list = [], optimization = 1):
2890 """Return the base_objects.Diagram which corresponds to this 2891 amplitude, using a recursive method for the wavefunctions.""" 2892 2893 vertices = base_objects.VertexList() 2894 2895 # Add vertices for all mothers 2896 for mother in self.get('mothers'): 2897 vertices.extend(mother.get_base_vertices(wf_dict, vx_list, 2898 optimization)) 2899 # Generate last vertex 2900 vertex = self.get_base_vertex(wf_dict, vx_list, optimization) 2901 2902 vertices.append(vertex) 2903 2904 return base_objects.Diagram({'vertices': vertices})
2905
2906 - def get_base_vertex(self, wf_dict, vx_list = [], optimization = 1):
2907 """Get a base_objects.Vertex corresponding to this amplitude.""" 2908 2909 # Generate last vertex 2910 legs = base_objects.LegList() 2911 for mother in self.get('mothers'): 2912 try: 2913 if mother.get('is_loop'): 2914 # Loop wavefunction should always be redefined 2915 raise KeyError 2916 leg = wf_dict[(mother.get('number'),False)] 2917 except KeyError: 2918 leg = base_objects.Leg({ 2919 'id': mother.get_pdg_code(), 2920 'number': mother.get('number_external'), 2921 'state': mother.get('leg_state'), 2922 'onshell': None, 2923 'loop_line':mother.get('is_loop') 2924 }) 2925 if optimization != 0 and not mother.get('is_loop'): 2926 wf_dict[(mother.get('number'),False)] = leg 2927 legs.append(leg) 2928 2929 return base_objects.Vertex({ 2930 'id': self.get('interaction_id'), 2931 'legs': legs})
2932
2933 - def get_s_and_t_channels(self, ninitial, model, new_pdg, reverse_t_ch = False):
2934 """Returns two lists of vertices corresponding to the s- and 2935 t-channels of this amplitude/diagram, ordered from the outermost 2936 s-channel and in/down towards the highest number initial state 2937 leg.""" 2938 2939 # Create a CanonicalConfigTag to ensure that the order of 2940 # propagators is canonical 2941 wf_dict = {} 2942 max_final_leg = 2 2943 if reverse_t_ch: 2944 max_final_leg = 1 2945 # Note that here we do not specify a FDStructure repository, so that 2946 # each loop diagram will recreate them. This is ok at this point because 2947 # we do not need to have a canonical ID for the FD structures. 2948 tag = CanonicalConfigTag(self.get_base_diagram(wf_dict). 2949 get_contracted_loop_diagram(model), model) 2950 2951 return tag.get_s_and_t_channels(ninitial, model, new_pdg, max_final_leg)
2952 2953
2954 - def get_color_indices(self):
2955 """Get the color indices corresponding to 2956 this amplitude and its mothers, using a recursive function.""" 2957 2958 if not self.get('mothers'): 2959 return [] 2960 2961 color_indices = [] 2962 2963 # Add color indices for all mothers 2964 for mother in self.get('mothers'): 2965 # This is where recursion happens 2966 color_indices.extend(mother.get_color_indices()) 2967 2968 # Add this amp's color index 2969 if self.get('interaction_id') not in [0,-1]: 2970 color_indices.append(self.get('color_key')) 2971 2972 return color_indices
2973
2974 - def find_outgoing_number(self):
2975 """Return 0. Needed to treat HelasAmplitudes and 2976 HelasWavefunctions on same footing.""" 2977 2978 return 0
2979
2980 - def get_conjugate_index(self):
2981 """Return the index of the particle that should be conjugated.""" 2982 2983 if not any([(wf.get('fermionflow') < 0 or wf.is_majorana()) for wf in \ 2984 self.get('mothers')]): 2985 return () 2986 2987 # Pick out first sorted mothers, then fermions 2988 mothers, self_index = \ 2989 self.get('mothers').sort_by_pdg_codes(self.get('pdg_codes')) 2990 fermions = HelasWavefunctionList([wf for wf in mothers if wf.is_fermion()]) 2991 2992 # Initialize indices with indices due to Majoranas with wrong order 2993 indices = fermions.majorana_conjugates() 2994 2995 # Check for fermions with negative fermion flow 2996 for i in range(0,len(fermions), 2): 2997 if fermions[i].get('fermionflow') < 0 or \ 2998 fermions[i+1].get('fermionflow') < 0: 2999 indices.append(i/2 + 1) 3000 3001 return tuple(sorted(indices))
3002
3003 - def get_vertex_leg_numbers(self, 3004 veto_inter_id=base_objects.Vertex.ID_to_veto_for_multichanneling, 3005 max_n_loop=0):
3006 """Get a list of the number of legs in vertices in this diagram, 3007 This function is only used for establishing the multi-channeling, so that 3008 we exclude from it all the fake vertices and the vertices resulting from 3009 shrunk loops (id=-2)""" 3010 3011 if max_n_loop == 0: 3012 max_n_loop = base_objects.Vertex.max_n_loop_for_multichanneling 3013 3014 vertex_leg_numbers = [len(self.get('mothers'))] if \ 3015 (self['interaction_id'] not in veto_inter_id) or \ 3016 (self['interaction_id']==-2 and len(self.get('mothers'))>max_n_loop) \ 3017 else [] 3018 for mother in self.get('mothers'): 3019 vertex_leg_numbers.extend(mother.get_vertex_leg_numbers( 3020 veto_inter_id = veto_inter_id)) 3021 3022 return vertex_leg_numbers
3023
3024 - def get_helas_call_dict(self,index=1,OptimizedOutput=False, 3025 specifyHel=True,**opt):
3026 """ return a dictionary to be used for formatting 3027 HELAS call.""" 3028 3029 if index == 1: 3030 flip = 0 3031 else: 3032 flip = 1 3033 3034 output = {} 3035 for i, mother in enumerate(self.get('mothers')): 3036 nb = mother.get('me_id') - flip 3037 output[str(i)] = nb 3038 if mother.get('is_loop'): 3039 output['WF%d' % i ] = 'L(1,%d)'%nb 3040 else: 3041 if specifyHel: 3042 output['WF%d' % i ] = '(1,WE(%d),H)'%nb 3043 else: 3044 output['WF%d' % i ] = '(1,WE(%d))'%nb 3045 3046 #fixed argument 3047 for i, coup in enumerate(self.get('coupling')): 3048 output['coup%d'%i] = str(coup) 3049 3050 output['out'] = self.get('number') - flip 3051 output['propa'] = '' 3052 output.update(opt) 3053 return output
3054 3055 3056
3057 - def set_coupling_color_factor(self):
3058 """Check if there is a mismatch between order of fermions 3059 w.r.t. color""" 3060 mothers = self.get('mothers') 3061 3062 # Sort mothers according to pdg codes if fermions with indentical 3063 # color but not identical pdg code. Needed for antisymmetric 3064 # color eps^{ijk}. 3065 for imo in range(len(mothers)-1): 3066 if mothers[imo].get('color') != 1 and \ 3067 mothers[imo].is_fermion() and \ 3068 mothers[imo].get('color') == mothers[imo+1].get('color') and \ 3069 mothers[imo].get('spin') == mothers[imo+1].get('spin') and \ 3070 mothers[imo].get('pdg_code') != mothers[imo+1].get('pdg_code'): 3071 mothers, my_index = \ 3072 mothers.sort_by_pdg_codes(self.get('pdg_codes')) 3073 break 3074 3075 if mothers != self.get('mothers') and \ 3076 not self.get('coupling').startswith('-'): 3077 # We have mismatch between fermion order for color and lorentz 3078 self.set('coupling', '-'+self.get('coupling'))
3079 3080 # Comparison between different amplitudes, to allow check for 3081 # identical processes. Note that we are then not interested in 3082 # interaction id, but in all other properties. 3083
3084 - def __eq__(self, other):
3085 """Comparison between different amplitudes, to allow check for 3086 identical processes. 3087 """ 3088 3089 if not isinstance(other, HelasAmplitude): 3090 return False 3091 3092 # Check relevant directly defined properties 3093 if self['lorentz'] != other['lorentz'] or \ 3094 self['coupling'] != other['coupling'] or \ 3095 self['number'] != other['number']: 3096 return False 3097 3098 # Check that mothers have the same numbers (only relevant info) 3099 return sorted([mother['number'] for mother in self['mothers']]) == \ 3100 sorted([mother['number'] for mother in other['mothers']])
3101
3102 - def __ne__(self, other):
3103 """Overloading the nonequality operator, to make comparison easy""" 3104 return not self.__eq__(other)
3105
3106 #=============================================================================== 3107 # HelasAmplitudeList 3108 #=============================================================================== 3109 -class HelasAmplitudeList(base_objects.PhysicsObjectList):
3110 """List of HelasAmplitude objects 3111 """ 3112
3113 - def is_valid_element(self, obj):
3114 """Test if object obj is a valid HelasAmplitude for the list.""" 3115 3116 return isinstance(obj, HelasAmplitude)
3117
3118 3119 #=============================================================================== 3120 # HelasDiagram 3121 #=============================================================================== 3122 -class HelasDiagram(base_objects.PhysicsObject):
3123 """HelasDiagram: list of HelasWavefunctions and a HelasAmplitude, 3124 plus the fermion factor associated with the corresponding diagram. 3125 """ 3126
3127 - def default_setup(self):
3128 """Default values for all properties""" 3129 3130 self['wavefunctions'] = HelasWavefunctionList() 3131 # In the optimized output the loop wavefunctions can be recycled as 3132 # well. If so, those are put in the list below, and are not mixed with 3133 # the tree wavefunctions above in order to keep the original structure. 3134 self['loop_wavefunctions'] = HelasWavefunctionList() 3135 # One diagram can have several amplitudes, if there are 3136 # different Lorentz or color structures associated with this 3137 # diagram 3138 self['amplitudes'] = HelasAmplitudeList() 3139 self['number'] = 0
3140
3141 - def filter(self, name, value):
3142 """Filter for valid diagram property values.""" 3143 3144 if name == 'wavefunctions' or name == 'loop_wavefunctions': 3145 if not isinstance(value, HelasWavefunctionList): 3146 raise self.PhysicsObjectError, \ 3147 "%s is not a valid HelasWavefunctionList object" % \ 3148 str(value) 3149 3150 if name == 'amplitudes': 3151 if not isinstance(value, HelasAmplitudeList): 3152 raise self.PhysicsObjectError, \ 3153 "%s is not a valid HelasAmplitudeList object" % \ 3154 str(value) 3155 3156 return True
3157
3158 - def get_sorted_keys(self):
3159 """Return particle property names as a nicely sorted list.""" 3160 3161 return ['wavefunctions', 'loop_wavefunctions', 'amplitudes']
3162
3163 - def calculate_orders(self):
3164 """Calculate the actual coupling orders of this diagram""" 3165 3166 wavefunctions = HelasWavefunctionList.extract_wavefunctions(\ 3167 self.get('amplitudes')[0].get('mothers')) 3168 3169 coupling_orders = {} 3170 for wf in wavefunctions + [self.get('amplitudes')[0]]: 3171 if not wf.get('orders'): continue 3172 for order in wf.get('orders').keys(): 3173 try: 3174 coupling_orders[order] += wf.get('orders')[order] 3175 except Exception: 3176 coupling_orders[order] = wf.get('orders')[order] 3177 3178 return coupling_orders
3179
3180 - def get_vertex_leg_numbers(self, 3181 veto_inter_id=base_objects.Vertex.ID_to_veto_for_multichanneling, 3182 max_n_loop=0):
3183 """Get a list of the number of legs in vertices in this diagram""" 3184 3185 if max_n_loop == 0: 3186 max_n_loop = base_objects.Vertex.max_n_loop_for_multichanneling 3187 3188 return self.get('amplitudes')[0].get_vertex_leg_numbers( 3189 veto_inter_id=veto_inter_id, max_n_loop=max_n_loop)
3190
3191 - def get_regular_amplitudes(self):
3192 """ For regular HelasDiagrams, it is simply all amplitudes. 3193 It is overloaded in LoopHelasDiagram""" 3194 3195 return self['amplitudes']
3196
3197 #=============================================================================== 3198 # HelasDiagramList 3199 #=============================================================================== 3200 -class HelasDiagramList(base_objects.PhysicsObjectList):
3201 """List of HelasDiagram objects 3202 """ 3203
3204 - def is_valid_element(self, obj):
3205 """Test if object obj is a valid HelasDiagram for the list.""" 3206 3207 return isinstance(obj, HelasDiagram)
3208
3209 #=============================================================================== 3210 # HelasMatrixElement 3211 #=============================================================================== 3212 -class HelasMatrixElement(base_objects.PhysicsObject):
3213 """HelasMatrixElement: list of processes with identical Helas 3214 calls, and the list of HelasDiagrams associated with the processes. 3215 3216 If initiated with an Amplitude, HelasMatrixElement calls 3217 generate_helas_diagrams, which goes through the diagrams of the 3218 Amplitude and generates the corresponding Helas calls, taking into 3219 account possible fermion flow clashes due to Majorana 3220 particles. The optional optimization argument determines whether 3221 optimization is used (optimization = 1, default), for maximum 3222 recycling of wavefunctions, or no optimization (optimization = 0) 3223 when each diagram is written independently of all previous 3224 diagrams (this is useful for running with restricted memory, 3225 e.g. on a GPU). For processes with many diagrams, the total number 3226 or wavefunctions after optimization is ~15% of the number of 3227 amplitudes (diagrams). 3228 3229 By default, it will also generate the color information (color 3230 basis and color matrix) corresponding to the Amplitude. 3231 """ 3232
3233 - def default_setup(self):
3234 """Default values for all properties""" 3235 3236 self['processes'] = base_objects.ProcessList() 3237 self['diagrams'] = HelasDiagramList() 3238 self['identical_particle_factor'] = 0 3239 self['color_basis'] = color_amp.ColorBasis() 3240 self['color_matrix'] = color_amp.ColorMatrix(color_amp.ColorBasis()) 3241 # base_amplitude is the Amplitude to be used in color 3242 # generation, drawing etc. For decay chain processes, this is 3243 # the Amplitude which corresponds to the combined process. 3244 self['base_amplitude'] = None 3245 # has_mirror_process is True if the same process but with the 3246 # two incoming particles interchanged has been generated 3247 self['has_mirror_process'] = False
3248
3249 - def filter(self, name, value):
3250 """Filter for valid diagram property values.""" 3251 3252 if name == 'processes': 3253 if not isinstance(value, base_objects.ProcessList): 3254 raise self.PhysicsObjectError, \ 3255 "%s is not a valid ProcessList object" % str(value) 3256 if name == 'diagrams': 3257 if not isinstance(value, HelasDiagramList): 3258 raise self.PhysicsObjectError, \ 3259 "%s is not a valid HelasDiagramList object" % str(value) 3260 if name == 'identical_particle_factor': 3261 if not isinstance(value, int): 3262 raise self.PhysicsObjectError, \ 3263 "%s is not a valid int object" % str(value) 3264 if name == 'color_basis': 3265 if not isinstance(value, color_amp.ColorBasis): 3266 raise self.PhysicsObjectError, \ 3267 "%s is not a valid ColorBasis object" % str(value) 3268 if name == 'color_matrix': 3269 if not isinstance(value, color_amp.ColorMatrix): 3270 raise self.PhysicsObjectError, \ 3271 "%s is not a valid ColorMatrix object" % str(value) 3272 if name == 'base_amplitude': 3273 if value != None and not \ 3274 isinstance(value, diagram_generation.Amplitude): 3275 raise self.PhysicsObjectError, \ 3276 "%s is not a valid Amplitude object" % str(value) 3277 if name == 'has_mirror_process': 3278 if not isinstance(value, bool): 3279 raise self.PhysicsObjectError, \ 3280 "%s is not a valid boolean" % str(value) 3281 return True
3282
3283 - def get_sorted_keys(self):
3284 """Return particle property names as a nicely sorted list.""" 3285 3286 return ['processes', 'identical_particle_factor', 3287 'diagrams', 'color_basis', 'color_matrix', 3288 'base_amplitude', 'has_mirror_process']
3289 3290 # Enhanced get function
3291 - def get(self, name):
3292 """Get the value of the property name.""" 3293 3294 if name == 'base_amplitude' and not self[name]: 3295 self['base_amplitude'] = self.get_base_amplitude() 3296 3297 return super(HelasMatrixElement, self).get(name)
3298 3299 # Customized constructor
3300 - def __init__(self, amplitude=None, optimization=1, 3301 decay_ids=[], gen_color=True):
3302 """Constructor for the HelasMatrixElement. In particular allows 3303 generating a HelasMatrixElement from an Amplitude, with 3304 automatic generation of the necessary wavefunctions 3305 """ 3306 3307 if amplitude != None: 3308 if isinstance(amplitude, diagram_generation.Amplitude): 3309 super(HelasMatrixElement, self).__init__() 3310 self.get('processes').append(amplitude.get('process')) 3311 self.set('has_mirror_process', 3312 amplitude.get('has_mirror_process')) 3313 self.generate_helas_diagrams(amplitude, optimization, decay_ids) 3314 self.calculate_fermionfactors() 3315 self.calculate_identical_particle_factor() 3316 if gen_color and not self.get('color_basis'): 3317 self.process_color() 3318 else: 3319 # In this case, try to use amplitude as a dictionary 3320 super(HelasMatrixElement, self).__init__(amplitude) 3321 else: 3322 super(HelasMatrixElement, self).__init__()
3323 3324 # Comparison between different amplitudes, to allow check for 3325 # identical processes. Note that we are then not interested in 3326 # interaction id, but in all other properties.
3327 - def __eq__(self, other):
3328 """Comparison between different matrix elements, to allow check for 3329 identical processes. 3330 """ 3331 3332 if not isinstance(other, HelasMatrixElement): 3333 return False 3334 3335 # If no processes, this is an empty matrix element 3336 if not self['processes'] and not other['processes']: 3337 return True 3338 3339 # Should only check if diagrams and process id are identical 3340 # Except in case of decay processes: then also initial state 3341 # must be the same 3342 if self['processes'] and not other['processes'] or \ 3343 self['has_mirror_process'] != other['has_mirror_process'] or \ 3344 self['processes'] and \ 3345 self['processes'][0]['id'] != other['processes'][0]['id'] or \ 3346 self['processes'][0]['is_decay_chain'] or \ 3347 other['processes'][0]['is_decay_chain'] or \ 3348 self['identical_particle_factor'] != \ 3349 other['identical_particle_factor'] or \ 3350 self['diagrams'] != other['diagrams']: 3351 return False 3352 return True
3353
3354 - def __ne__(self, other):
3355 """Overloading the nonequality operator, to make comparison easy""" 3356 return not self.__eq__(other)
3357
3358 - def process_color(self):
3359 """ Perform the simple color processing from a single matrix element 3360 (without optimization then). This is called from the initialization 3361 and pulled out here in order to have the correct treatment in daughter 3362 classes.""" 3363 logger.debug('Computing the color basis') 3364 self.get('color_basis').build(self.get('base_amplitude')) 3365 self.set('color_matrix', 3366 color_amp.ColorMatrix(self.get('color_basis')))
3367
3368 - def generate_helas_diagrams(self, amplitude, optimization=1,decay_ids=[]):
3369 """Starting from a list of Diagrams from the diagram 3370 generation, generate the corresponding HelasDiagrams, i.e., 3371 the wave functions and amplitudes. Choose between default 3372 optimization (= 1, maximum recycling of wavefunctions) or no 3373 optimization (= 0, no recycling of wavefunctions, useful for 3374 GPU calculations with very restricted memory). 3375 3376 Note that we need special treatment for decay chains, since 3377 the end product then is a wavefunction, not an amplitude. 3378 """ 3379 3380 assert isinstance(amplitude, diagram_generation.Amplitude), \ 3381 "Missing or erraneous arguments for generate_helas_diagrams" 3382 assert isinstance(optimization, int), \ 3383 "Missing or erraneous arguments for generate_helas_diagrams" 3384 self.optimization = optimization 3385 3386 diagram_list = amplitude.get('diagrams') 3387 process = amplitude.get('process') 3388 3389 model = process.get('model') 3390 if not diagram_list: 3391 return 3392 3393 # All the previously defined wavefunctions 3394 wavefunctions = [] 3395 # List of minimal information for comparison with previous 3396 # wavefunctions 3397 wf_mother_arrays = [] 3398 # Keep track of wavefunction number 3399 wf_number = 0 3400 3401 # Generate wavefunctions for the external particles 3402 external_wavefunctions = dict([(leg.get('number'), 3403 HelasWavefunction(leg, 0, model, 3404 decay_ids)) \ 3405 for leg in process.get('legs')]) 3406 3407 # Initially, have one wavefunction for each external leg. 3408 wf_number = len(process.get('legs')) 3409 3410 # For initial state bosons, need to flip part-antipart 3411 # since all bosons should be treated as outgoing 3412 for key in external_wavefunctions.keys(): 3413 wf = external_wavefunctions[key] 3414 if wf.is_boson() and wf.get('state') == 'initial' and \ 3415 not wf.get('self_antipart'): 3416 wf.set('is_part', not wf.get('is_part')) 3417 3418 # For initial state particles, need to flip PDG code (if has 3419 # antipart) 3420 for key in external_wavefunctions.keys(): 3421 wf = external_wavefunctions[key] 3422 if wf.get('leg_state') == False and \ 3423 not wf.get('self_antipart'): 3424 wf.flip_part_antipart() 3425 3426 # Now go through the diagrams, looking for undefined wavefunctions 3427 3428 helas_diagrams = HelasDiagramList() 3429 3430 # Keep track of amplitude number and diagram number 3431 amplitude_number = 0 3432 diagram_number = 0 3433 3434 for diagram in diagram_list: 3435 3436 # List of dictionaries from leg number to wave function, 3437 # keeps track of the present position in the tree. 3438 # Need one dictionary per coupling multiplicity (diagram) 3439 number_to_wavefunctions = [{}] 3440 3441 # Need to keep track of the color structures for each amplitude 3442 color_lists = [[]] 3443 3444 # Initialize wavefunctions for this diagram 3445 diagram_wavefunctions = HelasWavefunctionList() 3446 3447 vertices = copy.copy(diagram.get('vertices')) 3448 3449 # Single out last vertex, since this will give amplitude 3450 lastvx = vertices.pop() 3451 3452 # Go through all vertices except the last and create 3453 # wavefunctions 3454 for vertex in vertices: 3455 3456 # In case there are diagrams with multiple Lorentz/color 3457 # structures, we need to keep track of the wavefunctions 3458 # for each such structure separately, and generate 3459 # one HelasDiagram for each structure. 3460 # We use the array number_to_wavefunctions to keep 3461 # track of this, with one dictionary per chain of 3462 # wavefunctions 3463 # Note that all wavefunctions relating to this diagram 3464 # will be written out before the first amplitude is written. 3465 new_number_to_wavefunctions = [] 3466 new_color_lists = [] 3467 for number_wf_dict, color_list in zip(number_to_wavefunctions, 3468 color_lists): 3469 legs = copy.copy(vertex.get('legs')) 3470 last_leg = legs.pop() 3471 # Generate list of mothers from legs 3472 mothers = self.getmothers(legs, number_wf_dict, 3473 external_wavefunctions, 3474 wavefunctions, 3475 diagram_wavefunctions) 3476 inter = model.get('interaction_dict')[vertex.get('id')] 3477 3478 # Now generate new wavefunction for the last leg 3479 3480 # Need one amplitude for each color structure, 3481 done_color = {} # store link to color 3482 for coupl_key in sorted(inter.get('couplings').keys()): 3483 color = coupl_key[0] 3484 if color in done_color: 3485 wf = done_color[color] 3486 wf.get('coupling').append(inter.get('couplings')[coupl_key]) 3487 wf.get('lorentz').append(inter.get('lorentz')[coupl_key[1]]) 3488 continue 3489 wf = HelasWavefunction(last_leg, vertex.get('id'), model) 3490 wf.set('coupling', [inter.get('couplings')[coupl_key]]) 3491 if inter.get('color'): 3492 wf.set('inter_color', inter.get('color')[coupl_key[0]]) 3493 done_color[color] = wf 3494 wf.set('lorentz', [inter.get('lorentz')[coupl_key[1]]]) 3495 wf.set('color_key', color) 3496 wf.set('mothers', mothers) 3497 # Need to set incoming/outgoing and 3498 # particle/antiparticle according to the fermion flow 3499 # of mothers 3500 wf.set_state_and_particle(model) 3501 # Need to check for clashing fermion flow due to 3502 # Majorana fermions, and modify if necessary 3503 # Also need to keep track of the wavefunction number. 3504 wf, wf_number = wf.check_and_fix_fermion_flow(\ 3505 wavefunctions, 3506 diagram_wavefunctions, 3507 external_wavefunctions, 3508 wf_number) 3509 # Create new copy of number_wf_dict 3510 new_number_wf_dict = copy.copy(number_wf_dict) 3511 3512 # Store wavefunction 3513 try: 3514 wf = diagram_wavefunctions[\ 3515 diagram_wavefunctions.index(wf)] 3516 except ValueError, error: 3517 # Update wf number 3518 wf_number = wf_number + 1 3519 wf.set('number', wf_number) 3520 try: 3521 # Use wf_mother_arrays to locate existing 3522 # wavefunction 3523 wf = wavefunctions[wf_mother_arrays.index(\ 3524 wf.to_array())] 3525 # Since we reuse the old wavefunction, reset 3526 # wf_number 3527 wf_number = wf_number - 1 3528 except ValueError: 3529 diagram_wavefunctions.append(wf) 3530 3531 new_number_wf_dict[last_leg.get('number')] = wf 3532 3533 # Store the new copy of number_wf_dict 3534 new_number_to_wavefunctions.append(\ 3535 new_number_wf_dict) 3536 # Add color index and store new copy of color_lists 3537 new_color_list = copy.copy(color_list) 3538 new_color_list.append(coupl_key[0]) 3539 new_color_lists.append(new_color_list) 3540 3541 number_to_wavefunctions = new_number_to_wavefunctions 3542 color_lists = new_color_lists 3543 3544 # Generate all amplitudes corresponding to the different 3545 # copies of this diagram 3546 helas_diagram = HelasDiagram() 3547 diagram_number = diagram_number + 1 3548 helas_diagram.set('number', diagram_number) 3549 for number_wf_dict, color_list in zip(number_to_wavefunctions, 3550 color_lists): 3551 # Now generate HelasAmplitudes from the last vertex. 3552 if lastvx.get('id'): 3553 inter = model.get_interaction(lastvx.get('id')) 3554 keys = sorted(inter.get('couplings').keys()) 3555 pdg_codes = [p.get_pdg_code() for p in \ 3556 inter.get('particles')] 3557 else: 3558 # Special case for decay chain - amplitude is just a 3559 # placeholder for replaced wavefunction 3560 inter = None 3561 keys = [(0, 0)] 3562 pdg_codes = None 3563 3564 # Find mothers for the amplitude 3565 legs = lastvx.get('legs') 3566 mothers = self.getmothers(legs, number_wf_dict, 3567 external_wavefunctions, 3568 wavefunctions, 3569 diagram_wavefunctions).\ 3570 sort_by_pdg_codes(pdg_codes, 0)[0] 3571 # Need to check for clashing fermion flow due to 3572 # Majorana fermions, and modify if necessary 3573 wf_number = mothers.check_and_fix_fermion_flow(wavefunctions, 3574 diagram_wavefunctions, 3575 external_wavefunctions, 3576 None, 3577 wf_number, 3578 False, 3579 number_to_wavefunctions) 3580 done_color = {} 3581 for i, coupl_key in enumerate(keys): 3582 color = coupl_key[0] 3583 if inter and color in done_color.keys(): 3584 amp = done_color[color] 3585 amp.get('coupling').append(inter.get('couplings')[coupl_key]) 3586 amp.get('lorentz').append(inter.get('lorentz')[coupl_key[1]]) 3587 continue 3588 amp = HelasAmplitude(lastvx, model) 3589 if inter: 3590 amp.set('coupling', [inter.get('couplings')[coupl_key]]) 3591 amp.set('lorentz', [inter.get('lorentz')[coupl_key[1]]]) 3592 if inter.get('color'): 3593 amp.set('inter_color', inter.get('color')[color]) 3594 amp.set('color_key', color) 3595 done_color[color] = amp 3596 amp.set('mothers', mothers) 3597 amplitude_number = amplitude_number + 1 3598 amp.set('number', amplitude_number) 3599 # Add the list with color indices to the amplitude 3600 new_color_list = copy.copy(color_list) 3601 if inter: 3602 new_color_list.append(color) 3603 3604 amp.set('color_indices', new_color_list) 3605 3606 # Add amplitude to amplitdes in helas_diagram 3607 helas_diagram.get('amplitudes').append(amp) 3608 3609 # After generation of all wavefunctions and amplitudes, 3610 # first sort the wavefunctions according to number 3611 diagram_wavefunctions.sort(lambda wf1, wf2: \ 3612 wf1.get('number') - wf2.get('number')) 3613 3614 # Then make sure that all mothers come before daughters 3615 iwf = len(diagram_wavefunctions) - 1 3616 while iwf > 0: 3617 this_wf = diagram_wavefunctions[iwf] 3618 moved = False 3619 for i,wf in enumerate(diagram_wavefunctions[:iwf]): 3620 if this_wf in wf.get('mothers'): 3621 diagram_wavefunctions.pop(iwf) 3622 diagram_wavefunctions.insert(i, this_wf) 3623 this_wf.set('number', wf.get('number')) 3624 for w in diagram_wavefunctions[i+1:]: 3625 w.set('number',w.get('number')+1) 3626 moved = True 3627 break 3628 if not moved: iwf -= 1 3629 3630 # Finally, add wavefunctions to diagram 3631 helas_diagram.set('wavefunctions', diagram_wavefunctions) 3632 3633 if optimization: 3634 wavefunctions.extend(diagram_wavefunctions) 3635 wf_mother_arrays.extend([wf.to_array() for wf \ 3636 in diagram_wavefunctions]) 3637 else: 3638 wf_number = len(process.get('legs')) 3639 3640 # Append this diagram in the diagram list 3641 helas_diagrams.append(helas_diagram) 3642 3643 3644 self.set('diagrams', helas_diagrams) 3645 3646 # Sort all mothers according to the order wanted in Helas calls 3647 for wf in self.get_all_wavefunctions(): 3648 wf.set('mothers', HelasMatrixElement.sorted_mothers(wf)) 3649 3650 for amp in self.get_all_amplitudes(): 3651 amp.set('mothers', HelasMatrixElement.sorted_mothers(amp)) 3652 amp.set('color_indices', amp.get_color_indices())
3653 3654
3655 - def reuse_outdated_wavefunctions(self, helas_diagrams):
3656 """change the wavefunctions id used in the writer to minimize the 3657 memory used by the wavefunctions.""" 3658 3659 if not self.optimization: 3660 for diag in helas_diagrams: 3661 for wf in diag['wavefunctions']: 3662 wf.set('me_id',wf.get('number')) 3663 return helas_diagrams 3664 3665 # First compute the first/last appearance of each wavefunctions 3666 # first takes the line number and return the id of the created wf 3667 # last_lign takes the id of the wf and return the line number 3668 last_lign={} 3669 first={} 3670 pos=0 3671 for diag in helas_diagrams: 3672 for wf in diag['wavefunctions']: 3673 pos+=1 3674 for wfin in wf.get('mothers'): 3675 last_lign[wfin.get('number')] = pos 3676 assert wfin.get('number') in first.values() 3677 first[pos] = wf.get('number') 3678 for amp in diag['amplitudes']: 3679 pos+=1 3680 for wfin in amp.get('mothers'): 3681 last_lign[wfin.get('number')] = pos 3682 3683 # last takes the line number and return the last appearing wf at 3684 #that particular line 3685 last=collections.defaultdict(list) 3686 for nb, pos in last_lign.items(): 3687 last[pos].append(nb) 3688 tag = list(set(last.keys()+first.keys())) 3689 tag.sort() #lines number where something happen (new in/out) 3690 3691 # Create the replacement id dictionary 3692 outdated = [] # wf id which ar not use any more at this stage 3693 replace = {} # replacement directory 3694 max_wf = 0 3695 for nb in tag: 3696 if outdated and nb in first: 3697 replace[first[nb]] = outdated.pop() 3698 elif nb in first: 3699 assert first[nb] not in replace, '%s already assigned' % first[nb] 3700 max_wf += 1 3701 replace[first[nb]] = max_wf 3702 if nb in last: 3703 for value in last[nb]: 3704 outdated.append(replace[value]) 3705 3706 3707 #replace the id 3708 for diag in helas_diagrams: 3709 for wf in diag['wavefunctions']: 3710 wf.set('me_id', replace[wf.get('number')]) 3711 3712 return helas_diagrams
3713
3715 """This restore the original memory print and revert 3716 change the wavefunctions id used in the writer to minimize the 3717 memory used by the wavefunctions.""" 3718 3719 helas_diagrams = self.get('diagrams') 3720 3721 for diag in helas_diagrams: 3722 for wf in diag['wavefunctions']: 3723 wf.set('me_id',wf.get('number')) 3724 3725 return helas_diagrams
3726 3727
3728 - def insert_decay_chains(self, decay_dict):
3729 """Iteratively insert decay chains decays into this matrix 3730 element. 3731 * decay_dict: a dictionary from external leg number 3732 to decay matrix element. 3733 """ 3734 3735 # First need to reset all legs_with_decays 3736 for proc in self.get('processes'): 3737 proc.set('legs_with_decays', base_objects.LegList()) 3738 3739 # We need to keep track of how the 3740 # wavefunction numbers change 3741 replace_dict = {} 3742 for number in decay_dict.keys(): 3743 # Find all wavefunctions corresponding to this external 3744 # leg number 3745 replace_dict[number] = [wf for wf in \ 3746 filter(lambda wf: not wf.get('mothers') and \ 3747 wf.get('number_external') == number, 3748 self.get_all_wavefunctions())] 3749 3750 # Keep track of wavefunction and amplitude numbers, to ensure 3751 # unique numbers for all new wfs and amps during manipulations 3752 numbers = [self.get_all_wavefunctions()[-1].get('number'), 3753 self.get_all_amplitudes()[-1].get('number')] 3754 3755 # Check if there are any Majorana particles present in any diagrams 3756 got_majoranas = False 3757 for wf in self.get_all_wavefunctions() + \ 3758 sum([d.get_all_wavefunctions() for d in \ 3759 decay_dict.values()], []): 3760 if wf.get('fermionflow') < 0 or \ 3761 wf.get('self_antipart') and wf.is_fermion(): 3762 got_majoranas = True 3763 3764 # Now insert decays for all legs that have decays 3765 for number in decay_dict.keys(): 3766 3767 self.insert_decay(replace_dict[number], 3768 decay_dict[number], 3769 numbers, 3770 got_majoranas) 3771 3772 # Remove all diagrams that surpass overall coupling orders 3773 overall_orders = self.get('processes')[0].get('overall_orders') 3774 if overall_orders: 3775 ndiag = len(self.get('diagrams')) 3776 idiag = 0 3777 while self.get('diagrams')[idiag:]: 3778 diagram = self.get('diagrams')[idiag] 3779 orders = diagram.calculate_orders() 3780 remove_diagram = False 3781 for order in orders.keys(): 3782 try: 3783 if orders[order] > \ 3784 overall_orders[order]: 3785 remove_diagram = True 3786 except KeyError: 3787 pass 3788 if remove_diagram: 3789 self.get('diagrams').pop(idiag) 3790 else: 3791 idiag += 1 3792 3793 if len(self.get('diagrams')) < ndiag: 3794 # We have removed some diagrams - need to go through 3795 # diagrams, renumber them and set new wavefunctions 3796 wf_numbers = [] 3797 ndiagrams = 0 3798 for diagram in self.get('diagrams'): 3799 ndiagrams += 1 3800 diagram.set('number', ndiagrams) 3801 # Extract all wavefunctions contributing to this amplitude 3802 diagram_wfs = HelasWavefunctionList() 3803 for amplitude in diagram.get('amplitudes'): 3804 wavefunctions = \ 3805 sorted(HelasWavefunctionList.\ 3806 extract_wavefunctions(amplitude.get('mothers')), 3807 lambda wf1, wf2: wf1.get('number') - \ 3808 wf2.get('number')) 3809 for wf in wavefunctions: 3810 # Check if wavefunction already used, otherwise add 3811 if wf.get('number') not in wf_numbers and \ 3812 wf not in diagram_wfs: 3813 diagram_wfs.append(wf) 3814 wf_numbers.append(wf.get('number')) 3815 diagram.set('wavefunctions', diagram_wfs) 3816 3817 # Final cleaning out duplicate wavefunctions - needed only if 3818 # we have multiple fermion flows, i.e., either multiple replaced 3819 # wavefunctions or majorana fermions and multiple diagrams 3820 flows = reduce(lambda i1, i2: i1 * i2, 3821 [len(replace_dict[i]) for i in decay_dict.keys()], 1) 3822 diagrams = reduce(lambda i1, i2: i1 * i2, 3823 [len(decay_dict[i].get('diagrams')) for i in \ 3824 decay_dict.keys()], 1) 3825 3826 if flows > 1 or (diagrams > 1 and got_majoranas): 3827 3828 # Clean out any doublet wavefunctions 3829 3830 earlier_wfs = [] 3831 3832 earlier_wf_arrays = [] 3833 3834 mothers = self.get_all_wavefunctions() + self.get_all_amplitudes() 3835 mother_arrays = [w['mothers'].to_array() \ 3836 for w in mothers] 3837 3838 for diagram in self.get('diagrams'): 3839 3840 if diagram.get('number') > 1: 3841 earlier_wfs.extend(self.get('diagrams')[\ 3842 diagram.get('number') - 2].get('wavefunctions')) 3843 3844 i = 0 3845 diag_wfs = diagram.get('wavefunctions') 3846 3847 3848 # Remove wavefunctions and replace mothers 3849 while diag_wfs[i:]: 3850 try: 3851 new_wf = earlier_wfs[\ 3852 earlier_wfs.index(diag_wfs[i])] 3853 wf = diag_wfs.pop(i) 3854 3855 self.update_later_mothers(wf, new_wf, mothers, 3856 mother_arrays) 3857 except ValueError: 3858 i = i + 1 3859 3860 # When we are done with all decays, set wavefunction and 3861 # amplitude numbers 3862 for i, wf in enumerate(self.get_all_wavefunctions()): 3863 wf.set('number', i + 1) 3864 for i, amp in enumerate(self.get_all_amplitudes()): 3865 amp.set('number', i + 1) 3866 # Update fermion factors for all amplitudes 3867 amp.calculate_fermionfactor() 3868 # Update color indices 3869 amp.set('color_indices', amp.get_color_indices()) 3870 3871 # Calculate identical particle factors for 3872 # this matrix element 3873 self.identical_decay_chain_factor(decay_dict.values())
3874 3875
3876 - def insert_decay(self, old_wfs, decay, numbers, got_majoranas):
3877 """Insert a decay chain matrix element into the matrix element. 3878 * old_wfs: the wavefunctions to be replaced. 3879 They all correspond to the same external particle, but might 3880 have different fermion flow directions 3881 * decay: the matrix element for the decay chain 3882 * numbers: the present wavefunction and amplitude number, 3883 to allow for unique numbering 3884 3885 Note that: 3886 1) All amplitudes and all wavefunctions using the decaying wf 3887 must be copied as many times as there are amplitudes in the 3888 decay matrix element 3889 2) In the presence of Majorana particles, we must make sure 3890 to flip fermion flow for the decay process if needed. 3891 3892 The algorithm is the following: 3893 1) Multiply the diagrams with the number of diagrams Ndiag in 3894 the decay element 3895 2) For each diagram in the decay element, work on the diagrams 3896 which corresponds to it 3897 3) Flip fermion flow for the decay wavefunctions if needed 3898 4) Insert all auxiliary wavefunctions into the diagram (i.e., all 3899 except the final wavefunctions, which directly replace the 3900 original final state wavefunctions) 3901 4) Replace the wavefunctions recursively, so that we always replace 3902 each old wavefunctions with Namp new ones, where Namp is 3903 the number of amplitudes in this decay element 3904 diagram. Do recursion for wavefunctions which have this 3905 wavefunction as mother. Simultaneously replace any 3906 amplitudes which have this wavefunction as mother. 3907 """ 3908 3909 len_decay = len(decay.get('diagrams')) 3910 3911 number_external = old_wfs[0].get('number_external') 3912 3913 # Insert the decay process in the process 3914 for process in self.get('processes'): 3915 process.get('decay_chains').append(\ 3916 decay.get('processes')[0]) 3917 3918 # We need one copy of the decay element diagrams for each 3919 # old_wf to be replaced, since we need different wavefunction 3920 # numbers for them 3921 decay_elements = [copy.deepcopy(d) for d in \ 3922 [ decay.get('diagrams') ] * len(old_wfs)] 3923 3924 # Need to replace Particle in all wavefunctions to avoid 3925 # deepcopy 3926 for decay_element in decay_elements: 3927 for idiag, diagram in enumerate(decay.get('diagrams')): 3928 wfs = diagram.get('wavefunctions') 3929 decay_diag = decay_element[idiag] 3930 for i, wf in enumerate(decay_diag.get('wavefunctions')): 3931 wf.set('particle', wfs[i].get('particle')) 3932 wf.set('antiparticle', wfs[i].get('antiparticle')) 3933 3934 for decay_element in decay_elements: 3935 3936 # Remove the unwanted initial state wavefunctions from decay 3937 for decay_diag in decay_element: 3938 for wf in filter(lambda wf: wf.get('number_external') == 1, 3939 decay_diag.get('wavefunctions')): 3940 decay_diag.get('wavefunctions').remove(wf) 3941 3942 decay_wfs = sum([d.get('wavefunctions') for d in decay_element], []) 3943 3944 # External wavefunction offset for new wfs 3945 incr_new = number_external - \ 3946 decay_wfs[0].get('number_external') 3947 3948 for wf in decay_wfs: 3949 # Set number_external for new wavefunctions 3950 wf.set('number_external', wf.get('number_external') + incr_new) 3951 # Set unique number for new wavefunctions 3952 numbers[0] = numbers[0] + 1 3953 wf.set('number', numbers[0]) 3954 3955 # External wavefunction offset for old wfs, only the first 3956 # time this external wavefunction is replaced 3957 (nex, nin) = decay.get_nexternal_ninitial() 3958 incr_old = nex - 2 # due to the incoming particle 3959 wavefunctions = self.get_all_wavefunctions() 3960 for wf in wavefunctions: 3961 # Only modify number_external for wavefunctions above old_wf 3962 if wf.get('number_external') > number_external: 3963 wf.set('number_external', 3964 wf.get('number_external') + incr_old) 3965 3966 # Multiply the diagrams by Ndiag 3967 3968 diagrams = HelasDiagramList() 3969 for diagram in self.get('diagrams'): 3970 new_diagrams = [copy.copy(diag) for diag in \ 3971 [ diagram ] * (len_decay - 1)] 3972 # Update diagram number 3973 diagram.set('number', (diagram.get('number') - 1) * \ 3974 len_decay + 1) 3975 3976 for i, diag in enumerate(new_diagrams): 3977 # Set diagram number 3978 diag.set('number', diagram.get('number') + i + 1) 3979 # Copy over all wavefunctions 3980 diag.set('wavefunctions', 3981 copy.copy(diagram.get('wavefunctions'))) 3982 # Copy over the amplitudes 3983 amplitudes = HelasAmplitudeList(\ 3984 [copy.copy(amp) for amp in \ 3985 diag.get('amplitudes')]) 3986 # Renumber amplitudes 3987 for amp in amplitudes: 3988 numbers[1] = numbers[1] + 1 3989 amp.set('number', numbers[1]) 3990 diag.set('amplitudes', amplitudes) 3991 # Add old and new diagram to diagrams 3992 diagrams.append(diagram) 3993 diagrams.extend(new_diagrams) 3994 3995 self.set('diagrams', diagrams) 3996 3997 # Now we work by decay process diagram, parameterized by numdecay 3998 for numdecay in range(len_decay): 3999 4000 # Pick out the diagrams which correspond to this decay diagram 4001 diagrams = [self.get('diagrams')[i] for i in \ 4002 range(numdecay, len(self.get('diagrams')), len_decay)] 4003 4004 # Perform replacement for each of the wavefunctions in old_wfs 4005 for decay_element, old_wf in zip(decay_elements, old_wfs): 4006 4007 decay_diag = decay_element[numdecay] 4008 4009 # Find the diagrams which have old_wf 4010 my_diagrams = filter(lambda diag: (old_wf.get('number') in \ 4011 [wf.get('number') for wf in \ 4012 diag.get('wavefunctions')]), 4013 diagrams) 4014 4015 # Ignore possibility for unoptimizated generation for now 4016 if len(my_diagrams) > 1: 4017 raise self.PhysicsObjectError, \ 4018 "Decay chains not yet prepared for GPU" 4019 4020 for diagram in my_diagrams: 4021 4022 if got_majoranas: 4023 # If there are Majorana particles in any of 4024 # the matrix elements, we need to check for 4025 # fermion flow 4026 4027 # Earlier wavefunctions, will be used for fermion flow 4028 index = [d.get('number') for d in diagrams].\ 4029 index(diagram.get('number')) 4030 earlier_wavefunctions = \ 4031 sum([d.get('wavefunctions') for d in \ 4032 diagrams[:index]], []) 4033 4034 # Don't want to affect original decay 4035 # wavefunctions, so need to deepcopy 4036 decay_diag_wfs = copy.deepcopy(\ 4037 decay_diag.get('wavefunctions')) 4038 # Need to replace Particle in all 4039 # wavefunctions to avoid deepcopy 4040 for i, wf in enumerate(decay_diag.get('wavefunctions')): 4041 decay_diag_wfs[i].set('particle', \ 4042 wf.get('particle')) 4043 decay_diag_wfs[i].set('antiparticle', \ 4044 wf.get('antiparticle')) 4045 4046 # Complete decay_diag_wfs with the mother wavefunctions 4047 # to allow for independent fermion flow flips 4048 decay_diag_wfs = decay_diag_wfs.insert_own_mothers() 4049 4050 # These are the wavefunctions which directly replace old_wf 4051 final_decay_wfs = [amp.get('mothers')[1] for amp in \ 4052 decay_diag.get('amplitudes')] 4053 4054 # Since we made deepcopy, need to syncronize 4055 for i, wf in enumerate(final_decay_wfs): 4056 final_decay_wfs[i] = \ 4057 decay_diag_wfs[decay_diag_wfs.index(wf)] 4058 4059 # Remove final wavefunctions from decay_diag_wfs, 4060 # since these will be replaced separately by 4061 # replace_wavefunctions 4062 for wf in final_decay_wfs: 4063 decay_diag_wfs.remove(wf) 4064 4065 # Check fermion flow direction 4066 if old_wf.is_fermion() and \ 4067 old_wf.get_with_flow('state') != \ 4068 final_decay_wfs[0].get_with_flow('state'): 4069 4070 # Not same flow state - need to flip flow of wf 4071 4072 for i, wf in enumerate(final_decay_wfs): 4073 4074 # We use the function 4075 # check_majorana_and_flip_flow, as in the 4076 # helas diagram generation. Since we have 4077 # different flow, there is already a Majorana 4078 # particle along the fermion line. 4079 4080 final_decay_wfs[i], numbers[0] = \ 4081 wf.check_majorana_and_flip_flow(\ 4082 True, 4083 earlier_wavefunctions, 4084 decay_diag_wfs, 4085 {}, 4086 numbers[0]) 4087 4088 # Remove wavefunctions which are already present in 4089 # earlier_wavefunctions 4090 i = 0 4091 earlier_wavefunctions = \ 4092 sum([d.get('wavefunctions') for d in \ 4093 self.get('diagrams')[:diagram.get('number') - 1]], \ 4094 []) 4095 earlier_wf_numbers = [wf.get('number') for wf in \ 4096 earlier_wavefunctions] 4097 4098 i = 0 4099 mother_arrays = [w.get('mothers').to_array() for \ 4100 w in final_decay_wfs] 4101 while decay_diag_wfs[i:]: 4102 wf = decay_diag_wfs[i] 4103 try: 4104 new_wf = earlier_wavefunctions[\ 4105 earlier_wf_numbers.index(wf.get('number'))] 4106 # If the wavefunctions are not identical, 4107 # then we should keep this wavefunction, 4108 # and update its number so it is unique 4109 if wf != new_wf: 4110 numbers[0] = numbers[0] + 1 4111 wf.set('number', numbers[0]) 4112 continue 4113 decay_diag_wfs.pop(i) 4114 pres_mother_arrays = [w.get('mothers').to_array() for \ 4115 w in decay_diag_wfs[i:]] + \ 4116 mother_arrays 4117 self.update_later_mothers(wf, new_wf, 4118 decay_diag_wfs[i:] + \ 4119 final_decay_wfs, 4120 pres_mother_arrays) 4121 except ValueError: 4122 i = i + 1 4123 4124 # Since we made deepcopy, go through mothers and make 4125 # sure we are using the ones in earlier_wavefunctions 4126 for decay_wf in decay_diag_wfs + final_decay_wfs: 4127 mothers = decay_wf.get('mothers') 4128 for i, wf in enumerate(mothers): 4129 try: 4130 mothers[i] = earlier_wavefunctions[\ 4131 earlier_wf_numbers.index(wf.get('number'))] 4132 except ValueError: 4133 pass 4134 else: 4135 # If there are no Majorana particles, the 4136 # treatment is much simpler 4137 decay_diag_wfs = \ 4138 copy.copy(decay_diag.get('wavefunctions')) 4139 4140 # These are the wavefunctions which directly 4141 # replace old_wf 4142 final_decay_wfs = [amp.get('mothers')[1] for amp in \ 4143 decay_diag.get('amplitudes')] 4144 4145 # Remove final wavefunctions from decay_diag_wfs, 4146 # since these will be replaced separately by 4147 # replace_wavefunctions 4148 for wf in final_decay_wfs: 4149 decay_diag_wfs.remove(wf) 4150 4151 4152 diagram_wfs = diagram.get('wavefunctions') 4153 4154 old_wf_index = [wf.get('number') for wf in \ 4155 diagram_wfs].index(old_wf.get('number')) 4156 4157 diagram_wfs = diagram_wfs[0:old_wf_index] + \ 4158 decay_diag_wfs + diagram_wfs[old_wf_index:] 4159 4160 diagram.set('wavefunctions', HelasWavefunctionList(diagram_wfs)) 4161 4162 # Set the decay flag for final_decay_wfs, to 4163 # indicate that these correspond to decayed 4164 # particles 4165 for wf in final_decay_wfs: 4166 wf.set('onshell', True) 4167 4168 if len_decay == 1 and len(final_decay_wfs) == 1: 4169 # Can use simplified treatment, by just modifying old_wf 4170 self.replace_single_wavefunction(old_wf, 4171 final_decay_wfs[0]) 4172 else: 4173 # Multiply wavefunctions and amplitudes and 4174 # insert mothers using a recursive function 4175 self.replace_wavefunctions(old_wf, 4176 final_decay_wfs, 4177 diagrams, 4178 numbers) 4179 # Now that we are done with this set of diagrams, we need 4180 # to clean out duplicate wavefunctions (i.e., remove 4181 # identical wavefunctions which are already present in 4182 # earlier diagrams) 4183 for diagram in diagrams: 4184 4185 # We can have duplicate wfs only from previous copies of 4186 # this diagram 4187 earlier_wfs = sum([d.get('wavefunctions') for d in \ 4188 self.get('diagrams')[\ 4189 diagram.get('number') - numdecay - 1:\ 4190 diagram.get('number') - 1]], []) 4191 4192 later_wfs = sum([d.get('wavefunctions') for d in \ 4193 self.get('diagrams')[\ 4194 diagram.get('number'):]], []) 4195 4196 later_amps = sum([d.get('amplitudes') for d in \ 4197 self.get('diagrams')[\ 4198 diagram.get('number') - 1:]], []) 4199 4200 i = 0 4201 diag_wfs = diagram.get('wavefunctions') 4202 4203 # Remove wavefunctions and replace mothers, to make 4204 # sure we only have one copy of each wavefunction 4205 # number 4206 4207 mother_arrays = [w.get('mothers').to_array() for \ 4208 w in later_wfs + later_amps] 4209 4210 while diag_wfs[i:]: 4211 try: 4212 index = [w.get('number') for w in earlier_wfs].\ 4213 index(diag_wfs[i].get('number')) 4214 wf = diag_wfs.pop(i) 4215 pres_mother_arrays = [w.get('mothers').to_array() for \ 4216 w in diag_wfs[i:]] + \ 4217 mother_arrays 4218 self.update_later_mothers(wf, earlier_wfs[index], 4219 diag_wfs[i:] + later_wfs + later_amps, 4220 pres_mother_arrays) 4221 except ValueError: 4222 i = i + 1
4223
4224 - def update_later_mothers(self, wf, new_wf, later_wfs, later_wf_arrays):
4225 """Update mothers for all later wavefunctions""" 4226 4227 daughters = filter(lambda tup: wf.get('number') in tup[1], 4228 enumerate(later_wf_arrays)) 4229 4230 for (index, mothers) in daughters: 4231 try: 4232 # Replace mother 4233 later_wfs[index].get('mothers')[\ 4234 mothers.index(wf.get('number'))] = new_wf 4235 except ValueError: 4236 pass
4237
4238 - def replace_wavefunctions(self, old_wf, new_wfs, 4239 diagrams, numbers):
4240 """Recursive function to replace old_wf with new_wfs, and 4241 multiply all wavefunctions or amplitudes that use old_wf 4242 4243 * old_wf: The wavefunction to be replaced 4244 * new_wfs: The replacing wavefunction 4245 * diagrams - the diagrams that are relevant for these new 4246 wavefunctions. 4247 * numbers: the present wavefunction and amplitude number, 4248 to allow for unique numbering 4249 """ 4250 4251 # Pick out the diagrams which have the old_wf 4252 my_diagrams = filter(lambda diag: old_wf.get('number') in \ 4253 [wf.get('number') for wf in diag.get('wavefunctions')], 4254 diagrams) 4255 4256 # Replace old_wf with new_wfs in the diagrams 4257 for diagram in my_diagrams: 4258 4259 diagram_wfs = diagram.get('wavefunctions') 4260 4261 old_wf_index = [wf.get('number') for wf in \ 4262 diagram.get('wavefunctions')].index(old_wf.get('number')) 4263 diagram_wfs = diagram_wfs[:old_wf_index] + \ 4264 new_wfs + diagram_wfs[old_wf_index + 1:] 4265 4266 diagram.set('wavefunctions', HelasWavefunctionList(diagram_wfs)) 4267 4268 # Now need to take care of amplitudes and wavefunctions which 4269 # are daughters of old_wf (only among the relevant diagrams) 4270 4271 # Pick out diagrams with amplitudes which are daughters of old_wf 4272 amp_diagrams = filter(lambda diag: old_wf.get('number') in \ 4273 sum([[wf.get('number') for wf in amp.get('mothers')] \ 4274 for amp in diag.get('amplitudes')], []), 4275 diagrams) 4276 4277 for diagram in amp_diagrams: 4278 4279 # Amplitudes in this diagram that are daughters of old_wf 4280 daughter_amps = filter(lambda amp: old_wf.get('number') in \ 4281 [wf.get('number') for wf in amp.get('mothers')], 4282 diagram.get('amplitudes')) 4283 4284 new_amplitudes = copy.copy(diagram.get('amplitudes')) 4285 4286 # Loop over daughter_amps, to multiply each amp by the 4287 # number of replacement wavefunctions and substitute mothers 4288 for old_amp in daughter_amps: 4289 # Create copies of this amp 4290 new_amps = [copy.copy(amp) for amp in \ 4291 [ old_amp ] * len(new_wfs)] 4292 # Replace the old mother with the new ones 4293 for i, (new_amp, new_wf) in enumerate(zip(new_amps, new_wfs)): 4294 mothers = copy.copy(new_amp.get('mothers')) 4295 old_wf_index = [wf.get('number') for wf in mothers].index(\ 4296 old_wf.get('number')) 4297 # Update mother 4298 mothers[old_wf_index] = new_wf 4299 new_amp.set('mothers', mothers) 4300 # Update amp numbers for replaced amp 4301 numbers[1] = numbers[1] + 1 4302 new_amp.set('number', numbers[1]) 4303 4304 # Insert the new amplitudes in diagram amplitudes 4305 index = [a.get('number') for a in new_amplitudes].\ 4306 index(old_amp.get('number')) 4307 new_amplitudes = new_amplitudes[:index] + \ 4308 new_amps + new_amplitudes[index + 1:] 4309 4310 # Replace diagram amplitudes with the new ones 4311 diagram.set('amplitudes', HelasAmplitudeList(new_amplitudes)) 4312 4313 # Find wavefunctions that are daughters of old_wf 4314 daughter_wfs = filter(lambda wf: old_wf.get('number') in \ 4315 [wf1.get('number') for wf1 in wf.get('mothers')], 4316 sum([diag.get('wavefunctions') for diag in \ 4317 diagrams], [])) 4318 4319 # Loop over daughter_wfs, multiply them and replace mothers 4320 for daughter_wf in daughter_wfs: 4321 4322 # Pick out the diagrams where daughter_wf occurs 4323 wf_diagrams = filter(lambda diag: daughter_wf.get('number') in \ 4324 [wf.get('number') for wf in \ 4325 diag.get('wavefunctions')], 4326 diagrams) 4327 4328 if len(wf_diagrams) > 1: 4329 raise self.PhysicsObjectError, \ 4330 "Decay chains not yet prepared for GPU" 4331 4332 for diagram in wf_diagrams: 4333 4334 # Now create new wfs with updated mothers 4335 replace_daughters = [ copy.copy(wf) for wf in \ 4336 [daughter_wf] * len(new_wfs) ] 4337 4338 index = [wf.get('number') for wf in \ 4339 daughter_wf.get('mothers')].index(old_wf.get('number')) 4340 4341 # Replace the old mother with the new ones, update wf numbers 4342 for i, (new_daughter, new_wf) in \ 4343 enumerate(zip(replace_daughters, new_wfs)): 4344 mothers = copy.copy(new_daughter.get('mothers')) 4345 mothers[index] = new_wf 4346 new_daughter.set('mothers', mothers) 4347 numbers[0] = numbers[0] + 1 4348 new_daughter.set('number', numbers[0]) 4349 4350 # This is where recursion happens. We need to replace 4351 # the daughter wavefunction, and fix amplitudes and 4352 # wavefunctions which have it as mothers. 4353 4354 self.replace_wavefunctions(daughter_wf, 4355 replace_daughters, 4356 diagrams, 4357 numbers)
4358
4359 - def replace_single_wavefunction(self, old_wf, new_wf):
4360 """Insert decay chain by simply modifying wavefunction. This 4361 is possible only if there is only one diagram in the decay.""" 4362 4363 for key in old_wf.keys(): 4364 old_wf.set(key, new_wf[key])
4365
4366 - def identical_decay_chain_factor(self, decay_chains):
4367 """Calculate the denominator factor from identical decay chains""" 4368 4369 final_legs = [leg.get('id') for leg in \ 4370 filter(lambda leg: leg.get('state') == True, \ 4371 self.get('processes')[0].get('legs'))] 4372 4373 # Leg ids for legs being replaced by decay chains 4374 decay_ids = [decay.get('legs')[0].get('id') for decay in \ 4375 self.get('processes')[0].get('decay_chains')] 4376 4377 # Find all leg ids which are not being replaced by decay chains 4378 non_decay_legs = filter(lambda id: id not in decay_ids, 4379 final_legs) 4380 4381 # Identical particle factor for legs not being decayed 4382 identical_indices = {} 4383 for id in non_decay_legs: 4384 if id in identical_indices: 4385 identical_indices[id] = \ 4386 identical_indices[id] + 1 4387 else: 4388 identical_indices[id] = 1 4389 non_chain_factor = reduce(lambda x, y: x * y, 4390 [ math.factorial(val) for val in \ 4391 identical_indices.values() ], 1) 4392 4393 # Identical particle factor for decay chains 4394 # Go through chains to find identical ones 4395 chains = copy.copy(decay_chains) 4396 iden_chains_factor = 1 4397 while chains: 4398 ident_copies = 1 4399 first_chain = chains.pop(0) 4400 i = 0 4401 while i < len(chains): 4402 chain = chains[i] 4403 if HelasMatrixElement.check_equal_decay_processes(\ 4404 first_chain, chain): 4405 ident_copies = ident_copies + 1 4406 chains.pop(i) 4407 else: 4408 i = i + 1 4409 iden_chains_factor = iden_chains_factor * \ 4410 math.factorial(ident_copies) 4411 4412 self['identical_particle_factor'] = non_chain_factor * \ 4413 iden_chains_factor * \ 4414 reduce(lambda x1, x2: x1 * x2, 4415 [me.get('identical_particle_factor') \ 4416 for me in decay_chains], 1)
4417
4418 - def calculate_fermionfactors(self):
4419 """Generate the fermion factors for all diagrams in the matrix element 4420 """ 4421 for diagram in self.get('diagrams'): 4422 for amplitude in diagram.get('amplitudes'): 4423 amplitude.get('fermionfactor')
4424
4426 """Calculate the denominator factor for identical final state particles 4427 """ 4428 4429 self["identical_particle_factor"] = self.get('processes')[0].\ 4430 identical_particle_factor()
4431
4432 - def get_base_amplitude(self):
4433 """Generate a diagram_generation.Amplitude from a 4434 HelasMatrixElement. This is used to generate both color 4435 amplitudes and diagram drawing.""" 4436 4437 # Need to take care of diagram numbering for decay chains 4438 # before this can be used for those! 4439 4440 optimization = 1 4441 if len(filter(lambda wf: wf.get('number') == 1, 4442 self.get_all_wavefunctions())) > 1: 4443 optimization = 0 4444 4445 model = self.get('processes')[0].get('model') 4446 4447 wf_dict = {} 4448 vx_list = [] 4449 diagrams = base_objects.DiagramList() 4450 for diag in self.get('diagrams'): 4451 diagrams.append(diag.get('amplitudes')[0].get_base_diagram(\ 4452 wf_dict, vx_list, optimization)) 4453 4454 for diag in diagrams: 4455 diag.calculate_orders(self.get('processes')[0].get('model')) 4456 4457 return diagram_generation.Amplitude({\ 4458 'process': self.get('processes')[0], 4459 'diagrams': diagrams})
4460 4461 # Helper methods 4462
4463 - def getmothers(self, legs, number_to_wavefunctions, 4464 external_wavefunctions, wavefunctions, 4465 diagram_wavefunctions):
4466 """Generate list of mothers from number_to_wavefunctions and 4467 external_wavefunctions""" 4468 4469 mothers = HelasWavefunctionList() 4470 4471 for leg in legs: 4472 try: 4473 # The mother is an existing wavefunction 4474 wf = number_to_wavefunctions[leg.get('number')] 4475 except KeyError: 4476 # This is an external leg, pick from external_wavefunctions 4477 wf = external_wavefunctions[leg.get('number')] 4478 number_to_wavefunctions[leg.get('number')] = wf 4479 if not wf in wavefunctions and not wf in diagram_wavefunctions: 4480 diagram_wavefunctions.append(wf) 4481 mothers.append(wf) 4482 4483 return mothers
4484
4485 - def get_num_configs(self):
4486 """Get number of diagrams, which is always more than number of 4487 configs""" 4488 4489 model = self.get('processes')[0].\ 4490 get('model') 4491 4492 next, nini = self.get_nexternal_ninitial() 4493 return sum([d.get_num_configs(model, nini) for d in \ 4494 self.get('base_amplitude').get('diagrams')])
4495
4497 """Gives the total number of wavefunctions for this ME""" 4498 4499 out = max([wf.get('me_id') for wfs in self.get('diagrams') 4500 for wf in wfs.get('wavefunctions')]) 4501 if out: 4502 return out 4503 return sum([ len(d.get('wavefunctions')) for d in \ 4504 self.get('diagrams')])
4505
4506 - def get_all_wavefunctions(self):
4507 """Gives a list of all wavefunctions for this ME""" 4508 4509 return sum([d.get('wavefunctions') for d in \ 4510 self.get('diagrams')], [])
4511
4512 - def get_all_amplitudes(self):
4513 """Gives a list of all amplitudes for this ME""" 4514 4515 return sum([d.get('amplitudes') for d in \ 4516 self.get('diagrams')], [])
4517
4518 - def get_external_wavefunctions(self):
4519 """Gives the external wavefunctions for this ME""" 4520 4521 external_wfs = filter(lambda wf: not wf.get('mothers'), 4522 self.get('diagrams')[0].get('wavefunctions')) 4523 4524 external_wfs.sort(lambda w1, w2: w1.get('number_external') - \ 4525 w2.get('number_external')) 4526 4527 i = 1 4528 while i < len(external_wfs): 4529 if external_wfs[i].get('number_external') == \ 4530 external_wfs[i - 1].get('number_external'): 4531 external_wfs.pop(i) 4532 else: 4533 i = i + 1 4534 return external_wfs
4535
4536 - def get_number_of_amplitudes(self):
4537 """Gives the total number of amplitudes for this ME""" 4538 4539 return sum([ len(d.get('amplitudes')) for d in \ 4540 self.get('diagrams')])
4541
4542 - def get_nexternal_ninitial(self):
4543 """Gives (number or external particles, number of 4544 incoming particles)""" 4545 4546 external_wfs = filter(lambda wf: not wf.get('mothers'), 4547 self.get_all_wavefunctions()) 4548 4549 return (len(set([wf.get('number_external') for wf in \ 4550 external_wfs])), 4551 len(set([wf.get('number_external') for wf in \ 4552 filter(lambda wf: wf.get('leg_state') == False, 4553 external_wfs)])))
4554
4555 - def get_external_masses(self):
4556 """Gives the list of the strings corresponding to the masses of the 4557 external particles.""" 4558 4559 mass_list=[] 4560 external_wfs = sorted(filter(lambda wf: wf.get('leg_state') != \ 4561 'intermediate', self.get_all_wavefunctions()),\ 4562 key=lambda w: w['number_external']) 4563 external_number=1 4564 for wf in external_wfs: 4565 if wf.get('number_external')==external_number: 4566 external_number=external_number+1 4567 mass_list.append(wf.get('particle').get('mass')) 4568 4569 return mass_list
4570
4571 - def get_helicity_combinations(self):
4572 """Gives the number of helicity combinations for external 4573 wavefunctions""" 4574 4575 if not self.get('processes'): 4576 return None 4577 4578 model = self.get('processes')[0].get('model') 4579 4580 return reduce(lambda x, y: x * y, 4581 [ len(model.get('particle_dict')[wf.get('pdg_code')].\ 4582 get_helicity_states())\ 4583 for wf in self.get_external_wavefunctions() ], 1)
4584
4585 - def get_helicity_matrix(self, allow_reverse=True):
4586 """Gives the helicity matrix for external wavefunctions""" 4587 4588 if not self.get('processes'): 4589 return None 4590 4591 process = self.get('processes')[0] 4592 model = process.get('model') 4593 4594 return apply(itertools.product, [ model.get('particle_dict')[\ 4595 wf.get('pdg_code')].get_helicity_states(allow_reverse)\ 4596 for wf in self.get_external_wavefunctions()])
4597
4598 - def get_hel_avg_factor(self):
4599 """ Calculate the denominator factor due to the average over initial 4600 state spin only """ 4601 4602 model = self.get('processes')[0].get('model') 4603 initial_legs = filter(lambda leg: leg.get('state') == False, \ 4604 self.get('processes')[0].get('legs')) 4605 4606 return reduce(lambda x, y: x * y, 4607 [ len(model.get('particle_dict')[leg.get('id')].\ 4608 get_helicity_states())\ 4609 for leg in initial_legs ])
4610
4611 - def get_denominator_factor(self):
4612 """Calculate the denominator factor due to: 4613 Averaging initial state color and spin, and 4614 identical final state particles""" 4615 4616 model = self.get('processes')[0].get('model') 4617 4618 initial_legs = filter(lambda leg: leg.get('state') == False, \ 4619 self.get('processes')[0].get('legs')) 4620 4621 color_factor = reduce(lambda x, y: x * y, 4622 [ model.get('particle_dict')[leg.get('id')].\ 4623 get('color')\ 4624 for leg in initial_legs ]) 4625 4626 spin_factor = reduce(lambda x, y: x * y, 4627 [ len(model.get('particle_dict')[leg.get('id')].\ 4628 get_helicity_states())\ 4629 for leg in initial_legs ]) 4630 4631 return spin_factor * color_factor * self['identical_particle_factor']
4632
4633 - def generate_color_amplitudes(self, color_basis, diagrams):
4634 """ Return a list of (coefficient, amplitude number) lists, 4635 corresponding to the JAMPs for the HelasDiagrams and color basis passed 4636 in argument. The coefficients are given in the format (fermion factor, 4637 colorcoeff (frac), imaginary, Nc power). """ 4638 4639 if not color_basis: 4640 # No color, simply add all amplitudes with correct factor 4641 # for first color amplitude 4642 col_amp = [] 4643 for diagram in diagrams: 4644 for amplitude in diagram.get('amplitudes'): 4645 col_amp.append(((amplitude.get('fermionfactor'), 4646 1, False, 0), 4647 amplitude.get('number'))) 4648 return [col_amp] 4649 4650 # There is a color basis - create a list of coefficients and 4651 # amplitude numbers 4652 4653 col_amp_list = [] 4654 for i, col_basis_elem in \ 4655 enumerate(sorted(color_basis.keys())): 4656 4657 col_amp = [] 4658 for diag_tuple in color_basis[col_basis_elem]: 4659 res_amps = filter(lambda amp: \ 4660 tuple(amp.get('color_indices')) == diag_tuple[1], 4661 diagrams[diag_tuple[0]].get('amplitudes')) 4662 if not res_amps: 4663 raise self.PhysicsObjectError, \ 4664 """No amplitude found for color structure 4665 %s and color index chain (%s) (diagram %i)""" % \ 4666 (col_basis_elem, 4667 str(diag_tuple[1]), 4668 diag_tuple[0]) 4669 4670 for res_amp in res_amps: 4671 col_amp.append(((res_amp.get('fermionfactor'), 4672 diag_tuple[2], 4673 diag_tuple[3], 4674 diag_tuple[4]), 4675 res_amp.get('number'))) 4676 4677 col_amp_list.append(col_amp) 4678 4679 return col_amp_list
4680
4681 - def get_color_amplitudes(self):
4682 """Return a list of (coefficient, amplitude number) lists, 4683 corresponding to the JAMPs for this matrix element. The 4684 coefficients are given in the format (fermion factor, color 4685 coeff (frac), imaginary, Nc power).""" 4686 4687 return self.generate_color_amplitudes(self['color_basis'],self['diagrams'])
4688
4689 - def sort_split_orders(self, split_orders):
4690 """ Sort the 'split_orders' list given in argument so that the orders of 4691 smaller weights appear first. Do nothing if not all split orders have 4692 a hierarchy defined.""" 4693 order_hierarchy=\ 4694 self.get('processes')[0].get('model').get('order_hierarchy') 4695 if set(order_hierarchy.keys()).union(set(split_orders))==\ 4696 set(order_hierarchy.keys()): 4697 split_orders.sort(key=lambda order: order_hierarchy[order])
4698
4699 - def get_split_orders_mapping_for_diagram_list(self, diag_list, split_orders, 4700 get_amp_number_function = lambda amp: amp.get('number'), 4701 get_amplitudes_function = lambda diag: diag.get('amplitudes')):
4702 """ This a helper function for get_split_orders_mapping to return, for 4703 the HelasDiagram list given in argument, the list amp_orders detailed in 4704 the description of the 'get_split_orders_mapping' function. 4705 """ 4706 4707 order_hierarchy=\ 4708 self.get('processes')[0].get('model').get('order_hierarchy') 4709 # Find the list of amplitude numbers and what amplitude order they 4710 # correspond to. For its construction, amp_orders is a dictionary with 4711 # is then translated into an ordered list of tuples before being returned. 4712 amp_orders={} 4713 for diag in diag_list: 4714 diag_orders=diag.calculate_orders() 4715 # Add the WEIGHTED order information 4716 diag_orders['WEIGHTED']=sum(order_hierarchy[order]*value for \ 4717 order, value in diag_orders.items()) 4718 # Complement the missing split_orders with 0 4719 for order in split_orders: 4720 if not order in diag_orders.keys(): 4721 diag_orders[order]=0 4722 key = tuple([diag_orders[order] for order in split_orders]) 4723 try: 4724 amp_orders[key].extend([get_amp_number_function(amp) for amp in \ 4725 get_amplitudes_function(diag)]) 4726 except KeyError: 4727 amp_orders[key] = [get_amp_number_function(amp) for amp in \ 4728 get_amplitudes_function(diag)] 4729 amp_orders=[(order[0],tuple(order[1])) for order in amp_orders.items()] 4730 # Again make sure a proper hierarchy is defined and order the list 4731 # according to it if possible 4732 if set(order_hierarchy.keys()).union(set(split_orders))==\ 4733 set(order_hierarchy.keys()): 4734 # Order the contribution starting from the minimum WEIGHTED one 4735 amp_orders.sort(key= lambda elem: 4736 sum([order_hierarchy[split_orders[i]]*order_power for \ 4737 i, order_power in enumerate(elem[0])])) 4738 4739 return amp_orders
4740
4741 - def get_split_orders_mapping(self):
4742 """This function returns two lists, squared_orders, amp_orders. 4743 If process['split_orders'] is empty, the function returns two empty lists. 4744 4745 squared_orders : All possible contributing squared orders among those 4746 specified in the process['split_orders'] argument. The elements of 4747 the list are tuples of the format format (OrderValue1,OrderValue2,...) 4748 with OrderValue<i> correspond to the value of the <i>th order in 4749 process['split_orders'] (the others are summed over and therefore 4750 left unspecified). 4751 Ex for dijet with process['split_orders']=['QCD','QED']: 4752 => [(4,0),(2,2),(0,4)] 4753 4754 amp_orders : Exactly as for squared order except that this list specifies 4755 the contributing order values for the amplitude (i.e. not 'squared'). 4756 Also, the tuple describing the amplitude order is nested with a 4757 second one listing all amplitude numbers contributing to this order. 4758 Ex for dijet with process['split_orders']=['QCD','QED']: 4759 => [((2, 0), (2,)), ((0, 2), (1, 3, 4))] 4760 4761 Keep in mind that the orders of the element of the list is important as 4762 it dicatates the order of the corresponding "order indices" in the 4763 code output by the exporters. 4764 """ 4765 4766 split_orders=self.get('processes')[0].get('split_orders') 4767 # If no split_orders are defined, then return the obvious 4768 if len(split_orders)==0: 4769 return (),() 4770 4771 # First make sure that the 'split_orders' are ordered according to their 4772 # weight. 4773 self.sort_split_orders(split_orders) 4774 4775 amp_orders = self.get_split_orders_mapping_for_diagram_list(\ 4776 self.get('diagrams'),split_orders) 4777 4778 # Now we construct the interference splitting order matrix for 4779 # convenience 4780 squared_orders = [] 4781 for i, amp_order in enumerate(amp_orders): 4782 for j in range(0,i+1): 4783 key = tuple([ord1 + ord2 for ord1,ord2 in \ 4784 zip(amp_order[0],amp_orders[j][0])]) 4785 # We don't use the set structure here since keeping the 4786 # construction order guarantees us that the squared_orders will 4787 # also be ordered according to the WEIGHT. 4788 if not key in squared_orders: 4789 squared_orders.append(key) 4790 4791 return squared_orders, amp_orders
4792 4793 4794
4795 - def get_used_lorentz(self):
4796 """Return a list of (lorentz_name, conjugate_tag, outgoing) with 4797 all lorentz structures used by this HelasMatrixElement.""" 4798 4799 output = [] 4800 for wa in self.get_all_wavefunctions() + self.get_all_amplitudes(): 4801 if wa.get('interaction_id') in [0,-1]: 4802 continue 4803 output.append(wa.get_aloha_info()); 4804 4805 return output
4806
4807 - def get_used_couplings(self):
4808 """Return a list with all couplings used by this 4809 HelasMatrixElement.""" 4810 4811 tmp = [wa.get('coupling') for wa in \ 4812 self.get_all_wavefunctions() + self.get_all_amplitudes() \ 4813 if wa.get('interaction_id') not in [0,-1]] 4814 #some coupling have a minus one associated -> need to remove those 4815 return [ [t] if not t.startswith('-') else [t[1:]] for t2 in tmp for t in t2]
4816 4817
4818 - def get_mirror_processes(self):
4819 """Return a list of processes with initial states interchanged 4820 if has mirror processes""" 4821 4822 if not self.get('has_mirror_process'): 4823 return [] 4824 processes = base_objects.ProcessList() 4825 for proc in self.get('processes'): 4826 legs = copy.copy(proc.get('legs')) 4827 legs[0:2] = [legs[1],legs[0]] 4828 decay_legs = copy.copy(proc.get('legs_with_decays')) 4829 decay_legs[0:2] = [decay_legs[1],decay_legs[0]] 4830 process = copy.copy(proc) 4831 process.set('legs', legs) 4832 process.set('legs_with_decays', decay_legs) 4833 processes.append(process) 4834 return processes
4835 4836 @staticmethod
4837 - def check_equal_decay_processes(decay1, decay2):
4838 """Check if two single-sided decay processes 4839 (HelasMatrixElements) are equal. 4840 4841 Note that this has to be called before any combination of 4842 processes has occured. 4843 4844 Since a decay processes for a decay chain is always generated 4845 such that all final state legs are completely contracted 4846 before the initial state leg is included, all the diagrams 4847 will have identical wave function, independently of the order 4848 of final state particles. 4849 4850 Note that we assume that the process definitions have all 4851 external particles, corresponding to the external 4852 wavefunctions. 4853 """ 4854 4855 assert len(decay1.get('processes')) == 1 == len(decay2.get('processes')), \ 4856 "Can compare only single process HelasMatrixElements" 4857 4858 assert len(filter(lambda leg: leg.get('state') == False, \ 4859 decay1.get('processes')[0].get('legs'))) == 1 and \ 4860 len(filter(lambda leg: leg.get('state') == False, \ 4861 decay2.get('processes')[0].get('legs'))) == 1, \ 4862 "Call to check_decay_processes_equal requires " + \ 4863 "both processes to be unique" 4864 4865 # Compare bulk process properties (number of external legs, 4866 # identity factors, number of diagrams, number of wavefunctions 4867 # initial leg, final state legs 4868 if len(decay1.get('processes')[0].get("legs")) != \ 4869 len(decay2.get('processes')[0].get("legs")) or \ 4870 len(decay1.get('diagrams')) != len(decay2.get('diagrams')) or \ 4871 decay1.get('identical_particle_factor') != \ 4872 decay2.get('identical_particle_factor') or \ 4873 sum(len(d.get('wavefunctions')) for d in \ 4874 decay1.get('diagrams')) != \ 4875 sum(len(d.get('wavefunctions')) for d in \ 4876 decay2.get('diagrams')) or \ 4877 decay1.get('processes')[0].get('legs')[0].get('id') != \ 4878 decay2.get('processes')[0].get('legs')[0].get('id') or \ 4879 sorted([leg.get('id') for leg in \ 4880 decay1.get('processes')[0].get('legs')[1:]]) != \ 4881 sorted([leg.get('id') for leg in \ 4882 decay2.get('processes')[0].get('legs')[1:]]): 4883 return False 4884 4885 # Run a quick check to see if the processes are already 4886 # identical (i.e., the wavefunctions are in the same order) 4887 if [leg.get('id') for leg in \ 4888 decay1.get('processes')[0].get('legs')] == \ 4889 [leg.get('id') for leg in \ 4890 decay2.get('processes')[0].get('legs')] and \ 4891 decay1 == decay2: 4892 return True 4893 4894 # Now check if all diagrams are identical. This is done by a 4895 # recursive function starting from the last wavefunction 4896 # (corresponding to the initial state), since it is the 4897 # same steps for each level in mother wavefunctions 4898 4899 amplitudes2 = copy.copy(reduce(lambda a1, d2: a1 + \ 4900 d2.get('amplitudes'), 4901 decay2.get('diagrams'), [])) 4902 4903 for amplitude1 in reduce(lambda a1, d2: a1 + d2.get('amplitudes'), 4904 decay1.get('diagrams'), []): 4905 foundamplitude = False 4906 for amplitude2 in amplitudes2: 4907 if HelasMatrixElement.check_equal_wavefunctions(\ 4908 amplitude1.get('mothers')[-1], 4909 amplitude2.get('mothers')[-1]): 4910 foundamplitude = True 4911 # Remove amplitude2, since it has already been matched 4912 amplitudes2.remove(amplitude2) 4913 break 4914 if not foundamplitude: 4915 return False 4916 4917 return True
4918 4919 @staticmethod
4920 - def check_equal_wavefunctions(wf1, wf2):
4921 """Recursive function to check if two wavefunctions are equal. 4922 First check that mothers have identical pdg codes, then repeat for 4923 all mothers with identical pdg codes.""" 4924 4925 # End recursion with False if the wavefunctions do not have 4926 # the same mother pdgs 4927 if sorted([wf.get('pdg_code') for wf in wf1.get('mothers')]) != \ 4928 sorted([wf.get('pdg_code') for wf in wf2.get('mothers')]): 4929 return False 4930 4931 # End recursion with True if these are external wavefunctions 4932 # (note that we have already checked that the pdgs are 4933 # identical) 4934 if not wf1.get('mothers') and not wf2.get('mothers'): 4935 return True 4936 4937 mothers2 = copy.copy(wf2.get('mothers')) 4938 4939 for mother1 in wf1.get('mothers'): 4940 # Compare mother1 with all mothers in wf2 that have not 4941 # yet been used and have identical pdg codes 4942 equalmothers = filter(lambda wf: wf.get('pdg_code') == \ 4943 mother1.get('pdg_code'), 4944 mothers2) 4945 foundmother = False 4946 for mother2 in equalmothers: 4947 if HelasMatrixElement.check_equal_wavefunctions(\ 4948 mother1, mother2): 4949 foundmother = True 4950 # Remove mother2, since it has already been matched 4951 mothers2.remove(mother2) 4952 break 4953 if not foundmother: 4954 return False 4955 4956 return True
4957 4958 @staticmethod
4959 - def sorted_mothers(arg):
4960 """Gives a list of mother wavefunctions sorted according to 4961 1. The order of the particles in the interaction 4962 2. Cyclic reordering of particles in same spin group 4963 3. Fermions ordered IOIOIO... according to the pairs in 4964 the interaction.""" 4965 4966 assert isinstance(arg, (HelasWavefunction, HelasAmplitude)), \ 4967 "%s is not a valid HelasWavefunction or HelasAmplitude" % repr(arg) 4968 4969 if not arg.get('interaction_id'): 4970 return arg.get('mothers') 4971 4972 my_pdg_code = 0 4973 my_spin = 0 4974 if isinstance(arg, HelasWavefunction): 4975 my_pdg_code = arg.get_anti_pdg_code() 4976 my_spin = arg.get_spin_state_number() 4977 4978 sorted_mothers, my_index = arg.get('mothers').sort_by_pdg_codes(\ 4979 arg.get('pdg_codes'), my_pdg_code) 4980 4981 # If fermion, partner is the corresponding fermion flow partner 4982 partner = None 4983 if isinstance(arg, HelasWavefunction) and arg.is_fermion(): 4984 # Fermion case, just pick out the fermion flow partner 4985 if my_index % 2 == 0: 4986 # partner is after arg 4987 partner_index = my_index 4988 else: 4989 # partner is before arg 4990 partner_index = my_index - 1 4991 partner = sorted_mothers.pop(partner_index) 4992 # If partner is incoming, move to before arg 4993 if partner.get_spin_state_number() > 0: 4994 my_index = partner_index 4995 else: 4996 my_index = partner_index + 1 4997 4998 # Reorder fermions pairwise according to incoming/outgoing 4999 for i in range(0, len(sorted_mothers), 2): 5000 if sorted_mothers[i].is_fermion(): 5001 # This is a fermion, order between this fermion and its brother 5002 if sorted_mothers[i].get_spin_state_number() > 0 and \ 5003 sorted_mothers[i + 1].get_spin_state_number() < 0: 5004 # Switch places between outgoing and incoming 5005 sorted_mothers = sorted_mothers[:i] + \ 5006 [sorted_mothers[i+1], sorted_mothers[i]] + \ 5007 sorted_mothers[i+2:] 5008 elif sorted_mothers[i].get_spin_state_number() < 0 and \ 5009 sorted_mothers[i + 1].get_spin_state_number() > 0: 5010 # This is the right order 5011 pass 5012 else: 5013 # No more fermions in sorted_mothers 5014 break 5015 5016 # Put back partner into sorted_mothers 5017 if partner: 5018 sorted_mothers.insert(partner_index, partner) 5019 5020 # Next sort according to spin_state_number 5021 return HelasWavefunctionList(sorted_mothers)
5022
5023 #=============================================================================== 5024 # HelasMatrixElementList 5025 #=============================================================================== 5026 -class HelasMatrixElementList(base_objects.PhysicsObjectList):
5027 """List of HelasMatrixElement objects 5028 """ 5029
5030 - def is_valid_element(self, obj):
5031 """Test if object obj is a valid HelasMatrixElement for the list.""" 5032 5033 return isinstance(obj, HelasMatrixElement)
5034
5035 - def remove(self,obj):
5036 pos = (i for i in xrange(len(self)) if self[i] is obj) 5037 for i in pos: 5038 del self[i] 5039 break
5040
5041 #=============================================================================== 5042 # HelasDecayChainProcess 5043 #=============================================================================== 5044 -class HelasDecayChainProcess(base_objects.PhysicsObject):
5045 """HelasDecayChainProcess: If initiated with a DecayChainAmplitude 5046 object, generates the HelasMatrixElements for the core process(es) 5047 and decay chains. Then call combine_decay_chain_processes in order 5048 to generate the matrix elements for all combined processes.""" 5049
5050 - def default_setup(self):
5051 """Default values for all properties""" 5052 5053 self['core_processes'] = HelasMatrixElementList() 5054 self['decay_chains'] = HelasDecayChainProcessList()
5055
5056 - def filter(self, name, value):
5057 """Filter for valid process property values.""" 5058 5059 if name == 'core_processes': 5060 if not isinstance(value, HelasMatrixElementList): 5061 raise self.PhysicsObjectError, \ 5062 "%s is not a valid HelasMatrixElementList object" % \ 5063 str(value) 5064 5065 if name == 'decay_chains': 5066 if not isinstance(value, HelasDecayChainProcessList): 5067 raise self.PhysicsObjectError, \ 5068 "%s is not a valid HelasDecayChainProcessList object" % \ 5069 str(value) 5070 5071 return True
5072
5073 - def get_sorted_keys(self):
5074 """Return process property names as a nicely sorted list.""" 5075 5076 return ['core_processes', 'decay_chains']
5077
5078 - def __init__(self, argument=None):
5079 """Allow initialization with DecayChainAmplitude""" 5080 5081 if isinstance(argument, diagram_generation.DecayChainAmplitude): 5082 super(HelasDecayChainProcess, self).__init__() 5083 self.generate_matrix_elements(argument) 5084 elif argument: 5085 # call the mother routine 5086 super(HelasDecayChainProcess, self).__init__(argument) 5087 else: 5088 # call the mother routine 5089 super(HelasDecayChainProcess, self).__init__()
5090
5091 - def nice_string(self, indent = 0):
5092 """Returns a nicely formatted string of the matrix element processes.""" 5093 5094 mystr = "" 5095 5096 for process in self.get('core_processes'): 5097 mystr += process.get('processes')[0].nice_string(indent) + "\n" 5098 5099 if self.get('decay_chains'): 5100 mystr += " " * indent + "Decays:\n" 5101 for dec in self.get('decay_chains'): 5102 mystr += dec.nice_string(indent + 2) + "\n" 5103 5104 return mystr[:-1]
5105
5106 - def generate_matrix_elements(self, dc_amplitude):
5107 """Generate the HelasMatrixElements for the core processes and 5108 decay processes (separately)""" 5109 5110 assert isinstance(dc_amplitude, diagram_generation.DecayChainAmplitude), \ 5111 "%s is not a valid DecayChainAmplitude" % dc_amplitude 5112 5113 5114 # Extract the pdg codes of all particles decayed by decay chains 5115 # since these should not be combined in a MultiProcess 5116 decay_ids = dc_amplitude.get_decay_ids() 5117 5118 matrix_elements = HelasMultiProcess.generate_matrix_elements(\ 5119 dc_amplitude.get('amplitudes'), 5120 False, 5121 decay_ids) 5122 5123 self.set('core_processes', matrix_elements) 5124 5125 while dc_amplitude.get('decay_chains'): 5126 # Pop the amplitude to save memory space 5127 decay_chain = dc_amplitude.get('decay_chains').pop(0) 5128 self['decay_chains'].append(HelasDecayChainProcess(\ 5129 decay_chain))
5130 5131
5132 - def combine_decay_chain_processes(self, combine=True):
5133 """Recursive function to generate complete 5134 HelasMatrixElements, combining the core process with the decay 5135 chains. 5136 5137 * If the number of decay chains is the same as the number of 5138 decaying particles, apply each decay chain to the corresponding 5139 final state particle. 5140 * If the number of decay chains and decaying final state particles 5141 don't correspond, all decays applying to a given particle type are 5142 combined (without double counting). 5143 * combine allow to merge identical ME 5144 """ 5145 5146 # End recursion when there are no more decay chains 5147 if not self['decay_chains']: 5148 # Just return the list of matrix elements 5149 return self['core_processes'] 5150 5151 # decay_elements is a list of HelasMatrixElementLists with 5152 # all decay processes 5153 decay_elements = [] 5154 5155 for decay_chain in self['decay_chains']: 5156 # This is where recursion happens 5157 decay_elements.append(decay_chain.combine_decay_chain_processes(combine)) 5158 5159 # Store the result in matrix_elements 5160 matrix_elements = HelasMatrixElementList() 5161 # Store matrix element tags in me_tags, for precise comparison 5162 me_tags = [] 5163 # Store external id permutations 5164 permutations = [] 5165 5166 # List of list of ids for the initial state legs in all decay 5167 # processes 5168 decay_is_ids = [[element.get('processes')[0].get_initial_ids()[0] \ 5169 for element in elements] 5170 for elements in decay_elements] 5171 5172 while self['core_processes']: 5173 # Pop the process to save memory space 5174 core_process = self['core_processes'].pop(0) 5175 # Get all final state legs that have a decay chain defined 5176 fs_legs = filter(lambda leg: any([any([id == leg.get('id') for id \ 5177 in is_ids]) for is_ids in decay_is_ids]), 5178 core_process.get('processes')[0].get_final_legs()) 5179 # List of ids for the final state legs 5180 fs_ids = [leg.get('id') for leg in fs_legs] 5181 # Create a dictionary from id to (index, leg number) 5182 fs_numbers = {} 5183 fs_indices = {} 5184 for i, leg in enumerate(fs_legs): 5185 fs_numbers[leg.get('id')] = \ 5186 fs_numbers.setdefault(leg.get('id'), []) + \ 5187 [leg.get('number')] 5188 fs_indices[leg.get('id')] = \ 5189 fs_indices.setdefault(leg.get('id'), []) + \ 5190 [i] 5191 5192 decay_lists = [] 5193 # Loop over unique final state particle ids 5194 for fs_id in set(fs_ids): 5195 # decay_list has the leg numbers and decays for this 5196 # fs particle id: 5197 # decay_list = [[[n1,d1],[n2,d2]],[[n1,d1'],[n2,d2']],...] 5198 5199 decay_list = [] 5200 5201 # Two cases: Either number of decay elements is same 5202 # as number of decaying particles: Then use the 5203 # corresponding decay for each particle. Or the number 5204 # of decay elements is different: Then use any decay 5205 # chain which defines the decay for this particle. 5206 5207 chains = [] 5208 if len(fs_legs) == len(decay_elements) and \ 5209 all([fs in ids for (fs, ids) in \ 5210 zip(fs_ids, decay_is_ids)]): 5211 # The decay of the different fs parts is given 5212 # by the different decay chains, respectively. 5213 # Chains is a list of matrix element lists 5214 for index in fs_indices[fs_id]: 5215 chains.append(filter(lambda me: \ 5216 me.get('processes')[0].\ 5217 get_initial_ids()[0] == fs_id, 5218 decay_elements[index])) 5219 5220 if len(fs_legs) != len(decay_elements) or not chains or not chains[0]: 5221 # In second case, or no chains are found 5222 # (e.g. because the order of decays is reversed), 5223 # all decays for this particle type are used 5224 chain = sum([filter(lambda me: \ 5225 me.get('processes')[0].\ 5226 get_initial_ids()[0] == fs_id, 5227 decay_chain) for decay_chain in \ 5228 decay_elements], []) 5229 5230 chains = [chain] * len(fs_numbers[fs_id]) 5231 5232 red_decay_chains = [] 5233 for prod in itertools.product(*chains): 5234 5235 # Now, need to ensure that we don't append 5236 # duplicate chain combinations, e.g. (a>bc, a>de) and 5237 # (a>de, a>bc) 5238 5239 # Remove double counting between final states 5240 if sorted([p.get('processes')[0] for p in prod], 5241 lambda x1, x2: x1.compare_for_sort(x2)) \ 5242 in red_decay_chains: 5243 continue 5244 5245 # Store already used combinations 5246 red_decay_chains.append(\ 5247 sorted([p.get('processes')[0] for p in prod], 5248 lambda x1, x2: x1.compare_for_sort(x2))) 5249 5250 # Add the decays to the list 5251 decay_list.append(zip(fs_numbers[fs_id], prod)) 5252 5253 decay_lists.append(decay_list) 5254 5255 # Finally combine all decays for this process, 5256 # and combine them, decay by decay 5257 for decays in itertools.product(*decay_lists): 5258 5259 # Generate a dictionary from leg number to decay process 5260 decay_dict = dict(sum(decays, [])) 5261 5262 # Make sure to not modify the original matrix element 5263 model_bk = core_process.get('processes')[0].get('model') 5264 # Avoid Python copying the complete model every time 5265 for i, process in enumerate(core_process.get('processes')): 5266 process.set('model',base_objects.Model()) 5267 matrix_element = copy.deepcopy(core_process) 5268 # Avoid Python copying the complete model every time 5269 for i, process in enumerate(matrix_element.get('processes')): 5270 process.set('model', model_bk) 5271 core_process.get('processes')[i].set('model', model_bk) 5272 # Need to replace Particle in all wavefunctions to avoid 5273 # deepcopy 5274 org_wfs = core_process.get_all_wavefunctions() 5275 for i, wf in enumerate(matrix_element.get_all_wavefunctions()): 5276 wf.set('particle', org_wfs[i].get('particle')) 5277 wf.set('antiparticle', org_wfs[i].get('antiparticle')) 5278 5279 # Insert the decay chains 5280 logger.info("Combine %s with decays %s" % \ 5281 (core_process.get('processes')[0].nice_string().\ 5282 replace('Process: ', ''), \ 5283 ", ".join([d.get('processes')[0].nice_string().\ 5284 replace('Process: ', '') \ 5285 for d in decay_dict.values()]))) 5286 5287 matrix_element.insert_decay_chains(decay_dict) 5288 5289 if combine: 5290 me_tag = IdentifyMETag.create_tag(\ 5291 matrix_element.get_base_amplitude(), 5292 matrix_element.get('identical_particle_factor')) 5293 try: 5294 if not combine: 5295 raise ValueError 5296 # If an identical matrix element is already in the list, 5297 # then simply add this process to the list of 5298 # processes for that matrix element 5299 me_index = me_tags.index(me_tag) 5300 except ValueError: 5301 # Otherwise, if the matrix element has any diagrams, 5302 # add this matrix element. 5303 if matrix_element.get('processes') and \ 5304 matrix_element.get('diagrams'): 5305 matrix_elements.append(matrix_element) 5306 if combine: 5307 me_tags.append(me_tag) 5308 permutations.append(me_tag[-1][0].\ 5309 get_external_numbers()) 5310 else: # try 5311 other_processes = matrix_elements[me_index].get('processes') 5312 logger.info("Combining process with %s" % \ 5313 other_processes[0].nice_string().replace('Process: ', '')) 5314 for proc in matrix_element.get('processes'): 5315 other_processes.append(HelasMultiProcess.\ 5316 reorder_process(proc, 5317 permutations[me_index], 5318 me_tag[-1][0].get_external_numbers())) 5319 5320 return matrix_elements
5321
5322 #=============================================================================== 5323 # HelasDecayChainProcessList 5324 #=============================================================================== 5325 -class HelasDecayChainProcessList(base_objects.PhysicsObjectList):
5326 """List of HelasDecayChainProcess objects 5327 """ 5328
5329 - def is_valid_element(self, obj):
5330 """Test if object obj is a valid HelasDecayChainProcess for the list.""" 5331 5332 return isinstance(obj, HelasDecayChainProcess)
5333
5334 #=============================================================================== 5335 # HelasMultiProcess 5336 #=============================================================================== 5337 -class HelasMultiProcess(base_objects.PhysicsObject):
5338 """HelasMultiProcess: If initiated with an AmplitudeList, 5339 generates the HelasMatrixElements for the Amplitudes, identifying 5340 processes with identical matrix elements""" 5341
5342 - def default_setup(self):
5343 """Default values for all properties""" 5344 5345 self['matrix_elements'] = HelasMatrixElementList()
5346
5347 - def filter(self, name, value):
5348 """Filter for valid process property values.""" 5349 5350 if name == 'matrix_elements': 5351 if not isinstance(value, HelasMatrixElementList): 5352 raise self.PhysicsObjectError, \ 5353 "%s is not a valid HelasMatrixElementList object" % str(value) 5354 return True
5355
5356 - def get_sorted_keys(self):
5357 """Return process property names as a nicely sorted list.""" 5358 5359 return ['matrix_elements']
5360
5361 - def __init__(self, argument=None, combine_matrix_elements=True, 5362 matrix_element_opts={}, compute_loop_nc = False):
5363 """Allow initialization with AmplitudeList. Matrix_element_opts are 5364 potential options to be passed to the constructor of the 5365 HelasMatrixElements created. By default it is none, but when called from 5366 LoopHelasProcess, this options will contain 'optimized_output'.""" 5367 5368 5369 if isinstance(argument, diagram_generation.AmplitudeList): 5370 super(HelasMultiProcess, self).__init__() 5371 self.set('matrix_elements', self.generate_matrix_elements(argument, 5372 combine_matrix_elements = combine_matrix_elements, 5373 matrix_element_opts=matrix_element_opts, 5374 compute_loop_nc = compute_loop_nc)) 5375 elif isinstance(argument, diagram_generation.MultiProcess): 5376 super(HelasMultiProcess, self).__init__() 5377 self.set('matrix_elements', 5378 self.generate_matrix_elements(argument.get('amplitudes'), 5379 combine_matrix_elements = combine_matrix_elements, 5380 matrix_element_opts = matrix_element_opts, 5381 compute_loop_nc = compute_loop_nc)) 5382 elif isinstance(argument, diagram_generation.Amplitude): 5383 super(HelasMultiProcess, self).__init__() 5384 self.set('matrix_elements', self.generate_matrix_elements(\ 5385 diagram_generation.AmplitudeList([argument]), 5386 combine_matrix_elements = combine_matrix_elements, 5387 matrix_element_opts = matrix_element_opts, 5388 compute_loop_nc = compute_loop_nc)) 5389 elif argument: 5390 # call the mother routine 5391 super(HelasMultiProcess, self).__init__(argument) 5392 else: 5393 # call the mother routine 5394 super(HelasMultiProcess, self).__init__()
5395
5396 - def get_used_lorentz(self):
5397 """Return a list of (lorentz_name, conjugate, outgoing) with 5398 all lorentz structures used by this HelasMultiProcess.""" 5399 helas_list = [] 5400 5401 for me in self.get('matrix_elements'): 5402 helas_list.extend(me.get_used_lorentz()) 5403 5404 return list(set(helas_list))
5405
5406 - def get_used_couplings(self):
5407 """Return a list with all couplings used by this 5408 HelasMatrixElement.""" 5409 5410 coupling_list = [] 5411 5412 for me in self.get('matrix_elements'): 5413 coupling_list.extend([c for l in me.get_used_couplings() for c in l]) 5414 5415 return list(set(coupling_list))
5416
5417 - def get_matrix_elements(self):
5418 """Extract the list of matrix elements""" 5419 5420 return self.get('matrix_elements')
5421 5422 #=========================================================================== 5423 # generate_matrix_elements 5424 #=========================================================================== 5425 5426 @classmethod
5427 - def process_color(cls,matrix_element, color_information, compute_loop_nc=None):
5428 """ Process the color information for a given matrix 5429 element made of a tree diagram. compute_loop_nc is dummy here for the 5430 tree-level Nc and present for structural reasons only.""" 5431 5432 if compute_loop_nc: 5433 raise MadGraph5Error, "The tree-level function 'process_color' "+\ 5434 " of class HelasMultiProcess cannot be called with a value for compute_loop_nc" 5435 5436 # Define the objects stored in the contained color_information 5437 for key in color_information: 5438 exec("%s=color_information['%s']"%(key,key)) 5439 5440 # Always create an empty color basis, and the 5441 # list of raw colorize objects (before 5442 # simplification) associated with amplitude 5443 col_basis = color_amp.ColorBasis() 5444 new_amp = matrix_element.get_base_amplitude() 5445 matrix_element.set('base_amplitude', new_amp) 5446 5447 colorize_obj = col_basis.create_color_dict_list(\ 5448 matrix_element.get('base_amplitude')) 5449 #list_colorize = [] 5450 #list_color_basis = [] 5451 #list_color_matrices = [] 5452 5453 try: 5454 # If the color configuration of the ME has 5455 # already been considered before, recycle 5456 # the information 5457 col_index = list_colorize.index(colorize_obj) 5458 except ValueError: 5459 # If not, create color basis and color 5460 # matrix accordingly 5461 list_colorize.append(colorize_obj) 5462 col_basis.build() 5463 list_color_basis.append(col_basis) 5464 col_matrix = color_amp.ColorMatrix(col_basis) 5465 list_color_matrices.append(col_matrix) 5466 col_index = -1 5467 logger.info(\ 5468 "Processing color information for %s" % \ 5469 matrix_element.get('processes')[0].nice_string(print_weighted=False).\ 5470 replace('Process', 'process')) 5471 else: # Found identical color 5472 logger.info(\ 5473 "Reusing existing color information for %s" % \ 5474 matrix_element.get('processes')[0].nice_string(print_weighted=False).\ 5475 replace('Process', 'process')) 5476 5477 matrix_element.set('color_basis', 5478 list_color_basis[col_index]) 5479 matrix_element.set('color_matrix', 5480 list_color_matrices[col_index])
5481 5482 # Below is the type of HelasMatrixElement which should be created by this 5483 # HelasMultiProcess class 5484 matrix_element_class = HelasMatrixElement 5485 5486 @classmethod
5487 - def generate_matrix_elements(cls, amplitudes, gen_color = True, 5488 decay_ids = [], combine_matrix_elements = True, 5489 compute_loop_nc = False, matrix_element_opts = {}):
5490 """Generate the HelasMatrixElements for the amplitudes, 5491 identifying processes with identical matrix elements, as 5492 defined by HelasMatrixElement.__eq__. Returns a 5493 HelasMatrixElementList and an amplitude map (used by the 5494 SubprocessGroup functionality). decay_ids is a list of decayed 5495 particle ids, since those should not be combined even if 5496 matrix element is identical. 5497 The compute_loop_nc sets wheter independent tracking of Nc power coming 5498 from the color loop trace is necessary or not (it is time consuming). 5499 Matrix_element_opts are potential additional options to be passed to 5500 the HelasMatrixElements constructed.""" 5501 5502 assert isinstance(amplitudes, diagram_generation.AmplitudeList), \ 5503 "%s is not valid AmplitudeList" % type(amplitudes) 5504 5505 combine = combine_matrix_elements 5506 if 'mode' in matrix_element_opts and matrix_element_opts['mode']=='MadSpin': 5507 combine = False 5508 del matrix_element_opts['mode'] 5509 5510 # Keep track of already generated color objects, to reuse as 5511 # much as possible 5512 list_colorize = [] 5513 list_color_basis = [] 5514 list_color_matrices = [] 5515 5516 # Now this keeps track of the color matrices created from the loop-born 5517 # color basis. Keys are 2-tuple with the index of the loop and born basis 5518 # in the list above and the value is the resulting matrix. 5519 dict_loopborn_matrices = {} 5520 5521 # The dictionary below is simply a container for convenience to be 5522 # passed to the function process_color. 5523 color_information = { 'list_colorize' : list_colorize, 5524 'list_color_basis' : list_color_basis, 5525 'list_color_matrices' : list_color_matrices, 5526 'dict_loopborn_matrices' : dict_loopborn_matrices} 5527 5528 # List of valid matrix elements 5529 matrix_elements = HelasMatrixElementList() 5530 # List of identified matrix_elements 5531 identified_matrix_elements = [] 5532 # List of amplitude tags, synchronized with identified_matrix_elements 5533 amplitude_tags = [] 5534 # List of the external leg permutations for the amplitude_tags, 5535 # which allows to reorder the final state particles in the right way 5536 # for maximal process combination 5537 permutations = [] 5538 for amplitude in amplitudes: 5539 if isinstance(amplitude, diagram_generation.DecayChainAmplitude): 5540 # Might get multiple matrix elements from this amplitude 5541 tmp_matrix_element_list = HelasDecayChainProcess(amplitude).\ 5542 combine_decay_chain_processes(combine) 5543 # Use IdentifyMETag to check if matrix elements present 5544 matrix_element_list = [] 5545 for matrix_element in tmp_matrix_element_list: 5546 assert isinstance(matrix_element, HelasMatrixElement), \ 5547 "Not a HelasMatrixElement: %s" % matrix_element 5548 5549 # If the matrix element has no diagrams, 5550 # remove this matrix element. 5551 if not matrix_element.get('processes') or \ 5552 not matrix_element.get('diagrams'): 5553 continue 5554 5555 # Create IdentifyMETag 5556 amplitude_tag = IdentifyMETag.create_tag(\ 5557 matrix_element.get_base_amplitude()) 5558 try: 5559 if not combine: 5560 raise ValueError 5561 me_index = amplitude_tags.index(amplitude_tag) 5562 except ValueError: 5563 # Create matrix element for this amplitude 5564 matrix_element_list.append(matrix_element) 5565 if combine_matrix_elements: 5566 amplitude_tags.append(amplitude_tag) 5567 identified_matrix_elements.append(matrix_element) 5568 permutations.append(amplitude_tag[-1][0].\ 5569 get_external_numbers()) 5570 else: # try 5571 # Identical matrix element found 5572 other_processes = identified_matrix_elements[me_index].\ 5573 get('processes') 5574 # Reorder each of the processes 5575 # Since decay chain, only reorder legs_with_decays 5576 for proc in matrix_element.get('processes'): 5577 other_processes.append(cls.reorder_process(\ 5578 proc, 5579 permutations[me_index], 5580 amplitude_tag[-1][0].get_external_numbers())) 5581 logger.info("Combined %s with %s" % \ 5582 (matrix_element.get('processes')[0].\ 5583 nice_string().\ 5584 replace('Process: ', 'process '), 5585 other_processes[0].nice_string().\ 5586 replace('Process: ', 'process '))) 5587 # Go on to next matrix element 5588 continue 5589 else: # not DecayChainAmplitude 5590 # Create tag identifying the matrix element using 5591 # IdentifyMETag. If two amplitudes have the same tag, 5592 # they have the same matrix element 5593 amplitude_tag = IdentifyMETag.create_tag(amplitude) 5594 try: 5595 me_index = amplitude_tags.index(amplitude_tag) 5596 except ValueError: 5597 # Create matrix element for this amplitude 5598 logger.info("Generating Helas calls for %s" % \ 5599 amplitude.get('process').nice_string().\ 5600 replace('Process', 'process')) 5601 # Correctly choose the helas matrix element class depending 5602 # on the type of 'HelasMultiProcess' this static function 5603 # is called from. 5604 matrix_element_list = [cls.matrix_element_class(amplitude, 5605 decay_ids=decay_ids, 5606 gen_color=False, 5607 **matrix_element_opts)] 5608 me = matrix_element_list[0] 5609 if me.get('processes') and me.get('diagrams'): 5610 # Keep track of amplitude tags 5611 if combine_matrix_elements: 5612 amplitude_tags.append(amplitude_tag) 5613 identified_matrix_elements.append(me) 5614 permutations.append(amplitude_tag[-1][0].\ 5615 get_external_numbers()) 5616 else: 5617 matrix_element_list = [] 5618 else: 5619 # Identical matrix element found 5620 other_processes = identified_matrix_elements[me_index].\ 5621 get('processes') 5622 other_processes.append(cls.reorder_process(\ 5623 amplitude.get('process'), 5624 permutations[me_index], 5625 amplitude_tag[-1][0].get_external_numbers())) 5626 logger.info("Combined %s with %s" % \ 5627 (other_processes[-1].nice_string().\ 5628 replace('Process: ', 'process '), 5629 other_processes[0].nice_string().\ 5630 replace('Process: ', 'process '))) 5631 # Go on to next amplitude 5632 continue 5633 5634 # Deal with newly generated matrix elements 5635 for matrix_element in copy.copy(matrix_element_list): 5636 assert isinstance(matrix_element, HelasMatrixElement), \ 5637 "Not a HelasMatrixElement: %s" % matrix_element 5638 5639 # Add this matrix element to list 5640 matrix_elements.append(matrix_element) 5641 5642 if not gen_color: 5643 continue 5644 5645 # The treatment of color is quite different for loop amplitudes 5646 # than for regular tree ones. So the function below is overloaded 5647 # in LoopHelasProcess 5648 cls.process_color(matrix_element,color_information,\ 5649 compute_loop_nc=compute_loop_nc) 5650 5651 if not matrix_elements: 5652 raise InvalidCmd, \ 5653 "No matrix elements generated, check overall coupling orders" 5654 5655 return matrix_elements
5656 5657 @staticmethod
5658 - def reorder_process(process, org_perm, proc_perm):
5659 """Reorder the legs in the process according to the difference 5660 between org_perm and proc_perm""" 5661 5662 leglist = base_objects.LegList(\ 5663 [copy.copy(process.get('legs_with_decays')[i]) for i in \ 5664 diagram_generation.DiagramTag.reorder_permutation(\ 5665 proc_perm, org_perm)]) 5666 new_proc = copy.copy(process) 5667 new_proc.set('legs_with_decays', leglist) 5668 5669 if not new_proc.get('decay_chains'): 5670 new_proc.set('legs', leglist) 5671 5672 return new_proc
5673