diff --git a/madgraph/core/base_objects.py b/madgraph/core/base_objects.py index 94a0229c2..9e88ac149 100755 --- a/madgraph/core/base_objects.py +++ b/madgraph/core/base_objects.py @@ -34,6 +34,19 @@ logger = logging.getLogger('madgraph.base_objects') pjoin = os.path.join + +# Coupling order carried only by the right-hand (tree-level) amplitudes of an +# interference process ('left [LIxtree=QCD] right'): the squared-order +# constraint INTERF^2==1 then keeps the products of one left-hand and one +# right-hand amplitude, i.e. 2 Re(A_left A_right^*), and nothing else. It is +# added to the model in memory with hierarchy 0, so that WEIGHTED is +# unchanged, and is never printed in a process definition. +INTERFERENCE_ORDER = 'INTERF' + +def visible_orders(orders): + """The keys of a dictionary of (squared) coupling orders that a process + description shows: all but the hidden INTERFERENCE_ORDER.""" + return [key for key in orders if key != INTERFERENCE_ORDER] if madgraph.ordering: set = misc.OrderedSet @@ -1956,8 +1969,12 @@ def get_particles_hierarchy(self): with lowest value) where it has an interaction. """ - # Find coupling orders in model - coupling_orders = self.get('coupling_orders') + # Find coupling orders in model (not the hidden order of interference + # processes, which no interaction carries: with its hierarchy 0 it + # would otherwise add a level and move the particles of order-less + # interactions there) + coupling_orders = [k for k in self.get('coupling_orders') + if k != INTERFERENCE_ORDER] # Loop through the different coupling hierarchy values, so we # start with the most dominant and proceed to the least dominant hierarchy = sorted(list(set([self.get('order_hierarchy')[k] for \ @@ -1968,7 +1985,7 @@ def get_particles_hierarchy(self): for value in hierarchy: orders.append([ k for (k, v) in \ self.get('order_hierarchy').items() if \ - v == value ]) + v == value and k != INTERFERENCE_ORDER ]) # Extract the interaction that correspond to the different # coupling hierarchies, and the corresponding particles @@ -3389,6 +3406,10 @@ def calculate_orders(self, model): weight += sum([model.get('order_hierarchy')[c]*n for \ (c,n) in couplings.items()]) coupling_orders['WEIGHTED'] = weight + # the hidden order of interference processes, once in the model, is + # only listed by the diagrams which carry it + if not coupling_orders.get(INTERFERENCE_ORDER): + coupling_orders.pop(INTERFERENCE_ORDER, None) self.set('orders', coupling_orders) def pass_squared_order_constraints(self, diag_multiplier, squared_orders, @@ -3738,6 +3759,13 @@ def default_setup(self): # contribution 'QCD=4 QED=0', the pure interference 'QCD=2 QED=2' and # the pure QED contribution of order 'QCD=0 QED=4'. self['split_orders'] = [] + # Interference of two amplitude sets, 2Re(A_left A_right*), written + # as 'left [LIxtree=ORDERS] right' (loop-induced times tree) or + # 'left [treextree] right'. 'interference_mode' is '' for an ordinary + # process; 'interference_process' is then the right-hand process, + # carrying its own orders, s-channel requirements and exclusions. + self['interference_mode'] = '' + self['interference_process'] = None def filter(self, name, value): """Filter for valid process property values.""" @@ -3827,6 +3855,14 @@ def filter(self, name, value): import madgraph.interface.madgraph_interface as mg if value not in mg.MadGraphCmd._valid_nlo_modes: raise self.PhysicsObjectError("%s is not a valid NLO_mode" % str(value)) + + if name == 'interference_mode': + if value not in ['', 'LIxtree', 'treextree']: + raise self.PhysicsObjectError("%s is not a valid interference_mode" % str(value)) + + if name == 'interference_process': + if value is not None and not isinstance(value, Process): + raise self.PhysicsObjectError("%s is not a valid Process" % str(value)) return True def has_multiparticle_label(self): @@ -3913,7 +3949,55 @@ def get_sorted_keys(self): 'forbidden_onsh_s_channels', 'forbidden_s_channels', 'forbidden_particles', 'is_decay_chain', 'decay_chains', 'legs_with_decays', 'perturbation_couplings', 'has_born', - 'NLO_mode', 'split_orders', 'born_sq_orders'] + 'NLO_mode', 'split_orders', 'born_sq_orders'] + \ + [key for key in ('interference_mode', 'interference_process') + if key in self] # absent from a process pickled before them + + def get_interference_mode(self): + """'LIxtree', 'treextree' or '' (also for a process unpickled from + a version that did not know about interference processes).""" + + return dict.get(self, 'interference_mode', '') or '' + + def has_squared_order_selection(self): + """Whether only some squared orders of this process are kept: an + interference process ('[LIxtree=QCD]', '[treextree]'), or a + squared-order constraint other than the one implied by an amplitude + '==' / '>' constraint, here or in a decay chain. This is exactly when + nice_string shows a '^2' (or would, but for the hidden order), and it + is what the run card treats as an interference.""" + + if self.get_interference_mode(): + return True + for key, value in self['squared_orders'].items(): + constrained = self['constrained_orders'].get(key) + if constrained and constrained[0] == value / 2 and \ + constrained[1] == self.get_squared_order_type(key): + continue + return True + return any(decay.has_squared_order_selection() + for decay in self['decay_chains']) + + def interference_string(self): + """The tail of the process syntax for an interference process: the + '[ treextree ]' or '[ LIxtree = ORDERS ]' bracket followed by the + right-hand process, each piece ending with a space. It comes after + everything that belongs to the left-hand process (orders, squared + orders, exclusions), since what follows the bracket is read back as + the right-hand process. Empty for an ordinary process.""" + + mode = self.get_interference_mode() + if not mode: + return '' + if mode == 'treextree': + mystr = '[ treextree ] ' + else: + mystr = '[ LIxtree = %s ] ' % ' '.join(sorted( + self['perturbation_couplings'])) + right = self['interference_process'] + if right is not None: + mystr += right.nice_string(prefix=False, print_weighted=False) + ' ' + return mystr def nice_string(self, indent=0, print_weighted=True, prefix=True, print_perturbated=True): """Returns a nicely formated string about current process @@ -3970,7 +4054,7 @@ def nice_string(self, indent=0, print_weighted=True, prefix=True, print_perturba # Add orders if self['orders']: to_add = [] - for key in sorted(self['orders'].keys()): + for key in sorted(visible_orders(self['orders'])): if not print_weighted and key == 'WEIGHTED': continue value = int(self['orders'][key]) @@ -3997,8 +4081,10 @@ def nice_string(self, indent=0, print_weighted=True, prefix=True, print_perturba self['constrained_orders'][key][1], self['constrained_orders'][key][0]) for key in sorted(self['constrained_orders'].keys())) + ' ' - # Add perturbation_couplings - if print_perturbated and self['perturbation_couplings']: + # Add perturbation_couplings (for LIxtree: with the right-hand process, + # see interference_string) + if print_perturbated and self['perturbation_couplings'] and \ + self.get_interference_mode() != 'LIxtree': mystr = mystr + '[ ' if self['NLO_mode']!='tree': if self['NLO_mode']=='virt' and not self['has_born']: @@ -4012,7 +4098,7 @@ def nice_string(self, indent=0, print_weighted=True, prefix=True, print_perturba # Add squared orders if self['squared_orders']: to_add = [] - for key in sorted(self['squared_orders'].keys()): + for key in sorted(visible_orders(self['squared_orders'])): if not print_weighted and key == 'WEIGHTED': continue if key in self['constrained_orders']: @@ -4047,6 +4133,8 @@ def nice_string(self, indent=0, print_weighted=True, prefix=True, print_perturba forbpart = self['model'].get('particle_dict')[forb_id] mystr = mystr + forbpart.get_name() + ' ' + mystr = mystr + self.interference_string() + # Remove last space mystr = mystr[:-1] @@ -4104,7 +4192,7 @@ def input_string(self): prevleg = leg if self['orders']: - keys = list(self['orders'].keys()) + keys = visible_orders(self['orders']) keys.sort(reverse=True) mystr = mystr + " ".join([key + '=' + repr(self['orders'][key]) \ for key in keys]) + ' ' @@ -4112,7 +4200,7 @@ def input_string(self): # Add squared orders if self['squared_orders']: mystr = mystr + " ".join([key + '^2=' + repr(self['squared_orders'][key]) \ - for key in self['squared_orders']]) + ' ' + for key in visible_orders(self['squared_orders'])]) + ' ' # Add perturbation orders if self['perturbation_couplings']: @@ -4923,15 +5011,17 @@ def nice_string(self, indent=0, print_weighted=False, prefix=True): if self['orders']: mystr = mystr + " ".join([key + '=' + repr(self['orders'][key]) \ - for key in sorted(self['orders'])]) + ' ' + for key in sorted(visible_orders(self['orders']))]) + ' ' if self['constrained_orders']: mystr = mystr + " ".join('%s%s%d' % (key, operator, value) for (key,(value, operator)) in self['constrained_orders'].items()) + ' ' - # Add perturbation_couplings - if self['perturbation_couplings']: + # Add perturbation_couplings (for LIxtree: with the right-hand process, + # see interference_string) + if self['perturbation_couplings'] and \ + self.get_interference_mode() != 'LIxtree': mystr = mystr + '[ ' if self['NLO_mode']!='tree': if self['NLO_mode']=='virt' and not self['has_born']: @@ -4945,9 +5035,11 @@ def nice_string(self, indent=0, print_weighted=False, prefix=True): if self['squared_orders']: mystr = mystr + " ".join([key + '^2%s%d'%\ (self.get_squared_order_type(key),self['squared_orders'][key]) \ - for key in self['squared_orders'].keys() \ + for key in visible_orders(self['squared_orders']) \ if print_weighted or key!='WEIGHTED']) + ' ' + mystr = mystr + self.interference_string() + # Remove last space mystr = mystr[:-1] @@ -4989,7 +5081,11 @@ def get_process_with_legs(self, LegList): 'overall_orders': self.get('overall_orders'), 'split_orders': self.get('split_orders'), 'born_sq_orders': self.get('born_sq_orders'), - 'NLO_mode': self.get('NLO_mode') + 'NLO_mode': self.get('NLO_mode'), + # the right-hand process stays the ProcessDefinition template: + # its legs are matched to these ones at generation time + 'interference_mode': self.get_interference_mode(), + 'interference_process': dict.get(self, 'interference_process') }) def get_process(self, initial_state_ids, final_state_ids): diff --git a/madgraph/core/diagram_generation.py b/madgraph/core/diagram_generation.py index fcd165322..e65aa07fc 100755 --- a/madgraph/core/diagram_generation.py +++ b/madgraph/core/diagram_generation.py @@ -41,6 +41,91 @@ class NoDiagramException(InvalidCmd): pass +#=============================================================================== +# Interference of two amplitude sets: 'left [LIxtree=QCD] right' (loop-induced +# times tree, see loop_diagram_generation) and 'left [treextree] right' +#=============================================================================== +INTERFERENCE_ORDER = base_objects.INTERFERENCE_ORDER +# Output formats able to select squared split orders, hence to return the +# interference alone (the others would give |A_left + A_right|^2): the Fortran +# ones (GET_MATRIX) and the madmatrix ones (mg7, and standalone for the C++ +# matrix element), whose color sum pairs the amplitude split orders on every +# backend, CPU and GPU (PR #244) +TREE_INTERFERENCE_FORMATS = ['madevent', 'standalone_fortran', 'mg7', + 'standalone'] + +def register_interference_order(model): + """Add the hidden coupling order tagging the right-hand amplitudes of an + interference process to the model, in memory and idempotently. Its + hierarchy is 0, so that the WEIGHTED order of every diagram is unchanged, + and no interaction carries it.""" + + if INTERFERENCE_ORDER not in model.get('order_hierarchy'): + model.get('order_hierarchy')[INTERFERENCE_ORDER] = 0 + coupling_orders = model.get('coupling_orders') + if INTERFERENCE_ORDER not in coupling_orders: + coupling_orders.add(INTERFERENCE_ORDER) + +def prepare_interference_process(procdef): + """Complete the ProcessDefinition of an interference process + ('left [LIxtree=QCD] right' or 'left [treextree] right', see + MadGraphCmd.extract_interference_process) before its generation: + + - the hidden order INTERFERENCE_ORDER is registered in the model; + - the left-hand process gets the squared-order constraint INTERF^2==1 + and INTERF as split order, so that only the products of a left-hand + amplitude with a right-hand one are kept, also in the generated code; + - the right-hand process gets, when none is given, the minimal WEIGHTED + order exactly as 'generate ' would. + + The right-hand diagrams themselves are generated per subprocess, with the + legs of that subprocess: Amplitude.add_interference_diagrams (treextree) + and LoopAmplitude.set_interference_trees (LIxtree).""" + + mode = procdef.get_interference_mode() + if not mode: + return + if any(value < 0 for value in procdef.get('squared_orders').values()): + raise InvalidCmd("Negative squared-order constraints are not " + "supported for %s interference processes." % mode) + model = procdef.get('model') + register_interference_order(model) + + squared_orders = dict(procdef.get('squared_orders')) + squared_orders[INTERFERENCE_ORDER] = 1 + procdef.set('squared_orders', squared_orders) + sqorders_types = dict(procdef.get('sqorders_types')) + sqorders_types[INTERFERENCE_ORDER] = '==' + procdef.set('sqorders_types', sqorders_types) + if INTERFERENCE_ORDER not in procdef.get('split_orders'): + procdef.set('split_orders', + list(procdef.get('split_orders')) + [INTERFERENCE_ORDER]) + + right = procdef.get('interference_process') + if not right.get('orders') and not right.get('born_sq_orders'): + right.set('orders', MultiProcess.find_optimal_process_orders(right)) + right.check_expansion_orders() + +def specialise_interference_process(process): + """The right-hand process of an interference subprocess, with the + external legs of that subprocess. It is also stored back in the + subprocess, which then shows the tree process it interferes with.""" + + right = process.get('interference_process') + legs = base_objects.LegList([copy.copy(leg) for leg in process.get('legs')]) + if isinstance(right, base_objects.ProcessDefinition): + right_process = right.get_process_with_legs(legs) + else: + # already specialised to these legs (regeneration) + right_process = copy.copy(right) + right_process.set('legs', legs) + right_process.set('interference_mode', '') + right_process.set('interference_process', None) + # the process number is the one of the whole (left-hand) definition + right_process.set('id', 0) + process.set('interference_process', right_process) + return right_process + #=============================================================================== # DiagramTag mother class #=============================================================================== @@ -1267,7 +1352,17 @@ def generate_diagrams(self, returndiag=False, diagram_filter=False): # in this way. We shall do this only if the diagrams are not asked to # be returned, as it is the case for NLO because it this case the # interference are not necessarily among the diagrams generated here only. - if not returndiag and len(res)>0: + # 'left [treextree] right': the squared-order constraints are about + # the products with the right-hand diagrams, added at the end (see + # add_interference_diagrams); only the amplitude-level constraints of + # this (left-hand) process apply here + treextree = not returndiag and \ + process.get_interference_mode() == 'treextree' + if treextree: + for name, (value, operator) in \ + process.get('constrained_orders').items(): + res.filter_constrained_orders(name, value, operator) + elif not returndiag and len(res)>0: res = self.apply_squared_order_constraints(res) if diagram_filter: @@ -1300,6 +1395,9 @@ def generate_diagrams(self, returndiag=False, diagram_filter=False): pertur = sorted(self.get('process')['perturbation_couplings'])[0] self.get('process').get('legs').sort(pert=pertur) + if treextree: + res = self.add_interference_diagrams(res, diagram_filter) + # Set diagrams to res if not asked to be returned if not returndiag: self['diagrams'] = res @@ -1307,6 +1405,56 @@ def generate_diagrams(self, returndiag=False, diagram_filter=False): else: return not failed_crossing, res + def add_interference_diagrams(self, left_diagrams, diagram_filter=False): + """For an interference process 'left [treextree] right', add to the + diagrams of this (left-hand) process those of the right-hand process + with the same external legs, each tagged with one unit of the hidden + order INTERFERENCE_ORDER, and apply the squared-order constraints of + the process to the whole list: with INTERF^2==1 (see + prepare_interference_process) only the products of a left-hand and a + right-hand diagram, 2 Re(A_left A_right^*), contribute. All diagrams of + both sides are kept as integration channels. A user diagram filter + ('--diagram_filter') applies to both sides. Returns the new diagram + list, empty if either side has no diagram.""" + + process = self.get('process') + register_interference_order(process.get('model')) + right_process = specialise_interference_process(process) + + right_diagrams = base_objects.DiagramList() + if left_diagrams: + right_amplitude = Amplitude() + right_amplitude.set('process', right_process) + try: + right_amplitude.generate_diagrams(diagram_filter=diagram_filter) + right_diagrams = right_amplitude.get('diagrams') + except InvalidCmd: + pass + if not left_diagrams or not right_diagrams: + logger.info("No diagram for %s: no interference with %s." % ( + (right_process if left_diagrams else process).nice_string( + prefix=False), + (process if left_diagrams else right_process).nice_string( + prefix=False))) + return base_objects.DiagramList() + + for diagram in right_diagrams: + orders = dict(diagram.get('orders')) + orders[INTERFERENCE_ORDER] = 1 + diagram.set('orders', orders) + + res = base_objects.DiagramList(list(left_diagrams) + list(right_diagrams)) + while True: + new_res = res.apply_positive_sq_orders(res, + process.get('squared_orders'), + process.get('sqorders_types')) + if len(new_res) == len(res): + break + res = new_res + logger.info("Interference of %d with %d diagrams (%d contributing)" % + (len(left_diagrams), len(right_diagrams), len(res))) + return res + def apply_squared_order_constraints(self, diag_list): """Applies the user specified squared order constraints on the diagram list in argument.""" @@ -2683,10 +2831,13 @@ def get_fs_flavor(id, fsleg): # Check for successful crossings, unless we have specified # properties that break crossing symmetry + # (an interference process carries a right-hand process with + # its own s-channel constraints: no reuse by crossing either) if not process.get('required_s_channels') and \ not process.get('forbidden_onsh_s_channels') and \ not process.get('forbidden_s_channels') and \ - not process.get('is_decay_chain') and not diagram_filter: + not process.get('is_decay_chain') and not diagram_filter \ + and not process.get_interference_mode(): try: crossed_index = success_procs.index(sorted_legs) # The relabeling of legs for loop amplitudes is cumbersome @@ -2842,7 +2993,11 @@ def find_optimal_process_orders(process_definition, diagram_filter=False): hierarchy = list(hierarchydef.items()) hierarchy.sort() for key, value in hierarchydef.items(): - if value>1: + # an order of hierarchy 0 (like the hidden INTERFERENCE_ORDER) + # does not enter WEIGHTED + if value == 0: + continue + elif value>1: tmp.append('%s*%s' % (value,key)) else: tmp.append('%s' % key) @@ -2850,7 +3005,7 @@ def find_optimal_process_orders(process_definition, diagram_filter=False): # Run diagram generation with increasing max_order_now until # we manage to get diagrams while max_order_now < max_WEIGHTED_order: - logger.info("Trying coupling order WEIGHTED<=%d: WEIGTHED IS %s" % (max_order_now, wgtdef)) + logger.info("Trying coupling order WEIGHTED<=%d: WEIGHTED IS %s" % (max_order_now, wgtdef)) oldloglevel = logger.level logger.setLevel(logging.WARNING) diff --git a/madgraph/core/helas_objects.py b/madgraph/core/helas_objects.py index 3b3ae6a80..ef69162c8 100755 --- a/madgraph/core/helas_objects.py +++ b/madgraph/core/helas_objects.py @@ -4474,6 +4474,22 @@ def generate_helas_diagrams(self, amplitude, optimization=1,decay_ids=[]): # Add amplitude to amplitdes in helas_diagram helas_diagram.get('amplitudes').append(amp) + # A right-hand diagram of an interference process + # ('left [treextree] right') carries the hidden order tagging it, + # which no interaction has: pass it on to its amplitudes (on a + # copy, their orders being those of the interaction), so that the + # split orders of the generated code see it. + interference_tag = diagram.get('orders').get( + base_objects.INTERFERENCE_ORDER, 0) \ + if diagram.get('orders') else 0 + if interference_tag: + for amp in helas_diagram.get('amplitudes'): + orders = dict(amp.get('orders')) + orders[base_objects.INTERFERENCE_ORDER] = \ + orders.get(base_objects.INTERFERENCE_ORDER, 0) + \ + interference_tag + amp.set('orders', orders) + # After generation of all wavefunctions and amplitudes, # first sort the wavefunctions according to number diagram_wavefunctions.sort(key=lambda wf:wf.get('number')) diff --git a/madgraph/fks/fks_base.py b/madgraph/fks/fks_base.py index 6041ca5bd..d290b054e 100755 --- a/madgraph/fks/fks_base.py +++ b/madgraph/fks/fks_base.py @@ -920,7 +920,7 @@ def find_reals(self, pert_orders = []): # are imposed) if not pert_orders: if not self.born_amp['process']['orders']: - pert_orders = model['coupling_orders'] + pert_orders = MG.visible_orders(model['coupling_orders']) else: pert_orders = self.born_amp['process']['perturbation_couplings'] diff --git a/madgraph/interface/amcatnlo_interface.py b/madgraph/interface/amcatnlo_interface.py index 7f1eeac93..66295500f 100755 --- a/madgraph/interface/amcatnlo_interface.py +++ b/madgraph/interface/amcatnlo_interface.py @@ -52,6 +52,7 @@ import madgraph.iolibs.export_v4 as export_v4 import madgraph.iolibs.helas_call_writers as helas_call_writers import madgraph.loop.loop_base_objects as loop_base_objects +import madgraph.core.base_objects as base_objects import madgraph.core.diagram_generation as diagram_generation import madgraph.core.helas_objects as helas_objects @@ -752,7 +753,10 @@ def do_add(self, line, *args,**opt): # check that all the couplings of the model have been constrained # in the squared orders, otherwise set the others to zero - for o in myprocdef['model'].get('coupling_orders'): + # (not the hidden order an interference process adds to the model) + model_orders = base_objects.visible_orders( + myprocdef['model'].get('coupling_orders')) + for o in model_orders: if o not in myprocdef['squared_orders'].keys(): logger.warning('No squared order constraint for order %s. Setting to 0' % o) myprocdef['squared_orders'][o] = 0 @@ -761,7 +765,7 @@ def do_add(self, line, *args,**opt): # split all orders in the model, for the moment it's the simplest solution # mz02/2014 #if proc_type[1] != 'only': - myprocdef['split_orders'] += [o for o in myprocdef['model'].get('coupling_orders') \ + myprocdef['split_orders'] += [o for o in model_orders \ if o not in myprocdef['split_orders']] # now set the squared orders @@ -809,7 +813,7 @@ def do_add(self, line, *args,**opt): # squared-orders constraints. In that case, all kind of splittings/loop-particles # must be included if not myprocdef['orders'] and self.options['nlo_mixed_expansion']: - myprocdef['perturbation_couplings'] = list(myprocdef['model']['coupling_orders']) + myprocdef['perturbation_couplings'] = list(model_orders) self._curr_proc_defs.append(myprocdef) diff --git a/madgraph/interface/madgraph_interface.py b/madgraph/interface/madgraph_interface.py index 42c481a73..7ae6c0a5b 100755 --- a/madgraph/interface/madgraph_interface.py +++ b/madgraph/interface/madgraph_interface.py @@ -144,6 +144,119 @@ class DuplicateParticle(madgraph.InvalidCmd): same misleading message. """ +#=============================================================================== +# Interference of two amplitude sets: 'left [LIxtree=QCD] right' and +# 'left [treextree] right' +#=============================================================================== +# The bracket of an interference process. Its keyword is matched without +# regard to case, so that [LIxtree=QCD], [lixtree=QCD] and [TreeXTree] are all +# accepted; INTERFERENCE_MODES maps the lower-case spelling on the one stored +# in Process['interference_mode']. +INTERFERENCE_MODES = {'lixtree': 'LIxtree', 'treextree': 'treextree'} +_INTERFERENCE_BRACKET = re.compile( + r"\[\s*(?:(?P