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