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