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