diff --git a/Template/LO/SubProcesses/makefile b/Template/LO/SubProcesses/makefile index fbce3dfad..6122052f4 100644 --- a/Template/LO/SubProcesses/makefile +++ b/Template/LO/SubProcesses/makefile @@ -38,8 +38,16 @@ endif MATRIX_HEL = $(patsubst %.f,%.o,$(wildcard matrix*_orig.f)) MATRIX = $(patsubst %.f,%.o,$(wildcard matrix*_optim.f)) +# Crossing-symmetry routers: matrix*_router.f share a base subprocess's matrix +# element and are never recycled, so they are compiled into both the forhel and +# the optimized binaries alongside the recycled bases. When recycling is off the +# matrix*.f glob below already covers them. +ROUTER = $(patsubst %.f,%.o,$(wildcard matrix*_router.f)) ifeq ($(strip $(MATRIX_HEL)),) MATRIX = $(patsubst %.f,%.o,$(wildcard matrix*.f)) +else + MATRIX += $(ROUTER) + MATRIX_HEL += $(ROUTER) endif @@ -102,3 +110,10 @@ initcluster.o: message.inc clean: $(RM) *.o gensym madevent madevent_forhel + +# Track B cross-group crossing: present only in a dependent P directory, it makes +# the base group's matrix object(s) be symlinked from the base directory +# instead of recompiled here (see write_crossgroup_mk). Included LAST so its +# specific rules override the makefile's %.o pattern / $(MATRIX) static-pattern +# rules. Absent (a no-op) elsewhere. +-include crossgroup.mk diff --git a/aloha/__init__.py b/aloha/__init__.py index c9605e375..90d934a27 100755 --- a/aloha/__init__.py +++ b/aloha/__init__.py @@ -1,4 +1,12 @@ complex_mass = False # Tag for activating the complex mass scheme +t_channel_width = False # Whether to keep the width i*M*Gamma in the propagator + # denominator for spacelike (t-channel, P^2<0) momenta. + # False (default): drop it there -- the correct tree-level + # treatment outside the complex-mass scheme (a t-channel + # propagator has no pole to regulate, and the spurious + # width breaks gauge cancellations). True: keep the width + # in every propagator (legacy behaviour). Ignored when + # complex_mass is True (the width lives in the mass then). unitary_gauge = True # Tag choosing between Feynman Gauge or unitary gauge # 0/False: Feynman # 1/True: unitary diff --git a/aloha/aloha_writers.py b/aloha/aloha_writers.py index c9ec6da6c..5bbe4f5e0 100755 --- a/aloha/aloha_writers.py +++ b/aloha/aloha_writers.py @@ -956,8 +956,25 @@ def sort_fct(a, b): out.write(' denom = %(COUP)s/(%(denom)s)\n' % {'COUP': coup_name,\ 'denom':self.write_obj(self.routine.denominator)}) else: - out.write(' denom = %(COUP)s/(P%(i)s(0)**2-P%(i)s(1)**2-P%(i)s(2)**2-P%(i)s(3)**2 - M%(i)s * (M%(i)s -CI* W%(i)s))\n' % \ - {'i': self.outgoing, 'COUP': coup_name}) + p2 = 'P%(i)s(0)**2-P%(i)s(1)**2-P%(i)s(2)**2-P%(i)s(3)**2' \ + % {'i': self.outgoing} + wdenom = '%(p2)s - M%(i)s * (M%(i)s -CI* W%(i)s)' \ + % {'i': self.outgoing, 'p2': p2} + if aloha.t_channel_width: + out.write(' denom = %(COUP)s/(%(wd)s)\n' % + {'COUP': coup_name, 'wd': wdenom}) + else: + # spacelike (t-channel) propagator: no pole to regulate, + # so drop the width unless in the complex-mass scheme. + out.write(' if (dble(%(p2)s).gt.0d0) then\n' + % {'p2': p2}) + out.write(' denom = %(COUP)s/(%(wd)s)\n' % + {'COUP': coup_name, 'wd': wdenom}) + out.write(' else\n') + out.write(' denom = %(COUP)s/(%(p2)s - M%(i)s**2)\n' + % {'i': self.outgoing, 'COUP': coup_name, + 'p2': p2}) + out.write(' endif\n') else: if self.routine.denominator: if 'P1N' not in self.tag: @@ -2050,8 +2067,19 @@ def sort_fct(a, b): out.write(' denom = %(pre_coup)s%(coup)s%(post_coup)s/(%(denom)s);\n' % \ mydict) else: - out.write(' denom = %(pre_coup)s%(coup)s%(post_coup)s/((P%(i)s[0]*P%(i)s[0])-(P%(i)s[1]*P%(i)s[1])-(P%(i)s[2]*P%(i)s[2])-(P%(i)s[3]*P%(i)s[3]) - M%(i)s * (M%(i)s -cI* W%(i)s));\n' % \ - mydict) + p2 = '(P%(i)s[0]*P%(i)s[0])-(P%(i)s[1]*P%(i)s[1])-(P%(i)s[2]*P%(i)s[2])-(P%(i)s[3]*P%(i)s[3])' % mydict + wd = '%(p2)s - M%(i)s * (M%(i)s -cI* W%(i)s)' % dict(mydict, p2=p2) + if aloha.t_channel_width: + out.write(' denom = %(pre_coup)s%(coup)s%(post_coup)s/(%(wd)s);\n' % dict(mydict, wd=wd)) + else: + # spacelike (t-channel) propagator: no pole to regulate, + # so drop the width unless in the complex-mass scheme. + p2sign = '(%s)%s' % (p2, self.realoperator) if aloha.loop_mode else p2 + out.write(' if (%s > 0.){\n' % p2sign) + out.write(' denom = %(pre_coup)s%(coup)s%(post_coup)s/(%(wd)s);\n' % dict(mydict, wd=wd)) + out.write(' } else {\n') + out.write(' denom = %(pre_coup)s%(coup)s%(post_coup)s/(%(p2)s - M%(i)s*M%(i)s);\n' % dict(mydict, p2=p2)) + out.write(' }\n') else: if self.routine.denominator: raise Exception('modify denominator are not compatible with complex mass scheme') @@ -2583,8 +2611,17 @@ def sort_fct(a, b): out.write(' denom = %(COUP)s/(%(denom)s)\n' % {'COUP': coup_name,\ 'denom':self.write_obj(self.routine.denominator)}) else: - out.write(' denom = %(coup)s/(P%(i)s[0]**2-P%(i)s[1]**2-P%(i)s[2]**2-P%(i)s[3]**2 - M%(i)s * (M%(i)s -1j* W%(i)s))\n' % - {'i': self.outgoing,'coup':coup_name}) + p2 = 'P%(i)s[0]**2-P%(i)s[1]**2-P%(i)s[2]**2-P%(i)s[3]**2' % {'i': self.outgoing} + wd = '%(p2)s - M%(i)s * (M%(i)s -1j* W%(i)s)' % {'i': self.outgoing, 'p2': p2} + if aloha.t_channel_width: + out.write(' denom = %(coup)s/(%(wd)s)\n' % {'coup': coup_name, 'wd': wd}) + else: + # spacelike (t-channel) propagator: no pole to regulate, + # so drop the width unless in the complex-mass scheme. + out.write(' if (%s).real > 0:\n' % p2) + out.write(' denom = %(coup)s/(%(wd)s)\n' % {'coup': coup_name, 'wd': wd}) + out.write(' else:\n') + out.write(' denom = %(coup)s/(%(p2)s - M%(i)s**2)\n' % {'i': self.outgoing, 'coup': coup_name, 'p2': p2}) else: if self.routine.denominator: raise Exception('modify denominator are not compatible with complex mass scheme') diff --git a/madgraph/core/diagram_generation.py b/madgraph/core/diagram_generation.py index 1f239abf5..1b78c0b31 100755 --- a/madgraph/core/diagram_generation.py +++ b/madgraph/core/diagram_generation.py @@ -444,6 +444,13 @@ def default_setup(self): # has_mirror_process is True if the same process but with the # two incoming particles interchanged has been generated self['has_mirror_process'] = False + # Crossed subprocesses folded into this amplitude and NOT generated on + # their own (merge_crossing='record'): each entry is + # (crossed Process, base_permutation, crossed_permutation), enough for + # the exporter to reach the crossed process through this amplitude's + # crossing-aware SMATRIX (see MultiProcess.cross_amplitude for the same + # permutation pair). Empty in the historical modes. + self['crossed_processes'] = [] def __init__(self, argument=None): """Allow initialization with Process""" @@ -470,6 +477,9 @@ def filter(self, name, value): if name == 'has_mirror_process': if not isinstance(value, bool): raise self.PhysicsObjectError("%s is not a valid boolean" % str(value)) + if name == 'crossed_processes': + if not isinstance(value, list): + raise self.PhysicsObjectError("%s is not a valid list" % str(value)) return True def get(self, name): @@ -487,7 +497,8 @@ def get(self, name): def get_sorted_keys(self): """Return diagram property names as a nicely sorted list.""" - return ['process', 'diagrams', 'has_mirror_process'] + return ['process', 'diagrams', 'has_mirror_process', + 'crossed_processes'] def get_number_of_diagrams(self): """Returns number of diagrams for this amplitude""" @@ -1944,6 +1955,20 @@ def get_flavor(id, fsleg): non_permuted_procs.append(fast_proc) logger.info("Crossed process found for %s, reuse diagrams." % \ process.base_string()) + elif merge_crossing == 'record': + # Found crossing - do NOT generate a separate + # amplitude, but record the crossed process on the + # base so the exporter can still reach it through the + # base's crossing-aware SMATRIX (its partonic + # contribution is not lost, unlike merge_crossing=True). + amplitudes[crossed_index].get('crossed_processes')\ + .append((process, permutations[crossed_index], + permutation)) + logger.info("Crossed process %s recorded on %s " + "(not generated)." % + (process.base_string(), + amplitudes[crossed_index].get('process') + .base_string())) else: logger.info("Crossed process found for %s, do not generate diagrams." % \ process.base_string()) diff --git a/madgraph/core/helas_objects.py b/madgraph/core/helas_objects.py index 46d3b015e..6963917d1 100755 --- a/madgraph/core/helas_objects.py +++ b/madgraph/core/helas_objects.py @@ -3984,6 +3984,11 @@ def default_setup(self): # has_mirror_process is True if the same process but with the # two incoming particles interchanged has been generated self['has_mirror_process'] = False + # Crossed subprocesses folded into this ME and NOT generated on their + # own (merge_crossing='record'); carried over from the base amplitude so + # the exporter can reach them through this ME's crossing-aware SMATRIX. + # Each entry is (crossed Process, base_permutation, crossed_permutation). + self['crossed_processes'] = [] self['allowed_flavors'] = [] # list of all allowed flavors for the process self['allowed_flavors_with_iden'] = [] # list of all allowed flavors for the process but grouped by identical matrix-element self['allowed_flavors_with_iden_sign'] = [] # list of all allowed flavors for the process but grouped by identical matrix-element @@ -4023,6 +4028,9 @@ def filter(self, name, value): if name == 'has_mirror_process': if not isinstance(value, bool): raise self.PhysicsObjectError("%s is not a valid boolean" % str(value)) + if name == 'crossed_processes': + if not isinstance(value, list): + raise self.PhysicsObjectError("%s is not a valid list" % str(value)) return True def get_sorted_keys(self): @@ -4030,7 +4038,8 @@ def get_sorted_keys(self): return ['processes', 'identical_particle_factor', 'diagrams', 'color_basis', 'color_matrix', - 'base_amplitude', 'has_mirror_process'] + 'base_amplitude', 'has_mirror_process', + 'crossed_processes'] # Enhanced get function def get(self, name): @@ -4055,6 +4064,9 @@ def __init__(self, amplitude=None, optimization=1, self.get('processes').append(amplitude.get('process')) self.set('has_mirror_process', amplitude.get('has_mirror_process')) + if amplitude.get('crossed_processes'): + self.set('crossed_processes', + list(amplitude.get('crossed_processes'))) self.generate_helas_diagrams(amplitude, optimization, decay_ids) self.calculate_fermionfactors() self.calculate_identical_particle_factor() diff --git a/madgraph/interface/common_run_interface.py b/madgraph/interface/common_run_interface.py index c8217cad3..58011b970 100755 --- a/madgraph/interface/common_run_interface.py +++ b/madgraph/interface/common_run_interface.py @@ -218,6 +218,14 @@ def check_set(self, args): self.help_set() raise self.InvalidCmd('set needs an option and an argument') + if args[0] == 'zerowidth_tchannel': + raise self.InvalidCmd( + "'zerowidth_tchannel' is a generation-time option: the T-channel " + "width treatment is now baked into the matrix element (ALOHA) at " + "'output' time and cannot be changed at run time. Choose it in MG5 " + "before output ('set zerowidth_tchannel True|False') and regenerate " + "the process.") + if args[0] not in self._set_options + list(self.options.keys()): self.help_set() raise self.InvalidCmd('Possible options for set are %s' % \ @@ -6860,7 +6868,28 @@ def check_card_consistency(self): if 'dressed_ee' in proc_charac['limitations']: if self.run_card['lpp1'] not in [0,1,-1] or self.run_card['lpp1'] not in [0,1,-1]: raise InvalidCmd("dressed lepton mode is not available for this process (see warning associated to the code generation to understand why)") - # + + if 'crossing' in proc_charac['limitations']: + # Crossing reuses one matrix element across physically distinct + # (crossed) initial states, so a per-beam property is ambiguous. + if self.run_card['polbeam1'] or self.run_card['polbeam2']: + raise InvalidCmd( + "Beam polarisation is not compatible with crossing symmetry:\n" + "this process reuses a matrix element across crossed initial\n" + "states, for which a per-beam polarisation is ill-defined.\n" + "Regenerate the process with crossing disabled, e.g.\n" + " generate --use_crossing=False\n" + "and 'output' again, to run polarised beams.") + if 'eva' in (self.run_card['pdlabel'], + self.run_card['pdlabel1'], self.run_card['pdlabel2']): + raise InvalidCmd( + "The EVA luminosity is not compatible with crossing symmetry:\n" + "this process reuses a matrix element across crossed initial\n" + "states, for which the per-beam EVA density is ill-defined.\n" + "Regenerate the process with crossing disabled, e.g.\n" + " generate --use_crossing=False\n" + "and 'output' again, to use EVA.") + # if 'fix_scale' in proc_charac['limitations']: if not self.run_card['fixed_fac_scale'] or not self.run_card['fixed_ren_scale']: raise InvalidCmd("Your model is identified as having not SM running of the strong coupling.\n"+\ diff --git a/madgraph/interface/madgraph_interface.py b/madgraph/interface/madgraph_interface.py index 128fd656a..e9c9b0edc 100755 --- a/madgraph/interface/madgraph_interface.py +++ b/madgraph/interface/madgraph_interface.py @@ -565,6 +565,21 @@ def help_check(self): logger.info(" Fortran standalone (SA), and C++ standalone (SA) back-ends") logger.info(" at the same phase-space point. Requires gfortran / g++.") logger.info(" Example: check language p p > e+ e-",'$MG:color:GREEN') + logger.info("o crossing:",'$MG:color:GREEN') + logger.info(" Output the process to a standalone backend twice, with") + logger.info(" the crossing symmetry on (--use_crossing=True) and off,") + logger.info(" then compare each subprocess evaluated through the") + logger.info(" extended flavor-index crossing against its independent") + logger.info(" value. --exporter picks the backend (default standalone):") + logger.info(" standalone (fortran/f2py), standalone_cpp, standalone_mg7.") + logger.info(" Requires gfortran+f2py (standalone) or a C++ compiler") + logger.info(" (standalone_cpp / standalone_mg7).") + logger.info(" For standalone_mg7, --simd picks the vectorisation width:") + logger.info(" auto (default), none, sse4, avx2, 512y, 512z; and") + logger.info(" --precision picks the float type: m mixed (default), d double, f float.") + logger.info(" Example: check crossing g u > g u",'$MG:color:GREEN') + logger.info(" Example: check crossing g u > g u --exporter=standalone_cpp",'$MG:color:GREEN') + logger.info(" Example: check crossing g u > g u --exporter=standalone_mg7 --simd=avx2 --precision=d",'$MG:color:GREEN') logger.info("o cms:",'$MG:color:GREEN') logger.info(" Check the complex mass scheme consistency by comparing") logger.info(" it to the narrow width approximation in the off-shell") @@ -1088,7 +1103,16 @@ def check_check(self, args): '--collier_internal_stability_test':'False', '--collier_mode':'1', '--events': None, - '--skip_evt':0} + '--skip_evt':0, + # 'check crossing' backend: standalone (default), standalone_cpp + # or standalone_mg7. + '--exporter':'standalone', + # 'check crossing --exporter=standalone_mg7' vectorisation + # (SIMD) width: auto (default), none, sse4, avx2, 512y, 512z. + '--simd':'auto', + # 'check crossing --exporter=standalone_mg7' float precision: + # m mixed (default), d double, f float. + '--precision':'m'} if args[0] in ['cms'] or args[0].lower()=='cmsoptions': # increase the default energy to 5000 @@ -2293,7 +2317,8 @@ def complete_generate(self, text, line, begidx, endidx, formatting=True): return if text.startswith('--'): - return self.list_completion(text, ['--no_crossing', + return self.list_completion(text, ['--use_crossing=True', + '--use_crossing=False', '--no_warning=duplicate', '--diagram_filter', '--standalone']) @@ -2390,6 +2415,7 @@ def complete_check(self, text, line, begidx, endidx, formatting=True): cms_check_mode = len(args) >= 2 and args[1]=='cms' + crossing_check_mode = len(args) >= 2 and args[1]=='crossing' cms_options = ['--name=','--tweak=','--seed=','--offshellness=', '--lambdaCMS=','--show_plot=','--report=','--lambda_plot_range=','--recompute_width=', @@ -2399,6 +2425,25 @@ def complete_check(self, text, line, begidx, endidx, formatting=True): options = ['--energy='] if cms_options: options.extend(cms_options) + if crossing_check_mode: + # 'check crossing' only understands --energy, --exporter and (for + # standalone_mg7) --simd / --precision; the cms options above do not + # apply. + crossing_options = ['--energy=', '--exporter=', '--simd=', + '--precision='] + # Value completion for the crossing-specific options. + if args[-1] == '--exporter=': + return self.list_completion( + text, list(process_checks.CROSSING_EXPORTERS)) + elif args[-1] == '--simd=': + return self.list_completion( + text, list(process_checks.MG7_SIMD_CHOICES)) + elif args[-1] == '--precision=': + return self.list_completion( + text, list(process_checks.MG7_PRECISION_CHOICES)) + # Propose the options themselves once the user starts an option. + if text.startswith('-'): + return self.list_completion(text, crossing_options) # Directory continuation if args[-1].endswith(os.path.sep): @@ -3079,7 +3124,8 @@ class MadGraphCmd(HelpToCmd, CheckValidForCmd, CompleteForCmd, CmdExtended): _tutorial_opts = ['aMCatNLO', 'stop', 'MadLoop', 'MadGraph5'] _switch_opts = ['mg5','aMC@NLO','ML5'] _check_opts = ['full', 'timing', 'stability', 'profile', 'permutation', - 'gauge','lorentz', 'brs', 'cms', 'flavor', 'language'] + 'gauge','lorentz', 'brs', 'cms', 'flavor', 'language', + 'crossing'] _import_formats = ['model_v4', 'model', 'proc_v4', 'command', 'banner'] _install_opts = ['Delphes', 'MadAnalysis4', 'ExRootAnalysis', 'update', 'Golem95', 'QCDLoop', 'maddm', 'maddump', @@ -3213,6 +3259,8 @@ class MadGraphCmd(HelpToCmd, CheckValidForCmd, CompleteForCmd, CmdExtended): _curr_helas_model = None _curr_exporter = None _second_exporter = None + # UI flag --use_crossing (default on); see do_add. + _use_crossing = True _done_export = False _curr_decaymodel = None @@ -3335,20 +3383,61 @@ def do_add(self, line): standalone_only = False if '--standalone' in args: standalone_only = True - merge_crossing = True - args.remove('--standalone') + args.remove('--standalone') - merge_crossing = False - if '--no_crossing' in args: - merge_crossing = True - args.remove('--no_crossing') + # Crossing symmetry is used by default. --use_crossing (bare) or + # --use_crossing=True keep it on, --use_crossing=False turns it off. + # --standalone does not affect it. + use_crossing = True + for arg in args[:]: + if arg == '--use_crossing': + use_crossing = True + args.remove(arg) + elif arg.startswith('--use_crossing='): + value = arg.split('=', 1)[1] + if value.lower() in ['true', 't', '1', 'yes', 'on']: + use_crossing = True + elif value.lower() in ['false', 'f', '0', 'no', 'off']: + use_crossing = False + else: + raise self.InvalidCmd('--use_crossing expects True or ' + 'False, got \'%s\'' % value) + args.remove(arg) + # Crossed subprocesses are ALWAYS kept (merge_crossing=False, the + # historical 3.x default): use_crossing only decides later, at the + # exporter stage, whether they collapse into a single extended-FLAV_IDX + # matrix element (fortran standalone) or are written out as their own + # matrix elements (every other output, incl. madevent). The old + # merge_crossing=True / --no_crossing path DROPS the crossed processes + # from the amplitude list ("do not generate diagrams"), silently losing + # those partonic contributions, so it must not be reachable from + # --use_crossing: use_crossing=False has to remain a complete output. + # + # merge_crossing='record' keeps the partonic contribution: the crossed + # process is recorded on the base (not generated on its own) and reached + # through the base's crossing-aware SMATRIX. This is the DEFAULT for a + # crossing-enabled generation -> the standalone output is one directory + # per base ME, and the grouped backends (madevent/mg7) reconstruct the + # crossed subprocesses at output time (see do_output). A process that + # breaks crossing (s-channel constraint, decay chain, loop, ...) falls + # back to full generation per-process inside generate_matrix_elements. + # --use_crossing=False keeps the complete unmerged generation, and + # MG_MERGE_CROSSING=off is a debug escape hatch to the same. + merge_crossing = 'record' if use_crossing else False + if os.environ.get('MG_MERGE_CROSSING') == 'off': + merge_crossing = False # Check the validity of the arguments self.check_add(args) if args[0] == 'model': return self.add_model(args[1:]) - + + # Remember the choice for the exporter: the crossing machinery is only + # written out in the fortran standalone if every process of the current + # generation asked for it (reset by clean_process/do_generate). + self._use_crossing = self._use_crossing and use_crossing + # special option for 1->N to avoid generation of kinematically forbidden #decay. if args[-1].startswith('--optimize'): @@ -4272,6 +4361,36 @@ def create_lambda_values_list(lower_bound, N): options['report'] = option[1].lower() elif option[0]=='--seed': options['seed'] = int(option[1]) + elif option[0]=='--exporter': + # Backend for 'check crossing': which standalone output to build + # and run the crossing self-check against. + if option[1] not in process_checks.CROSSING_EXPORTERS: + raise self.InvalidCmd( + "The '--exporter' option for 'check crossing' must be " + "one of %s, not '%s'." % ( + ', '.join(process_checks.CROSSING_EXPORTERS), + option[1])) + options['exporter'] = option[1] + elif option[0]=='--simd': + # Vectorisation width for 'check crossing --exporter= + # standalone_mg7' (ignored by the other backends). + if option[1] not in process_checks.MG7_SIMD_CHOICES: + raise self.InvalidCmd( + "The '--simd' option for 'check crossing' must be one " + "of %s, not '%s'." % ( + ', '.join(process_checks.MG7_SIMD_CHOICES), + option[1])) + options['simd'] = option[1] + elif option[0]=='--precision': + # Floating-point precision for 'check crossing --exporter= + # standalone_mg7' (ignored by the other backends). + if option[1] not in process_checks.MG7_PRECISION_CHOICES: + raise self.InvalidCmd( + "The '--precision' option for 'check crossing' must be " + "one of %s, not '%s'." % ( + ', '.join(process_checks.MG7_PRECISION_CHOICES), + option[1])) + options['precision'] = option[1] elif option[0]=='--name': if '.' in option[1]: raise self.InvalidCmd("Do not specify the extension in the"+ @@ -4525,6 +4644,24 @@ def create_lambda_values_list(lower_bound, N): # specified below where the user must be sure to have writing access. output_path = os.getcwd() + # The crossing check does not use the analytic MatrixElementEvaluator / + # gauge / CMS machinery: it regenerates the process to fortran + # standalone twice (crossing on and off) and compares the compiled + # matrix elements. Route it here and return early. + if args[0] == 'crossing': + options['proc_line'] = proc_line + options.setdefault('exporter', 'standalone') + crossing_result = process_checks.check_crossing( + myprocdef, param_card=param_card, options=options, cmd=self) + text = ('Crossing symmetry check (crossing on vs off, exporter=%s):' + '\n' % options['exporter']) + text += process_checks.output_crossing(crossing_result) + '\n' + logging.getLogger('madgraph.check_cmd').info(text) + process_checks.clean_added_globals(process_checks.ADDED_GLOBAL) + if not options['reuse']: + process_checks.clean_up(self._mgme_dir) + return + if args[0] in ['timing','stability', 'profile'] and not \ myprocdef.get('perturbation_couplings'): raise self.InvalidCmd("Only loop processes can have their "+ @@ -4968,6 +5105,8 @@ def clean_process(self): self._uses_polarization = False self._uses_density_matrix = False self._uses_quarkonia = False + # Reset the --use_crossing choice (a new process definition starts) + self._use_crossing = True # Reset _done_export, since we have new process self._done_export = False # Also reset _export_format and _export_dir @@ -9089,19 +9228,25 @@ def set2_acknowledged_v3_1_syntax(self, args, log=True): def help_set2_zerowidth_tchannel(self): logger.info("zerowidth_tchannel ",'$MG:color:GREEN') - logger.info(" > (default: True) [Used ONLY for tree-level output with madevent]") - logger.info(" > set the width to zero for all T-channel propagator --no impact on complex-mass scheme mode") + logger.info(" > (default: True) [generation/output-time option for tree-level output]") + logger.info(" > drop the width in the propagator denominator for spacelike (t-channel,") + logger.info(" > P^2<0) momenta. Done inside the ALOHA routine (runtime sign of P^2), so it") + logger.info(" > applies to every tree-level output. No impact in complex-mass-scheme mode.") def set2_zerowidth_tchannel(self, args, log=True): """Set whether the code should use zero-width for t-channel propagators. Default is set to True. (since v2.8.0) - Example: set zerowidth_tchannel False - """ + The treatment is now performed inside the ALOHA propagator routine (it + drops the width for spacelike, P^2<0, momenta); this flag is therefore an + output-time (code-generation) option and propagates to aloha here. + Example: set zerowidth_tchannel False + """ args = ['zerowidth_tchannel'] + args self.check_set(args) - self.options[args[0]] = banner_module.ConfigFile.format_variable(args[1], bool, args[0]) + self.options[args[0]] = banner_module.ConfigFile.format_variable(args[1], bool, args[0]) + aloha.t_channel_width = not self.options[args[0]] def set2_store_rwgt_info(self,args, log=True): """Set whether the code should generate systematics information in the output LHE file at NLO @@ -9760,22 +9905,29 @@ def export(self, nojpeg = False, main_file_name = "", group_processes=True, """Export a generated amplitude to file.""" + # T-channel width treatment is now baked into ALOHA (the propagator + # routine drops the width i*M*Gamma for spacelike, P^2<0, momenta -- the + # correct tree-level treatment outside the complex-mass scheme). Propagate + # the zerowidth_tchannel option to the aloha flag it now controls, so the + # generated propagator routines carry the runtime sign check. A 1->N decay + # has no t-channel, so keep every width there (as the legacy code did). + zerowidth_tchannel = self.options['zerowidth_tchannel'] + if self._curr_amps and self._curr_amps[0].get_ninitial() == 1: + zerowidth_tchannel = False + aloha.t_channel_width = not zerowidth_tchannel + # Define the helas call writer if hasattr(self._curr_exporter, 'helas_exporter') and self._curr_exporter.helas_exporter: self._curr_helas_model = self._curr_exporter.helas_exporter(self._curr_model, options=self.options) - elif self._curr_exporter.exporter == 'cpp': + elif self._curr_exporter.exporter == 'cpp': self._curr_helas_model = helas_call_writers.CPPUFOHelasCallWriter(self._curr_model) - elif self._curr_exporter.exporter == 'gpu': + elif self._curr_exporter.exporter == 'gpu': self._curr_helas_model = helas_call_writers.GPUFOHelasCallWriter(self._curr_model) elif self._curr_exporter.exporter == 'v4': if self._model_v4_path: self._curr_helas_model = helas_call_writers.FortranHelasCallWriter(self._curr_model) else: - options = {'zerowidth_tchannel': self.options['zerowidth_tchannel']} - if self._curr_amps and self._curr_amps[0].get_ninitial() == 1: - options['zerowidth_tchannel'] = False - self._curr_helas_model = helas_call_writers.FortranUFOHelasCallWriter(self._curr_model, - options=options) + self._curr_helas_model = helas_call_writers.FortranUFOHelasCallWriter(self._curr_model) else: raise Exception('unable to associate an helas format') @@ -9840,6 +9992,70 @@ def generate_matrix_elements(self, group_processes=True): grouping_criteria = self._curr_exporter.grouped_mode if grouping_criteria == 'gpu': grouping_criteria = 'madevent' + + # merge_crossing='record' skipped generating the crossed + # subprocesses so the standalone output collapses to one + # directory per base. The grouped (madevent) backends need + # them back as integration units -- each crossing is its own + # partonic channel with its own PDF/phase-space -- so expand + # the recorded metadata into crossed amplitudes here, reusing + # the base's diagrams via cross_amplitude (no diagram + # regeneration). The normal grouping + crossing routing then + # handles them exactly as an unmerged (merge_crossing=False) + # generation would. + # The standalone exporters ('standalone' fortran and + # 'standalone_mg7' cudacpp) consume the crossed_processes + # metadata directly -- they fold the crossings into the base + # directory and reach them through the base's crossing-aware + # SMATRIX/sigmaKin (extended flavor id), so they must NOT + # reconstruct. Every other (summation / event-generation) + # backend needs the crossings back as integration units, and + # reconstructing is also the safe default for any format that + # does not implement folding (it just reproduces the complete + # unmerged output). + if self._export_format not in ('standalone', 'standalone_mg7') and \ + any(amp.get('crossed_processes') + for amp in non_dc_amps): + if self.options['group_subprocesses'] == 'Auto': + collect_mirror = True + else: + collect_mirror = self.options['group_subprocesses'] + + def _fastproc(amp): + return tuple(l.get('id') for l in + amp.get('process').get('legs')) + + # Read the recorded crossings before clearing them. + originals = [(amp, amp.get('crossed_processes')) + for amp in non_dc_amps] + expanded = diagram_generation.AmplitudeList() + seen = {} # fast_proc -> amplitude, for mirror folding + for amp, _crossed in originals: + amp.set('crossed_processes', []) + expanded.append(amp) + seen[_fastproc(amp)] = amp + # Record mode stores a crossing and its beam-swap as two + # separate entries (neither is in the amplitude list when + # the other is met, so the generator's mirror check never + # fires); fold the beam-swap back into has_mirror_process + # here, exactly as generate_matrix_elements would. + for amp, crossed in originals: + for (proc, base_perm, cross_perm) in crossed: + xamp = diagram_generation.MultiProcess.\ + cross_amplitude(amp, proc, base_perm, + cross_perm) + xamp.set('crossed_processes', []) + fp = _fastproc(xamp) + mirror = (fp[1], fp[0]) + fp[2:] + if collect_mirror and mirror in seen and \ + proc.get_ninitial() == 2: + seen[mirror].set('has_mirror_process', True) + continue + xamp.set('has_mirror_process', False) + expanded.append(xamp) + seen[fp] = xamp + non_dc_amps = expanded + if non_dc_amps: subproc_groups.extend(\ group_subprocs.SubProcessGroup.group_amplitudes(\ diff --git a/madgraph/interface/reweight_interface.py b/madgraph/interface/reweight_interface.py index a2ddef2c4..7d3bbd087 100755 --- a/madgraph/interface/reweight_interface.py +++ b/madgraph/interface/reweight_interface.py @@ -1907,15 +1907,16 @@ def create_standalone_tree_directory(self, data ,second=False): self.model, real_only=True, ewsudakov=self.inc_sudakov) else: commandline += self.get_LO_definition_from_NLO(proc, self.model, ewsudakov=self.inc_sudakov) - # --no_crossing skips the generation of crossed subprocesses (e.g. - # u~ g > h u~ when u g > h u is already there). That's fine when + # --use_crossing=False skips the generation of crossed subprocesses + # (e.g. u~ g > h u~ when u g > h u is already there). That's fine when # flavor grouping is on, because the merged matrix element handles # all signs internally. Without flavor grouping, however, the # crossed subprocesses must be generated as separate entries -- # otherwise antiparticle events have nothing to match against in - # id_to_path. Only emit --no_crossing when both conditions hold. + # id_to_path. Only emit it when both conditions hold. if not self.keep_ordering and self._reweight_use_flavor_grouping(): - commandline = commandline.replace('add process', 'add process --no_crossing') + commandline = commandline.replace('add process', + 'add process --use_crossing=False') commandline = commandline.replace('add process', 'generate',1) logger.info(commandline) try: @@ -2158,7 +2159,7 @@ def load_interface_model(self, second=False): #if not self.keep_ordering: # for i,line in enumerate(data['processes']): - # data['processes'][i] = '%s --no_crossing' % line + # data['processes'][i] = '%s --use_crossing=False' % line # 0. clean previous run ------------------------------------------------ diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index 354815b36..517da9868 100755 --- a/madgraph/iolibs/export_cpp.py +++ b/madgraph/iolibs/export_cpp.py @@ -705,6 +705,10 @@ def __init__(self, matrix_elements, cpp_helas_call_writer, process_string = "", self.process_name = self.get_process_name() self.process_class = "CPPProcess" + # Emit the crossing-symmetry machinery (extended flavor_id carrying a + # crossing). Off by default; ProcessExporterCPP.generate_subprocess_- + # directory turns it on for standalone_cpp when --use_crossing is set. + self.use_crossing = False self.path = path self.helas_call_writer = cpp_helas_call_writer @@ -936,6 +940,8 @@ def get_process_class_definitions(self, write=True): """The complete class definition for the process""" replace_dict = {} + # Default (no-crossing) fill; overridden in the single_helicities branch. + replace_dict['cross_member_decl'] = '' # Extract model name replace_dict['model_name'] = self.model_name @@ -981,15 +987,18 @@ def get_process_class_definitions(self, write=True): replace_dict['wfct_size'] = wfct_size + cross_repl = self.get_crossing_replace_dict(self.matrix_elements[0]) + replace_dict['cross_member_decl'] = cross_repl['cross_member_decl'] replace_dict['all_sigma_kin_definitions'] = \ """// Calculate wavefunctions - void calculate_wavefunctions(const int perm[], const int hel[], const int flavor[]); + void calculate_wavefunctions(const int perm[], const int hel[], const int flavor[]%(cross_cw_sig_extra)s); static const int nwavefuncs = %(nwfct)d; MG5_%(model_name)s::ALOHAOBJ w[nwavefuncs]; """ % \ {'nwfct':len(self.wavefunctions), 'sizew': wfct_size, - 'model_name': self.model_name + 'model_name': self.model_name, + 'cross_cw_sig_extra': cross_repl['cross_cw_sig_extra'], } replace_dict['all_matrix_definitions'] = \ @@ -1069,7 +1078,11 @@ def get_process_function_definitions(self, write=True): process = self.matrix_elements[0].get('processes')[0] sym_data = ProcessExporterFortran._get_broken_symmetry_data(process, nincoming) ProcessExporterFortran._fill_broken_sym_replace_dict(replace_dict, sym_data) - + + # ident_cross() companion of broken_sym() (empty unless crossing is on). + replace_dict['ident_cross_function'] = \ + self.get_crossing_replace_dict(self.matrix_elements[0])['ident_cross_function'] + if write: file = self.read_template_file(self.process_definition_template) %\ replace_dict @@ -1177,6 +1190,10 @@ def get_calculate_wavefunctions(self, wavefunctions, amplitudes, write=True): self.helas_call_writer.use_flavor_mask = (n_flavors > 0) self.helas_call_writer.me_n_flavors = n_flavors self.helas_call_writer.me_active_flavor_mask = active_flavor_mask + # When crossing is on, the external HELAS calls must permute the + # helicity through perm[] and multiply their NSF flag by ic[] (both set + # up by sigmaKin); mirror of the fortran use_crossing_ic gate. + self.helas_call_writer.use_crossing_ic = getattr(self, 'use_crossing', False) try: replace_dict['wavefunction_calls'] = "\n".join(\ self.helas_call_writer.get_wavefunction_calls(\ @@ -1188,6 +1205,7 @@ def get_calculate_wavefunctions(self, wavefunctions, amplitudes, write=True): self.helas_call_writer.use_flavor_mask = False self.helas_call_writer.me_n_flavors = 0 self.helas_call_writer.me_active_flavor_mask = None + self.helas_call_writer.use_crossing_ic = False if write: file = self.read_template_file(self.process_wavefunction_template) % \ @@ -1388,6 +1406,215 @@ def fmt_uint64_2d(dtype, name, matrix): n_flavors, active_flavor_mask) + @staticmethod + def _cpp_int_array(values): + """Flat C++ initialiser '{a, b, c}' for a list of ints.""" + return '{%s}' % ', '.join(str(int(v)) for v in values) + + @staticmethod + def _cpp_int_array2d(flat, ncols): + """Nested C++ initialiser '{{...}, {...}}' from a flat list, ncols wide.""" + rows = ['{%s}' % ', '.join(str(int(v)) for v in flat[i:i + ncols]) + for i in range(0, len(flat), ncols)] + return '{%s}' % ', '.join(rows) + + def get_crossing_replace_dict(self, matrix_element): + """Fill the crossing-machinery holes of the C++ standalone templates. + + Mirrors export_v4.fill_crossing_replace_dict for the standalone_cpp + backend. When self.use_crossing is False every hole gets the plain, + pre-crossing code so the output is byte-for-byte the old one; when it is + True the extended flavor_id (a flavor AND a crossing) is decoded in + sigmaKin, the momenta/helicities are permuted through the crossing and + the swapped legs' NSF flag is flipped (via the ic[] array the HELAS + calls now read), and the denominator is split into the crossing- + dependent initial-state spin*color (spincol_cross) times the flavor- + dependent identical-final-state factor (ident_cross). + """ + # Plain (no-crossing) fills: identical to the historical template. + plain = { + 'fidx': 'flavor_id', + 'cross_tables_decode': '', + 'cross_perm_block': ('int perm[nexternal];\n' + 'for(int i = 0; i < nexternal; i++){\n' + ' perm[i]=i;\n' + '}'), + 'cross_cw_args': '', + 'cross_return': + 'return matrix_element * broken_sym(flavor) / denominator;', + 'cross_cw_sig_extra': '', + 'cross_member_decl': '', + 'ident_cross_function': '', + # Historical good-helicity filter (byte-identical to pre-crossing). + 'cross_ghidx_setup': '', + 'cross_goodhel_gate': + 'goodhel[flavor_id][ihel] || ntry[flavor_id] < 2', + 'cross_goodhel_train': + 'if (t != 0. && !goodhel[flavor_id][ihel]){\n' + ' goodhel[flavor_id][ihel]=true;\n' + ' ngood[flavor_id] ++;\n' + ' igood[flavor_id][ngood[flavor_id]] = ihel;\n' + ' }', + } + if not self.use_crossing: + return plain + + tables = ProcessExporterFortran.compute_crossing_tables( + self, matrix_element) + nexternal = tables['nexternal'] + ninitial = tables['ninitial'] + ncross = (nexternal + 1) * (nexternal + 1) + + spincol_init = self._cpp_int_array(tables['spincol']) + perm_init = self._cpp_int_array2d(tables['perm'], nexternal) + ic_init = self._cpp_int_array2d(tables['ic'], nexternal) + basepid_init = self._cpp_int_array(tables['basepid']) + src_init = self._cpp_int_array(tables['source']) + # Good-helicity remap: instead of the baked ghremap[ncross*ncomb] row + # table, keep only the per-crossing filterable flag and resolve the + # gating identity row at runtime (see cross_ghidx_setup) -- the same + # NCROSS*NCOMB -> NCROSS shrink the fortran path does via CROSS_GHIDX. + # allow_reverse False so it matches the order helicities[] is emitted in. + ghfilt_init = self._cpp_int_array( + ProcessExporterFortran.compute_ghfilt( + self, matrix_element, allow_reverse=False)) + + cross_tables_decode = ( + "// Crossing symmetry: flavor_id carries a flavor AND a crossing.\n" + "// cross = flavor_id / nflavors\n" + "// flav_use = flavor_id %% nflavors (index used for masking)\n" + "// A crossing permutes momenta/helicities between slots and flips\n" + "// each swapped leg's NSF flag; the denominator splits into the\n" + "// crossing-dependent initial-state spin*color (spincol_cross) and\n" + "// the flavor-dependent identical-final-state factor (ident_cross).\n" + "const int ncross = %(ncross)d;\n" + "static const int spincol_cross[ncross] = %(spincol)s;\n" + "static const int cross_perm[ncross][nexternal] = %(perm)s;\n" + "static const int cross_ic[ncross][nexternal] = %(ic)s;\n" + "// ghfilt[cross] = 1 if this crossing's good-helicity filter is a\n" + "// clean bijection of the identity rows, 0 otherwise (initial-\n" + "// initial swap, inapplicable, or non-bijection). The gating\n" + "// identity row itself is recomputed per row at runtime (see the\n" + "// good-helicity loop) rather than stored as an ncross*ncomb table.\n" + "// See ProcessExporterFortran.compute_ghfilt.\n" + "static const int ghfilt[ncross] = %(ghfilt)s;\n" + "int cross = flavor_id / nflavors;\n" + "int flav_use = flavor_id %% nflavors;\n" + "// A null spin*color entry (out of range, impossible, or an\n" + "// overlapping swap) means an identically-zero matrix element.\n" + "if (cross < 0 || cross >= ncross || spincol_cross[cross] == 0)\n" + " return 0.;" + ) % {'ncross': ncross, 'spincol': spincol_init, + 'perm': perm_init, 'ic': ic_init, 'ghfilt': ghfilt_init} + + cross_perm_block = ( + "int perm[nexternal];\n" + "int ic[nexternal];\n" + "for(int i = 0; i < nexternal; i++){\n" + " perm[i] = cross_perm[cross][i];\n" + " ic[i] = cross_ic[cross][i];\n" + "}") + + cross_return = ( + "// Uncrossed: historical path (IDEN via denominator, BROKEN_SYM\n" + "// correcting the identical-particle count per flavor). Crossed:\n" + "// rebuild the denominator from the crossed initial-state spin*color\n" + "// and the identical final-state factor of the actual flavors.\n" + "if (cross == 0)\n" + " return matrix_element * broken_sym(flavor) / denominator;\n" + "return matrix_element / " + "(spincol_cross[cross] * ident_cross(cross, flavor));") + + ident_cross_function = ( + "//------------------------------------------------------------------\n" + "// Identical-final-state factor (product of n!) of the crossed\n" + "// process. Flavor dependent, so computed at runtime: two crossed\n" + "// final legs are identical when they carry the same flavor group\n" + "// (same representative PDG, conjugated already when the leg swapped\n" + "// side) and the same position inside it. FLAVOR is not permuted by\n" + "// the crossing, so slot k reads the position of the original leg\n" + "// that moved into it, via src_cross.\n" + "int CPPProcess::ident_cross(int cross, const int* flavor)\n" + "{\n" + " const int ncross = %(ncross)d;\n" + " static const int basepid_cross[ncross * nexternal] = %(basepid)s;\n" + " static const int src_cross[ncross * nexternal] = %(src)s;\n" + " const int off = cross * nexternal;\n" + " bool used[nexternal];\n" + " for (int k = 0; k < nexternal; k++) used[k] = false;\n" + " int fact = 1;\n" + " for (int k = %(ninitial)d; k < nexternal; k++)\n" + " {\n" + " if (used[k]) continue;\n" + " int n = 1;\n" + " for (int l = k + 1; l < nexternal; l++)\n" + " {\n" + " if (used[l]) continue;\n" + " if (basepid_cross[off + k] == basepid_cross[off + l] &&\n" + " flavor[src_cross[off + k]] == flavor[src_cross[off + l]])\n" + " {\n" + " used[l] = true;\n" + " n = n + 1;\n" + " fact = fact * n;\n" + " }\n" + " }\n" + " }\n" + " return fact;\n" + "}" + ) % {'ncross': ncross, 'basepid': basepid_init, 'src': src_init, + 'ninitial': ninitial} + + return { + 'fidx': 'flav_use', + 'cross_tables_decode': cross_tables_decode, + 'cross_perm_block': cross_perm_block, + 'cross_cw_args': ', ic', + 'cross_return': cross_return, + 'cross_cw_sig_extra': ', const int ic[]', + 'cross_member_decl': ' int ident_cross(int cross, const int* flavor);', + 'ident_cross_function': ident_cross_function, + # The good-helicity filter is shared per flavor but consulted and + # trained through the crossing's row permutation sigma^-1: a crossed + # row is good iff its identity counterpart is. Rather than store the + # whole sigma^-1 (ghremap[ncross*ncomb]), recompute the gating + # identity row here: inverse-permute + sign-flip the crossed row's + # config (perm/ic already hold cross_perm[cross]/cross_ic[cross]), + # then find the identity row carrying it. ghidx = -1 disables the + # filter for a non-filterable crossing (ghfilt[cross] == 0: compute + # the row, never train). For cross 0 perm/ic are the identity so + # ghidx == ihel, exactly the historical filter. The search is only + # reached while scanning (ntry < 10), so it is off the hot path. + 'cross_ghidx_setup': + 'int ghidx = -1;\n' + ' if (ghfilt[cross]){\n' + ' int tgt[nexternal];\n' + ' for(int k = 0; k < nexternal; k++){\n' + ' tgt[perm[k]] = ic[k] * helicities[ihel][k];\n' + ' }\n' + ' for(int r = 0; r < ncomb; r++){\n' + ' bool same = true;\n' + ' for(int k = 0; k < nexternal; k++){\n' + ' if (helicities[r][k] != tgt[k]){\n' + ' same = false;\n' + ' }\n' + ' }\n' + ' if (same){\n' + ' ghidx = r;\n' + ' break;\n' + ' }\n' + ' }\n' + ' }\n' + ' ', + 'cross_goodhel_gate': + 'ghidx < 0 || goodhel[flav_use][ghidx] || ntry[flav_use] < 2', + 'cross_goodhel_train': + 'if (t != 0. && ghidx >= 0 && !goodhel[flav_use][ghidx]){\n' + ' goodhel[flav_use][ghidx]=true;\n' + ' ngood[flav_use] ++;\n' + ' igood[flav_use][ngood[flav_use]] = ihel;\n' + ' }', + } + def get_sigmaKin_lines(self, color_amplitudes, write=True): """Get sigmaKin_lines for function definition for Pythia 8 .cc file""" @@ -1399,6 +1626,10 @@ def get_sigmaKin_lines(self, color_amplitudes, write=True): replace_dict = {} assert len(self.matrix_elements) == 1 + # Crossing-symmetry holes (identity fills when use_crossing is off). + replace_dict.update( + self.get_crossing_replace_dict(self.matrix_elements[0])) + # Number of helicity combinations replace_dict['ncomb'] = \ self.matrix_elements[0].get_helicity_combinations() @@ -1569,9 +1800,11 @@ def get_all_sigmaKin_lines(self, color_amplitudes, class_name): ret_lines = [] if self.single_helicities: + cross_cw_sig_extra = \ + self.get_crossing_replace_dict(self.matrix_elements[0])['cross_cw_sig_extra'] ret_lines.append(\ - "void %s::calculate_wavefunctions(const int perm[], const int hel[], const int flavor[]){" % \ - class_name) + "void %s::calculate_wavefunctions(const int perm[], const int hel[], const int flavor[]%s){" % \ + (class_name, cross_cw_sig_extra)) ret_lines.append("// Calculate wavefunctions for all processes") ret_lines.append(self.get_calculate_wavefunctions(\ self.wavefunctions, self.amplitudes)) @@ -2622,6 +2855,10 @@ class ProcessExporterCPP(VirtualExporter): grouped_mode = False exporter = 'cpp' + # Only the plain standalone_cpp exporter emits the crossing machinery; the + # matchbox/pythia8/mg7 subclasses write their own templates and override + # this back to False. + supports_crossing = True default_opt = {'clean': False, 'complex_mass':False, 'export_format':'madevent', 'mp': False, @@ -2737,7 +2974,108 @@ def get_mg5_info_lines(cls): #=============================================================================== # generate_subprocess_directory #=============================================================================== - def write_check_sa_cpp(self, matrix_element, dirpath): + def _get_check_sa_cpp_crossing_example(self, matrix_element, maxflavor, + nexternal, use_crossing): + """C++ block for check_sa.cpp demonstrating the crossed matrix elements. + + Returns '' when crossing is not active for this backend/matrix element, + leaving the driver unchanged. Otherwise it mirrors the Fortran + check_sa.f demonstration: a loop over every way of crossing particle 1 + and particle 2 with a final-state particle (and over each flavor) that, + for each, evaluates the crossed matrix element and prints its signed + PDGs and value. The whole section is gated behind `if(false)` so it is + present only as a ready-to-enable example. + + flavor_id is 0-based in C++: flavor_id = cross*nflav + flav0, with + cross = flip1*(nexternal+1) + flip2 (flip1/flip2 the partners of + particle 1/2), matching sigmaKin's decode. standalone_cpp has no runtime + PDG accessor, so the signed PDG of each flavor_id is precomputed here + into demo_pdg[flavor_id*nexternal + slot] the same way + GET_PDG_FOR_FLAVOR does (conjugating swapped legs, zeros for an + impossible/overlapping crossing). Each evaluation uses a FRESH + CPPProcess so the shared good-helicity cache cannot contaminate it. + """ + if not use_crossing: + return '' + + tables = ProcessExporterFortran.compute_crossing_tables( + self, matrix_element) + spincol = tables['spincol'] + perm = tables['perm'] + ic = tables['ic'] + nx = tables['nexternal'] + ncross = len(spincol) + # The flavor count sigmaKin decodes against (CPPProcess::nflavors); read + # from the same source that fills %(nflav)d so the demo_pdg table indexes + # by flavor_id exactly as the runtime does. + n_flav = len(matrix_element.get_external_flavors_with_iden()) + # Physical signed PDGs (basepid holds internal group codes like 81, not + # the physical PDG the user expects). + _, pdg_flat, antipdg_flat = \ + ProcessExporterFortran._build_flav_pdg_tables(self, matrix_element) + pdg_rows = len(pdg_flat) // nx + + # demo_pdg[flavor_id*nexternal + slot], flavor_id = cross*nflav+flav0. + demo_pdg = [] + for cross in range(ncross): + for flav0 in range(n_flav): + # Guard in the unlikely case the pdg table has fewer rows than + # nflavors: fall back to the first flavor rather than overrun. + row = flav0 if flav0 < pdg_rows else 0 + for k in range(nx): + if spincol[cross] == 0: + demo_pdg.append(0) + continue + src = perm[cross * nx + k] + if ic[cross * nx + k] == 1: + demo_pdg.append(pdg_flat[row * nx + src]) + else: + demo_pdg.append(antipdg_flat[row * nx + src]) + + sep = (' cout << " ---------------------------------------------------' + '--------------------------" << endl;') + lines = [ + ' // Crossing-symmetry examples (crossed processes); see the', + ' // matching block in the Fortran check_sa.f. Gated behind', + ' // if(false): flip it to true to actually print them. Each', + ' // flavor_id is evaluated on a fresh CPPProcess so the shared', + ' // good-helicity cache cannot contaminate the crossed value.', + ' if(false){', + ' const int nflav = process.nflavors;', + ' const int nin = process.ninitial;', + ' const int nx = process.nexternal;', + ' static const int demo_pdg[%d] = {%s};' + % (len(demo_pdg), ', '.join(str(p) for p in demo_pdg)), + ' cout << endl << " Crossing-symmetry examples (crossed ' + 'processes):" << endl << endl;', + ' for(int flip1 = nin+1; flip1 <= nx; flip1++){', + ' for(int flip2 = nin+1; flip2 <= nx; flip2++){', + ' for(int j = 1; j <= nflav; j++){', + ' // cross = (partner of p1)*(nx+1) + (partner of p2)', + ' int cross = flip1*(nx+1) + flip2;', + ' int flavor_id = cross*nflav + (j-1);', + ' CPPProcess xproc("../../Cards/param_card.dat");', + ' xproc.setMomenta(p);', + ' double xme = xproc.sigmaKin(flavor_id);', + ' cout << "PARTICLE #1 crossed with particle # " ' + '<< flip1 << endl;', + ' cout << "PARTICLE #2 crossed with particle # " ' + '<< flip2 << endl;', + ' cout << "PDG";', + ' for(int s = 0; s < nx; s++) cout << " " ' + '<< demo_pdg[flavor_id*nx + s];', + ' cout << " FLAV_IDX " << flavor_id << endl;', + ' cout << "Matrix element = " << xme' + ' << " GeV^" << -(2*xproc.nexternal-8) << endl;', + sep, + ' }', + ' }', + ' }', + ' }', + ] + return '\n'.join(lines) + + def write_check_sa_cpp(self, matrix_element, dirpath, use_crossing=False): """Write a per-process check_sa.cpp with flavor arrays filled in. This mirrors the Fortran ``write_check_sa`` in ``export_v4.py``: @@ -2816,6 +3154,8 @@ def write_check_sa_cpp(self, matrix_element, dirpath): 'nexternal': nexternal, 'flavor_arr': flavor_arr_str, 'pdg_arr': pdg_arr_str, + 'crossing_example': self._get_check_sa_cpp_crossing_example( + matrix_element, maxflavor, nexternal, use_crossing), } with open(pjoin(dirpath, 'check_sa.cpp'), 'w') as fout: fout.write(content) @@ -2828,7 +3168,18 @@ def generate_subprocess_directory(self, matrix_element, cpp_helas_call_writer, #matrix_element = copy.deepcopy(matrix_element) process_exporter_cpp = self.oneprocessclass(matrix_element,cpp_helas_call_writer) - + # Enable the crossing machinery for standalone_cpp when the process was + # generated with --use_crossing (default on) and the process does not + # pin a specific s-channel (which a crossing would not preserve). Only a + # single-ME directory carries the flavor tables the crossing needs. + process_exporter_cpp.use_crossing = bool( + getattr(self, 'supports_crossing', False) + and self.opt.get('use_crossing', False) + and len(process_exporter_cpp.matrix_elements) == 1 + and not ProcessExporterFortran.breaks_crossing_symmetry( + process_exporter_cpp.matrix_elements[0].get('processes')[0])) + + # Create the directory PN_xx_xxxxx in the specified path proc_dir_name = "P%d_%s" % (process_exporter_cpp.process_number, process_exporter_cpp.process_name) @@ -2846,7 +3197,8 @@ def generate_subprocess_directory(self, matrix_element, cpp_helas_call_writer, for file in self.to_link_in_P: ln('../%s' % file) # Write a per-process check_sa.cpp with flavor info filled in - self.write_check_sa_cpp(matrix_element, dirpath) + self.write_check_sa_cpp(matrix_element, dirpath, + use_crossing=process_exporter_cpp.use_crossing) return proc_dir_name @staticmethod @@ -2864,10 +3216,12 @@ def finalize(self, *args, **opts): class ProcessExporterMatchbox(ProcessExporterCPP): oneprocessclass = OneProcessExporterMatchbox + supports_crossing = False class ProcessExporterPythia8(ProcessExporterCPP): oneprocessclass = OneProcessExporterPythia8 grouped_mode = 'madevent' + supports_crossing = False #=============================================================================== # generate_process_files_pythia8 @@ -3175,6 +3529,7 @@ def read_template_file(cls, *args, **opts): class ProcessExporterMG7(ProcessExporterCPP): """ Extends the standalone CPP exporter to add files needed to run madevent7 / madnis """ + supports_crossing = False s= _file_path + 'iolibs/template_files/' dirs_to_create = ['bin', 'src', 'lib', 'Cards', 'SubProcesses'] # mg7_v5 builds api.so in the P* folders (instead of the standalone_cpp @@ -3206,6 +3561,18 @@ def generate_subprocess_directory( """ Override of super().generate_subprocess_directory """ process_exporter_mg7 = self.oneprocessclass(matrix_element,cpp_helas_call_writer) + # Enable the crossing machinery (extended flavor id) when the process was + # generated with --use_crossing (default on) and the process does not pin + # a specific s-channel (which a crossing would not preserve). Only a + # single-ME directory carries the flavor tables the crossing needs. When + # off, use_crossing stays False and the output is byte-identical. + process_exporter_mg7.use_crossing = bool( + getattr(self, 'supports_crossing', False) + and self.opt.get('use_crossing', False) + and len(process_exporter_mg7.matrix_elements) == 1 + and not ProcessExporterFortran.breaks_crossing_symmetry( + process_exporter_mg7.matrix_elements[0].get('processes')[0])) + # Create the directory PN_xx_xxxxx in the specified path proc_dir_name = process_exporter_mg7.name dirpath = pjoin(self.dir_path, 'SubProcesses', proc_dir_name) @@ -3464,6 +3831,9 @@ def ExportCPPFactory(cmd, group_subprocesses=False, cmd_options={}): opt = dict(cmd.options) opt['output_options'] = cmd_options + # --use_crossing of the generate/add process command (default on). Only the + # standalone_cpp exporter honors it; the others ignore this key. + opt['use_crossing'] = getattr(cmd, '_use_crossing', True) cformat = cmd._export_format if cformat == 'pythia8': diff --git a/madgraph/iolibs/export_mg7.py b/madgraph/iolibs/export_mg7.py index 5ca4b9e28..a4289d495 100644 --- a/madgraph/iolibs/export_mg7.py +++ b/madgraph/iolibs/export_mg7.py @@ -162,6 +162,39 @@ def set_channels_colors_map(self): self.active_color_map.append(active_colors) i += 1 + @staticmethod + def get_color_code_tables(color_flow_dicts, legs): + """(codes, slots) -- the canonical colour-flow code of each flow plus the + slot structure needed to decode it, or (None, None) when the flows have + no usable code (a sextet, or an epsilon structure). + + Same encoding as the fortran madevent output (see export_v4: + _color_flow_code / _color_flow_decode): flip the initial-state pair so + every colour index connects to an anticolour index, then digit i is the + anticolour SLOT that colour slot i connects to, and + code = sum_i digit_i * N^i. `slots` is {"color": [...], "acolor": [...]} + with 1-based leg numbers, and is flow independent -- it is fixed by the + colour representations, so one table serves every flow. + + Consumers decode a code back to the per-leg tags rather than looking the + flow up in the ICOLUP-style "color_flows" table.""" + from madgraph.iolibs.export_v4 import ProcessExporterFortranME as _E + states = [l.get("state") for l in legs] + flows = [[tuple(cf[l.get("number")]) for l in legs] + for cf in color_flow_dicts] + if any(c < 0 or a < 0 for fl in flows for c, a in fl): + return None, None # sextet: negative tag, not representable + conns = [_E._color_flow_canon(fl, states) for fl in flows] + codes = [_E._color_flow_code(c) for c in conns] + if any(c is None for c in codes) or len(set(codes)) != len(codes): + return None, None + colslots, acolslots = _E._color_flow_slots(conns[0]) + if not acolslots or any(_E._color_flow_slots(c) != (colslots, acolslots) + for c in conns[1:]): + return None, None + return codes, {"color": [l + 1 for l in colslots], + "acolor": [l + 1 for l in acolslots]} + def get_subprocess_info(self, proc_dir, lib_me_path): n_external, n_initial = self.matrix_element.get_nexternal_ninitial() if self.color_basis: @@ -179,8 +212,11 @@ def get_subprocess_info(self, proc_dir, lib_me_path): [[color_flow_dict[leg.get("number")][i] for i in [0, 1]] for leg in legs] for color_flow_dict in color_flow_dicts ] + color_codes, color_slots = self.get_color_code_tables( + color_flow_dicts, legs) else: color_flows = [[[0, 0]] * n_external] + color_codes, color_slots = None, None # We need the both particle and antiparticle wf_ids, since the identity # depends on the direction of the wf. @@ -231,7 +267,15 @@ def get_subprocess_info(self, proc_dir, lib_me_path): "me_path": lib_me_path, "path": proc_dir, "flavors": flavors, + # ICOLUP-style per-flow tags. Still needed by the LHE writer to + # reconstruct the colour of INTERNAL (propagator/decay) lines, so it + # cannot be dropped just because the code gives the external legs. "color_flows": color_flows, + # canonical colour-flow code of each flow + the (flow independent) + # slot structure to decode it; null when the flows have no usable + # code, in which case a consumer falls back to "color_flows". + "color_codes": color_codes, + "color_slots": color_slots, "pdg_color_types": pdg_color_types, "diagram_count": len(self.diagrams), "helicities": list(self.matrix_element.get_helicity_matrix()), diff --git a/madgraph/iolibs/export_v4.py b/madgraph/iolibs/export_v4.py index 525478ac5..2e8b0ebfa 100755 --- a/madgraph/iolibs/export_v4.py +++ b/madgraph/iolibs/export_v4.py @@ -227,6 +227,12 @@ class ProcessExporterFortran(VirtualExporter): jamp_optim = False run_card_class = None use_flavor_mask = True + # Whether this exporter can honor the --use_crossing of the generate/add + # command, i.e. emit a matrix element whose FLAV_IDX carries a crossing. + # Only the fortran standalone implements the machinery, so every other + # exporter must refuse the request rather than silently write code that + # cannot answer a crossed FLAV_IDX (see _check_crossing_support). + supports_crossing = False def __init__(self, dir_path = "", opt=None): """Initiate the ProcessExporterFortran with directory information""" @@ -234,12 +240,13 @@ def __init__(self, dir_path = "", opt=None): self.dir_path = dir_path self.model = None self.beam_polarization = [True,True] - + self.opt = dict(self.default_opt) if opt: self.opt.update(opt) self.cmd_options = self.opt['output_options'] self._configure_flavor_mask_from_cmd_options() + self._check_crossing_support() #place holder to pass information to the run_interface self.proc_characteristic = banner_mod.ProcCharacteristic() @@ -438,6 +445,83 @@ def _build_flav_table_flat(self, matrix_element): p, pdg_to_group_pos, max_group_size)) return (n_flavors, flav_table_flat) + def _build_flav_pdg_tables(self, matrix_element): + """Return (n_flavors, pdg_flat, antipdg_flat) for this matrix element. + + The FLAVOR array threaded through matrix.f holds unsigned group + *positions* (see _build_flav_table_flat), which is all the matrix + element needs: every member of a flavor group shares the couplings, so + the position alone selects the mask. A caller working in PDG codes -- + the f2py layer -- cannot use that: a position means nothing without + knowing which group and which leg it belongs to, and nothing in the + generated code maps one back to a PDG. These tables are that missing + map, and they are the only thing standing between an f2py caller and + being able to ask for a process by its PDG codes. + + Two tables are emitted rather than one, both column-major + (leg-fastest, matching FLAV_TABLE): + + - pdg_flat: the signed PDG of each leg for each flavor. + - antipdg_flat: the PDG of the *antiparticle* of that same leg. + + The antiparticle table exists because a crossing conjugates every leg + that swaps between the initial and the final state, and conjugation is + NOT "negate the PDG": a self-conjugate particle (the gluon, 21) must + stay itself. Tabulating both here lets the generated fortran pick one + or the other by the sign of SGN(k) -- which GET_CROSS_PERM already + computes -- instead of trying to re-derive the model's conjugation rule + at runtime. It is the same get_anti_pdg_code() that + get_iden_cross_lines uses to build BASEPID_CROSS_TABLE, so the two stay + consistent by construction. + + The per-leg sign comes from the process's own leg id (e.g. -81 for an + incoming anti-quark), while the magnitude comes from the group member + sitting at that position; a leg that is not part of a merged group + (a gluon) keeps its own PDG whatever the flavor. + """ + + allowed_flavors = matrix_element.compute_flavor_masks() + process = matrix_element.get('processes')[0] + model = process.get('model') + leg_ids = [leg.get('id') for leg in process.get('legs')] + nexternal = len(leg_ids) + + if not allowed_flavors: + allowed_flavors = [tuple([1] * nexternal)] + + merged_particles = (model.get('merged_particles') or {}) if model else {} + + def leg_pdg(leg_id, pos): + """The signed PDG of a leg whose flavor sits at group position pos.""" + members = merged_particles.get(abs(leg_id)) + if not members: + # Not a merged leg: its PDG does not depend on the flavor. + return int(leg_id) + try: + magnitude = int(members[int(pos) - 1]) + except (IndexError, ValueError, TypeError): + return int(leg_id) + # The group id carries the particle/antiparticle sign of the leg. + return magnitude if leg_id > 0 else -magnitude + + pdg_flat = [] + antipdg_flat = [] + for flavor in allowed_flavors: + for leg, pos in enumerate(flavor): + pdg = leg_pdg(leg_ids[leg], pos) + pdg_flat.append(pdg) + try: + antipdg_flat.append( + model.get('particle_dict')[pdg].get_anti_pdg_code()) + except KeyError: + # No such particle in the model (should not happen): fall + # back to the naive conjugation rather than crash the + # export. A wrong entry here can only mis-*match* a PDG + # request, never corrupt a matrix element. + antipdg_flat.append(-pdg) + + return (len(allowed_flavors), pdg_flat, antipdg_flat) + def _build_flav_index_lookup(self, matrix_element, n_flavors, flav_table_flat): """Build the expanded GET_FLAVOR_INDEX lookup for decay-chain MEs. @@ -580,13 +664,56 @@ def _make_flavor_array_fortran_function(self, func_name, n_flavors, 'flav_table_data': ', '.join(str(v) for v in flav_table_flat), } + def _make_flavor_pdg_fortran_function(self, func_name, n_flavors, pdg_flat, + antipdg_flat, cross_snippets, + nexternal_decl='include'): + """Return the complete Fortran GET_PDG_FOR_FLAVOR routine as a string. + + Emitted via the %(flavor_pdg_function)s placeholder. It is the inverse + of the GET_FLAVOR/GET_FLAVOR_INDEX pair in the PDG vocabulary: those two + only ever speak group positions, so without this an f2py caller has no + way to learn which physical process a FLAV_IDX denotes -- let alone + which one a *crossed* FLAV_IDX denotes. + + *cross_snippets* is the (decl, decode, apply) triple filled by + fill_crossing_replace_dict: with crossing on it defers to + GET_CROSS_PERM so the permutation/conjugation follows exactly the same + code path the matrix element itself uses; with crossing off there is no + crossing to decode and the plain table lookup is emitted. + Same args/convention as _make_flavor_index_fortran_function. + """ + template_path = pjoin(_file_path, 'iolibs', 'template_files', + 'fortran_matrix_flavor_pdg_fct.inc') + template = open(template_path).read() + + if nexternal_decl == 'include': + nexternal_lines = " include 'nexternal.inc'" + else: + nexternal_lines = (' INTEGER NEXTERNAL\n' + ' PARAMETER (NEXTERNAL=%d)' % int(nexternal_decl)) + + decl, decode, apply_block = cross_snippets + return template % { + 'func_name': func_name, + 'nexternal_decl': nexternal_lines, + 'nflav': n_flavors, + 'pdg_table_data': ', '.join(str(v) for v in pdg_flat), + 'antipdg_table_data': ', '.join(str(v) for v in antipdg_flat), + 'pdg_cross_decl': decl, + 'pdg_cross_decode': decode, + 'pdg_cross_apply': apply_block, + } + #=========================================================================== # process exporter fortran switch between group and not grouped #=========================================================================== def export_processes(self, matrix_elements, fortran_model, second_exporter=None, second_helas=None): """Make the switch between grouped and not grouped output""" - + calls = 0 + self._crossgroup = {} # (group_idx, me_idx) -> base info; Track B below + self._crossgroup_dirs = [] # (dependent_dir, base_dir) for the parallel makefile + self._crossgroup_helperms = {} # base_dir -> {base_proc_id -> [hel perms]} if isinstance(matrix_elements, group_subprocs.SubProcessGroupList): # check handling for the polarization for m in matrix_elements: @@ -599,11 +726,39 @@ def export_processes(self, matrix_elements, fortran_model, second_exporter=None, self.beam_polarization[beamid-1] = False break + # Cross-group crossing (Track B): a group whose matrix element is a + # crossing of another group's reuses (symlinks) that base group's + # compiled matrix element. Detect it here, where every group is + # visible, and hand the per-group routing to generate_subprocess_ + # directory (keyed by the same enumerate index it receives). + self._crossgroup = self.compute_crossgroup_routing(matrix_elements) + # The MEs that serve as a cross-group base must publish their per-flow + # JAMP2 (so dependents can reselect colour natively); gate that emission + # to these MEs only, keeping every other madevent ME byte-identical. + self._crossgroup_base_mes = set( + id(cg['base_me']) for cg in self._crossgroup.values()) + if self._crossgroup: + logger.info('Cross-group crossing: %d subprocess(es) will reuse ' + 'a base group\'s matrix element via crossing.' + % len({k[0] for k in self._crossgroup})) + # A shared matrix element now spans physically distinct (crossed) + # initial states, so a per-beam property is ill-defined. Tag it so + # check_card_consistency blocks beam polarisation / EVA (same guard + # as the within-group case; see fill of 'limitations' there). + if 'crossing' not in self.proc_characteristic['limitations']: + self.proc_characteristic['limitations'].append('crossing') + for (group_number, me_group) in enumerate(matrix_elements): calls = calls + self.generate_subprocess_directory(\ me_group, fortran_model, group_number, second_exporter=second_exporter, second_helas=second_helas ) + if self._crossgroup_dirs: + self.write_crossgroup_parallel_makefile( + pjoin(self.dir_path, 'SubProcesses')) + if self._crossgroup_helperms: + self.write_crossgroup_helunion( + pjoin(self.dir_path, 'SubProcesses')) else: # check handling for the polarization self.beam_polarization = [True,True] @@ -974,6 +1129,31 @@ def write_matrix_element_v4(self): """ pass + def _check_crossing_support(self): + """Refuse to export when a crossing was asked for and cannot be given. + + `--use_crossing` (on by default) tells the generation not to write out + the crossed subprocesses separately, because the matrix element is + expected to reach them through an extended FLAV_IDX instead. Only the + fortran standalone implements that decoding: any other exporter would + write a matrix element that silently misses those subprocesses, so it + has to error out and name the way to get a valid output back. + """ + + if self.supports_crossing: + return + if not self.opt.get('use_crossing', False): + return + + raise InvalidCmd( + "The '%s' output does not support crossing symmetry, which the " + "process was generated with. Crossing symmetry is only implemented " + "for the fortran standalone output; every other output needs the " + "crossed subprocesses to be generated explicitly.\n" + "Regenerate the process with --use_crossing=False (e.g. " + "'generate --use_crossing=False') and run the output " + "again." % self.opt.get('export_format', 'unknown')) + def _configure_flavor_mask_from_cmd_options(self): """Honor `--mask=True|False` from the output command line.""" @@ -1676,11 +1856,22 @@ def write_leshouche_file(self, writer, matrix_element): #=========================================================================== # get_leshouche_lines #=========================================================================== - def get_leshouche_lines(self, matrix_element, numproc): - """Write the leshouche.inc file for MG4""" + def get_leshouche_lines(self, matrix_element, numproc, drop_icolup=False): + """Write the leshouche.inc file for MG4 + + With *drop_icolup* the ICOLUP table is omitted for any ME whose colour + flows have a canonical code: the consumer (addmothers) rebuilds the tags + from colorflow.inc instead, so the table would be dead weight. It is + still written for an ME without a usable code, which is what addmothers + falls back to. Only the madevent exporters set this -- MadWeight ships + no addmothers.f and keeps reading ICOLUP.""" # Extract number of external particles (nexternal, ninitial) = matrix_element.get_nexternal_ninitial() + if drop_icolup and self._color_code_tables(matrix_element): + drop_icolup = True + else: + drop_icolup = False lines = [] real_iproc = -1 @@ -1718,7 +1909,7 @@ def get_leshouche_lines(self, matrix_element, numproc): # Here goes the color connections corresponding to the JAMPs # Only one output, for the first subproc! - if iproc == 0: + if iproc == 0 and not drop_icolup: # If no color basis, just output trivial color flow if not matrix_element.get('color_basis'): for i in [1, 2]: @@ -1971,6 +2162,73 @@ def get_helicity_lines(self, matrix_element,array_name='NHEL', add_nb_comb=False return "\n".join(helicity_line_list) + @staticmethod + def _fortran_data_stmt(name, values, per_line=10): + """Emit a fixed-form 'DATA name /v1,v2,.../' statement with + continuation lines (column-6 '&') so long value lists stay within the + Fortran line-length limit. name may contain an implied-DO, e.g. + '(STATES(I,1),I=1,3)'.""" + strs = ["%d" % v for v in values] + if len(strs) <= per_line: + return " DATA %s /%s/" % (name, ",".join(strs)) + lines = [" DATA %s /" % name] + for i in range(0, len(strs), per_line): + seg = strs[i:i + per_line] + tail = "," if i + per_line < len(strs) else "/" + lines.append(" & %s%s" % (",".join(seg), tail)) + return "\n".join(lines) + + def _helstate_data(self, matrix_element): + """Return the Fortran DATA blocks for the canonical helicity + encoder/decoder that replaces the explicit NHEL config table. + + A helicity configuration is encoded as a single mixed-radix integer + (the 'canonical code') over the per-leg helicity states, with the last + external leg as the least-significant digit -- matching the + itertools.product ordering used by get_helicity_matrix(). For a + non-polarized process this makes the code of the i-th row exactly i, so + HELALLOW is simply [1..NCOMB] and nothing is relabelled; a polarization + restriction ({0}/{L}/...) keeps the *full* per-leg multiplicity as the + radix (so helicity 0 / longitudinal stays a first-class state) and + leaves HELALLOW as the selected, non-contiguous subset of codes. + + Returns a dict with keys: + maxhel - max per-leg helicity multiplicity (STATES 1st dim) + nhstate_data - DATA for NHSTATE(NEXTERNAL) (states per leg) + states_data - DATA for STATES(MAXHEL,NEXTERNAL) (helicity values) + hel_allow_data - DATA for HELALLOW(NCOMB) (allowed codes) + """ + model = matrix_element.get('processes')[0].get('model') + pdict = model.get('particle_dict') + ext = matrix_element.get_external_wavefunctions() + # Full per-leg helicity states, allow_reverse=True so the value order + # matches get_helicity_matrix() for non-polarized legs (code==row). + states = [pdict[wf.get('pdg_code')].get_helicity_states(True) + for wf in ext] + nstate = [len(s) for s in states] + nexternal = len(ext) + maxhel = max(nstate) if nstate else 1 + + # Allowed canonical codes: encode each enumerated helicity row. + allowed = [] + for row in matrix_element.get_helicity_matrix(): + code = 0 + for k, val in enumerate(row): + code = code * nstate[k] + states[k].index(val) + allowed.append(code + 1) + + states_lines = [] + for k in range(nexternal): + vals = [states[k][i] if i < nstate[k] else 0 + for i in range(maxhel)] + states_lines.append(self._fortran_data_stmt( + '(STATES(I,%d),I=1,%d)' % (k + 1, maxhel), vals)) + + return {'maxhel': maxhel, + 'nhstate_data': self._fortran_data_stmt('NHSTATE', nstate), + 'states_data': "\n".join(states_lines), + 'hel_allow_data': self._fortran_data_stmt('HELALLOW', allowed)} + def get_ic_line(self, matrix_element): """Return the IC definition line coming after helicities, required by switchmom in madevent""" @@ -2105,6 +2363,982 @@ def get_den_factor_line(self, matrix_element): return "DATA IDEN/%2r/" % \ matrix_element.get_denominator_factor() + @staticmethod + def get_crossing_permutation(cross, nexternal): + """Return (perm, ic, valid) for the crossing code CROSS. + + CROSS decomposes as I*(NEXTERNAL+1)+J, with I and J the crossing + partners of particle 1 and particle 2 (0 meaning "leave that particle + alone"). The base is NEXTERNAL+1, not NEXTERNAL, so that I and J range + over 0..NEXTERNAL and can therefore designate the last particle too. + perm[slot] is the 0-based index of the original leg sitting in that + slot, and ic[slot] is -1 for a leg that changed between the initial and + the final state. This mirrors exactly what APPLY_CROSSING does in the + generated fortran, so both stay in sync. + + *valid* is False for the overlapping-swap codes, which must not be used. + CROSS asks for two independent transpositions, (particle1, I) and + (particle2, J). When BOTH are active and they share a slot they no + longer compose into an involution but into a 3-cycle, and the two code + paths that consume this permutation (GET_PDG_FOR_FLAVOR building the + signature, and APPLY_CROSSING_TABLE evaluating the matrix element) then + disagree, one applying the permutation and the other its inverse -- + invisible for disjoint swaps (all involutions) but wrong for a cycle. + Such a code is pure redundancy: every physical process it could reach is + also reached by a DISJOINT swap, so it is marked invalid and its callers + refuse it (SPINCOL_CROSS_TABLE gets 0, which SMATRIX and + GET_PDG_FOR_FLAVOR both map to a null result). The two transpositions + {1,I} and {2,J} are both active iff I not in {0,1} and J not in {0,2} + (I==1 / J==2 swap a particle with itself, a no-op like 0), and they + overlap iff I==2 or J==1 or I==J. + """ + base = nexternal + 1 + i_part = cross // base + j_part = cross % base + perm = list(range(nexternal)) + ic = [1] * nexternal + + valid = not (i_part not in (0, 1) and j_part not in (0, 2) + and (i_part == 2 or j_part == 1 or i_part == j_part)) + + def swap(slot_a, slot_b): + perm[slot_a], perm[slot_b] = perm[slot_b], perm[slot_a] + ic[slot_a] = -ic[slot_a] + ic[slot_b] = -ic[slot_b] + + # I==1 (resp. J==2) would swap a particle with itself: degenerate, so + # treated as "no crossing" just like 0. + if i_part not in (0, 1): + swap(0, i_part - 1) + if j_part not in (0, 2): + swap(1, j_part - 1) + return perm, ic, valid + + @staticmethod + def breaks_crossing_symmetry(process): + """True if `process` constrains a specific s-channel propagator. + + Crossing permutes legs between the initial and the final state, so a + channel that is s-channel in the generated process is not s-channel in + its crossings. A constraint naming a specific s-channel therefore does + not survive the crossing and the crossing machinery must not be emitted: + - required_s_channels (the `> A >` syntax) + - forbidden_s_channels (the `$$` syntax, diagram removed) + `forbidden_onsh_s_channels` (a single `$`) only forbids the on-shell + *region* of a kept diagram, so it does not break crossing symmetry and + is deliberately not listed here. + + Works for both Process and ProcessDefinition (same attributes), and + recurses into decay chains, whose constraints bind just as much. + """ + if process.get('required_s_channels') or \ + process.get('forbidden_s_channels'): + return True + return any(ProcessExporterFortran.breaks_crossing_symmetry(decay) + for decay in process.get('decay_chains')) + + def fill_crossing_replace_dict(self, matrix_element, replace_dict, + use_crossing): + """Fill the crossing-machinery holes of matrix_standalone_v4.inc. + + The extended FLAV_IDX (a flavor *and* a crossing) and everything + decoding it are only written out when the process was generated with + --use_crossing=True (the default) *and* the process definition pins no + specific s-channel (see breaks_crossing_symmetry). Otherwise the + crossed subprocesses are generated as separate matrix elements instead, + so the crossing machinery would be dead code: the tables, the + APPLY_CROSSING/GET_CROSS_PERM/GET_SPINCOL_CROSS/GET_IDENT_CROSS routines + are not emitted at all and each hole below gets the plain code path + (FLAV_IDX is then a bare flavor index in [1,NFLAV]). + + Requires proc_prefix, nflav and den_factor_line to be set already. + """ + prefix = replace_dict['proc_prefix'] + + if not use_crossing: + replace_dict.update({ + 'crossing_routines': '', + 'iden_cross_lines': '', + 'smatrix_cross_decl': + 'C Generated without crossing symmetry: FLAV_IDX is a plain' + '\nC flavor index, there is no crossing to decode.', + 'smatrix_cross_decode': '', + 'smatrix_cross_apply': '', + 'smatrix_goodhel_gate': + ' IF (GOODHEL(IHEL,FLAV_USE) .OR. NTRY(FLAV_USE)' + ' .LT. 20.OR.USERHEL.NE.-1) THEN', + 'smatrix_goodhel_train': + ' IF (T .NE. 0D0 .AND. .NOT. ' + 'GOODHEL(IHEL,FLAV_USE)) THEN\n' + ' GOODHEL(IHEL,FLAV_USE)=.TRUE.\n' + ' ENDIF', + 'smatrix_matrix_call': + ' T=%sMATRIX(P ,NHEL(1,IHEL),JC(1),FLAV_USE)' + % prefix, + 'smatrix_iden_line': + 'C IDEN carries the identical-particle factor of the' + ' representative\nC flavor; BROKEN_SYM corrects it for' + ' the actual one.' + '\n ANS=ANS/DBLE(IDEN)*%sBROKEN_SYM(FLAVOR)' % prefix, + 'inter_rescale_decl': '', + 'inter_rescale_body': + 'C The static IDEN GET_INTER divides by carries the' + ' identical-particle\nC factor of the representative' + ' flavor, so BROKEN_SYM must correct it for\nC the actual' + ' one, exactly as SMATRIX does with ANS/IDEN*BROKEN_SYM.' + '\n RESCALE = DBLE(%sBROKEN_SYM(FLAVOR))' % prefix, + 'density_cross_apply': self.CROSS_PASSTHROUGH % { + 'nhel_copy': 'NHELUSE(:,:) = NHEL(:,:)'}, + 'allinter_cross_apply': ' IC(:)=1\n' + self.CROSS_PASSTHROUGH % { + 'nhel_copy': 'NHELUSE(:) = NHEL(:)'}, + 'pdg_cross_snippets': self.PDG_CROSS_SNIPPETS_OFF, + 'nhel_idx_decl': + 'C Generated without crossing symmetry: FLAV_IDX_IN is a' + ' plain\nC flavor index, so only BROKEN_SYM can move the' + ' denominator.', + 'nhel_idx_body': + 'C Mirrors SMATRIX exactly: ANS=ANS/IDEN*BROKEN_SYM means' + ' the effective\nC denominator is IDEN/BROKEN_SYM. The' + ' division is exact -- BROKEN_SYM is\nC the ratio of the' + ' representative to the actual identical-particle\nC count,' + ' and IDEN carries the representative one as a factor.' + '\n IDEN_STAR = IDEN_STAR / %sBROKEN_SYM(FLAVOR)' % prefix, + }) + return + + replace_dict['iden_cross_lines'] = \ + self.get_iden_cross_lines(matrix_element) + replace_dict.update(dict( + (key, value % {'proc_prefix': prefix, + 'den_factor_line': replace_dict['den_factor_line']}) + for key, value in self.CROSSING_SNIPPETS.items())) + # CROSS_GHIDX (in the crossing routines below) recomputes the crossed + # -> identity helicity row map at runtime; it needs only the small + # per-crossing GHFILT flag plus the STATES/NHSTATE the encoder uses (in + # get_helicity_matrix()'s default allow_reverse=True order, so the map is + # built in the same order the NHEL table is emitted). + hel_data = self._helstate_data(matrix_element) + replace_dict['maxhel'] = hel_data['maxhel'] + replace_dict['nhstate_data'] = hel_data['nhstate_data'] + replace_dict['states_data'] = hel_data['states_data'] + replace_dict['ghfilt_data'] = self.format_integer_data_lines( + 'GHFILT', self.compute_ghfilt(matrix_element, allow_reverse=True)) + replace_dict['pdg_cross_snippets'] = tuple( + snippet % {'proc_prefix': prefix} + for snippet in self.PDG_CROSS_SNIPPETS_ON) + replace_dict['nhel_idx_decl'] = ( + ' INTEGER %(prefix)sGET_SPINCOL_CROSS\n' + ' INTEGER %(prefix)sGET_IDENT_CROSS' % {'prefix': prefix}) + replace_dict['nhel_idx_body'] = ( + 'C Mirrors SMATRIX branch for branch: IDEN/BROKEN_SYM uncrossed,\n' + 'C GET_SPINCOL_CROSS*GET_IDENT_CROSS crossed. Keeping the CROSS=0\n' + 'C branch on the old path (rather than letting the crossed formula\n' + 'C cover it) is what guarantees no change for existing callers.\n' + ' IF (NHI_CROSS .EQ. 0) THEN\n' + ' IDEN_STAR = IDEN_STAR / %(prefix)sBROKEN_SYM(FLAVOR)\n' + ' ELSE\n' + ' IDEN_STAR = %(prefix)sGET_SPINCOL_CROSS(NHI_CROSS)\n' + ' & * %(prefix)sGET_IDENT_CROSS(NHI_CROSS, FLAVOR)\n' + ' ENDIF' % {'prefix': prefix}) + crossing_template = pjoin(_file_path, 'iolibs', 'template_files', + 'matrix_standalone_crossing_v4.inc') + replace_dict['crossing_routines'] = \ + open(crossing_template).read() % replace_dict + + def fill_crossing_replace_dict_me(self, matrix_element, replace_dict, + use_crossing, proc_id): + """Fill the crossing holes of matrix_madevent_group_v4.inc. + + The madevent group SMATRIX differs structurally from the standalone one + (runtime IFLAV, GOODHEL/NTRY carry a flavor dimension, IVEC, and the NSF + flags are baked into the helas calls rather than read from an IC array), + so it gets its own holes and OFF fills. With crossing off every hole + reproduces the historical madevent code, so a non-crossing output is + unchanged; the extended-FLAV_IDX decode / APPLY_CROSSING path is only + written out when use_crossing is True (added in the ON slice). + """ + pid = str(proc_id) + if not use_crossing: + replace_dict.update({ + 'smatrix_me_cross_decl': + 'C Generated without crossing symmetry: IFLAV is a plain' + '\nC flavor index, there is no crossing to decode.', + 'smatrix_me_cross_decode': '', + 'me_flav_key': 'IFLAV', + 'me_goodhel_idx': 'I', + 'me_goodhel_train_guard': '', + 'smatrix_me_goodhel_or': '', + 'me_matrix_args': 'P ,NHEL(1,I),IFLAV,I,AMP2, JAMP2, IVEC', + 'smatrix_me_iden_line': + ' ANS=ANS/DBLE(IDEN)*BROKEN_SYM%s(FLAVOR_FOR_SYM)' % pid, + 'crossing_routines_me': '', + 'me_matrix_ic_param': '', + 'me_matrix_ic_decl': '', + # helicity-recycling template variant (matrix_hel): + 'smatrix_hel_cross_decl': + 'C Generated without crossing symmetry: IFLAV is a plain' + '\nC flavor index, there is no crossing to decode.', + 'smatrix_hel_cross_decode': '', + 'hel_matrix_call_args': 'P ,IFLAV, TS, AMP2, JAMP2, IVEC', + 'hel_matrix_ic_param': '', + }) + return + + # ON path. The crossing routines must not collide across the matrix.f + # files linked into one group executable, so they are named with a + # per-proc_id qualifier (GET_CROSS_PERM stays prefix-less in standalone). + # + # NFLAV must be the count that the madevent GET_FLAVOR table is sized by, + # i.e. get_external_flavors_with_iden() (== replace_dict 'max_flavor', + # what MAXFLAVPERPROC/FLAVOR(NEXTERNAL,max_flavor) use), NOT the standalone + # _build_flav_table_flat() (compute_flavor_masks): for a merged group ME + # the two differ (e.g. Q Q~ > g g: iden 1 vs masks 4), and the extended + # FLAV_IDX decode CROSS=(IFLAV-1)/NFLAV, FLAV=mod(IFLAV-1,NFLAV)+1 must + # land FLAV in [1, max_flavor]. This also matches compute_crossing_pdg_ + # entries (used by partition_crossing_classes), so the routed FLAV_IDX + # decodes the same way here. + nflav = len(matrix_element.get_external_flavors_with_iden()) + cp = 'CR%s_' % pid + crossing_template = pjoin(_file_path, 'iolibs', 'template_files', + 'matrix_standalone_crossing_v4.inc') + hel_data = self._helstate_data(matrix_element) + crossing_routines = open(crossing_template).read() % { + 'proc_prefix': cp, + 'nflav': nflav, + 'iden_cross_lines': self.get_iden_cross_lines(matrix_element), + 'maxhel': hel_data['maxhel'], + 'nhstate_data': hel_data['nhstate_data'], + 'states_data': hel_data['states_data'], + 'ghfilt_data': self.format_integer_data_lines( + 'GHFILT', self.compute_ghfilt(matrix_element, + allow_reverse=True))} + replace_dict.update({ + 'smatrix_me_cross_decl': ( + ' INTEGER NFLAV\n' + ' PARAMETER (NFLAV=%(nflav)d)\n' + ' INTEGER FLAV_USE, CROSSUSE, IDENUSE, XKCR\n' + ' INTEGER IC(NEXTERNAL), IC0(NEXTERNAL)\n' + ' REAL*8 PUSE(0:3,NEXTERNAL)\n' + ' INTEGER NHELUSE(NEXTERNAL,NCOMB)\n' + ' INTEGER %(cp)sGET_SPINCOL_CROSS\n' + ' INTEGER %(cp)sGET_IDENT_CROSS\n' + # runtime good-helicity remap: GHIDXA(I) is the identity row that + # gates crossed row I (0 = not filterable), precomputed once per + # SMATRIX call from the crossing permutation XGPERM/XGSGN. + ' INTEGER GHIDXA(NCOMB), XGPERM(NEXTERNAL)\n' + ' INTEGER XGSGN(NEXTERNAL), XGDUM, XGH' + ) % {'nflav': nflav, 'cp': cp}, + # Decode the crossing and build the crossed P/NHEL/IC once, before the + # helicity loop. An unusable crossing (spin*color = 0) has a zero ME. + 'smatrix_me_cross_decode': ( + ' CROSSUSE = (IFLAV-1) / NFLAV\n' + ' IDENUSE = %(cp)sGET_SPINCOL_CROSS(CROSSUSE)\n' + ' IF (IDENUSE.EQ.0) THEN\n' + ' ANS = 0D0\n' + ' IHEL = 1\n' + ' ICOL = 1\n' + ' RETURN\n' + ' ENDIF\n' + ' DO XKCR=1,NEXTERNAL\n' + ' IC0(XKCR) = 1\n' + ' ENDDO\n' + ' CALL %(cp)sAPPLY_CROSSING_TABLE(IFLAV, NCOMB, P, NHEL,\n' + ' & IC0, PUSE, NHELUSE, IC, FLAV_USE)\n' + # Precompute the crossed->identity helicity-row map once (the + # crossing permutation does not depend on the row), so the shared + # GOODHEL filter (keyed by the reduced FLAV_USE) can gate crossed + # rows through it just like the standalone. CROSS=0 gives + # GHIDXA(I)=I, i.e. the historical unfiltered-flavor behaviour. + ' CALL %(cp)sGET_CROSS_PERM(IFLAV, XGPERM, XGSGN, XGDUM)\n' + ' DO XGH=1,NCOMB\n' + ' CALL %(cp)sCROSS_GHIDX(CROSSUSE, XGPERM, XGSGN,\n' + ' & NHEL(1,XGH), GHIDXA(XGH))\n' + ' ENDDO' + ) % {'cp': cp}, + 'me_flav_key': 'FLAV_USE', + # The shared GOODHEL filter (keyed by the reduced flavor) is gated + # and trained through the runtime remap GHIDXA: crossed row I is good + # iff identity row GHIDXA(I) is. GHIDXA(I)=0 (non-filterable crossing) + # forces the row to be computed (.OR. GHIDXA(I).EQ.0) and never + # trained (GHIDXA(I).NE.0 guard). The index is clamped with MAX(...,1) + # because the gate reads GOODHEL before the .EQ.0 guard and fortran + # does not short-circuit .OR.; the clamped value is only ever read + # when GHIDXA(I).EQ.0 already forces the branch true, so it is inert. + 'me_goodhel_idx': 'MAX(GHIDXA(I),1)', + 'me_goodhel_train_guard': 'GHIDXA(I).NE.0 .AND. ', + 'smatrix_me_goodhel_or': ' .OR. GHIDXA(I).EQ.0', + 'me_matrix_args': + 'PUSE ,NHELUSE(1,I),IC,FLAV_USE,I,AMP2, JAMP2, IVEC', + # Uncrossed keeps IDEN/BROKEN_SYM; crossed rebuilds the denominator + # as initial spin*color (per crossing) times the identical-final + # factor of the actual flavors (per flavor). + 'smatrix_me_iden_line': ( + ' IF (CROSSUSE.EQ.0) THEN\n' + ' ANS=ANS/DBLE(IDEN)*BROKEN_SYM%(pid)s(FLAVOR_FOR_SYM)\n' + ' ELSE\n' + ' ANS=ANS/DBLE(IDENUSE*%(cp)sGET_IDENT_CROSS(CROSSUSE,\n' + ' & FLAVOR_FOR_SYM))\n' + ' ENDIF' + ) % {'pid': pid, 'cp': cp}, + 'crossing_routines_me': crossing_routines, + 'me_matrix_ic_param': 'IC,', + 'me_matrix_ic_decl': ' INTEGER IC(NEXTERNAL)', + # Helicity-recycling variant (matrix_hel -> matrix_optim). The + # recycled MATRIX bakes the base good-helicity set; feeding it the + # crossed momenta PUSE and IC evaluates that set at the crossed + # kinematics, which by H=sigma(K) is exactly the crossed ME -- no + # NHEL table (nor a helicity remap) is needed here. + 'smatrix_hel_cross_decl': ( + ' INTEGER NFLAV\n' + ' PARAMETER (NFLAV=%(nflav)d)\n' + ' INTEGER FLAV_USE, CROSSUSE, IDENUSE, XKCR\n' + ' INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL), IC(NEXTERNAL)\n' + ' REAL*8 PUSE(0:3,NEXTERNAL)\n' + ' INTEGER %(cp)sGET_SPINCOL_CROSS\n' + ' INTEGER %(cp)sGET_IDENT_CROSS' + ) % {'nflav': nflav, 'cp': cp}, + 'smatrix_hel_cross_decode': ( + ' CROSSUSE = (IFLAV-1) / NFLAV\n' + ' IDENUSE = %(cp)sGET_SPINCOL_CROSS(CROSSUSE)\n' + ' IF (IDENUSE.EQ.0) THEN\n' + ' ANS = 0D0\n' + ' IHEL = 1\n' + ' ICOL = 1\n' + ' RETURN\n' + ' ENDIF\n' + ' CALL %(cp)sGET_CROSS_PERM(IFLAV, PERM, SGN, FLAV_USE)\n' + ' DO XKCR=1,NEXTERNAL\n' + ' PUSE(0,XKCR) = P(0,PERM(XKCR))\n' + ' PUSE(1,XKCR) = P(1,PERM(XKCR))\n' + ' PUSE(2,XKCR) = P(2,PERM(XKCR))\n' + ' PUSE(3,XKCR) = P(3,PERM(XKCR))\n' + ' IC(XKCR) = SGN(XKCR)\n' + ' ENDDO' + ) % {'cp': cp}, + 'hel_matrix_call_args': 'PUSE ,IC, FLAV_USE, TS, AMP2, JAMP2, IVEC', + 'hel_matrix_ic_param': 'IC,', + }) + + # (decl, decode, apply) for GET_PDG_FOR_FLAVOR without crossing: FLAV_IDX_IN + # is a bare flavor index, so there is nothing to permute or conjugate. + PDG_CROSS_SNIPPETS_OFF = ( + 'C Generated without crossing symmetry: FLAV_IDX_IN is a plain\n' + 'C flavor index, so the PDGs are read straight off the table.', + ' FP_FLAV = FLAV_IDX_IN', + """ DO FP_I = 1, NEXTERNAL + PDGS(FP_I) = FP_PDG_TABLE(FP_I, FP_FLAV) + ENDDO""") + + # The same three holes with crossing on. GET_CROSS_PERM is reused rather + # than re-deriving I/J here, so the PDGs reported can never disagree with + # the legs the matrix element actually evaluates: PERM(K) is the input slot + # landing in crossed slot K and SGN(K)=-1 marks exactly the legs that + # swapped between the initial and the final state, which are the ones the + # crossed process sees as their own antiparticle. + PDG_CROSS_SNIPPETS_ON = ( + """ INTEGER FP_PERM(NEXTERNAL), FP_SGN(NEXTERNAL) + INTEGER FP_CROSS + INTEGER %(proc_prefix)sGET_SPINCOL_CROSS""", + ' CALL %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX_IN, FP_PERM, FP_SGN,\n' + ' & FP_FLAV)', + """C A crossing with a null spin*color entry is one SMATRIX itself maps +C to a zero matrix element (out of range, or not applicable). Report no +C PDGs for it rather than a signature that cannot be evaluated. + FP_CROSS = (FLAV_IDX_IN-1) / NFLAV + IF (%(proc_prefix)sGET_SPINCOL_CROSS(FP_CROSS) .EQ. 0) THEN + RETURN + ENDIF + DO FP_I = 1, NEXTERNAL + IF (FP_SGN(FP_I) .EQ. 1) THEN + PDGS(FP_I) = FP_PDG_TABLE(FP_PERM(FP_I), FP_FLAV) + ELSE + PDGS(FP_I) = FP_ANTI_TABLE(FP_PERM(FP_I), FP_FLAV) + ENDIF + ENDDO""") + + # Copy the arguments through unchanged: same shape as the crossing block it + # replaces, so its (single) caller does not have to know which is which. + CROSS_PASSTHROUGH = """C No crossing to apply: the arguments go through unchanged. + PUSE(:,:) = P(:,:) + %(nhel_copy)s + ICUSE(:) = IC(:) + DO IPART=1,N_CHANGING + CPOS(IPART) = POS(IPART) + ENDDO""" + + # The crossing-aware variants of the same holes. Kept here rather than in + # the template because the template can only hold one variant per hole. + CROSSING_SNIPPETS = { + 'smatrix_cross_decl': """C CROSSUSE is the crossing carried by FLAV_IDX and IDENUSE the initial +C state spin*color average of the process it crosses into. + INTEGER IDENUSE, CROSSUSE + INTEGER %(proc_prefix)sGET_SPINCOL_CROSS + INTEGER %(proc_prefix)sGET_IDENT_CROSS +C Crossed copies of the arguments, built ONCE per SMATRIX call (see the +C BEGIN CODE section). They are only touched when a crossing is actually +C requested, so the uncrossed path pays nothing for them. + REAL*8 PUSE(0:3,NEXTERNAL) + INTEGER NHELUSE(NEXTERNAL,NCOMB) + INTEGER ICUSE(NEXTERNAL) + INTEGER DUMFLAV +C GHIDX is the identity row whose shared GOODHEL bit gates the current +C crossed row, recomputed at runtime by CROSS_GHIDX (which owns the small +C per-crossing GHFILT flag table); XGPERM/XGSGN are the crossing's slot +C permutation and NSF signs, fetched once per call (see smatrix_cross_apply). + INTEGER GHIDX + INTEGER XGPERM(NEXTERNAL), XGSGN(NEXTERNAL), XGDUM""", + + 'smatrix_cross_decode': """C CROSS = (FLAV_IDX-1)/NFLAV is the crossing to apply. IDENUSE is 0 for a +C crossing that cannot be applied, whose matrix element is identically zero. + CROSSUSE = (FLAV_IDX-1) / NFLAV + IDENUSE = %(proc_prefix)sGET_SPINCOL_CROSS(CROSSUSE) + IF (IDENUSE.EQ.0) THEN + ANS = 0D0 + RETURN + ENDIF""", + + 'smatrix_cross_apply': """C Fetch the crossing's slot permutation / NSF signs once (the good-helicity +C gate below reuses them per helicity via CROSS_GHIDX). Cheap, and the +C identity crossing returns the identity permutation. + CALL %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX, XGPERM, XGSGN, XGDUM) +C Apply the crossing ONCE, here, rather than once per helicity: the whole +C NHEL table is permuted in one go (the crossing is a fixed slot +C permutation, identical for every row) together with the momenta and the +C NSF/NSV flags. When CROSSUSE is 0 nothing is copied at all and the loop +C below passes the original arrays straight through, exactly as it did +C before crossings existed. + IF (CROSSUSE.NE.0) THEN + CALL %(proc_prefix)sAPPLY_CROSSING_TABLE(FLAV_IDX, NCOMB, P, NHEL, + & JC, PUSE, NHELUSE, ICUSE, DUMFLAV) + ENDIF""", + + 'smatrix_goodhel_gate': """C The good-helicity filter (GOODHEL) is shared by every crossing of a +C flavor, but a crossing permutes and flips helicities, so a crossed row +C and its identity counterpart are different rows. CROSS_GHIDX sends crossed +C row IHEL to the identity row that gates it (sigma^-1, recomputed from the +C config); GHIDX=0 means the crossing is not filterable (an initial-initial +C swap, or a crossing that cannot be applied) so its every helicity is +C computed. For CROSSUSE=0 it returns IHEL, exactly the historical gate. + CALL %(proc_prefix)sCROSS_GHIDX(CROSSUSE, XGPERM, XGSGN, + & NHEL(1,IHEL), GHIDX) + IF (GHIDX.EQ.0 .OR. GOODHEL(GHIDX,FLAV_USE) .OR. NTRY(FLAV_USE).LT.20 .OR. USERHEL.NE.-1) THEN""", + + 'smatrix_goodhel_train': """C Train the SHARED filter through the same map: mark the IDENTITY row +C GHIDX good, so GOODHEL always stores the identity pattern whatever +C crossing is being evaluated. GHIDX=0 (non-filterable crossing) never +C trains. For CROSSUSE=0 GHIDX=IHEL, so this is the historical training. + IF (T .NE. 0D0 .AND. GHIDX.NE.0 .AND. .NOT.GOODHEL(GHIDX,FLAV_USE)) THEN + GOODHEL(GHIDX,FLAV_USE)=.TRUE. + ENDIF""", + + 'smatrix_matrix_call': """ IF (CROSSUSE.EQ.0) THEN + T=%(proc_prefix)sMATRIX(P ,NHEL(1,IHEL),JC(1),FLAV_USE) + ELSE + T=%(proc_prefix)sMATRIX(PUSE,NHELUSE(1,IHEL),ICUSE(1) + & ,FLAV_USE) + ENDIF""", + + 'smatrix_iden_line': """C Uncrossed: keep the historical path untouched (IDEN carries the +C representative's identical factor and BROKEN_SYM corrects it per flavor). +C Crossed: BROKEN_SYM's tables describe the uncrossed final state and +C cannot express the crossed one, so rebuild the denominator instead as +C initial state spin*color (per crossing) times the identical final state +C factor of the actual crossed flavors (per flavor). + IF (CROSSUSE.EQ.0) THEN + ANS=ANS/DBLE(IDEN)*%(proc_prefix)sBROKEN_SYM(FLAVOR) + ELSE + ANS=ANS/DBLE(IDENUSE*%(proc_prefix)sGET_IDENT_CROSS(CROSSUSE, + & FLAVOR)) + ENDIF""", + + 'inter_rescale_decl': """ INTEGER CROSS, DCROSS, IDEN + INTEGER %(proc_prefix)sGET_SPINCOL_CROSS + INTEGER %(proc_prefix)sGET_IDENT_CROSS + %(den_factor_line)s""", + + 'inter_rescale_body': """ CROSS = (FLAV_IDX-1)/NFLAV + IF (CROSS.EQ.0) THEN +C Uncrossed: the static IDEN carries the identical-particle factor of the +C representative flavor, so BROKEN_SYM must correct it for the actual one, +C exactly as SMATRIX does with ANS/IDEN*BROKEN_SYM. + RESCALE = DBLE(%(proc_prefix)sBROKEN_SYM(FLAVOR)) + ELSE +C Crossed: BROKEN_SYM's tables describe the uncrossed final state and are +C useless here; rebuild the whole denominator instead (see SMATRIX) and +C undo the IDEN that GET_INTER divided by. + DCROSS = %(proc_prefix)sGET_SPINCOL_CROSS(CROSS) + & * %(proc_prefix)sGET_IDENT_CROSS(CROSS, FLAVOR) + IF (DCROSS.EQ.0) THEN + RESCALE = 0D0 + ELSE + RESCALE = DBLE(IDEN)/DBLE(DCROSS) + ENDIF + ENDIF""", + + 'density_cross_apply': """ CALL %(proc_prefix)sAPPLY_CROSSING_TABLE(FLAV_IDX, NB_NHEL, P, NHEL, + & IC, PUSE, NHELUSE, ICUSE, DUMFLAV) +C POS is given in uncrossed slots; PERM(K) is the uncrossed slot sitting in +C crossed slot K, so invert it to move POS into the crossed numbering. + CALL %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX, PERM, SGN, DUMFLAV) + DO IPART=1,N_CHANGING + DO I=1,NEXTERNAL + IF (PERM(I).EQ.POS(IPART)) CPOS(IPART) = I + ENDDO + ENDDO""", + + 'allinter_cross_apply': """C IC starts at +1 everywhere; APPLY_CROSSING flips it for the legs that the +C crossing carried by FLAV_IDX moves across. + IC(:)=1 + CALL %(proc_prefix)sAPPLY_CROSSING(FLAV_IDX, P, NHEL, IC, PUSE, + & NHELUSE, ICUSE, DUMFLAV) + CALL %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX, PERM, SGN, DUMFLAV) + DO IPART = 1, N_CHANGING + DO I = 1, NEXTERNAL + IF (PERM(I).EQ.POS(IPART)) CPOS(IPART) = I + ENDDO + ENDDO""", + } + + def get_iden_cross_lines(self, matrix_element): + """Return the DATA lines backing the crossing-dependent denominator. + + SMATRIX must divide by the averaging/symmetry factor of the *crossed* + process. That factor splits in two, and the two halves must be handled + differently: + + - the initial state spin*color average changes with the crossing (a + gluon pulled into the initial state takes the color average from 3 to + 8) but NOT with the flavor, since every particle of a flavor group + shares its spin and color. It is emitted as SPINCOL_CROSS_TABLE, + indexed by CROSS. + - the identical final state factor changes with the FLAVOR: e.g. + d d~ > g u u~ crossed gives d g > d u u~ (nothing identical) while + d d~ > g d d~ crossed gives d g > d d d~ (two identical d). It cannot + be tabulated on CROSS alone, and the existing BROKEN_SYM cannot help: + its tables describe the *uncrossed* final state, so for this process + it emits COMP_OLD=1 and returns 1 whatever flavor array it is given. + It is therefore computed at runtime by GET_IDENT_CROSS, from the two + tables below. + + BASEPID_CROSS_TABLE gives, per slot of the crossed process, the + representative PDG of the particle landing there (conjugated when the + leg swapped between the initial and the final state), which identifies + its flavor group. SRC_CROSS_TABLE gives the FLAVOR entry to read for + that slot: FLAVOR is not permuted by the crossing, so slot k must look + up the position of the original leg that moved into it. Two crossed + final legs are identical iff they share both. + + Both tables are flattened as CROSS*NEXTERNAL + (slot-1). + + A crossing that cannot be applied gets a 0 spin*color entry, which + SMATRIX maps to a null matrix element. + """ + tables = self.compute_crossing_tables(matrix_element) + spincol = tables['spincol'] + basepid = tables['basepid'] + # SRC_CROSS_TABLE is 1-based in the fortran (FLAVOR is indexed 1..N). + source = [s + 1 for s in tables['source']] + + return '\n'.join([ + self.format_integer_data_lines('SPINCOL_CROSS_TABLE', spincol), + self.format_integer_data_lines('BASEPID_CROSS_TABLE', basepid), + self.format_integer_data_lines('SRC_CROSS_TABLE', source)]) + + def compute_crossing_tables(self, matrix_element): + """Build the crossing tables as plain python int lists (model-agnostic). + + Returns a dict with, for every crossing code CROSS in + 0..(NEXTERNAL+1)**2-1: + 'spincol' : SPINCOL_CROSS_TABLE[CROSS], the initial-state spin*color + average of the crossed process (0 = crossing that must not + be applied: out of range, impossible, or an overlapping + swap, see get_crossing_permutation); + 'basepid' : flattened CROSS*NEXTERNAL+slot -> representative signed PDG + of the particle landing in that crossed slot (conjugated + when the leg swapped between the initial and the final + state); + 'source' : flattened CROSS*NEXTERNAL+slot -> 0-based index of the + original leg that moved into that slot (FLAVOR is NOT + permuted, so this says which FLAVOR entry a slot reads); + 'perm' : flattened CROSS*NEXTERNAL+slot -> 0-based perm[slot]; + 'ic' : flattened CROSS*NEXTERNAL+slot -> +-1 NSF sign of that slot; + 'nexternal', 'ninitial'. + + Both the fortran (get_iden_cross_lines) and the C++ standalone exporter + consume this, so the two backends can never disagree about a crossing. + """ + process = matrix_element.get('processes')[0] + model = process.get('model') + legs = process.get('legs') + nexternal = len(legs) + leg_ids = [leg.get('id') for leg in legs] + # polarization restricts the number of helicity states of a leg; it is + # attached to the leg, and a crossing moves legs around, so carry it. + polarizations = [leg.get('polarization') for leg in legs] + + def particle(pdg): + return model.get('particle_dict')[pdg] + + ninitial = len([leg for leg in legs if not leg.get('state')]) + + spincol = [] + basepid = [] + source = [] + perm_flat = [] + ic_flat = [] + # CROSS = I*(NEXTERNAL+1)+J with I and J both in 0..NEXTERNAL. + for cross in range((nexternal + 1) * (nexternal + 1)): + perm, ic, valid = ProcessExporterFortran.get_crossing_permutation( + cross, nexternal) + if not valid: + # Overlapping-swap code: pure redundancy, and inconsistent + # between GET_PDG_FOR_FLAVOR and APPLY_CROSSING (see + # get_crossing_permutation). A 0 spin*color marks it as a + # crossing that must not be applied, exactly as for one that + # genuinely cannot be; both SMATRIX and GET_PDG_FOR_FLAVOR then + # refuse it via GET_SPINCOL_CROSS==0. + spincol.append(0) + slot_ids = list(leg_ids) + else: + try: + # A leg that swapped between the initial and the final state + # is seen as its own antiparticle by the crossed process. + slot_ids = [leg_ids[perm[slot]] if ic[slot] == 1 + else particle(leg_ids[perm[slot]]).get_anti_pdg_code() + for slot in range(nexternal)] + + # The crossing always keeps slots 1..ninitial initial. + factor = 1 + for slot in range(ninitial): + pol = polarizations[perm[slot]] + factor *= len(pol) if pol else \ + len(particle(slot_ids[slot]).get_helicity_states()) + # get('color') is signed for antiparticles; only the + # size of the representation matters for the average. + factor *= abs(particle(slot_ids[slot]).get('color')) + spincol.append(factor) + except (KeyError, IndexError): + spincol.append(0) + slot_ids = list(leg_ids) + + basepid.extend(slot_ids) + source.extend(perm[slot] for slot in range(nexternal)) + perm_flat.extend(perm) + ic_flat.extend(ic) + + # Sanity: for the identity crossing, spin*color times the identical + # factor of the representative flavor must rebuild the static IDEN, + # else this and get_denominator_factor have drifted apart. + rep_final = [leg_ids[slot] for slot in range(ninitial, nexternal)] + rep_identical = 1 + for pdg in set(rep_final): + rep_identical *= math.factorial(rep_final.count(pdg)) + assert spincol[0] * rep_identical == \ + matrix_element.get_denominator_factor(), \ + 'Crossing denominator disagrees with get_denominator_factor: ' \ + '%s*%s vs %s' % (spincol[0], rep_identical, + matrix_element.get_denominator_factor()) + + return {'spincol': spincol, 'basepid': basepid, 'source': source, + 'perm': perm_flat, 'ic': ic_flat, + 'nexternal': nexternal, 'ninitial': ninitial} + + def compute_crossing_pdg_entries(self, matrix_element, zero_based=True): + """Enumerate the reachable extended flavor indices and their crossed PDG. + + Returns a list of ``(index, cross, flav0, pdg_tuple)`` for every crossing + code CROSS that can actually be applied (SPINCOL_CROSS_TABLE[CROSS] != 0, + i.e. skipping the out-of-range / impossible / overlapping-swap codes) and + every flavor ``flav0`` in ``0..NFLAV-1``: + + * ``index`` -- the extended flavor index that selects (CROSS, flav0). + The C++/mg7 backends decode it 0-based as ``cross*NFLAV + flav0``; the + fortran one is 1-based (``index+1``). ``zero_based`` picks which. + * ``cross`` -- the crossing code (0 == identity). + * ``flav0`` -- the 0-based reduced flavor. + * ``pdg_tuple`` -- the *signed physical* PDG of each leg, in the leg order + the momenta must be supplied in for that index (legs permuted and + conjugated where they swapped between the initial and the final state). + + This is the python twin of the fortran runtime GET_PDG_FOR_FLAVOR: the + C++ and mg7 standalones have no runtime PDG accessor, so their crossed + PDG signatures are computed here instead (the same logic that fills the + check_sa demo table). All three backends therefore agree on the mapping + pdg <-> extended index by construction. Both helpers are referenced + through the class so a non-Fortran ``self`` (the C++/mg7 exporter, or a + throwaway) can reuse them unbound. + """ + tables = ProcessExporterFortran.compute_crossing_tables( + self, matrix_element) + spincol = tables['spincol'] + perm = tables['perm'] + ic = tables['ic'] + nx = tables['nexternal'] + ncross = len(spincol) + n_flav = len(matrix_element.get_external_flavors_with_iden()) + _, pdg_flat, antipdg_flat = \ + ProcessExporterFortran._build_flav_pdg_tables(self, matrix_element) + pdg_rows = len(pdg_flat) // nx + + entries = [] + for cross in range(ncross): + if spincol[cross] == 0: + continue + for flav0 in range(n_flav): + # Guard against a pdg table with fewer rows than nflavors. + row = flav0 if flav0 < pdg_rows else 0 + pdg = [] + for k in range(nx): + src = perm[cross * nx + k] + if ic[cross * nx + k] == 1: + pdg.append(pdg_flat[row * nx + src]) + else: + pdg.append(antipdg_flat[row * nx + src]) + index = cross * n_flav + flav0 + if not zero_based: + index += 1 + entries.append((index, cross, flav0, tuple(pdg))) + return entries + + def partition_crossing_classes(self, matrix_elements): + """Route each subprocess *flavor* to a base matrix element via crossing. + + The crossing relates whole flavor combinations, not whole modules: a + flavor-merged matrix element bundles flavors that cross to *different* + bases (e.g. within a group ``u u~ > u u~`` is a crossing of ``u u > u u`` + while its module-mate ``d d~ > u u~`` is not). So the sharing that lets + one matrix.f serve several subprocesses is decided per flavor: a + module can drop its own matrix.f only when EVERY one of its flavors is + a genuine crossing (cross != 0) of some *base* module's flavor; otherwise + it stays a base and keeps its own matrix.f. + + Bases are chosen greedily in order. Returns ``(bases, routing)``: + + * ``bases`` -- the matrix_element indices that keep their own + matrix.f (their SMATRIX, driven by an extended FLAV_IDX, also serves + the flavors routed to them). + * ``routing`` -- a list parallel to ``matrix_elements``; ``routing[i]`` + has one ``(base_index, iflav)`` per flavor of member ``i`` (in flavor + order), naming the base module whose ``SMATRIX`` evaluates that flavor + and the 1-based extended ``FLAV_IDX`` to call it with. A base routes + each of its own flavors to itself with the plain (cross 0) index. + + Signatures are the crossed physical PDG tuples of compute_crossing_pdg_ + entries, the same key check_crossing matches on, so the momentum order a + member supplies already matches what the base SMATRIX expects for that + index. + """ + n = len(matrix_elements) + # Per ME: identity signature of each flavor (flavor order) and the map + # from any crossed signature it can reach to (cross, 1-based FLAV_IDX). + sig_by_flav = [] + crossmap = [] + for me in matrix_elements: + sbf = {} + cm = {} + for idx, cross, flav0, pdg in \ + self.compute_crossing_pdg_entries(me, zero_based=False): + if cross == 0: + sbf[flav0] = pdg + cm.setdefault(pdg, (cross, idx)) + nflav = (max(sbf) + 1) if sbf else 0 + sig_by_flav.append([sbf[f] for f in range(nflav)]) + crossmap.append(cm) + + bases = [] + routing = [None] * n + for i in range(n): + cover = [] + coverable = bool(bases) # nothing to route to before the first base + for sig in sig_by_flav[i]: + hit = None + for b in bases: + cx = crossmap[b].get(sig) + if cx is not None and cx[0] != 0: # a genuine crossing of b + hit = (b, cx[1]) + break + if hit is None: + coverable = False + break + cover.append(hit) + if coverable: + routing[i] = cover # drop i's matrix.f; route each flavor + else: + bases.append(i) # i keeps its own matrix.f (a base) + routing[i] = [(i, crossmap[i][sig][1]) for sig in sig_by_flav[i]] + return bases, routing + + def compute_crossgroup_routing(self, subproc_groups): + """Cross-group crossing (Track B): find whole subprocess GROUPS whose + matrix element is a crossing of another group's, so the dependent group + can REUSE (symlink) the base group's compiled matrix element instead of + generating and compiling its own. Used for e.g. lepton/photon beams where + each initial state lands in its own single-process P directory and the + crossings relate different P directories (partition_crossing_classes is + group-agnostic -- it clusters by crossed-PDG signature -- so it is fed the + flat list of every group's matrix elements). + + Returns a dict keyed by ``(group_enum_idx, me_idx)`` for the DEPENDENT + members only; each value carries the base group's directory, the base + SMATRIX's proc_id, the base matrix_element (for the COLMAP/CONFIGMAP + remaps) and the crossed 1-based FLAV_IDX per flavor. Bases are absent + (they keep their own matrix element). Only a dependent whose EVERY flavor + crosses to a SINGLE base matrix element is routed; anything else keeps its + own matrix element (so the sharing is always a clean whole-ME reuse). + """ + if not self.opt.get('use_crossing', False): + return {} + # Consider only groups whose every member is a within-group BASE (no + # router). A group that ALREADY has within-group crossing routing (the + # hadronic p p groups where several crossings co-locate under a `j` + # multiparticle) is left to Track A -- mixing its base(s) with those + # routers is fragile, so it is excluded here. The lepton/photon single- + # process groups are all bases; a p p run additionally exposes the cross- + # P-directory crossings that within-group routing cannot reach (e.g. + # g g > q q~ vs q q~ > g g, in their own P directories). + flat = [] # (group_enum_idx, me_idx, matrix_element) + for gi, group in enumerate(subproc_groups): + mes_g = group.get('matrix_elements') + g_bases, _ = self.partition_crossing_classes(mes_g) + if len(g_bases) < len(mes_g): + continue # within-group routing -> leave to Track A + for mi, me in enumerate(mes_g): + flat.append((gi, mi, me)) + if not flat: + return {} + # A pinned s-channel does not survive crossing (see breaks_crossing_ + # symmetry): fall back to independent matrix elements. + if any(self.breaks_crossing_symmetry(proc) + for (_, _, me) in flat for proc in me.get('processes')): + return {} + mes = [me for (_, _, me) in flat] + bases, routing = self.partition_crossing_classes(mes) + result = {} + for flat_i, (gi, mi, me) in enumerate(flat): + if flat_i in bases: + continue + route = routing[flat_i] # per flavor: (base_flat, iflav) + base_flats = set(bflat for (bflat, _) in route) + if len(base_flats) != 1: + # flavors cross to different bases (a merged group): no single ME + # to symlink, keep this member's own matrix element. + continue + base_gi, base_mi, base_me = flat[base_flats.pop()] + base_group = subproc_groups[base_gi] + result[(gi, mi)] = { + 'base_dir': 'P%d_%s' % (base_group.get('number'), + base_group.get('name')), + 'base_proc_id': base_mi + 1, + 'base_me': base_me, + 'flav_idx': [iflav for (_, iflav) in route], + } + return result + + def compute_ghremap(self, matrix_element, allow_reverse=True): + """Build the good-helicity remap table for the crossing filter. + + The good-helicity filter (GOODHEL) is shared by all crossings of a + flavor, but a crossing permutes and flips helicities, so identity and + crossed have different good-helicity SETS. The crossed set is the + identity set transformed by the crossing's own helicity-row permutation + sigma, where sigma sends identity row h to the row whose config is + (ic[k]*nhel[perm[k], h])_k -- permute the legs and flip the helicity of + the swapped ones, with (perm, ic) from get_crossing_permutation. A + crossed row H is therefore good iff the identity row sigma^-1(H) is + good, so the filter can stay shared as long as it is consulted (and + trained) through sigma^-1. See standalone-cross-symmetry memory. + + Returns a flat list of length NCROSS*NCOMB indexed CROSS*NCOMB + H (H + the 0-based helicity row), each entry being the 0-based identity row + sigma^-1(H) that gates crossed row H, or None when the crossing must + not be filtered (compute every helicity, never train): + - CROSS==0 -> the identity (entry == H): the uncrossed path is + completely unchanged; + - a genuine crossing whose active partners are all final particles -> + sigma^-1(H); + - an initial-initial swap, or an invalid / inapplicable crossing -> + None. The sigma relation only holds when the active partners are + final; an initial-initial swap breaks it (it overcounts at 2->3), + so those disable the filter and keep the full-computation result. + + allow_reverse must match the order the NHEL table is emitted in for the + backend consuming the result (True for the fortran get_helicity_lines, + False for the C++ get_helicity_matrix). + """ + # Reference the class explicitly (not self) so the C++ standalone + # exporter can reuse this via ProcessExporterFortran.compute_ghremap + # with a non-Fortran self, exactly like compute_crossing_tables. + tables = ProcessExporterFortran.compute_crossing_tables( + self, matrix_element) + spincol = tables['spincol'] + nexternal = tables['nexternal'] + ninitial = tables['ninitial'] + base = nexternal + 1 + ncross = base * base + hel_matrix = [tuple(row) for row in + matrix_element.get_helicity_matrix(allow_reverse)] + ncomb = len(hel_matrix) + row_index = {row: h for h, row in enumerate(hel_matrix)} + + remap = [] + for cross in range(ncross): + perm, ic, valid = \ + ProcessExporterFortran.get_crossing_permutation(cross, nexternal) + i_part, j_part = cross // base, cross % base + final_only = ((i_part in (0, 1) or i_part > ninitial) and + (j_part in (0, 2) or j_part > ninitial)) + derivable = (valid and spincol[cross] != 0 and + (cross == 0 or final_only)) + block = [None] * ncomb + if derivable: + for h in range(ncomb): + config = tuple(ic[k] * hel_matrix[h][perm[k]] + for k in range(nexternal)) + big_h = row_index.get(config) + if big_h is None: + # The permuted config is not a table row: the crossing + # is not a bijection on the rows, so it cannot be + # derived. Disable the filter for it (safe fallback). + block = [None] * ncomb + break + block[big_h] = h + remap.extend(block) + return remap + + def compute_ghfilt(self, matrix_element, allow_reverse=True): + """Per-crossing filterability flags for the runtime good-helicity remap. + + Returns a list of length NCROSS: 1 if crossing CROSS is filterable (its + helicity-row permutation sigma is a clean bijection -- see + compute_ghremap), 0 otherwise (initial-initial swap, inapplicable, or a + non-bijection). This is the small flag table that replaces the full + GHREMAP(NCROSS*NCOMB) row table: at runtime the row map itself is + recomputed by permuting+sign-flipping the config and re-encoding it (see + the CROSS_GHIDX routine), so only the per-crossing yes/no survives as + DATA. A whole compute_ghremap block is either fully derivable or fully + None, so this loses nothing.""" + # Reference the class explicitly (not self) so a non-Fortran self (the + # C++ standalone exporter) can reuse this via + # ProcessExporterFortran.compute_ghfilt, exactly like compute_ghremap. + remap = ProcessExporterFortran.compute_ghremap( + self, matrix_element, allow_reverse) + nexternal = matrix_element.get_nexternal_ninitial()[0] + ncross = (nexternal + 1) * (nexternal + 1) + ncomb = len(remap) // ncross + return [0 if all(x is None for x in remap[c * ncomb:(c + 1) * ncomb]) + else 1 for c in range(ncross)] + + @staticmethod + def format_integer_data_lines(name, values, per_line=10): + """Emit 'DATA (name(I),I=a,b) /.../' lines for a 0-based table.""" + lines = [] + for start in range(0, len(values), per_line): + chunk = values[start:start + per_line] + lines.append(' DATA (%s(I),I=%d,%d) /%s/' % + (name, start, start + len(chunk) - 1, + ','.join(str(value) for value in chunk))) + return '\n'.join(lines) + def get_icolamp_lines(self, mapconfigs, matrix_element, num_matrix_element): """Return the ICOLAMP matrix, showing which JAMPs contribute to which configs (diagrams).""" @@ -3354,6 +4588,11 @@ class ProcessExporterFortranSA(ProcessExporterFortran): f2py_wrapper_all ="f2py_wrapper_all.inc" f2py_matrix_splitter = "f2py_splitter.py" jamp_optim = True + # The only exporter implementing the extended FLAV_IDX decoding. The + # per-matrix-element cases it still cannot cross (msP/msF, matchbox, + # split orders) are handled by the use_crossing_ic gate in + # write_matrix_element_v4, which falls back to the uncrossed code. + supports_crossing = True default_vector_size = 0 # When True, emit per-call IAND(WF_FLAVOR_MASK/AMP_FLAVOR_MASK, # CURRENT_FLAV_BIT) guards in MATRIX so that wavefunctions and amplitudes @@ -3687,11 +4926,18 @@ def write_f2py_splitter(self): end """ nhel_template = """subroutine %(f2py_prefix)sf77_%(prefix)sget_nhel_entry(NHEL) - integer %(prefix)snhel(%(next)s,%(ncombs)s), NHEL(%(next)s,%(ncombs)s) - common/%(prefix)sPROCESS_NHEL/%(prefix)sNHEL - NHEL(:,:) = %(prefix)snhel(:,:) + integer NHEL(%(next)s,%(ncombs)s) + integer idendummy +C Fill NHEL through GET_NHEL rather than reading the PROCESS_NHEL common +C directly. With the canonical helicity encoder/decoder the table is +C materialized at runtime (GET_NHEL calls FILL_NHEL), so an early caller +C -- e.g. reweighting building its per-config helicity map at init, before +C any matrix-element evaluation -- would otherwise read a table of zeros. +C Every standalone matrix.f defines GET_NHEL (materializing or DATA-backed), +C so this also stays correct for split-order processes. + call %(prefix)sget_nhel(idendummy, NHEL) return - end + end """ f2py_prefix = '' @@ -4175,6 +5421,18 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, if 'sa_symmetry' not in self.opt: self.opt['sa_symmetry']=False + # --use_crossing of the generate command (default on); see + # fill_crossing_replace_dict. + if 'use_crossing' not in self.opt: + self.opt['use_crossing']=True + + # ... and gated off per matrix element for processes whose definition + # pins a specific s-channel, which no crossing of them preserves. This + # is decided here rather than in the interface so that one constrained + # `add process` does not disable crossing for the unconstrained ones. + use_crossing = self.opt['use_crossing'] and \ + not any(self.breaks_crossing_symmetry(proc) + for proc in matrix_element.get('processes')) # The proc_id is for MadEvent grouping which is never used in SA. @@ -4202,6 +5460,20 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, fortran_model.use_flavor_mask = (n_mask > 0) fortran_model.me_n_flavors = n_mask fortran_model.me_active_flavor_mask = active_flavor_mask + # Only matrix_standalone_v4.inc hands GET_AMP the crossed IC built by + # APPLY_CROSSING, so it is the only one whose NSF/NSV flags may go + # through IC. The other variants selected below (msP, msF, matchbox, + # splitOrders) have no IC to read and must keep the bare flag. Mirror + # the template choice made further down; split_orders is only fetched + # again here, which is side-effect free. + fortran_model.use_crossing_ic = ( + use_crossing + and self.matrix_template == 'matrix_standalone_v4.inc' + and self.opt['export_format'] not in ('standalone_msP', + 'standalone_msF', + 'matchbox', + 'madloop_matchbox') + and not matrix_element.get('processes')[0].get('split_orders')) try: # Extract helas calls helas_calls = fortran_model.get_matrix_element_calls(\ @@ -4210,6 +5482,7 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, fortran_model.use_flavor_mask = False fortran_model.me_n_flavors = 0 fortran_model.me_active_flavor_mask = None + fortran_model.use_crossing_ic = False replace_dict['helas_calls'] = "\n".join(helas_calls) @@ -4230,9 +5503,18 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, ncomb = matrix_element.get_helicity_combinations() replace_dict['ncomb'] = ncomb - # Extract helicity lines + # Extract helicity lines. helicity_lines (the explicit NHEL config DATA + # table) is still consumed by the msP/msF/splitOrders standalone + # templates; matrix_standalone_v4.inc instead uses the canonical + # encoder/decoder tables below (NHSTATE/STATES/HELALLOW) and + # materializes PROCESS_NHEL at runtime via FILL_NHEL. helicity_lines = self.get_helicity_lines(matrix_element) replace_dict['helicity_lines'] = helicity_lines + hel_data = self._helstate_data(matrix_element) + replace_dict['maxhel'] = hel_data['maxhel'] + replace_dict['nhstate_data'] = hel_data['nhstate_data'] + replace_dict['states_data'] = hel_data['states_data'] + replace_dict['hel_allow_data'] = hel_data['hel_allow_data'] # Extract overall denominator # Averaging initial state color, spin, and identical FS particles @@ -4376,6 +5658,48 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, fa_func_name, n_table, flav_table_flat, nexternal_decl=bs_nexternal) + # Per-crossing denominator and the routines decoding an extended + # FLAV_IDX. Only matrix_standalone_v4.inc has these holes, and they are + # left empty when the process was generated with --use_crossing=False. + self.fill_crossing_replace_dict(matrix_element, replace_dict, + use_crossing) + + # GET_PDG_FOR_FLAVOR (extended FLAV_IDX -> per-leg PDG). Must come after + # fill_crossing_replace_dict, which decides whether it decodes a + # crossing or just reads the table. Only matrix_standalone_v4.inc has + # the hole; the key is set unconditionally since an unused replace_dict + # entry is harmless and the other templates then stay byte-identical. + n_pdg_flav, pdg_flat, antipdg_flat = \ + self._build_flav_pdg_tables(matrix_element) + replace_dict['flavor_pdg_function'] = \ + self._make_flavor_pdg_fortran_function( + replace_dict['proc_prefix'] + 'GET_PDG_FOR_FLAVOR', + n_pdg_flav, pdg_flat, antipdg_flat, + replace_dict['pdg_cross_snippets'], + nexternal_decl=bs_nexternal) + + # f2py entry points taking an extended FLAV_IDX (the only way a python + # caller can request a crossing, and reach GET_DENSITY_IDX / + # GET_ALL_INTER_IDX / GET_NHEL_IDX / GET_PDG_FOR_FLAVOR). Those routines + # only exist in matrix_standalone_v4.inc, so the wrappers are emitted + # only there; the other standalone templates get an empty hole (the + # placeholder lives in the shared matrix_standalone_f2py.inc). The + # snippet is pre-formatted here because the outer '% replace_dict' pass + # does not re-scan an inserted value for further %(...)s. + if matrix_template == 'matrix_standalone_v4.inc': + flav_idx_tmpl = open(pjoin(_file_path, 'iolibs', 'template_files', + 'matrix_standalone_f2py_flav_idx.inc')).read() + nexternal_val = int(replace_dict['nexternal']) + replace_dict['f2py_flav_idx_wrappers'] = flav_idx_tmpl % { + 'proc_prefix': replace_dict['proc_prefix'], + 'nexternal': nexternal_val, + 'nflav': replace_dict['nflav'], + 'ncomb': replace_dict['ncomb'], + 'ncross': (nexternal_val + 1) ** 2, + } + else: + replace_dict['f2py_flav_idx_wrappers'] = '' + replace_dict['template_file'] = pjoin(_file_path, 'iolibs', 'template_files', matrix_template) replace_dict['template_file2'] = pjoin(_file_path, \ 'iolibs/template_files/split_orders_helping_functions.inc') @@ -4402,6 +5726,210 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, #=========================================================================== # write_check_sa #=========================================================================== + def _crossed_signatures(self, matrix_element): + """(signatures, complete) for the crossed subprocesses folded into this + matrix element (merge_crossing='record'), so check_sa can demo exactly + the crossings that are real subprocesses of the generation -- not every + mathematically valid crossing of the base. + + Each signature is a representative signed-PDG tuple in the crossed leg + order, matched at RUNTIME against GET_PDG_FOR_FLAVOR (whose python twin + is compute_crossing_pdg_entries). Matching on the PDG rather than the + extended index avoids the NFLAV-convention gap between the crossing-PDG + enumeration and the runtime flavor table. + + A recorded crossed process may carry merged multiparticle labels (e.g. + _quark = 81). Rather than resolve each such leg independently to one + flavor -- which would fabricate an unphysical signature for a + flavor-changing vertex, e.g. a W coupling two same-flavor quarks -- each + recorded process is matched LABEL-AWARE against the reachable set, which + already encodes the correct flavor pairings; the first reachable + instantiation is taken as the representative. Mirror pairs are collapsed + (the chosen signature's beam swap is also marked seen). 'complete' is + False only when a recorded process has NO reachable instantiation, so + the caller falls back to the full loop rather than hide a real + crossing.""" + crossed = matrix_element.get('crossed_processes') + if not crossed: + return [], True + model = matrix_element.get('processes')[0].get('model') + merged = model.get('merged_particles') + + def leg_matches(leg_id, pdg): + # Does the reachable PDG instantiate this recorded leg id? A merged + # label matches any member flavor of the same sign; a concrete + # particle matches only itself. + a = abs(leg_id) + if a in merged: + return (leg_id > 0) == (pdg > 0) and abs(pdg) in merged[a] + return pdg == leg_id + + ninitial = matrix_element.get_nexternal_ninitial()[1] + # signatures the runtime can actually reach (physical crossings) + reachable = [tuple(pdg) for (_i, _c, _f, pdg) in + self.compute_crossing_pdg_entries(matrix_element)] + sigs, seen, complete = [], set(), True + for (proc, _bp, _xp) in crossed: + legs = [l.get('id') for l in proc.get('legs')] + orients = [legs] + if ninitial == 2: # try the beam-swapped orientation + orients.append([legs[1], legs[0]] + legs[2:]) + hit = None + for orient in orients: + for r in reachable: + if len(r) == len(orient) and \ + all(leg_matches(L, P) for L, P in zip(orient, r)): + hit = r + break + if hit is not None: + break + if hit is None: + complete = False + continue + mirror = (hit[1], hit[0]) + hit[2:] if ninitial == 2 else hit + if hit in seen or mirror in seen: + continue # mirror partner already taken + sigs.append(hit) + seen.add(hit) + seen.add(mirror) + return sigs, complete + + def _get_check_sa_crossing_example(self, matrix_element, proc_prefix): + """Fortran block for check_sa.f demonstrating the crossed matrix elements. + + Returns '' when crossing is not active for this matrix element (flag + off, or an s-channel constraint disables it), so the driver is + unchanged. Otherwise it scans every crossing of the base -- FLIP1 and + FLIP2 each range over 1..NEXTERNAL, choosing which two legs sit in the + initial slots -- and, for each, evaluates the crossed matrix element and + prints the momenta actually used next to their signed PDGs. + + Only the crossings that are REAL subprocesses of the generation (folded + in via merge_crossing='record') are shown, not every mathematically + valid crossing: their representative signed-PDG signatures are loaded + into XCSIG (from _crossed_signatures) and each enumerated crossing is + kept only if GET_PDG_FOR_FLAVOR matches an XCSIG row. When a folded + crossing has no reachable signature (e.g. a flavor-changing W), the + signatures are 'incomplete' and the block falls back to showing every + applicable crossing (non-zero PDG, minus the FLIP1=1,FLIP2=2 identity). + + The crossing code is CROSS = FLIP1*(NEXTERNAL+1) + FLIP2, matching + GET_CROSS_PERM's decode (i_part = CROSS/(NEXTERNAL+1), + j_part = CROSS mod (NEXTERNAL+1)); FLAV_IDX = CROSS*NFLAV + flav, with + NFLAV emitted as the literal matrix.f value so the encoding matches + exactly. Degenerate crossings (e.g. FLIP1==FLIP2) decode to all-zero + PDGs and are skipped by both the match and the fallback. + """ + use_crossing = self.opt.get('use_crossing', True) and \ + not any(self.breaks_crossing_symmetry(proc) + for proc in matrix_element.get('processes')) + if not use_crossing: + return '' + + # NFLAV as matrix.f computes it, so CROSS*NFLAV+flav decodes correctly. + # It is assigned to a local NFLAV here so the loop body reads generically + # (FLAV_IDX = I*NFLAV+J) instead of a bare literal. The loop is gated + # behind IF(.FALSE.) unless crossed subprocesses were folded into this ME + # (merge_crossing='record'): then those partonic contributions have no + # directory of their own and this driver is the only place they are + # exercised, so the demo is enabled to actually evaluate them. + n_table, _ = self._build_flav_table_flat(matrix_element) + if not matrix_element.get('crossed_processes'): + # Nothing folded in: keep the dormant example (present but disabled). + loop_gate = '.false.' + else: + loop_gate = '.true.' + sigs, complete = self._crossed_signatures(matrix_element) + + sep = (' write (*,*) "-------------------------------------' + '----------------------------------------"') + + # For the FLAV_IDX already set: print the crossed process -- its per-leg + # PDG next to the momenta used to evaluate it. Every crossing shown here + # keeps the massive particles final and only relabels the massless + # partons, so its mass pattern is P's slot for slot; a standalone + # (non-crossed) run of that subprocess would draw the very same RAMBO + # point (identical hard-coded seed, sqrt(s) and per-slot masses). So the + # base P IS that point, printed row k = P(:,k) with the crossed PDG + # XPDG(k) -- copy/paste-comparable with the subprocess's own check. + # XPDG is already set for this FLAV_IDX by the loop body above. + demo_one = [ + ' CALL %sSMATRIX(P, FLAV_IDX, MATELEM)' % proc_prefix, + " write (*,*) 'FLAV_IDX', FLAV_IDX", + " write (*,*) ' PDG E px" + " py pz'", + ' DO XCK=1,NEXTERNAL', + " write (*,'(1X,I6,4(1X,E15.7))') XPDG(XCK),", + ' & P(0,XCK), P(1,XCK), P(2,XCK), P(3,XCK)', + ' ENDDO', + ' write (*,*) "Matrix element = ", MATELEM,' + ' " GeV^",-(2*nexternal-8)', + sep, + ] + + lines = [ + ' if(%s) then' % loop_gate, + ' write (*,*)', + ' write (*,*) " Crossed processes (folded into this matrix' + ' element):"', + ' write (*,*)', + ' NFLAV = %d' % n_table, + ] + if sigs and complete: + # Load the signed-PDG signatures of the folded crossings, then show + # only the crossings whose runtime PDG matches one of them (the real + # subprocesses of this generation, not every valid crossing). + lines.append(' XCNSIG = %d' % len(sigs)) + for s, sig in enumerate(sigs, 1): + for k, pid in enumerate(sig, 1): + lines.append(' XCSIG(%d,%d) = %d' % (k, s, pid)) + match_cond = 'XCMATCH' + else: + # A folded crossing could not be matched to a runtime PDG (e.g. a + # flavor-changing W subprocess): fall back to every crossing that is + # applicable here (all-zero PDG = not applicable, skipped), so no real + # subprocess is hidden. + lines.append(' XCNSIG = 0') + match_cond = 'XCVALID' + lines += [ + 'C FLIP1/FLIP2 pick which legs sit in the two initial slots;', + 'C 1..NEXTERNAL spans every crossing (FLIP1=1,FLIP2=2 = base).', + ' DO FLIP1=1,NEXTERNAL', + ' DO FLIP2=1,NEXTERNAL', + ' DO J=1,NFLAV', + ' I = FLIP1*(NEXTERNAL+1) + FLIP2', + ' FLAV_IDX = I*NFLAV+J', + ' CALL %sGET_PDG_FOR_FLAVOR(FLAV_IDX, XPDG)' % proc_prefix, + ] + if sigs and complete: + lines += [ + 'C Keep this crossing only if its PDG matches a folded', + 'C subprocess signature.', + ' XCMATCH = .FALSE.', + ' DO XCS=1,XCNSIG', + ' XCVALID = .TRUE.', + ' DO XCK=1,NEXTERNAL', + ' IF (XPDG(XCK).NE.XCSIG(XCK,XCS))' + ' XCVALID = .FALSE.', + ' ENDDO', + ' IF (XCVALID) XCMATCH = .TRUE.', + ' ENDDO', + ] + else: + lines += [ + 'C Applicable here iff its PDG signature is not all-zero,', + 'C skipping the identity (base process, shown above).', + ' XCVALID = .FALSE.', + ' DO XCK=1,NEXTERNAL', + ' IF (XPDG(XCK).NE.0) XCVALID = .TRUE.', + ' ENDDO', + ' IF (FLIP1.EQ.1 .AND. FLIP2.EQ.2) XCVALID = .FALSE.', + ] + lines.append(' IF (.NOT.%s) CYCLE' % match_cond) + lines.extend(demo_one) + lines += [' ENDDO', ' ENDDO', ' ENDDO', ' endif'] + return '\n'.join(lines) + def write_check_sa(self, writer, matrix_element, proc_prefix=''): if self.format != 'standalone': @@ -4474,6 +6002,14 @@ def write_check_sa(self, writer, matrix_element, proc_prefix=''): replace_dict['maxflavor'] = maxflavor replace_dict['flavor_def'] = '\n '.join(flavor_text) + # Crossing-symmetry demonstration: when crossing is active for this + # matrix element, evaluate one genuinely crossed process (the first + # valid non-identity crossing) at the same phase-space point and print + # its per-leg PDG (via GET_PDG_FOR_FLAVOR) and matrix element, so that + # `make check` visibly exercises the crossing machinery. + replace_dict['crossing_example'] = \ + self._get_check_sa_crossing_example(matrix_element, proc_prefix) + fsock = open(pjoin(self.mgme_dir, 'madgraph', 'iolibs', 'template_files', 'check_sa.f'), 'r') text = fsock.read() fsock.close() @@ -4526,8 +6062,11 @@ class ProcessExporterFortranMatchBox(ProcessExporterFortranSA): matrix_template = "matrix_standalone_matchbox.inc" - - @staticmethod + # Inherits from the standalone exporter but writes its own template, which + # has no crossing machinery: the capability does not carry over. + supports_crossing = False + + @staticmethod def get_color_string_lines(matrix_element): """Return the color matrix definition lines for this matrix element. Split rows in chunks of size n.""" @@ -5249,6 +6788,26 @@ def write_auto_dsig_file(self, writer, matrix_element, proc_id = ""): replace_dict['proc_id'] = proc_id replace_dict['numproc'] = 1 + # Flavor lookup + SMATRIX call default to this subprocess's own matrix + # element; a cross-group dependent (Track B) overrides them below to route + # to a base group's symlinked crossing-aware SMATRIX. + replace_dict['dsig_xg_decl'] = '' + replace_dict['dsig_xg_decl_vec'] = '' + replace_dict['dsig_xg_decl_multi'] = '' + replace_dict['dsig_xg_helper'] = '' + replace_dict['dsig_getflavor'] = \ + ' CALL GET_FLAVOR%s(IFLAV, FLAVOR)' % proc_id + replace_dict['dsig_smatrix_call'] = ( + ' CALL SMATRIX%s(P1, IFLAV, RHEL, RCOL,channel,1, DSIGUU,' + ' selected_hel(1), selected_col(1))' % proc_id) + # ... and the same for the vectorised (SMATRIX_MULTI) path. + replace_dict['dsig_getflavor_vec'] = \ + ' CALL GET_FLAVOR%s(IFLAV_VEC(IVEC), FLAVOR)' % proc_id + replace_dict['dsig_smatrix_vec_name'] = 'SMATRIX%s' % proc_id + replace_dict['dsig_smatrix_vec_flav'] = 'IFLAV_VEC(IVEC)' + replace_dict['dsig_smatrix_vec_chan'] = 'channels(IVEC)' + replace_dict['dsig_smatrix_vec_post'] = '' + # Set dsig_line if ninitial == 1: # No conversion, since result of decay should be given in GeV @@ -5777,6 +7336,10 @@ def generate_subprocess_directory(self, matrix_element, self.write_leshouche_file(writers.FortranWriter(filename), matrix_element) + filename = pjoin(Ppath, 'colorflow.inc') + self.write_colorflow_file(writers.FortranWriter(filename), + matrix_element) + filename = pjoin(Ppath, 'maxamps.inc') nb_flavor_per_proc = matrix_element.get_nb_flavors() # Compute actual MAXPROC: for merged processes each flavor combination @@ -6101,7 +7664,40 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, 'set_amp2_line': 'ANS=ANS*AMP2(MAPCONFIG(ICONFIG))/XTOT', 'flavor_mask_decl':'', 'flavor_mask_setup':''} - + + # Cross-group (Track B) colour selection: an ME that serves as a base for a + # crossed dependent publishes its per-flow JAMP2 (in its own flow order) so + # the dependent can reselect colour natively (see _dsig_crossgroup_fills / + # XG_SELCOL). Emitted only for those bases -- every other madevent ME keeps + # both holes empty and is byte-identical. + if id(matrix_element) in getattr(self, '_crossgroup_base_mes', set()): + replace_dict['xg_jamp2_decl'] = ( + 'C Cross-group (Track B): publish this ME\'s per-flow JAMP2 so a' + '\nC crossed dependent can reselect colour in its own flow space.' + '\n DOUBLE PRECISION XG_JAMP2(0:MAXFLOW,VECSIZE_MEMMAX)' + '\n COMMON/TO_XG_JAMP2/XG_JAMP2') + replace_dict['xg_jamp2_pub'] = ( + ' DO I=0,INT(JAMP2(0))' + '\n XG_JAMP2(I,IVEC) = JAMP2(I)' + '\n ENDDO') + else: + replace_dict['xg_jamp2_decl'] = '' + replace_dict['xg_jamp2_pub'] = '' + + # Crossing holes of matrix_madevent_group_v4.inc: the group SMATRIX + # decodes the extended FLAV_IDX and evaluates the crossed process through + # a runtime IC. Only that template carries the holes (the single-process + # matrix_madevent_v4.inc does not), and a process whose definition pins a + # specific s-channel has its crossings generated separately, so it stays + # on the plain path. When off the fills reproduce the historical code. + me_use_crossing = ( + self.opt.get('use_crossing', False) + and self.matrix_file == 'matrix_madevent_group_v4.inc' + and not any(self.breaks_crossing_symmetry(proc) + for proc in matrix_element.get('processes'))) + self.fill_crossing_replace_dict_me(matrix_element, replace_dict, + me_use_crossing, proc_id) + mask_decl, mask_setup, n_flavors, active_flavor_mask = \ self._get_flavor_mask_blocks(matrix_element) @@ -6111,12 +7707,17 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, fortran_model.use_flavor_mask = (n_flavors > 0) fortran_model.me_n_flavors = n_flavors fortran_model.me_active_flavor_mask = active_flavor_mask + # With crossing on, the external wavefunction NSF/NSV flag is multiplied + # by IC(i) so a leg crossed between the initial and final state flips + # (the crossed P/NHEL/IC are built by APPLY_CROSSING in SMATRIX). + fortran_model.use_crossing_ic = me_use_crossing try: helas_calls = fortran_model.get_matrix_element_calls(matrix_element) finally: fortran_model.use_flavor_mask = False fortran_model.me_n_flavors = 0 fortran_model.me_active_flavor_mask = None + fortran_model.use_crossing_ic = False if fortran_model.width_tchannel_set_tozero and not ProcessExporterFortranME.done_warning_tchannel: logger.info("Some T-channel width have been set to zero [new since 2.8.0]\n if you want to keep this width please set \"zerowidth_tchannel\" to False", '$MG:BOLD') ProcessExporterFortranME.done_warning_tchannel = True @@ -6354,12 +7955,406 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, replace_dict['return_value'] = (len([call for call in helas_calls if call.find('#') != 0]), ncolor) return replace_dict + #=========================================================================== + # _crossgroup_base_files + #=========================================================================== + def _crossgroup_base_files(self, base_proc_id): + """Base-group matrix-element source files a cross-group dependent symlinks + into its own P directory so the makefile compiles the shared crossing- + aware SMATRIX there too. Correctness-first: the source is reused (symlink) + but each directory still compiles its own object; sharing the compiled .o + is a later build step. With helicity recycling the base keeps + matrix_orig.f plus the template for the run-time optimised copy, + otherwise a single matrix.f.""" + if self.opt.get('hel_recycling'): + return ['matrix%d_orig.f' % base_proc_id, + 'template_matrix%d.f' % base_proc_id] + return ['matrix%d.f' % base_proc_id] + + def write_crossgroup_mk(self, base_dir, base_proc_id): + """Write crossgroup.mk in the current (dependent) P directory. Included by + the shared makefile (`-include crossgroup.mk`), it makes the base group's + matrix object file be SYMLINKED from the base directory rather than + recompiled from the symlinked source -- the whole point of the reuse. It is + built in the base directory first (the specific rule overrides the + makefile's %.o:%.f pattern; the recursive rule is the standalone ordering + fallback -- the top-level parallel makefile also orders base before + dependents). + + With helicity recycling BOTH matrix_orig.o (the full matrix element) and + matrix_optim.o are shared: gen_ximprove bakes the base optim over the + UNION good-hel of the crossing class (see crossgroup_helunion.dat), so it + covers every member. Without recycling the single matrix.o is the full, + shareable object.""" + objs = ['matrix%d.o' % base_proc_id] + if self.opt.get('hel_recycling'): + objs = ['matrix%d_orig.o' % base_proc_id, + 'matrix%d_optim.o' % base_proc_id] + lines = ['# Track B cross-group crossing: reuse the base group\'s compiled', + '# matrix element (%s) instead of recompiling the symlinked source.' + % base_dir] + for o in objs: + base_o = pjoin('..', base_dir, o) + lines.append('%s: %s' % (o, base_o)) + lines.append('\tln -sf %s %s' % (base_o, o)) + lines.append('%s:' % base_o) + lines.append('\t+$(MAKE) -C %s %s' % (pjoin('..', base_dir), o)) + open('crossgroup.mk', 'w').write('\n'.join(lines) + '\n') + + def write_crossgroup_helunion(self, subproc_path): + """Write crossgroup_helunion.dat in each cross-group BASE directory. Each + line is ` p1 p2 ... pNCOMB`, a base->base helicity + permutation of one dependent crossing: the dependent is good at helicity h + iff p[h] is good for the base. gen_ximprove reads it and bakes the base + optim over the UNION good-hel of the class (G_base plus the images under + these permutations), so a single compiled optim serves every member.""" + for base_dir, per_proc in self._crossgroup_helperms.items(): + lines = [] + for base_proc_id, perms in sorted(per_proc.items()): + for pi in perms: + lines.append('%d %s' % (base_proc_id, + ' '.join(str(x) for x in pi))) + if lines: + with open(pjoin(subproc_path, base_dir, + 'crossgroup_helunion.dat'), 'w') as f: + f.write('\n'.join(lines) + '\n') + + def write_crossgroup_parallel_makefile(self, subproc_path): + """Write SubProcesses/makefile_madevent so every P directory builds with a + single `make -f makefile_madevent -jN` (madevent binaries) or `... forhel`. + Cross-group dependents (Track B) are ordered AFTER their base directory so + the base's shared objects exist to be symlinked in; make's dependency graph + then gives both the ordering and full parallelism. Each target just + delegates to that directory's own makefile.""" + lines = [ + '# Generated (Track B): build every P directory in one parallel call:', + '# make -f makefile_madevent -j # the madevent binaries', + '# make -f makefile_madevent -j forhel # the madevent_forhel ones', + '# Cross-group dependents are ordered after their base directory.', + 'PDIRS := $(shell cat subproc.mg 2>/dev/null | tr -d " \\t")', + 'MADEVENT := $(addsuffix /madevent,$(PDIRS))', + 'FORHEL := $(addsuffix /madevent_forhel,$(PDIRS))', + '', + '.PHONY: all forhel $(MADEVENT) $(FORHEL)', + 'all: $(MADEVENT)', + 'forhel: $(FORHEL)', + '', + '$(MADEVENT) $(FORHEL):', + '\t+$(MAKE) -C $(@D) $(@F)', + '', + '# cross-group ordering (dependent directory waits for its base):', + ] + for dep, base in self._crossgroup_dirs: + lines.append('%s/madevent: %s/madevent' % (dep, base)) + lines.append('%s/madevent_forhel: %s/madevent_forhel' % (dep, base)) + open(pjoin(subproc_path, 'makefile_madevent'), 'w').write( + '\n'.join(lines) + '\n') + + #=========================================================================== + # _dsig_crossgroup_fills + #=========================================================================== + def _crossed_helicity_configs(self, base_me, cross, signed=True): + """The base helicity rows transformed by the crossing. Two consumers need + two DIFFERENT transforms, selected by `signed`: + + * signed=True -- the good-hel-set remap (_crossgroup_base_helperm): + crossed[hb][k] = base_row[PERM[k]]*SGN[k]. This is the table-space + permutation sigma the GHREMAP relation validates (_GOODHEL_PROBE): a + base row is good WHEN CROSSED iff sigma^-1 of it is good for the base's + own process, so the shared optim's good-hel union is + G_base U sigma(G_base). SGN belongs here because the crossed physical + config bh[PERM[k]]*SGN[k]*IC_IN[PERM[k]] reduces to the bare table value + bh[PERM[k]]*SGN[k] once the common IC_IN[PERM[k]] is stripped. + + * signed=False -- the event helicity LABEL (_crossgroup_helmap): + crossed[hb][k] = base_row[PERM[k]], exactly what APPLY_CROSSING_TABLE + writes into NHEL (it permutes NHEL -- NHEL(XK)=NHEL_IN(PERM(XK)) -- but + flips only the IC/NSF flags -- IC(XK)=SGN(XK)*IC_IN(PERM(XK))). The LHE + label is the raw NHEL table value (unwgt.f: jpart(7,i)=nhel(i)), never + NHEL*IC, and the base MATRIX gives leg k the physical spinor helicity + NHEL(k)*IC(k)=base_row[PERM[k]]*SGN[k]*IC_IN[PERM[k]] + =base_row[PERM[k]]*IC_dep[k] (SGN[k]*IC_IN[PERM[k]] is exactly slot k's + own NSF in the dependent), matching the dependent's native label + NHEL_dep[k]*IC_dep[k] iff NHEL_dep[k]=base_row[PERM[k]] -- NO extra sign. + Multiplying SGN here double-counts the flip and mislabels every + fermion/vector leg that swaps initial<->final. + + Returns (base_rows, crossed_rows) as tuples in the base NHEL order.""" + bh = [tuple(x) for x in base_me.get_helicity_matrix()] + tables = ProcessExporterFortran.compute_crossing_tables(self, base_me) + nx = tables['nexternal'] + P = [tables['perm'][cross * nx + k] for k in range(nx)] + S = [tables['ic'][cross * nx + k] for k in range(nx)] if signed \ + else [1] * nx + crossed = [tuple(row[P[k]] * S[k] for k in range(nx)) for row in bh] + return bh, crossed + + def _crossgroup_base_helperm(self, base_me, cross): + """1-based base->base helicity permutation of a crossing: pi[hb] = the base + index whose NHEL row equals the crossed row of hb. So the dependent for + this crossing has good helicity hb iff pi[hb] is good for the base -- which + is how gen_ximprove expands the base optim over the UNION good-hel of the + class so it can be shared. Uses the SIGNED crossed config (the GHREMAP + sigma), unlike the event-label helmap. Returns None if not a clean + permutation.""" + bh, crossed = self._crossed_helicity_configs(base_me, cross) + bhpos = {cfg: i for i, cfg in enumerate(bh)} + pi = [bhpos.get(c, -1) for c in crossed] + if -1 in pi or sorted(pi) != list(range(len(bh))): + return None + return [p + 1 for p in pi] + + def _diagram_leg_subsets(self, me): + """Per diagram number, the set of its internal propagators' canonical + external-leg subsets -- a crossing-covariant topology signature (a + propagator is the set of external legs whose momenta flow through it, and + a subset and its complement are the same propagator). get_s_and_t_channels + numbers the propagators negative, external-inward; the final t-channel + 'propagator' is a single external leg and is dropped (canonical length 1). + Returns (dict diagram_number -> frozenset of subsets, nexternal).""" + nx, nini = me.get_nexternal_ninitial() + model = me.get('processes')[0].get('model') + npdg = model.get_first_non_pdg() + allset = frozenset(range(1, nx + 1)) + canon = lambda s: min(s, allset - s, key=lambda x: (len(x), sorted(x))) + out = {} + for diag in me.get('diagrams'): + sch, tch = diag.get('amplitudes')[0].get_s_and_t_channels( + nini, model, npdg) + ext = {i: frozenset([i]) for i in range(1, nx + 1)} + subs = set() + for vert in list(sch) + list(tch): + legs = vert.get('legs') + daughters = [l.get('number') for l in legs[:-1]] + s = frozenset().union(*[ext.get(d, frozenset([d])) + for d in daughters]) if daughters \ + else frozenset() + ext[legs[-1].get('number')] = s + if 2 <= len(canon(s)): + subs.add(canon(s)) + out[diag.get('number')] = frozenset(subs) + return out, nx + + def _crossgroup_configmap(self, dep_me, base_me, cross): + """1-based map from a dependent diagram number to the base diagram number + of the same topology under the crossing. The dependent's genps samples its + own config's poles, but the base SMATRIX enhances AMP2(channel), so channel + must name the matching BASE diagram; otherwise the importance sampling is + mis-paired (this only affects the variance, never the result -- summing the + channels gives the full integral for any bijective pairing). Returns the + identity if the diagrams cannot be cleanly matched.""" + bsub, nx = self._diagram_leg_subsets(base_me) + dsub, _ = self._diagram_leg_subsets(dep_me) + ngraphs = len(dep_me.get('diagrams')) + bsig = {frozenset(v): k for k, v in bsub.items()} + tables = ProcessExporterFortran.compute_crossing_tables(self, base_me) + P = [tables['perm'][cross * nx + k] for k in range(nx)] + d2b = {k + 1: P[k] + 1 for k in range(nx)} # dep leg -> base leg + allset = frozenset(range(1, nx + 1)) + canon = lambda s: min(s, allset - s, key=lambda x: (len(x), sorted(x))) + cmap = list(range(1, ngraphs + 1)) + for dd, ds in dsub.items(): + if not 1 <= dd <= ngraphs: + return list(range(1, ngraphs + 1)) + sig = frozenset(canon(frozenset(d2b[l] for l in sub)) for sub in ds) + if sig in bsig: + cmap[dd - 1] = bsig[sig] + if sorted(cmap) != list(range(1, ngraphs + 1)): + return list(range(1, ngraphs + 1)) + return cmap + + def _dsig_crossgroup_fills(self, matrix_element, proc_id, crossgroup): + """Fill the cross-group (Track B) holes of auto_dsig_v4.inc for a + dependent subprocess that has no matrix element of its own and routes to + a base group's symlinked crossing-aware SMATRIX. + + * beams -- the dependent cannot define its own GET_FLAVOR (it would clash + with the symlinked base's), so its FLAVOR table (group-position coded, + exactly as GET_FLAVOR would return) is inlined as DSIG_XGFLAV and + indexed by IFLAV for the PDF. + * SMATRIX -- dispatch to the base SMATRIX with the crossed FLAV_IDX + (DSIG_XGROUTE(IFLAV)) instead of IFLAV; the base crosses the momenta and + rebuilds the crossed denominator internally so ANS is this subprocess's + matrix element. Momenta/PDF/phase space stay this subprocess's own. + * event helicity/colour -- the base returns selected_hel/selected_col in + ITS enumeration; the event is written through this subprocess's own + get_helicities / ICOLUP, so remap the index base -> dependent per flavor + (DSIG_XGHEL / DSIG_XGCOL). Colour is the identity for colourless. + * multi-channel -- the base enhances AMP2(channel) in its diagram + numbering; translate this subprocess's channel to the matching base + diagram (DSIG_XGCONFIG) so importance sampling stays paired. + All four maps are emitted only when non-identity. + """ + base_proc_id = crossgroup['base_proc_id'] + flav_idx = crossgroup['flav_idx'] # per dep flavor -> base FLAV_IDX + base_me = crossgroup['base_me'] + nflav_base = len(base_me.get_external_flavors_with_iden()) + all_flv = matrix_element.get_external_flavors_with_iden() + model = self.model or matrix_element.get('processes')[0].get('model') + pdg_to_group_pos, max_group_size = self._build_flavor_group_lookup(model) + + # Column-major flat DATA (leg fastest, then flavor) -- avoids an implied- + # do index variable, which need not be declared in every program unit. + positions = [str(self._map_flavor_to_group_pos( + f, pdg_to_group_pos, max_group_size)) + for flav in all_flv for f in flav[0]] + decl = [' INTEGER DSIG_XGFLAV(NEXTERNAL,%d)' % len(all_flv), + ' DATA DSIG_XGFLAV /%s/' % ','.join(positions), + ' INTEGER DSIG_XGROUTE(%d)' % len(all_flv), + ' DATA DSIG_XGROUTE /%s/' % ','.join(str(x) for x in flav_idx)] + + # Per-flavor colour map (base flow -> dependent flow). + colmap = [self._router_colmap(matrix_element, base_me, + (iflav - 1) // nflav_base) + for iflav in flav_idx] + ncol = len(colmap[0]) if colmap else 0 + # Event helicity: relabel the base's selected helicity code into this + # (crossed) subprocess's canonical code by permuting the code's + # mixed-radix digits with the crossing permutation (GET_CROSS_PERM), + # decoded directly by this subprocess's get_nhel. Replaces the explicit + # base->dep helicity map. GET_CROSS_PERM takes the extended base index + # (DSIG_XGROUTE(flav)); cross 0 gives the identity permutation. + nhstate = [len(s) for s in base_me.get_helicity_per_particle()] + decl += [' INTEGER XPERM(NEXTERNAL), XSGN(NEXTERNAL), XDUMF', + ' INTEGER XBDIG(NEXTERNAL), XHR, XHK', + ' INTEGER XNHS(NEXTERNAL)', + ' DATA XNHS /%s/' % ','.join(str(n) for n in nhstate)] + hel_post = ( + '\n CALL CR%s_GET_CROSS_PERM(DSIG_XGROUTE({flav}), XPERM,' + ' XSGN, XDUMF)' + '\n IF (selected_hel{idx}.GE.1) THEN' + '\n XHR = selected_hel{idx} - 1' + '\n DO XHK=NEXTERNAL,1,-1' + '\n XBDIG(XHK) = MOD(XHR, XNHS(XHK))' + '\n XHR = XHR / XNHS(XHK)' + '\n ENDDO' + '\n selected_hel{idx} = 0' + '\n DO XHK=1,NEXTERNAL' + '\n selected_hel{idx} = selected_hel{idx} * XNHS(XPERM(XHK))' + ' + XBDIG(XPERM(XHK))' + '\n ENDDO' + '\n selected_hel{idx} = selected_hel{idx} + 1' + '\n ENDIF') % base_proc_id + # Colour: unlike helicity, a base->dep index relabel of selected_col is + # NOT sufficient. The base SMATRIX picked its flow with select_color, + # which masks the base-order JAMP2 with THIS (dependent) binary's ICOLAMP + # + ICONFIG -- mismatched in flow order AND config space -- so the picked + # flow can be incompatible with the sampled config and addmothers fails + # to reduce its ICOLUP. Reselect natively instead: permute the base's + # published per-flow JAMP2 (COMMON/TO_XG_JAMP2) into this subprocess's + # flow order (DSIG_XGCOL) and run this subprocess's own SELECT_COLOR + # (its own ICOLAMP + ICONFIG), via the XG_SELCOL helper below. Bit-for-bit + # a native colour selection. Only needed when colmap is non-identity + # (identity/colourless: the base's selection is already in this order). + identity_col = list(range(1, ncol + 1)) + col_active = ncol > 0 and any(cm != identity_col for cm in colmap) + dsig_xg_helper = '' + col_scalar_call, col_vec_call = '', '' + if col_active: + col_flat = ','.join(str(x) for col in colmap for x in col) + dsig_xg_helper = self._crossgroup_colsel_helper( + proc_id, ncol, len(colmap), col_flat) + col_scalar_call = ('\n CALL XG_SELCOL%s(RCOL, IFLAV, 1,' + ' SELECTED_COL(1))' % proc_id) + col_vec_call = ('\n CALL XG_SELCOL%s(COL_RAND(IVEC),' + ' IFLAV_VEC(IVEC), IVEC, SELECTED_COL(IVEC))' + % proc_id) + + # Multi-channel config remap: the base SMATRIX enhances AMP2(channel) in + # ITS diagram numbering, but this subprocess's genps samples its own + # config's poles, so translate the channel to the matching base diagram. + ngraphs = len(base_me.get('diagrams')) + configmap = [self._crossgroup_configmap(matrix_element, base_me, + (iflav - 1) // nflav_base) + for iflav in flav_idx] + chan_scalar, chan_vec = 'channel', 'channels(IVEC)' + if any(cm != list(range(1, ngraphs + 1)) for cm in configmap): + decl.append(' INTEGER DSIG_XGCONFIG(%d,%d)' + % (ngraphs, len(configmap))) + decl.append(' DATA DSIG_XGCONFIG /%s/' + % ','.join(str(x) for col in configmap for x in col)) + chan_scalar = 'DSIG_XGCONFIG(channel, IFLAV)' + chan_vec = 'DSIG_XGCONFIG(channels(IVEC), IFLAV_VEC(IVEC))' + + # DSIG_XG* are used from three separate program units (DSIG, DSIG_VEC, + # SMATRIX_MULTI); declare them in each. + decl_block = '\n'.join(decl) + '\n' + + return { + 'dsig_xg_decl': decl_block, + 'dsig_xg_decl_vec': decl_block, + 'dsig_xg_decl_multi': decl_block, + 'dsig_xg_helper': dsig_xg_helper, + 'dsig_getflavor': ' FLAVOR(:) = DSIG_XGFLAV(:, IFLAV)', + 'dsig_smatrix_call': ( + ' CALL SMATRIX%d(P1, DSIG_XGROUTE(IFLAV), RHEL, RCOL, %s,' + ' 1, DSIGUU, selected_hel(1), selected_col(1))' + % (base_proc_id, chan_scalar) + + hel_post.format(idx='(1)', flav='IFLAV') + + col_scalar_call), + # vectorised (SMATRIX_MULTI) path: same routing. The MULTI wrapper + # itself keeps this subprocess's own name (it is defined in this + # auto_dsig); only the inner base-SMATRIX call + flavor are routed. + 'dsig_getflavor_vec': + ' FLAVOR(:) = DSIG_XGFLAV(:, IFLAV_VEC(IVEC))', + 'dsig_smatrix_vec_name': 'SMATRIX%d' % base_proc_id, + 'dsig_smatrix_vec_flav': 'DSIG_XGROUTE(IFLAV_VEC(IVEC))', + 'dsig_smatrix_vec_chan': chan_vec, + 'dsig_smatrix_vec_post': ( + hel_post.format(idx='(IVEC)', flav='IFLAV_VEC(IVEC)') + + col_vec_call), + } + + def _crossgroup_colsel_helper(self, proc_id, ncol, nflav, col_flat): + """Emit XG_SELCOL, the cross-group (Track B) colour-selection + helper for a dependent subprocess. It permutes the base ME's published + per-flow JAMP2 (COMMON/TO_XG_JAMP2, base flow order) into this + subprocess's flow order via DSIG_XGCOL (base flow -> dep flow) and runs + this subprocess's own SELECT_COLOR (its ICOLAMP + the live ICONFIG), so + the returned flow is native to this subprocess -- consistent with its + ICOLUP and its sampled config, unlike a bare base->dep index relabel of + the base's own (mismatched) selection. The DATA is column-major (flow + fastest, then flavor); the writer wraps the long line.""" + return '\n'.join([ + ' SUBROUTINE XG_SELCOL%s(RCOL, IFLAV, IVEC, ICOL)' % proc_id, + ' IMPLICIT NONE', + " INCLUDE 'genps.inc'", + " INCLUDE 'nexternal.inc'", + " INCLUDE 'maxconfigs.inc'", + " INCLUDE 'maxamps.inc'", + " INCLUDE '../../Source/vector.inc'", + ' DOUBLE PRECISION RCOL', + ' INTEGER IFLAV, IVEC, ICOL', + ' INTEGER I', + ' INTEGER MAPCONFIG(0:LMAXCONFIGS), ICONFIG', + ' COMMON/TO_MCONFIGS/MAPCONFIG, ICONFIG', + ' DOUBLE PRECISION XG_JAMP2(0:MAXFLOW,VECSIZE_MEMMAX)', + ' COMMON/TO_XG_JAMP2/XG_JAMP2', + ' DOUBLE PRECISION JD(0:MAXFLOW)', + ' INTEGER DSIG_XGCOL(%d,%d)' % (ncol, nflav), + ' DATA DSIG_XGCOL /%s/' % col_flat, + ' JD(0) = XG_JAMP2(0,IVEC)', + ' DO I=1,%d' % ncol, + ' JD(DSIG_XGCOL(I,IFLAV)) = XG_JAMP2(I,IVEC)', + ' ENDDO', + ' CALL SELECT_COLOR(RCOL, JD, ICONFIG, 1, ICOL, IVEC)', + ' END', + ]) + #=========================================================================== # write_auto_dsig_file #=========================================================================== - def write_auto_dsig_file(self, writer, matrix_element, proc_id = ""): + def write_auto_dsig_file(self, writer, matrix_element, proc_id = "", + crossgroup=None): """Write the auto_dsig.f file for the differential cross section - calculation, includes pdf call information""" + calculation, includes pdf call information. + + When ``crossgroup`` is given (Track B, cross-group crossing) this + subprocess has no matrix element of its own: it symlinks a base group's + crossing-aware SMATRIX and routes to it. The flavor lookup and the + SMATRIX call are then filled with the routed variants (see + _dsig_crossgroup_fills); everything else (PDFs, cuts, phase space) stays + this subprocess's own.""" if not matrix_element.get('processes') or \ not matrix_element.get('diagrams'): @@ -6413,6 +8408,26 @@ def write_auto_dsig_file(self, writer, matrix_element, proc_id = ""): replace_dict['proc_id'] = proc_id replace_dict['numproc'] = 1 + # Flavor lookup + SMATRIX call default to this subprocess's own matrix + # element; a cross-group dependent (Track B) overrides them below to route + # to a base group's symlinked crossing-aware SMATRIX. + replace_dict['dsig_xg_decl'] = '' + replace_dict['dsig_xg_decl_vec'] = '' + replace_dict['dsig_xg_decl_multi'] = '' + replace_dict['dsig_xg_helper'] = '' + replace_dict['dsig_getflavor'] = \ + ' CALL GET_FLAVOR%s(IFLAV, FLAVOR)' % proc_id + replace_dict['dsig_smatrix_call'] = ( + ' CALL SMATRIX%s(P1, IFLAV, RHEL, RCOL,channel,1, DSIGUU,' + ' selected_hel(1), selected_col(1))' % proc_id) + # ... and the same for the vectorised (SMATRIX_MULTI) path. + replace_dict['dsig_getflavor_vec'] = \ + ' CALL GET_FLAVOR%s(IFLAV_VEC(IVEC), FLAVOR)' % proc_id + replace_dict['dsig_smatrix_vec_name'] = 'SMATRIX%s' % proc_id + replace_dict['dsig_smatrix_vec_flav'] = 'IFLAV_VEC(IVEC)' + replace_dict['dsig_smatrix_vec_chan'] = 'channels(IVEC)' + replace_dict['dsig_smatrix_vec_post'] = '' + # Set dsig_line if ninitial == 1: # No conversion, since result of decay should be given in GeV @@ -6491,8 +8506,15 @@ def write_auto_dsig_file(self, writer, matrix_element, proc_id = ""): replace_dict['ncomb']= ncomb helicity_lines = self.get_helicity_lines(matrix_element, add_nb_comb=True) replace_dict['helicity_lines'] = helicity_lines - - context = {'read_write_good_hel':True} + # Canonical helicity decoder tables for GET_NHEL: the per-event helicity + # label is the mixed-radix code, so GET_NHEL decodes it (per-leg states) + # rather than indexing an NHEL config table. + hel_data = self._helstate_data(matrix_element) + replace_dict['maxhel'] = hel_data['maxhel'] + replace_dict['nhstate_data'] = hel_data['nhstate_data'] + replace_dict['states_data'] = hel_data['states_data'] + + context = {'read_write_good_hel':True} if not isinstance(self, ProcessExporterFortranMEGroup): replace_dict['read_write_good_hel'] = self.read_write_good_hel(ncomb) context['nogrouping'] = True @@ -6524,9 +8546,14 @@ def write_auto_dsig_file(self, writer, matrix_element, proc_id = ""): f, pdg_to_group_pos, max_group_size)) for f in flav[0]] replace_dict['get_flavor_matrix'] += ' DATA (FLAVOR(i, %d),i= 1, NEXTERNAL) /%s/\n' % (i+1, ', '.join(flav_positions)) - + # Cross-group dependent (Track B): override the flavor lookup + SMATRIX + # call to route to the symlinked base group's crossing-aware SMATRIX. + if crossgroup is not None: + replace_dict.update( + self._dsig_crossgroup_fills(matrix_element, proc_id, crossgroup)) + if writer: file = open(pjoin(_file_path, \ 'iolibs/template_files/auto_dsig_v4.inc')).read() @@ -7296,6 +9323,205 @@ def write_driver(self, writer, ncomb, n_grouped_proc, v5=True): else: return replace_dict + def _module_color_flows(self, matrix_element): + """Return the colour-flow decomposition (leshouche ICOLUP) of an ME as a + list, one entry per flow, of (colour, anticolour) per leg in leg order. + None if the ME has no colour basis.""" + if not matrix_element.get('color_basis'): + return None + proc = matrix_element.get('processes')[0] + legs = proc.get_legs_with_decays() + ninitial = matrix_element.get_nexternal_ninitial()[1] + repr_dict = {l.get('number'): + proc.get('model').get_particle(l.get('id')).get_color() + * (-1) ** (1 + l.get('state')) for l in legs} + flows = matrix_element.get('color_basis').color_flow_decomposition( + repr_dict, ninitial) + return [[tuple(cf[l.get('number')]) for l in legs] for cf in flows] + + @staticmethod + def _color_flow_canon(flow, states): + """Label-independent canonical form of one colour flow: the set of + (colour-leg, anticolour-leg) connections, with INITIAL-state legs + swapping the two roles so that every colour index connects to an + anticolour index (the LHE convention runs initial-state colour lines + 'through', so without this swap a label can sit in the same slot on two + legs and the flow is not a bijection). Shared by _router_colmap + (topology matching) and _color_flow_code.""" + col, anti = {}, {} + for leg, (c, a) in enumerate(flow): + if states[leg] is False: + c, a = a, c + if c: + col.setdefault(c, []).append(leg) + if a: + anti.setdefault(a, []).append(leg) + conns = set() + for lbl in set(list(col) + list(anti)): + for cc, aa in zip(sorted(col.get(lbl, [])), + sorted(anti.get(lbl, []))): + conns.add((cc, aa)) + return frozenset(conns) + + @staticmethod + def _color_flow_code(conns): + """Canonical integer code of a colour flow from its canonical + connections (see _color_flow_canon). + + Order the colour slots and the anticolour slots by leg -- a gluon holds + one slot of each kind, a sextet two -- then digit i is the index of the + anticolour slot that colour slot i connects to, and + + code = sum_i digit_i * N^i (N = number of anticolour slots) + + This is the colour analogue of the canonical helicity code. It is + injective over a process's colour basis, and crossing-covariant: + relabelling the legs with the crossing permutation carries the base + code onto the crossed process's own code (the initial-state flip is + what makes the connectivity invariant under a crossing, exactly as the + conjugate+state flip cancellation does for the helicity). Note the code + space is N^N while only the basis flows are realised, so -- like the + helicity allowed-list -- the codes are a sparse subset.""" + ordered = sorted(conns) + acol = sorted(a for _c, a in conns) + nslot = len(acol) + code = 0 + used = set() + for i, (_c, a) in enumerate(ordered): + slot = -1 + for j, aa in enumerate(acol): + if aa == a and j not in used: + slot = j + break + if slot < 0: + return None + used.add(slot) + code += slot * (nslot ** i) + return code + + @staticmethod + def _color_flow_slots(conns): + """(colour-slot legs, anticolour-slot legs) of a process, each ordered by + leg, read off one canonical flow. + + This is FLOW-INDEPENDENT process data -- which legs carry a colour resp. + anticolour index is fixed by the colour representations (after the + initial-state flip), not by which flow is picked -- so it is the colour + analogue of the per-leg helicity-state counts, and it is all a decoder + needs besides the code itself.""" + return ([c for c, _a in sorted(conns)], + sorted(a for _c, a in conns)) + + @staticmethod + def _color_flow_decode(code, colslots, acolslots): + """Inverse of _color_flow_code: rebuild a flow's canonical connections + from its code and the process's slot structure (see _color_flow_slots). + + digit_i = (code // N^i) %% N is the anticolour slot that colour slot i + connects to. NOTE: for a leg carrying two slots of the same kind (a + sextet) encode/decode must agree on the tie-break between its slots; + that case is untested.""" + nslot = len(acolslots) + if nslot == 0: + return frozenset() + conns = set() + for i, cleg in enumerate(colslots): + digit = (code // (nslot ** i)) % nslot + conns.add((cleg, acolslots[digit])) + return frozenset(conns) + + def _color_flow_codes(self, matrix_element): + """Canonical colour-flow codes of an ME, one per colour-basis flow in + basis order. None if the ME has no colour basis or a flow is not a clean + colour<->anticolour bijection.""" + flows = self._module_color_flows(matrix_element) + if not flows: + return None + states = [l.get('state') for l in + matrix_element.get('processes')[0].get_legs_with_decays()] + codes = [] + for fl in flows: + code = self._color_flow_code(self._color_flow_canon(fl, states)) + if code is None: + return None + codes.append(code) + return codes + + def _color_code_tables(self, matrix_element): + """Per-ME colour tables for the generated fortran, or None if the ME has + no usable colour code: (codes, colour-slot legs, anticolour-slot legs), + the two slot lists 1-based so they index the fortran leg arrays. + + This is ALL the colour data an ME needs, and it is per-ME rather than + per-(base, crossing) pair: the slot structure is flow-independent (see + _color_flow_slots) and the codes are label-independent, so any crossing + of this ME reuses the same three arrays.""" + flows = self._module_color_flows(matrix_element) + if not flows: + return None + # A negative tag marks a colour SEXTET (color_flow_decomposition stores + # it in the opposite slot, so one leg carries two slots of the same + # kind). The code has no room for that sign, and a decoder rebuilding + # the tags could not restore it, so leave those to the ICOLUP table. + if any(c < 0 or a < 0 for fl in flows for c, a in fl): + return None + states = [l.get('state') for l in + matrix_element.get('processes')[0].get_legs_with_decays()] + conns = [self._color_flow_canon(fl, states) for fl in flows] + codes = [self._color_flow_code(c) for c in conns] + if any(c is None for c in codes) or len(set(codes)) != len(codes): + return None + colslots, acolslots = self._color_flow_slots(conns[0]) + if not acolslots: + return None + # flow-independence is what lets a single table serve every crossing + for c in conns[1:]: + if self._color_flow_slots(c) != (colslots, acolslots): + return None + return (codes, [l + 1 for l in colslots], [l + 1 for l in acolslots]) + + #=========================================================================== + # get_colorflow_lines / write_colorflow_file + #=========================================================================== + def get_colorflow_lines(self, matrix_element, numproc): + """DATA lines of colorflow.inc for one subprocess: the canonical + colour-flow CODE of each flow plus the slot structure needed to decode + it (see _color_flow_code / _color_flow_decode). + + addmothers rebuilds the event's colour tags from these instead of + reading the ICOLUP table, which is why leshouche.inc can drop ICOLUP + whenever this is emitted. NCOLSLOT is 0 when the ME has no usable code + (no colour, a sextet, or an epsilon structure); addmothers then falls + back to ICOLUP, which get_leshouche_lines still writes in that case.""" + tables = self._color_code_tables(matrix_element) + if not tables: + return ["DATA NCOLSLOT(%d)/0/" % (numproc + 1)] + codes, colslots, acolslots = tables + return [ + "DATA NCOLSLOT(%d)/%d/" % (numproc + 1, len(colslots)), + "DATA (ICOLCSL(i,%d),i=1,%d)/%s/" % ( + numproc + 1, len(colslots), + ",".join(str(l) for l in colslots)), + "DATA (ICOLASL(i,%d),i=1,%d)/%s/" % ( + numproc + 1, len(acolslots), + ",".join(str(l) for l in acolslots)), + "DATA (ICOLCODE(i,%d),i=1,%d)/%s/" % ( + numproc + 1, len(codes), + ",".join(str(c) for c in codes)), + ] + + def write_colorflow_file(self, writer, matrix_element): + """Write colorflow.inc for a single (non-grouped) subprocess.""" + writer.writelines(self.get_colorflow_lines(matrix_element, 0)) + return True + + def write_leshouche_file(self, writer, matrix_element): + """Write leshouche.inc, without the ICOLUP table when the colour code + can supply the tags (see get_colorflow_lines).""" + writer.writelines(self.get_leshouche_lines(matrix_element, 0, + drop_icolup=True)) + return True + #=========================================================================== # write_addmothers #=========================================================================== @@ -7549,14 +9775,224 @@ class ProcessExporterFortranMEGroup(ProcessExporterFortranME): matrix_file = "matrix_madevent_group_v4.inc" grouped_mode = 'madevent' + # The group SMATRIX decodes an extended FLAV_IDX (M0) and the router lets + # crossed subprocesses share a base's matrix element, so this exporter can + # honour --use_crossing (the _check_crossing_support gate lets it through). + supports_crossing = True default_opt = {'clean': False, 'complex_mass':False, 'export_format':'madevent', 'mp': False, 'v5_model': True, 'output_options':{}, 'hel_recycling': True } - - + + + #=========================================================================== + # write_matrix_router_file + #=========================================================================== + def _router_colmap(self, router_me, base_me, cross): + """Map each base colour-flow index to this subprocess's flow index. + + The base picks a colour flow in its own basis and events are written + through this subprocess's ICOLUP, whose flow ORDER can differ (the + crossed colour reps decompose the shared colour basis in another order). + Crossing a base flow (leg j <- base flow leg perm^-1(j), colour <-> + anticolour when that leg swapped initial/final) gives the physical flow; + it is matched to the local flow of the same topology (label independent). + Returns a 1-based list indexed by the base flow; identity if unmatchable. + """ + bflows = self._module_color_flows(base_me) + rflows = self._module_color_flows(router_me) + if not bflows or not rflows or len(bflows) != len(rflows): + return list(range(1, len(rflows or []) + 1)) + nx = router_me.get_nexternal_ninitial()[0] + rstates = [l.get('state') for l in + router_me.get('processes')[0].get_legs_with_decays()] + perm, ic, _valid = self.get_crossing_permutation(cross, nx) + inv = [0] * nx + for s, leg in enumerate(perm): + inv[leg] = s + + def canon(flow): + # Topology (label independent), shared with the colour-flow code. + return self._color_flow_canon(flow, rstates) + + rindex = {} + for j, fl in enumerate(rflows): + rindex.setdefault(canon(fl), j + 1) + colmap = [] + for icol, bf in enumerate(bflows): + crossed = [] + for j in range(nx): + c, a = bf[inv[j]] + if ic[inv[j]] == -1: + c, a = a, c + crossed.append((c, a)) + colmap.append(rindex.get(canon(crossed), icol + 1)) + return colmap + + def write_matrix_router_file(self, writer, matrix_element, fortran_model, + proc_id="", config_map=[], subproc_number="", + routing=None, matrix_elements=None): + """Write a light matrix.f for a crossed subprocess that shares a base + subprocess's matrix element. It keeps only GET_FLAVOR (for the PDF) + and a router SMATRIX that, per flavor, calls the base SMATRIX with the + crossed FLAV_IDX from partition_crossing_classes; the heavy MATRIX is + not emitted. get_nhel lives in auto_dsig.f, so it is unaffected. + + The base returns the selected colour flow in the base's flow order, + which must be translated to this subprocess's so the event's ICOLUP is + right. That goes through the canonical colour-flow CODE: decode the + base's code, relabel the legs with the crossing permutation, re-encode + and look the result up in this subprocess's own code table (see + _color_flow_code). The tables are per-ME and shared by every crossing of + the same base, and the same code is what _router_colmap computes at + generation time -- kept as the fallback for an ME with no usable code. + Momenta, PDGs and the helicity index already come out in this + subprocess's own convention.""" + # Reuse the full builder (writer=None) to get the flavor table and the + # info/process/nexternal/max_flavor holes; nothing heavy is written. + replace_dict = self.write_matrix_element_v4( + None, matrix_element, fortran_model, proc_id=proc_id, + config_map=config_map, subproc_number=subproc_number) + dispatch = [] + decl = [] + # Shared temporaries for the runtime helicity encode below. The base + # returns its selected helicity as ITS canonical code; the event is + # written through THIS module's get_nhel, which decodes THIS + # (crossed) module's code -- so relabel by permuting the code's + # mixed-radix digits with the crossing permutation (GET_CROSS_PERM), + # exactly the dependent-vs-base relation dep_states[k]==base_states[PERM[k]]. + encode_used = False + col_used = False + baked_nhs = {} # base_index -> baked base-NHSTATE array name + baked_col = {} # base_index -> baked base colour table names + dep_col = self._color_code_tables(matrix_element) + for flav0, (base_index, iflav) in enumerate(routing): + base_me = matrix_elements[base_index] + nflav_base = len(base_me.get_external_flavors_with_iden()) + cross = (iflav - 1) // nflav_base + colmap = self._router_colmap(matrix_element, base_me, cross) + kw = 'IF' if flav0 == 0 else 'ELSE IF' + dispatch.append(' %s (IFLAV.EQ.%d) THEN' % (kw, flav0 + 1)) + dispatch.append( + ' CALL SMATRIX%d(P, %d, RHEL, RCOL, channel, IVEC, ANS,' + ' IHEL, ICOL)' % (base_index + 1, iflav)) + perm_called = False + # Encode the crossed helicity code (skip cross 0 = identity). + if cross != 0: + encode_used = True + perm_called = True + if base_index not in baked_nhs: + nsname = 'XNHS%d' % (base_index + 1) + nhstate = [len(s) for s in + base_me.get_helicity_per_particle()] + decl.append(' INTEGER %s(NEXTERNAL)' % nsname) + decl.append(' DATA %s /%s/' % ( + nsname, ','.join(str(n) for n in nhstate))) + baked_nhs[base_index] = nsname + nsname = baked_nhs[base_index] + dispatch += [ + ' CALL CR%d_GET_CROSS_PERM(%d, XPERM, XSGN, XDUMF)' + % (base_index + 1, iflav), + ' XHR = IHEL - 1', + ' DO XHK=NEXTERNAL,1,-1', + ' XBDIG(XHK) = MOD(XHR, %s(XHK))' % nsname, + ' XHR = XHR / %s(XHK)' % nsname, + ' ENDDO', + ' IHEL = 0', + ' DO XHK=1,NEXTERNAL', + ' IHEL = IHEL * %s(XPERM(XHK)) + XBDIG(XPERM(XHK))' + % nsname, + ' ENDDO', + ' IHEL = IHEL + 1', + ] + # The base's flow index has to be translated to this subprocess's; + # skip it when the orders already agree (identity map). + if not (colmap and colmap != list(range(1, len(colmap) + 1))): + continue + base_col = self._color_code_tables(base_me) + if (dep_col and base_col + and len(base_col[1]) == len(dep_col[1]) + and len(base_col[2]) == len(dep_col[2])): + # Canonical route: translate through the colour-flow CODE. + # Decode the base's code into its connections, relabel the legs + # with the crossing permutation, re-encode in this subprocess's + # slot order and look the result up in its own code table. The + # tables are per-ME (shared by every crossing of the same base), + # where COLMAP was one array per base-flavor pair. + col_used = True + if base_index not in baked_col: + bcode, bcs, bas = base_col + names = ('XCCD%d' % (base_index + 1), + 'XCCS%d' % (base_index + 1), + 'XCAS%d' % (base_index + 1)) + for nm, vals in zip(names, (bcode, bcs, bas)): + decl.append(' INTEGER %s(%d)' % (nm, len(vals))) + decl.append(' DATA %s /%s/' % ( + nm, ','.join(str(x) for x in vals))) + baked_col[base_index] = names + cdn, csn, asn = baked_col[base_index] + ns = len(dep_col[1]) + if not perm_called: + dispatch.append( + ' CALL CR%d_GET_CROSS_PERM(%d, XPERM, XSGN,' + ' XDUMF)' % (base_index + 1, iflav)) + encode_used = True + dispatch += [ + ' IF (ICOL.GE.1.AND.ICOL.LE.%d) THEN' % len(colmap), + ' XCBAS = %s(ICOL)' % cdn, + ' XCNEW = 0', + ' DO XCI=1,%d' % ns, + ' XCL = XPERM(XDCS(XCI))', + ' XCJ = 1', + ' DO XCK=1,%d' % ns, + ' IF (%s(XCK).EQ.XCL) XCJ = XCK' % csn, + ' ENDDO', + ' XCD = MOD(XCBAS / %d**(XCJ-1), %d)' % (ns, ns), + ' XCL = XPERM(%s(XCD+1))' % asn, + ' DO XCK=1,%d' % ns, + ' IF (XDAS(XCK).EQ.XCL) XCD = XCK-1', + ' ENDDO', + ' XCNEW = XCNEW + XCD * %d**(XCI-1)' % ns, + ' ENDDO', + ' DO XCK=1,%d' % len(dep_col[0]), + ' IF (XDCD(XCK).EQ.XCNEW) ICOL = XCK', + ' ENDDO', + ' ENDIF', + ] + else: + # No usable code (no colour basis, or a flow that is not a + # clean colour<->anticolour bijection): keep the explicit map. + cname = 'COLMAP_%s_%d' % (proc_id, flav0 + 1) + decl.append(' INTEGER %s(%d)' % (cname, len(colmap))) + decl.append(' DATA %s /%s/' % ( + cname, ','.join(str(x) for x in colmap))) + dispatch.append(' IF (ICOL.GE.1.AND.ICOL.LE.%d)' + ' ICOL = %s(ICOL)' % (len(colmap), cname)) + if dispatch: + dispatch.append(' ENDIF') + if col_used: + dcode, dcs, das = dep_col + for nm, vals in (('XDCD', dcode), ('XDCS', dcs), ('XDAS', das)): + decl = [' INTEGER %s(%d)' % (nm, len(vals)), + ' DATA %s /%s/' % ( + nm, ','.join(str(x) for x in vals))] + decl + decl = [' INTEGER XCI, XCJ, XCK, XCD, XCL, XCNEW, XCBAS'] \ + + decl + if encode_used: + decl = [' INTEGER XPERM(NEXTERNAL), XSGN(NEXTERNAL), XDUMF', + ' INTEGER XBDIG(NEXTERNAL), XHR, XHK'] + decl + replace_dict['smatrix_router_decl'] = '\n'.join(decl) + replace_dict['smatrix_router_dispatch'] = '\n'.join(dispatch) + tpl = open(pjoin(_file_path, 'iolibs', 'template_files', + 'matrix_madevent_group_router_v4.inc')).read() + writer.writelines(misc.apply_template(tpl, replace_dict)) + # Router adds no new matrix-element calls; report the module's own color + # count so the group's maxflow sizing stays an upper bound. + calls, ncolor = replace_dict['return_value'] + return 0, ncolor + #=========================================================================== # generate_subprocess_directory #=========================================================================== @@ -7628,9 +10064,95 @@ def generate_subprocess_directory(self, subproc_group, except KeyError: self.proc_characteristic['hel_recycling'] = False self.opt['hel_recycling'] = False + + # Crossing merge: partition the group's matrix elements so that a base + # subprocess keeps its own (crossing-aware) matrix element and the others + # -- whose every flavor is a crossing of a base flavor -- get only a + # light router matrix.f that dispatches to the base SMATRIX with the + # crossed FLAV_IDX (see partition_crossing_classes / the router template). + group_use_crossing = ( + self.opt.get('use_crossing', False) + and not any(self.breaks_crossing_symmetry(proc) + for me in matrix_elements + for proc in me.get('processes'))) + if group_use_crossing: + crossing_bases, crossing_routing = \ + self.partition_crossing_classes(matrix_elements) + crossing_bases = set(crossing_bases) + # Flag the run interface that this output relies on crossing: a shared + # matrix element is reused across physically distinct (crossed) initial + # states. That is fine for the unpolarised proton PDFs, but it is NOT + # compatible with per-beam polarisation or the EVA luminosity, which + # depend on the actual beam particle. Tag the limitation only when + # crossing is materially applied (a router, or a base that evaluates a + # cross>0 flavor), so ordinary polarised runs are not blocked for + # nothing. check_card_consistency turns this into a clear error. + crossing_applied = len(crossing_bases) < len(matrix_elements) or any( + (iflav - 1) // len(matrix_elements[base_index] + .get_external_flavors_with_iden()) > 0 + for route in crossing_routing if route is not None + for (base_index, iflav) in route) + if crossing_applied and \ + 'crossing' not in self.proc_characteristic['limitations']: + self.proc_characteristic['limitations'].append('crossing') + else: + crossing_bases, crossing_routing = None, None + for ime, matrix_element in \ enumerate(matrix_elements): - if self.opt['hel_recycling']: + crossgroup = self._crossgroup.get((group_number, ime)) + if crossgroup is not None: + # Cross-group dependent (Track B): this subprocess's matrix + # element is a crossing of a base group's, in another P directory. + # It generates NO matrix element of its own -- it symlinks the + # base group's compiled crossing-aware SMATRIX (built once there) + # and its auto_dsig routes to it with the crossed FLAV_IDX. Only + # the flavor table (for the PDF) and phase space stay local. + for fname in self._crossgroup_base_files(crossgroup['base_proc_id']): + ln(pjoin('..', crossgroup['base_dir'], fname), log=False) + # Reuse the base group's COMPILED objects (do not recompile the + # symlinked source): crossgroup.mk (included by the shared makefile) + # symlinks matrix_{orig,optim}.o from the base dir, building them + # there first. Also record the dir pair for the parallel top-level + # makefile written at finalize. + self.write_crossgroup_mk(crossgroup['base_dir'], + crossgroup['base_proc_id']) + self._crossgroup_dirs.append((subprocdir, crossgroup['base_dir'])) + # Record this dependent's base->base helicity permutation(s) so + # the base optim can be baked over the UNION good-hel and shared. + base_me = crossgroup['base_me'] + nflav_base = len(base_me.get_external_flavors_with_iden()) + perms = self._crossgroup_helperms.setdefault( + crossgroup['base_dir'], {}).setdefault( + crossgroup['base_proc_id'], []) + for iflav in crossgroup['flav_idx']: + pi = self._crossgroup_base_helperm( + base_me, (iflav - 1) // nflav_base) + if pi is not None and pi != list(range(1, len(pi) + 1)) \ + and pi not in perms: + perms.append(pi) + # ncolor for maxflow sizing: crossing preserves the colour basis, + # so the dependent's own count is the base's. writer=None writes + # nothing, it only returns the flavor/colour bookkeeping. + rd = self.write_matrix_element_v4( + None, matrix_element, fortran_model, proc_id=str(ime+1), + config_map=subproc_group.get('diagram_maps')[ime], + subproc_number=group_number) + calls, ncolor = 0, rd['return_value'][1] + elif crossing_routing is not None and ime not in crossing_bases: + # A router shares a base's matrix element and holds no helicities + # to recycle. Name it matrix_router.f so the makefile globs it + # into both build targets while gen_ximprove (which recycles + # matrix*_orig.f) leaves it alone. + filename = 'matrix%d_router.f' % (ime+1) + calls, ncolor = self.write_matrix_router_file( + writers.FortranWriter(filename), matrix_element, + fortran_model, proc_id=str(ime+1), + config_map=subproc_group.get('diagram_maps')[ime], + subproc_number=group_number, + routing=crossing_routing[ime], + matrix_elements=matrix_elements) + elif self.opt['hel_recycling']: filename = 'matrix%d_orig.f' % (ime+1) replace_dict = self.write_matrix_element_v4(None, matrix_element, @@ -7698,7 +10220,8 @@ def generate_subprocess_directory(self, subproc_group, filename = 'auto_dsig%d.f' % (ime+1) self.write_auto_dsig_file(writers.FortranWriter(filename), matrix_element, - str(ime+1)) + str(ime+1), + crossgroup=crossgroup) # Keep track of needed quantities tot_calls += int(calls) @@ -7767,6 +10290,10 @@ def generate_subprocess_directory(self, subproc_group, self.write_leshouche_file(writers.FortranWriter(filename), subproc_group) + filename = 'colorflow.inc' + self.write_colorflow_file(writers.FortranWriter(filename), + subproc_group) + filename = 'maxamps.inc' # get number of non identical flavor for each matrix element file #for me in matrix_elements: @@ -8271,11 +10798,21 @@ def write_leshouche_file(self, writer, subproc_group): for iproc, matrix_element in \ enumerate(subproc_group.get('matrix_elements')): all_lines.extend(self.get_leshouche_lines(matrix_element, - iproc)) + iproc, drop_icolup=True)) # Write the file writer.writelines(all_lines) return True + def write_colorflow_file(self, writer, subproc_group): + """Write colorflow.inc for a subprocess group (one entry per ME).""" + + all_lines = [] + for iproc, matrix_element in \ + enumerate(subproc_group.get('matrix_elements')): + all_lines.extend(self.get_colorflow_lines(matrix_element, iproc)) + writer.writelines(all_lines) + return True + def finalize(self,*args, second_exporter=None, **opts): @@ -11587,8 +14124,12 @@ def ExportV4Factory(cmd, noclean, output_type='default', group_subprocesses=True opt.update({'clean': not noclean, 'complex_mass': cmd.options['complex_mass_scheme'], 'export_format':cmd._export_format, - 'mp': False, - 'sa_symmetry':False, + 'mp': False, + 'sa_symmetry':False, + # --use_crossing of the generate/add process command: when off, + # the standalone matrix.f is written without any crossing + # machinery (see ProcessExporterFortranSA.write_matrix_element_v4). + 'use_crossing': getattr(cmd, '_use_crossing', True), 'model': cmd._curr_model.get('name'), 'v5_model': False if cmd._model_v4_path else True, 'running': cmd._curr_model.get('running_elements'), diff --git a/madgraph/iolibs/gen_infohtml.py b/madgraph/iolibs/gen_infohtml.py index 2f5dee8e8..db308ea1d 100755 --- a/madgraph/iolibs/gen_infohtml.py +++ b/madgraph/iolibs/gen_infohtml.py @@ -236,18 +236,28 @@ def define_info_tables(self): return text def get_diagram_nb(self, proc, id): - - path = os.path.join(self.dir, 'SubProcesses', proc, 'matrix%s.f' % id) + nb_diag = 0 - pat = re.compile(r'''Amplitude\(s\) for diagram number (\d+)''' ) - if not os.path.exists(path): - path = os.path.join(self.dir, 'SubProcesses', proc, 'matrix%s_orig.f' % id) + path = None + for suffix in ('%s.f', '%s_orig.f', '%s_router.f'): + cand = os.path.join(self.dir, 'SubProcesses', proc, + 'matrix' + suffix % id) + if os.path.exists(cand): + path = cand + break + # A crossing-router subprocess shares a base subprocess's matrix element + # (its matrix_router.f holds no diagrams of its own), so it has no + # diagram-number comment: count 0. + if path is None: + return 0 text = open(path).read() + match = None for match in re.finditer(pat, text): pass - nb_diag += int(match.groups()[0]) - + if match is not None: + nb_diag += int(match.groups()[0]) + return nb_diag diff --git a/madgraph/iolibs/helas_call_writers.py b/madgraph/iolibs/helas_call_writers.py index 2910e6a66..5a65ef1ef 100755 --- a/madgraph/iolibs/helas_call_writers.py +++ b/madgraph/iolibs/helas_call_writers.py @@ -285,9 +285,12 @@ def get_wavefunction_call(self, wavefunction): call = fct(wavefunction) - if self.options['zerowidth_tchannel'] and wavefunction.is_t_channel(): - call, n = re.subn(r',\s*fk_(?!ZERO)\w*\s*,', ', ZERO,', str(call), flags=re.I) - if n: + if self.options['zerowidth_tchannel'] and wavefunction.is_t_channel(): + # The width i*M*Gamma is now dropped inside the ALOHA propagator + # routine itself, at runtime, for spacelike (P^2<0) momenta -- see + # aloha.t_channel_width / aloha_writers. We no longer rewrite the call + # to pass ZERO; we only flag a non-zero width here for the notice. + if re.search(r',\s*fk_(?!ZERO)\w*\s*,', str(call), flags=re.I): self.width_tchannel_set_tozero = True return call @@ -1042,6 +1045,11 @@ def __init__(self, argument={}, hel_sum = False, options={}): self.use_flavor_mask = False self.me_n_flavors = 0 self.me_active_flavor_mask = None + # When True the external wavefunction NSF/NSV flag is multiplied by + # IC(i), letting the caller cross a leg between the initial and the + # final state. Only the exporters whose template passes a meaningful + # IC turn this on (see generate_external_wavefunction). + self.use_crossing_ic = False super(FortranUFOHelasCallWriter, self).__init__(argument, options=options) def format_helas_object(self, prefix, number): @@ -1203,14 +1211,30 @@ def generate_external_wavefunction(self,argument): else: call = call + "%(mass)s," call = call + "NHEL(%(number_external)d)," + wf_object = self.format_helas_object('W(', '%(me_id)d') if argument.get('spin') == 2: - call = call + "%(state_id)+d, FLAVOR(%(number_external)d),{0})".format(\ - self.format_helas_object('W(','%(me_id)d')) + suffix = ", FLAVOR(%(number_external)d)," + wf_object + ")" else: - call = call + "%(state_id)+d,{0})".format(\ - self.format_helas_object('W(','%(me_id)d')) - - call_function = lambda wf: call % wf.get_external_helas_call_dict() + suffix = "," + wf_object + ")" + # Two variants of the NSF/NSV flag: bare, or multiplied by IC so + # that the caller can flip a leg between the initial and the final + # state (crossing). Flipping that flag is what crosses the leg: + # helas stores the momentum as p*nsf and uses nhel*nsf. Only + # templates that actually pass a meaningful IC may use the second + # form -- several (e.g. the madevent MATRIX) declare IC as a local + # and never set it, so reading it there would give garbage. + call = (call + "%(state_id)+d" + suffix, + call + "%(state_id)+d*IC(%(number_external)d)" + suffix) + + if isinstance(call, tuple): + # The flag is read at emission time, not here: a single writer + # instance is reused across outputs (standalone then madevent), so + # the choice must not be baked into the cached call. + call_function = lambda wf: \ + call[1 if self.use_crossing_ic else 0] % \ + wf.get_external_helas_call_dict() + else: + call_function = lambda wf: call % wf.get_external_helas_call_dict() self.add_wavefunction(argument.get_call_key(), call_function) def generate_all_other_helas_objects(self,argument): @@ -1676,6 +1700,10 @@ def __init__(self, argument={}, options={}): self.use_flavor_mask = False self.me_n_flavors = 0 self.me_active_flavor_mask = None + # When True, external HELAS calls permute the helicity through perm[] + # and multiply their NSF flag by ic[] so a crossed leg flips (set by the + # standalone_cpp exporter around calculate_wavefunctions generation). + self.use_crossing_ic = False super(CPPUFOHelasCallWriter, self).__init__(argument, options=options) def _flavor_mask_prefix(self, obj, kind): @@ -1703,6 +1731,34 @@ def _flavor_mask_prefix(self, obj, kind): bit = (idx - 1) % 64 return 'if ((%s[%d] & (1ULL << %d)) != 0ULL) ' % (array, word, bit) + def _cpp_external_call(self, wf, routine, spin, is_boson): + """Build the ixxxxx/oxxxxx/vxxxxx/sxxxxx call for an external leg. + + When self.use_crossing_ic is False this reproduces the historical call + byte-for-byte. When True the helicity is read through perm[] and the NSF + flag is multiplied by ic[], so a leg the crossing moved between the + initial and the final state flips (helas folds the momentum sign change + and the helicity flip out of that flag).""" + n = wf.get('number_external') - 1 + me = wf.get('me_id') - 1 + if not is_boson: + # For fermions, need particle/antiparticle + nsf = - (-1) ** wf.get_with_flow('is_part') + else: + # For bosons (incl. scalars), need initial/final + nsf = (-1) ** (wf.get('state') == 'initial') + cross = getattr(self, 'use_crossing_ic', False) + hel_tok = ('hel[perm[%d]]' % n) if cross else ('hel[%d]' % n) + nsf_tok = ('%+d*ic[%d]' % (nsf, n)) if cross else ('%+d' % nsf) + if spin == 1: + return '%s(p[perm[%d]],%s,w[%d]);' % (routine, n, nsf_tok, me) + elif spin == 2: + return '%s(p[perm[%d]],mME[%d],%s,%s, flavor[%d],w[%d]);' % \ + (routine, n, n, hel_tok, nsf_tok, n, me) + else: + return '%s(p[perm[%d]],mME[%d],%s,%s,w[%d]);' % \ + (routine, n, n, hel_tok, nsf_tok, me) + def generate_helas_call(self, argument): """Routine for automatic generation of C++ Helas calls according to just the spin structure of the interaction. @@ -1743,50 +1799,17 @@ def generate_helas_call(self, argument): if isinstance(argument, helas_objects.HelasWavefunction) and \ not argument.get('mothers'): # String is just ixxxxx, oxxxxx, vxxxxx or sxxxxx - call = call + HelasCallWriter.mother_dict[\ + routine = HelasCallWriter.mother_dict[\ argument.get_spin_state_number()].lower() # Fill out with X up to 6 positions - call = call + 'x' * (6 - len(call)) - # Specify namespace for Helas calls - call = call + "(p[perm[%d]]," - if argument.get('spin') != 1: - # For non-scalars, need mass and helicity - call = call + "mME[%d],hel[%d]," - if argument.get('spin') == 2: - call = call + "%+d, flavor[%i],w[%d]);" - else: - call = call + "%+d,w[%d]);" - if argument.get('spin') == 1: - call_function = lambda wf: call % \ - (wf.get('number_external')-1, - # For boson, need initial/final here - (-1) ** (wf.get('state') == 'initial'), - wf.get('me_id')-1) - elif argument.is_boson(): - call_function = lambda wf: call % \ - (wf.get('number_external')-1, - wf.get('number_external')-1, - wf.get('number_external')-1, - # For boson, need initial/final here - (-1) ** (wf.get('state') == 'initial'), - wf.get('me_id')-1) - elif argument.get('spin') == 2: - call_function = lambda wf: call % \ - (wf.get('number_external')-1, - wf.get('number_external')-1, - wf.get('number_external')-1, - # For fermions, need particle/antiparticle - - (-1) ** wf.get_with_flow('is_part'), - wf.get('number_external')-1, - wf.get('me_id')-1) - else: - call_function = lambda wf: call % \ - (wf.get('number_external')-1, - wf.get('number_external')-1, - wf.get('number_external')-1, - # For fermions, need particle/antiparticle - - (-1) ** wf.get_with_flow('is_part'), - wf.get('me_id')-1) + routine = routine + 'x' * (6 - len(routine)) + spin = argument.get('spin') + is_boson = argument.is_boson() + # The crossing decision (use_crossing_ic) is read at emission time, + # inside the cached lambda, because one session reuses the writer + # across a crossing output and a plain one (see the fortran writer). + call_function = lambda wf: self._cpp_external_call( + wf, routine, spin, is_boson) else: if isinstance(argument, helas_objects.HelasWavefunction): outgoing = argument.find_outgoing_number() diff --git a/madgraph/iolibs/template_files/addmothers.f b/madgraph/iolibs/template_files/addmothers.f index 85fb5b596..a776539b6 100644 --- a/madgraph/iolibs/template_files/addmothers.f +++ b/madgraph/iolibs/template_files/addmothers.f @@ -65,7 +65,19 @@ subroutine addmothers(ip,jpart,pb,isym,jsym,rscale,aqcd,aqed,buff, integer icolup(2,nexternal,maxflow,maxsproc) include 'leshouche.inc' include 'coloramps.inc' - + +c Canonical colour-flow code: the event's colour tags are rebuilt from it +c instead of being read out of the ICOLUP table (which leshouche.inc then +c does not even write). NCOLSLOT(numproc) is the number of colour slots of +c that subprocess, or 0 when the flows have no usable code (no colour, a +c sextet, or an epsilon structure) -- then ICOLUP is there and is used. + integer ncolslot(maxsproc) + integer icolcsl(nexternal,maxsproc) + integer icolasl(nexternal,maxsproc) + integer icolcode(maxflow,maxsproc) + integer nslot,ccode,idig,icleg,ialeg,itag + include 'colorflow.inc' + logical OnBW(-nexternal:0) !Set if event is on B.W. common/to_BWEvents/ OnBW CHARACTER temp*600,temp0*7,integ*1,float*18 @@ -131,6 +143,33 @@ subroutine addmothers(ip,jpart,pb,isym,jsym,rscale,aqcd,aqed,buff, is_LC = .false. icol = abs(icol) endif + if (ncolslot(numproc).gt.0) then +c Rebuild the tags from the canonical colour-flow code. Slot k of the code +c connects colour slot ICOLCSL(k) to anticolour slot ICOLASL(digit+1); give +c each connection its own tag. icolalt is already zeroed above, so only the +c connected slots need writing. The two roles are swapped back on the +c initial-state legs: color_flow_decomposition reverses that pair to follow +c the les houches convention, and the code is built on the unreversed form. + nslot = ncolslot(numproc) + ccode = icolcode(icol,numproc) + do k=1,nslot + idig = mod(ccode/nslot**(k-1), nslot) + icleg = icolcsl(k,numproc) + ialeg = icolasl(idig+1,numproc) + itag = 500+k + if (icleg.le.nincoming) then + icolalt(2,isym(icleg,jsym))=itag + else + icolalt(1,isym(icleg,jsym))=itag + endif + if (ialeg.le.nincoming) then + icolalt(1,isym(ialeg,jsym))=itag + else + icolalt(2,isym(ialeg,jsym))=itag + endif + enddo + maxcolor=500+nslot + else do i=1,nexternal icolalt(1,isym(i,jsym))=icolup(1,i,icol,numproc) icolalt(2,isym(i,jsym))=icolup(2,i,icol,numproc) @@ -139,6 +178,7 @@ subroutine addmothers(ip,jpart,pb,isym,jsym,rscale,aqcd,aqed,buff, if (abs(icolup(2,i,icol, numproc)).gt.maxcolor) maxcolor=icolup(2,i,icol, numproc) enddo endif + endif diff --git a/madgraph/iolibs/template_files/auto_dsig_v4.inc b/madgraph/iolibs/template_files/auto_dsig_v4.inc index 4730c4fb5..23b623038 100644 --- a/madgraph/iolibs/template_files/auto_dsig_v4.inc +++ b/madgraph/iolibs/template_files/auto_dsig_v4.inc @@ -106,7 +106,7 @@ c double precision P1(0:3, nexternal) integer channel double precision rwgt_value -C +%(dsig_xg_decl)sC C DATA C %(pdf_data)s @@ -166,7 +166,7 @@ C Continue only if IMODE is 0, 4 or 5 WGT = WGT * %(maxflavor)d endif endif - CALL GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR) +%(dsig_getflavor)s %(passcuts_begin)s ## if( nogrouping) { ! for no grouping update the scale here (done in main autodsig for grouping @@ -214,7 +214,7 @@ C and IFLAV are still set above so SMATRIX gets valid arguments. rwgt_value=1d0 endif - CALL SMATRIX%(proc_id)s(P1, IFLAV, RHEL, RCOL,channel,1, DSIGUU, selected_hel(1), selected_col(1)) +%(dsig_smatrix_call)s DSIGUU = DSIGUU* rwgt_value @@ -389,9 +389,9 @@ c C Per-event MLM graph: igraphs(1) from REWGT (0 = no MLM) integer igraph(VECSIZE_MEMMAX) common/vec_igraph/igraph -C +%(dsig_xg_decl_vec)sC C DATA -C +C %(pdf_data_vec)s C ---------- C BEGIN CODE @@ -442,7 +442,7 @@ C Select a flavor combination (need to do here for right sign) %(get_channel_vec)s - CALL GET_FLAVOR%(proc_id)s(IFLAV_VEC(IVEC), FLAVOR) +%(dsig_getflavor_vec)s if (IMODE.eq.0) then ALL_RWGT(IVEC) = REWGT(all_PP(0,1,IVEC),FLAVOR,ivec) else @@ -549,22 +549,22 @@ C Per-event MLM graph: igraphs(1) from REWGT (0 = no MLM) INTEGER VECSIZE_USED integer ivec - +%(dsig_xg_decl_multi)s %(additional_header)s %(OMP_PREFIX)s DO IVEC=1, VECSIZE_USED - call SMATRIX%(proc_id)s(p_multi(0,1,IVEC), - & IFLAV_VEC(IVEC), + call %(dsig_smatrix_vec_name)s(p_multi(0,1,IVEC), + & %(dsig_smatrix_vec_flav)s, & hel_rand(IVEC), & col_rand(IVEC), - & channels(IVEC), + & %(dsig_smatrix_vec_chan)s, & IVEC, & out(IVEC), & selected_hel(IVEC), & selected_col(IVEC) - & ) + & )%(dsig_smatrix_vec_post)s ENDDO %(OMP_POSTFIX)s @@ -573,19 +573,32 @@ C Per-event MLM graph: igraphs(1) from REWGT (0 = no MLM) integer FUNCTION GET_NHEL%(proc_id)s(hel, ipart) c if hel>0 return the helicity of particule ipart for the selected helicity configuration -c if hel=0 return the number of helicity state possible for that particle +c if hel=0 return the number of helicity state possible for that particle implicit none - integer hel,i, ipart + integer hel, i, ipart Include 'nexternal.inc' - integer one_nhel(nexternal) - INTEGER NCOMB - PARAMETER ( NCOMB=%(ncomb)d) - INTEGER NHEL(NEXTERNAL,0:NCOMB) - %(helicity_lines)s - - get_nhel%(proc_id)s = nhel(ipart, iabs(hel)) + INTEGER MAXHEL + PARAMETER (MAXHEL=%(maxhel)d) + INTEGER NHSTATE(NEXTERNAL), STATES(MAXHEL,NEXTERNAL) + %(nhstate_data)s + %(states_data)s + INTEGER XGW, XGD, XGK + IF (hel.EQ.0) THEN +c Number of helicity states for particle ipart. + get_nhel%(proc_id)s = NHSTATE(ipart) + ELSE +c Decode the canonical mixed-radix helicity code into particle ipart's +c helicity value (last external leg = least-significant digit). + XGW = 1 + DO XGK = ipart+1, NEXTERNAL + XGW = XGW * NHSTATE(XGK) + ENDDO + XGD = MOD((IABS(hel)-1)/XGW, NHSTATE(ipart)) + get_nhel%(proc_id)s = STATES(XGD+1, ipart) + ENDIF return end +%(dsig_xg_helper)s %(ADDITIONAL_FCT)s diff --git a/madgraph/iolibs/template_files/check_sa.cpp b/madgraph/iolibs/template_files/check_sa.cpp index 9da836ec1..ccc5c5773 100644 --- a/madgraph/iolibs/template_files/check_sa.cpp +++ b/madgraph/iolibs/template_files/check_sa.cpp @@ -65,5 +65,7 @@ int main(int argc, char** argv){ cout << " -----------------------------------------------------------------------------" << endl; } +%(crossing_example)s + return 0; } diff --git a/madgraph/iolibs/template_files/check_sa.f b/madgraph/iolibs/template_files/check_sa.f index a5eb27674..431972850 100644 --- a/madgraph/iolibs/template_files/check_sa.f +++ b/madgraph/iolibs/template_files/check_sa.f @@ -42,6 +42,19 @@ PROGRAM DRIVER INTEGER PDG_FOR_FLAVOR(NEXTERNAL,MAXFLAVOR) INTEGER FLAV_IDX INTEGER %(proc_prefix)sGET_FLAVOR_INDEX +C Signed per-leg PDG of a crossed process (filled by GET_PDG_FOR_FLAVOR), +C the two crossing-partner loop indices, and the number of flavor +C combinations; used only by the crossing-symmetry demonstration below. + INTEGER XPDG(NEXTERNAL) + INTEGER FLIP1, FLIP2, NFLAV +C Per-leg loop index and the two match flags of the crossing demonstration. + INTEGER XCK + LOGICAL XCVALID, XCMATCH +C Representative signed-PDG signatures of the crossed subprocesses folded +C into this matrix element; a crossing is demonstrated when its runtime PDG +C (GET_PDG_FOR_FLAVOR) matches one of them. + INTEGER XCSIG(NEXTERNAL, (NEXTERNAL+1)*(NEXTERNAL+1)) + INTEGER XCNSIG, XCS C C EXTERNAL C @@ -131,6 +144,8 @@ PROGRAM DRIVER write (*,*) "-----------------------------------------------------------------------------" enddo +%(crossing_example)s + if (%(use_density)s)then do I=1, MAXFLAVOR write (*,*) "==== density matrix for flavor", I, diff --git a/madgraph/iolibs/template_files/cpp_process_class.inc b/madgraph/iolibs/template_files/cpp_process_class.inc index b64dfd4f2..0285c00af 100644 --- a/madgraph/iolibs/template_files/cpp_process_class.inc +++ b/madgraph/iolibs/template_files/cpp_process_class.inc @@ -68,5 +68,6 @@ private: // function to compute missing symmetry factors after flavor consolidation int broken_sym(const int* flavor); +%(cross_member_decl)s }; diff --git a/madgraph/iolibs/template_files/cpp_process_function_definitions.inc b/madgraph/iolibs/template_files/cpp_process_function_definitions.inc index db3a44fee..0e0816b18 100644 --- a/madgraph/iolibs/template_files/cpp_process_function_definitions.inc +++ b/madgraph/iolibs/template_files/cpp_process_function_definitions.inc @@ -91,3 +91,5 @@ int CPPProcess::broken_sym(const int* flavor) } return total_factor; } + +%(ident_cross_function)s diff --git a/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc b/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc index dccadc17f..6c90819b3 100644 --- a/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc +++ b/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc @@ -6,47 +6,40 @@ std::complex **wfs; const int denominator = %(den_factors)s; // Flavor lookup table %(flavor_table)s +%(cross_tables_decode)s +const int* flavor = &flavor_table[%(fidx)s][0]; -const int* flavor = &flavor_table[flavor_id][0]; - -ntry[flavor_id]++; +ntry[%(fidx)s]++; // Define permutation -int perm[nexternal]; -for(int i = 0; i < nexternal; i++){ - perm[i]=i; -} +%(cross_perm_block)s double matrix_element = 0.; -if (sum_hel[flavor_id] == 0 || ntry[flavor_id] < 10){ +if (sum_hel[%(fidx)s] == 0 || ntry[%(fidx)s] < 10){ // Calculate the matrix element for all helicities for(int ihel = 0; ihel < ncomb; ihel ++){ - if (goodhel[flavor_id][ihel] || ntry[flavor_id] < 2){ - calculate_wavefunctions(perm, helicities[ihel], flavor); + %(cross_ghidx_setup)sif (%(cross_goodhel_gate)s){ + calculate_wavefunctions(perm, helicities[ihel], flavor%(cross_cw_args)s); %(get_matrix_t_lines)s matrix_element += t; // Store which helicities give non-zero result - if (t != 0. && !goodhel[flavor_id][ihel]){ - goodhel[flavor_id][ihel]=true; - ngood[flavor_id] ++; - igood[flavor_id][ngood[flavor_id]] = ihel; - } + %(cross_goodhel_train)s } } - jhel[flavor_id] = 0; - sum_hel[flavor_id]=min(sum_hel[flavor_id], ngood[flavor_id]); + jhel[%(fidx)s] = 0; + sum_hel[%(fidx)s]=min(sum_hel[%(fidx)s], ngood[%(fidx)s]); } else { // Only use the "good" helicities - for(int j=0; j < sum_hel[flavor_id]; j++){ - jhel[flavor_id]++; - if (jhel[flavor_id] >= ngood[flavor_id]) jhel[flavor_id]=0; - double hwgt = double(ngood[flavor_id])/double(sum_hel[flavor_id]); - int ihel = igood[flavor_id][jhel[flavor_id]]; - calculate_wavefunctions(perm, helicities[ihel], flavor); + for(int j=0; j < sum_hel[%(fidx)s]; j++){ + jhel[%(fidx)s]++; + if (jhel[%(fidx)s] >= ngood[%(fidx)s]) jhel[%(fidx)s]=0; + double hwgt = double(ngood[%(fidx)s])/double(sum_hel[%(fidx)s]); + int ihel = igood[%(fidx)s][jhel[%(fidx)s]]; + calculate_wavefunctions(perm, helicities[ihel], flavor%(cross_cw_args)s); %(get_matrix_t_lines)s matrix_element += t*hwgt; } } -return matrix_element * broken_sym(flavor) / denominator; +%(cross_return)s diff --git a/madgraph/iolibs/template_files/f2py_flavor_dispatch.py b/madgraph/iolibs/template_files/f2py_flavor_dispatch.py index a2b70e503..07c1b5fa5 100644 --- a/madgraph/iolibs/template_files/f2py_flavor_dispatch.py +++ b/madgraph/iolibs/template_files/f2py_flavor_dispatch.py @@ -19,6 +19,26 @@ >>> me.initialisemodel('param_card.dat') >>> ans = me.get_value(P, alphas, nhel, 3) # by flavor index >>> ans = me.get_value(P, alphas, nhel, [1, -1, 2, -2]) # by flavor array + +Crossing / PDG matching +----------------------- +When the module was generated with crossing symmetry on, a single flavor index +also carries a *crossing*: the extended ``FLAV_IDX = cross*NFLAV + flav`` makes +the one generated matrix element evaluate any process related to it by moving +legs between the initial and the final state. The caller usually does not want +to think in those indices -- they have a physical process as a list of signed +PDG codes and want the right index. ``find_pdg`` does that lookup and +``matrix_element_pdg`` / ``get_value_pdg`` call straight through: + +>>> me.find_pdg([2, 21, 2, 21]) # u g > u g from a u u~ > g g module +4 +>>> ans = me.get_value_pdg(P, alphas, nhel, [2, 21, 2, 21]) + +The PDG list is matched in the leg order the momenta are given in: the index +``find_pdg`` returns is exactly the one to pass to the ``*_idx`` entry points +together with momenta in that same order. A crossed leg is conjugated (an +incoming ``u~`` that a crossing turns into an outgoing ``u`` matches pdg +2), +which is why the match is on signed PDG codes. """ import numbers @@ -102,6 +122,99 @@ def smatrixhel(self, p, hel, flavor): def get_value(self, p, alphas, nhel, flavor): return self._call('get_value', [p, alphas, nhel, flavor]) + # -- crossing / PDG matching --------------------------------------------- + def _find_one(self, suffix): + """Return the single module function whose (lowercased) name ends with + *suffix*, or None. Cached under a distinct key so it never collides + with the (array, idx) pairs stored by _resolve.""" + key = ('one', suffix) + if key in self._cache: + return self._cache[key] + found = None + for name in dir(self.module): + if name.lower().endswith(suffix): + found = getattr(self.module, name) + break + self._cache[key] = found + return found + + def flavor_layout(self): + """Return (nflav, nexternal, ncross) from GET_FLAVOR_LAYOUT. + + ncross = (nexternal+1)**2 is the number of crossing codes, so the + extended index ranges over 1 .. ncross*nflav. Raises if the module was + built without the crossing entry points (an old or non-standalone-v4 + output).""" + func = self._find_one('get_flavor_layout') + if func is None: + raise AttributeError( + "This module exposes no 'get_flavor_layout': it was not built " + "with the crossing/PDG entry points.") + nflav, nexternal, ncross = func() + return int(nflav), int(nexternal), int(ncross) + + def pdg_for_index(self, flav_idx): + """Signed per-leg PDG codes of the process an extended FLAV_IDX selects, + or None if the index names no valid flavor/crossing. + + The codes are in the leg order the momenta must be supplied in for that + index; a leg that the crossing moved between the initial and the final + state is conjugated.""" + func = self._find_one('get_pdg_for_flavor') + if func is None: + raise AttributeError( + "This module exposes no 'get_pdg_for_flavor': it was not built " + "with the crossing/PDG entry points.") + pdgs = tuple(int(x) for x in func(flav_idx)) + # The Fortran routine zero-fills PDGS for an index it cannot resolve. + if all(code == 0 for code in pdgs): + return None + return pdgs + + def _pdg_map(self): + """{signed-PDG-tuple: extended FLAV_IDX} over every valid index. + + Built once and cached. When two indices give the same PDG signature in + the same leg order (physically the same process, e.g. a crossing that + coincides with the identity for a symmetric flavor) the first is kept: + they evaluate to the same matrix element.""" + if 'pdg_map' in self._cache: + return self._cache['pdg_map'] + nflav, _nexternal, ncross = self.flavor_layout() + mapping = {} + for cross in range(ncross): + for flav in range(1, nflav + 1): + flav_idx = cross * nflav + flav + pdgs = self.pdg_for_index(flav_idx) + if pdgs is not None: + mapping.setdefault(pdgs, flav_idx) + self._cache['pdg_map'] = mapping + return mapping + + def find_pdg(self, pdgs): + """Extended FLAV_IDX whose crossed process is *pdgs* (signed, in the + given leg order), or None if no crossing of the generated matrix + element reproduces it.""" + return self._pdg_map().get(tuple(int(code) for code in pdgs)) + + def _require_pdg(self, pdgs): + flav_idx = self.find_pdg(pdgs) + if flav_idx is None: + raise ValueError( + "No crossing of the generated matrix element yields the " + "process %s" % (tuple(int(code) for code in pdgs),)) + return flav_idx + + def matrix_element_pdg(self, p, pdgs): + """SMATRIX for the process *pdgs*, reached through crossing. Momenta + must be given in the same leg order as *pdgs*.""" + return self.smatrix(p, self._require_pdg(pdgs)) + + def get_value_pdg(self, p, alphas, nhel, pdgs): + """get_value for the process *pdgs*, reached through crossing. Momenta + must be given in the same leg order as *pdgs*.""" + return self.get_value(p, alphas, nhel, self._require_pdg(pdgs)) + # -- pass-through for the model initialiser ------------------------------- def initialisemodel(self, path): for name in dir(self.module): diff --git a/madgraph/iolibs/template_files/fortran_matrix_flavor_pdg_fct.inc b/madgraph/iolibs/template_files/fortran_matrix_flavor_pdg_fct.inc new file mode 100644 index 000000000..61841c7f5 --- /dev/null +++ b/madgraph/iolibs/template_files/fortran_matrix_flavor_pdg_fct.inc @@ -0,0 +1,69 @@ + SUBROUTINE %(func_name)s(FLAV_IDX_IN, PDGS) +C Return the signed PDG code of every leg of the process FLAV_IDX_IN +C selects, INCLUDING the crossing it carries. +C +C This is the bridge between the two vocabularies of this file. Inside +C matrix.f a flavor is an unsigned group *position*: that is all the +C matrix element needs, since every member of a flavor group shares the +C couplings. A caller speaking PDG codes cannot work with that -- a +C position is meaningless without knowing the group and the leg -- and +C nothing else generated here maps one back. GET_FLAVOR_INDEX only goes +C the other way and only accepts positions. +C +C FLAV_IDX_IN is the *extended* index: it carries a flavor and a +C crossing (see GET_CROSS_PERM). The PDGs returned are therefore those +C of the process actually evaluated, i.e. after the crossing has moved +C the legs around AND conjugated every leg that swapped between the +C initial and the final state -- an incoming u~ crossed into a final +C slot comes back as an outgoing u. Handing an f2py caller the +C uncrossed PDGs would be useless: it is precisely the crossed +C signature it has to match a request against. +C +C Conjugation is NOT a sign flip: a self-conjugate particle (the gluon) +C is its own antiparticle. Both tables are therefore filled at export +C time with the model's own anti-pdg rule, and this routine only picks +C the one SGN designates. +C +C PDGS is set to 0 on every leg when FLAV_IDX_IN names no valid flavor +C or a crossing that cannot be applied, so a caller can test PDGS(1)==0 +C rather than having to pre-validate the index. + IMPLICIT NONE +%(nexternal_decl)s + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) +C +C ARGUMENTS +C + INTEGER FLAV_IDX_IN + INTEGER PDGS(NEXTERNAL) +CF2PY INTENT(IN) :: FLAV_IDX_IN +CF2PY INTENT(OUT) :: PDGS(NEXTERNAL) +C +C LOCAL +C + INTEGER FP_I, FP_FLAV + INTEGER FP_PDG_TABLE(NEXTERNAL, NFLAV) + INTEGER FP_ANTI_TABLE(NEXTERNAL, NFLAV) + DATA FP_PDG_TABLE /%(pdg_table_data)s/ + DATA FP_ANTI_TABLE /%(antipdg_table_data)s/ +%(pdg_cross_decl)s + + DO FP_I = 1, NEXTERNAL + PDGS(FP_I) = 0 + ENDDO +C Guard before decoding: a negative index would make the MOD/divide +C below wrap onto a valid-looking flavor and crossing. + IF (FLAV_IDX_IN .LT. 1) THEN + RETURN + ENDIF + +%(pdg_cross_decode)s + + IF (FP_FLAV .LT. 1 .OR. FP_FLAV .GT. NFLAV) THEN + RETURN + ENDIF + +%(pdg_cross_apply)s + + RETURN + END diff --git a/madgraph/iolibs/template_files/madmatrix/check_sa.cc b/madgraph/iolibs/template_files/madmatrix/check_sa.cc index 68e93edb5..ce2081173 100644 --- a/madgraph/iolibs/template_files/madmatrix/check_sa.cc +++ b/madgraph/iolibs/template_files/madmatrix/check_sa.cc @@ -386,7 +386,11 @@ namespace } }; - inline double rn() + // Persistent RANMAR state, re-seedable via reset_rng() so a caller can draw + // the SAME first phase-space point for several mass permutations (used by the + // crossing demo to show each crossed subprocess at the point a standalone run + // of it would generate). + inline Random& rng() { static Random rand; static bool init = true; @@ -395,10 +399,16 @@ namespace init = false; rand.rmarin( 1802, 9373 ); } + return rand; + } + inline void reset_rng() { rng().rmarin( 1802, 9373 ); } + + inline double rn() + { double ran; while( true ) { - ran = rand.ranmar(); + ran = rng().ranmar(); if( ran > 1e-16 ) break; } return ran; @@ -749,6 +759,86 @@ namespace << std::string( SEP79, '-' ) << std::endl; } + // === Crossed subprocesses folded into this base matrix element === + // The exporter lists their extended flavor ids in crossing_demo.dat; show + // each at the RAMBO point generated for ITS OWN mass permutation (the crossed + // legs carry the same particles as the base, relabelled, so the crossed + // masses are a permutation of the base masses). + { + std::vector demo_ids; + std::ifstream fdemo( "crossing_demo.dat" ); + unsigned int _did; + while( fdemo >> _did ) demo_ids.push_back( _did ); + if( !demo_ids.empty() ) + { + std::cout << std::endl + << " Crossed processes folded into this matrix element:" + << std::endl; + for( unsigned int fid : demo_ids ) + { + // Crossed masses: base mass of the leg carrying the same |PDG|. + std::vector xmasses( CPPProcess::npar ); + for( int k = 0; k < CPPProcess::npar; ++k ) + { + const int pk = std::abs( CPPProcess::flavorPDG( (int)fid, k ) ); + double mk = 0.; + for( int j = 0; j < CPPProcess::npar; ++j ) + if( std::abs( CPPProcess::flavorPDG( 0, j ) ) == pk ) { mk = (double)masses[j]; break; } + xmasses[k] = mk; + } + double xwgt = 0.; + classic_rambo::reset_rng(); // draw the FIRST point for this mass permutation + std::vector> xpoint = + classic_rambo::get_momenta( CPPProcess::npari, (double)kEnergy, xmasses, xwgt ); + for( int ip4 = 0; ip4 < 4; ++ip4 ) + for( int ipar = 0; ipar < CPPProcess::npar; ++ipar ) + for( unsigned int ievt = 0; ievt < nevt; ++ievt ) + umamiMomenta[(std::size_t)ip4 * CPPProcess::npar * nevt + (std::size_t)ipar * nevt + ievt] = xpoint[ipar][ip4]; + std::fill( flvVec.begin(), flvVec.end(), fid ); +#ifdef MGONGPUCPP_GPUIMPL + gpuMemcpy( devUmamiMomenta.data(), umamiMomenta.data(), umamiMomenta.size() * sizeof( double ), gpuMemcpyHostToDevice ); + gpuMemcpy( devFlv.data(), flvVec.data(), nevt * sizeof( unsigned int ), gpuMemcpyHostToDevice ); +#endif + UmamiInputKey in_keys[3] = { UMAMI_IN_MOMENTA, UMAMI_IN_FLAVOR_INDEX, UMAMI_IN_ALPHA_S }; + UmamiOutputKey out_keys[1] = { UMAMI_OUT_MATRIX_ELEMENT }; +#ifdef MGONGPUCPP_GPUIMPL + const void* inputs[3] = { devUmamiMomenta.data(), devFlv.data(), devAlphaS.data() }; + void* outputs[1] = { devUmamiMEs.data() }; +#else + const void* inputs[3] = { umamiMomenta.data(), flvVec.data(), alphasVec.data() }; + void* outputs[1] = { umamiMEs.data() }; +#endif + UmamiStatus xst = umami_matrix_element( + umami_handle, nevt, nevt, 0, 3, in_keys, inputs, 1, out_keys, outputs ); + if( xst != UMAMI_SUCCESS ) + { + std::cerr << "ERROR! crossed umami_matrix_element failed (flavorID=" << fid << ")" << std::endl; + continue; + } +#ifdef MGONGPUCPP_GPUIMPL + gpuMemcpy( hstUmamiMEs.data(), devUmamiMEs.data(), nevt * sizeof( double ), gpuMemcpyDeviceToHost ); + const double* xmes = hstUmamiMEs.data(); +#else + const double* xmes = umamiMEs.data(); +#endif + std::cout << std::endl << " flavorID " << fid << std::endl + << " PDG E px py pz" << std::endl; + for( int ipar = 0; ipar < CPPProcess::npar; ++ipar ) + std::cout << std::scientific << std::setprecision( 7 ) + << std::setw( 6 ) << CPPProcess::flavorPDG( (int)fid, ipar ) + << std::setw( 16 ) << xpoint[ipar][0] + << std::setw( 16 ) << xpoint[ipar][1] + << std::setw( 16 ) << xpoint[ipar][2] + << std::setw( 16 ) << xpoint[ipar][3] + << std::endl << std::defaultfloat; + std::cout << " Matrix element = " << std::scientific << std::setprecision( 16 ) + << xmes[0] << " GeV^" << kMEGeVExponent << std::endl + << std::defaultfloat + << std::string( SEP79, '-' ) << std::endl; + } + } + } + umami_free( umami_handle ); return 0; } diff --git a/madgraph/iolibs/template_files/madmatrix/coloramps.h b/madgraph/iolibs/template_files/madmatrix/coloramps.h index 027f1aa44..1a69fd614 100644 --- a/madgraph/iolibs/template_files/madmatrix/coloramps.h +++ b/madgraph/iolibs/template_files/madmatrix/coloramps.h @@ -63,6 +63,24 @@ namespace mgOnGpu %(is_LC)s }; + // Canonical colour-flow CODE of each colour flow (the MG7 colour encoding, the + // same integer the Fortran madevent output writes into colorflow.inc and that + // subprocesses.json carries as "color_codes"). colorflowcode[icol] is the + // self-describing code of colour flow icol (0-based, the select_col_and_diag + // index minus one). A consumer that writes the event colour returns THIS code + // instead of the raw flow index, and decodes it with the flow-independent slot + // structure ("color_slots" in subprocesses.json) rather than looking the flow + // up in an ICOLUP-style table. + // + // colorflowcode_valid is false when the flows have no usable code (a colour + // sextet's two-slot leg, or an epsilon/epsilon-bar structure): the caller then + // falls back to the per-flow tag table. See the fortran side in + // export_v4._color_flow_code and the encoder in export_mg7.get_color_code_tables. + constexpr bool colorflowcode_valid = %(colorflowcode_valid)s; + __device__ constexpr int colorflowcode[%(nb_color)s] = { // note: a trailing comma in the initializer list is allowed +%(colorflowcode_lines)s + }; + } #endif // COLORAMPS_H diff --git a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc index 082c373aa..6043f27b5 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc @@ -176,11 +176,12 @@ namespace mg5amcCpu #endif static int cNGoodHel; static int cGoodHel[ncomb]; +%(goodhel_percross_statics)s // Host-side flavor table: single source of truth for PDG ids (used by both the // constructor copy into cFlavors and the public CPPProcess::flavorPDG accessor). %(all_flavors)s - +%(crossing_decl)s //-------------------------------------------------------------------------- #ifdef MGONGPUCPP_GPUIMPL @@ -265,7 +266,7 @@ namespace mg5amcCpu int CPPProcess::flavorPDG( int iflavor, int ipar ) { - return flavorPDGs[iflavor][ipar]; +%(flavorpdg_body)s } //-------------------------------------------------------------------------- @@ -549,9 +550,9 @@ namespace mg5amcCpu for( int ihel = 0; ihel < ncomb; ihel++ ) isGoodHel[ihel] = false; (void)iflavorVec; // flavor is forced below to scan every flavor combination unsigned int hgFlavorVec[maxtry0] = {}; // forced single-flavor index buffer - for( int iflav = 0; iflav < nmaxflavor; ++iflav ) +%(goodhel_percross_decl)s for( int iflav = 0; iflav < %(goodhel_scan_count)s; ++iflav ) { - for( int i = 0; i < maxtry0; ++i ) hgFlavorVec[i] = (unsigned int)iflav; + %(goodhel_scan_skip)sfor( int i = 0; i < maxtry0; ++i ) hgFlavorVec[i] = (unsigned int)iflav; for( int ipagV2 = 0; ipagV2 < npagV2; ++ipagV2 ) { #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT /* clang-format off */ @@ -589,20 +590,20 @@ namespace mg5amcCpu { //if ( !isGoodHel[ihel] ) std::cout << "sigmaKin_getGoodHel ihel=" << ihel << " TRUE" << std::endl; isGoodHel[ihel] = true; - } +%(goodhel_percross_record)s } #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT const int ievt2 = ievt00 + ieppV + neppV; if( allMEs[ievt2] != 0 ) // NEW IMPLEMENTATION OF GETGOODHEL (#630): COMPARE EACH HELICITY CONTRIBUTION TO 0 { //if ( !isGoodHel[ihel] ) std::cout << "sigmaKin_getGoodHel ihel=" << ihel << " TRUE" << std::endl; isGoodHel[ihel] = true; - } +%(goodhel_percross_record)s } #endif } } } } // end loop over flavor combinations (per-flavor good-helicity union) - } +%(goodhel_percross_build)s } #endif //-------------------------------------------------------------------------- diff --git a/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc b/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc index 227301a6e..a36de699b 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc @@ -116,13 +116,13 @@ #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT fptype_sv MEs_ighel2[ncomb] = {}; // sum of MEs for all good helicities up to ighel (for the second neppV page) #endif - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < %(sigmakin_hel_bound)s; ighel++ ) { - const int ihel = cGoodHel[ighel]; +%(sigmakin_perlane_decl)s const int ihel = %(sigmakin_ihel_expr)s; cxtype_sv jamp_sv[nParity * ncolor] = {}; // fixed nasty bug (omitting 'nParity' caused memory corruptions after calling calculate_jamps) // **NB! in "mixed" precision, using SIMD, calculate_jamps computes MEs for TWO neppV pages with a single channelId! #924 bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - calculate_jamps( ihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( ihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00%(calc_jamps_ihlane_arg)s ); color_sum_cpu( allMEs, jamp_sv, ievt00 ); MEs_ighel[ighel] = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 ) ); #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT @@ -134,18 +134,18 @@ { const int ievt = ievt00 + ieppV; //printf( "sigmaKin: ievt=%%4d rndhel=%%f\n", ievt, allrndhel[ievt] ); - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < %(sigmakin_hel_bound)s; ighel++ ) { #if defined MGONGPU_CPPSIMD //printf( "sigmaKin: ievt=%%4d ighel=%%d MEs_ighel=%%f\n", ievt, ighel, MEs_ighel[ighel][ieppV] ); - const bool okhel = allrndhel[ievt] < ( MEs_ighel[ighel][ieppV] / MEs_ighel[cNGoodHel - 1][ieppV] ); + const bool okhel = allrndhel[ievt] < ( MEs_ighel[ighel][ieppV] / MEs_ighel[%(sigmakin_hel_bound)s - 1][ieppV] ); #else //printf( "sigmaKin: ievt=%%4d ighel=%%d MEs_ighel=%%f\n", ievt, ighel, MEs_ighel[ighel] ); - const bool okhel = allrndhel[ievt] < ( MEs_ighel[ighel] / MEs_ighel[cNGoodHel - 1] ); + const bool okhel = allrndhel[ievt] < ( MEs_ighel[ighel] / MEs_ighel[%(sigmakin_hel_bound)s - 1] ); #endif if( okhel ) { - const int ihelF = cGoodHel[ighel] + 1; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] + const int ihelF = %(selected_hel_code_1)s; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] allselhel[ievt] = ihelF; //printf( "sigmaKin: ievt=%%4d ihel=%%4d\n", ievt, ihelF ); break; @@ -154,12 +154,12 @@ #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT const int ievt2 = ievt00 + ieppV + neppV; //printf( "sigmaKin: ievt=%%4d rndhel=%%f\n", ievt2, allrndhel[ievt2] ); - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < %(sigmakin_hel_bound)s; ighel++ ) { //printf( "sigmaKin: ievt=%%4d ighel=%%d MEs_ighel=%%f\n", ievt2, ighel, MEs_ighel2[ighel][ieppV] ); - if( allrndhel[ievt2] < ( MEs_ighel2[ighel][ieppV] / MEs_ighel2[cNGoodHel - 1][ieppV] ) ) + if( allrndhel[ievt2] < ( MEs_ighel2[ighel][ieppV] / MEs_ighel2[%(sigmakin_hel_bound)s - 1][ieppV] ) ) { - const int ihelF = cGoodHel[ighel] + 1; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] + const int ihelF = %(selected_hel_code_2)s; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] allselhel[ievt2] = ihelF; //printf( "sigmaKin: ievt=%%4d ihel=%%4d\n", ievt2, ihelF ); break; @@ -291,7 +291,7 @@ const int ievt0 = ipagV * neppV; fptype* MEs = E_ACCESS::ieventAccessRecord( allMEs, ievt0 ); fptype_sv& MEs_sv = E_ACCESS::kernelAccess( MEs ); - MEs_sv = MEs_sv * broken_symmetry_factor(iflavorVec[ievt0]) / helcolDenominators[0]; +%(sigmakin_denominator)s if( mulChannelWeight && allChannelIds != nullptr ) // fix segfault #892 (not 'channelIds[0] != 0') { const unsigned int channelId = getChannelId( allChannelIds, ievt0, false ); diff --git a/madgraph/iolibs/template_files/madmatrix/umami.cc b/madgraph/iolibs/template_files/madmatrix/umami.cc index d19c93bb9..571a323a3 100644 --- a/madgraph/iolibs/template_files/madmatrix/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/umami.cc @@ -486,31 +486,46 @@ extern "C" std::vector permutation; std::size_t rounded_count; + // The SIMD grouping key is the REDUCED flavor (id % nmaxflavor), not the + // full extended flavorID. The extended id encodes both a reduced flavor and + // a crossing (id = cross*nmaxflavor + flavor). CPPProcess only requires the + // reduced flavor to be constant across a SIMD vector (the wavefunction + // flavor is read once per vector); the crossing is applied per event by the + // momentum gather, so one vector may legitimately mix crossings. Indexing + // the grouping arrays by the full id (as an earlier version did) overflowed + // them whenever a crossing was present (id >= nmaxflavor), corrupting the + // stack and crashing (SIGABRT/SIGSEGV). constexpr std::size_t flavor_count = CPPProcess::nmaxflavor; HostBufferBase flavor_indices( ((count + page_size2 - 1) / page_size2 + flavor_count) * page_size2 ); bool sort_flavors = vector_size > 1 && flavor_count > 1 && flavor_indices_in; - if ( sort_flavors ) + if ( sort_flavors ) { permutation.resize(count); std::size_t voffset = 0; std::size_t vector_indices[flavor_count] = {}; std::size_t vector_counts[flavor_count] = {}; // determine permutation of inputs such that all entries in a SIMD vector - // have the same flavor index + // share the same reduced flavor (they may still carry different crossings) for( std::size_t i_event = 0; i_event < count; ++i_event ) { unsigned int flav = flavor_indices_in[i_event + offset]; - auto& vcount = vector_counts[flav]; - auto& vindex = vector_indices[flav]; + unsigned int rflav = flav % (unsigned int)CPPProcess::nmaxflavor; + auto& vcount = vector_counts[rflav]; + auto& vindex = vector_indices[rflav]; if ( vcount == 0 ) { vindex = voffset * page_size2; + // Pre-fill the whole page with a valid padding id (crossing 0 of this + // reduced flavor) so that unused tail lanes never index the crossing + // tables out of range; real events overwrite their own slot below. for ( std::size_t i = 0; i < page_size2; ++i) { - flavor_indices[voffset * page_size2 + i] = flav; + flavor_indices[voffset * page_size2 + i] = rflav; } voffset += 1; } - permutation[i_event] = vindex + vcount; + const std::size_t slot = vindex + vcount; + permutation[i_event] = slot; + flavor_indices[slot] = flav; // per-event full extended id (flavor + crossing) vcount = (vcount + 1) % page_size2; } rounded_count = voffset * page_size2; diff --git a/madgraph/iolibs/template_files/matrix_madevent_group_router_v4.inc b/madgraph/iolibs/template_files/matrix_madevent_group_router_v4.inc new file mode 100644 index 000000000..056af8b9c --- /dev/null +++ b/madgraph/iolibs/template_files/matrix_madevent_group_router_v4.inc @@ -0,0 +1,53 @@ + SUBROUTINE SMATRIX%(proc_id)s(P, IFLAV, RHEL, RCOL, channel, IVEC, ANS, IHEL, ICOL) +C +%(info_lines)s +C +C MadGraph5_aMC@NLO for Madevent Version +C +C Crossing-symmetry router: this subprocess shares its matrix element with a +C base subprocess of the same group. Each of its flavors is a crossing of a +C base flavor, so instead of its own (heavy) MATRIX it dispatches to the base +C SMATRIX with the extended FLAV_IDX that reproduces the crossed process. The +C base SMATRIX crosses the momenta P (supplied in this subprocess's own leg +C order) and rebuilds the crossed denominator, so ANS is this subprocess's +C matrix element. Only the flavor table (GET_FLAVOR) is kept here for the PDF. +C +%(process_lines)s +C + IMPLICIT NONE + INCLUDE 'nexternal.inc' + REAL*8 P(0:3,NEXTERNAL), ANS + DOUBLE PRECISION RHEL, RCOL + INTEGER channel, IVEC, IFLAV, IHEL, ICOL +C Per-flavor colour-flow remap: the base returns the selected colour flow in +C its own basis, but events are written through this subprocess's leshouche +C ICOLUP, whose flows can be ordered differently (the crossed colour reps +C decompose the shared colour basis in another order). COLMAP sends the base +C flow index to the local flow with the same colour topology. (The helicity +C index needs no such map: this subprocess's get_nhel already enumerates the +C crossed helicities in the base's order.) +%(smatrix_router_decl)s + ANS = 0D0 + IHEL = 1 + ICOL = 1 +%(smatrix_router_dispatch)s + END + + + SUBROUTINE GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR_OUT) +C Returns the flavor array for a given flavor index IFLAV + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER IFLAV, I + INTEGER FLAVOR_OUT(NEXTERNAL) + INTEGER FLAVOR(NEXTERNAL,%(max_flavor)s) + %(get_flavor_matrix)s + FLAVOR_OUT(:) = FLAVOR(:, IFLAV) + END + + + SUBROUTINE PRINT_ZERO_AMP_%(proc_id)s() + INTEGER I + I = 1 + RETURN + END diff --git a/madgraph/iolibs/template_files/matrix_madevent_group_v4.inc b/madgraph/iolibs/template_files/matrix_madevent_group_v4.inc index b1523fc0e..c92db91c4 100644 --- a/madgraph/iolibs/template_files/matrix_madevent_group_v4.inc +++ b/madgraph/iolibs/template_files/matrix_madevent_group_v4.inc @@ -64,6 +64,7 @@ C INTEGER I,IDEN INTEGER FLAVOR(NEXTERNAL) INTEGER FLAVOR_FOR_SYM(NEXTERNAL) +%(smatrix_me_cross_decl)s C Per-row FLAVOR lookup used by BROKEN_SYM. The IFLAV-indexed FLAVOR C table above can collapse distinct same-flavor / different-flavor C leshouche rows that share the same coupling group; BROKEN_SYM needs @@ -90,6 +91,7 @@ C logical init_mode common /to_determine_zero_hel/init_mode DOUBLE PRECISION AMP2(MAXAMPS), JAMP2(0:MAXFLOW) +%(xg_jamp2_decl)s INTEGER NB_SPIN_STATE_in(2) @@ -128,8 +130,9 @@ C C ---------- C BEGIN CODE C ---------- - CALL GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR) - NTRY(IFLAV,%(proc_id)s)=NTRY(IFLAV,%(proc_id)s)+1 +%(smatrix_me_cross_decode)s + CALL GET_FLAVOR%(proc_id)s(%(me_flav_key)s, FLAVOR) + NTRY(%(me_flav_key)s,%(proc_id)s)=NTRY(%(me_flav_key)s,%(proc_id)s)+1 IF (multi_channel) THEN DO I=1,NDIAGS @@ -149,8 +152,8 @@ C ---------- ! If HEL_PICKED==-1, this means that calls to other matrix where in initialization mode as well for the helicity. IF ((ISHEL.EQ.0.and.ISUM_HEL.eq.0).or.(DS_get_dim_status('Helicity').eq.0).or.(HEL_PICKED.eq.-1)) THEN DO I=1,NCOMB - IF (GOODHEL(I,IFLAV,%(proc_id)s) .OR. NTRY(IFLAV,%(proc_id)s).LE.MAXTRIES.or.(ISUM_HEL.NE.0)) THEN - T=MATRIX%(proc_id)s(P ,NHEL(1,I),IFLAV,I,AMP2, JAMP2, IVEC) + IF (GOODHEL(%(me_goodhel_idx)s,%(me_flav_key)s,%(proc_id)s) .OR. NTRY(%(me_flav_key)s,%(proc_id)s).LE.MAXTRIES.or.(ISUM_HEL.NE.0)%(smatrix_me_goodhel_or)s) THEN + T=MATRIX%(proc_id)s(%(me_matrix_args)s) %(beam_polarization)s IF (ISUM_HEL.NE.0.and.DS_get_dim_status('Helicity').eq.0.and.ALLOW_HELICITY_GRID_ENTRIES) then call DS_add_entry('Helicity',I,T) @@ -159,7 +162,7 @@ C ---------- TS(I)=T ENDIF ENDDO - IF(NTRY(IFLAV,%(proc_id)s).EQ.(MAXTRIES+1).and.DS_get_dim_status('Helicity').ne.-1) THEN + IF(NTRY(%(me_flav_key)s,%(proc_id)s).EQ.(MAXTRIES+1).and.DS_get_dim_status('Helicity').ne.-1) THEN call reset_cumulative_variable() ! avoid biais of the initialization ENDIF IF (ISUM_HEL.NE.0) then @@ -176,20 +179,20 @@ C ---------- CALL DS_SET_GRID_MODE('Helicity','init') endif ELSE - IF(NTRY(IFLAV,%(proc_id)s).LE.MAXTRIES)THEN + IF(NTRY(%(me_flav_key)s,%(proc_id)s).LE.MAXTRIES)THEN DO I=1,NCOMB IF(init_mode) THEN IF (DABS(TS(I)).GT.ANS*LIMHEL/NCOMB) THEN PRINT *, 'Matrix Element/Good Helicity: %(proc_id)s ', i, 'IMIRROR', IMIRROR ENDIF - ELSE IF (.NOT.GOODHEL(I,IFLAV,%(proc_id)s) .AND. (DABS(TS(I)).GT.ANS*LIMHEL/NCOMB)) THEN - GOODHEL(I,IFLAV,%(proc_id)s)=.TRUE. + ELSE IF (%(me_goodhel_train_guard)s.NOT.GOODHEL(%(me_goodhel_idx)s,%(me_flav_key)s,%(proc_id)s) .AND. (DABS(TS(I)).GT.ANS*LIMHEL/NCOMB)) THEN + GOODHEL(%(me_goodhel_idx)s,%(me_flav_key)s,%(proc_id)s)=.TRUE. NGOOD = NGOOD +1 - PRINT *,'Added good helicity ',I, 'for process %(proc_id)s flavor ',IFLAV,TS(I)*NCOMB/ANS,' in event ',NTRY(IFLAV,%(proc_id)s) + PRINT *,'Added good helicity ',I, 'for process %(proc_id)s flavor ',IFLAV,TS(I)*NCOMB/ANS,' in event ',NTRY(%(me_flav_key)s,%(proc_id)s) ENDIF ENDDO endif - IF(NTRY(IFLAV,%(proc_id)s).EQ.MAXTRIES)THEN + IF(NTRY(%(me_flav_key)s,%(proc_id)s).EQ.MAXTRIES)THEN ISHEL=MIN(ISUM_HEL,NGOOD) ENDIF ENDIF @@ -197,7 +200,7 @@ C ---------- C The helicity configuration was chosen already by genps and put in a common block defined in genps.inc. I = HEL_PICKED - T=MATRIX%(proc_id)s(P ,NHEL(1,I),IFLAV,I,AMP2, JAMP2, IVEC) + T=MATRIX%(proc_id)s(%(me_matrix_args)s) %(beam_polarization)s c Always one helicity at a time @@ -254,11 +257,13 @@ c Set right sign for ANS, based on sign of chosen helicity ELSE FLAVOR_FOR_SYM(:) = FLAVOR(:) ENDIF - ANS=ANS/DBLE(IDEN)*BROKEN_SYM%(proc_id)s(FLAVOR_FOR_SYM) +%(smatrix_me_iden_line)s +%(xg_jamp2_pub)s call select_color(rcol, jamp2, iconfig,%(proc_id)s, icol, ivec) END +%(crossing_routines_me)s SUBROUTINE GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR_OUT) @@ -274,7 +279,7 @@ C Returns the flavor array for a given flavor index IFLAV END -REAL*8 FUNCTION MATRIX%(proc_id)s(P,NHEL,IFLAV, IHEL,AMP2, JAMP2, IVEC) +REAL*8 FUNCTION MATRIX%(proc_id)s(P,NHEL,%(me_matrix_ic_param)sIFLAV, IHEL,AMP2, JAMP2, IVEC) C %(info_lines)s C @@ -314,6 +319,7 @@ C ARGUMENTS C REAL*8 P(0:3,NEXTERNAL) INTEGER IFLAV +%(me_matrix_ic_decl)s INTEGER NHEL(NEXTERNAL), FLAVOR(NEXTERNAL) INTEGER IHEL INTEGER IVEC diff --git a/madgraph/iolibs/template_files/matrix_madevent_group_v4_hel.inc b/madgraph/iolibs/template_files/matrix_madevent_group_v4_hel.inc index 45bcdb821..c7b1326a8 100644 --- a/madgraph/iolibs/template_files/matrix_madevent_group_v4_hel.inc +++ b/madgraph/iolibs/template_files/matrix_madevent_group_v4_hel.inc @@ -51,6 +51,7 @@ C INTEGER I,IDEN INTEGER FLAVOR(NEXTERNAL) INTEGER FLAVOR_FOR_SYM(NEXTERNAL) +%(smatrix_hel_cross_decl)s C Per-row FLAVOR lookup used by BROKEN_SYM. The IFLAV-indexed FLAVOR C table above can collapse distinct same-flavor / different-flavor C leshouche rows that share the same coupling group; BROKEN_SYM needs @@ -70,6 +71,7 @@ C GLOBAL VARIABLES C include '../../Source/vector.inc' ! defines VECSIZE_MEMMAX DOUBLE PRECISION AMP2(MAXAMPS), JAMP2(0:MAXFLOW) +%(xg_jamp2_decl)s C @@ -96,7 +98,8 @@ ${helicity_lines} C ---------- C BEGIN CODE C ---------- - CALL GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR) +%(smatrix_hel_cross_decode)s + CALL GET_FLAVOR%(proc_id)s(%(me_flav_key)s, FLAVOR) IF (multi_channel) THEN DO I=1,NDIAGS @@ -113,8 +116,8 @@ C ---------- TS(:) = 0d0 - call MATRIX%(proc_id)s(P ,IFLAV, TS, AMP2, JAMP2, IVEC) - DO I=1,NCOMB + call MATRIX%(proc_id)s(%(hel_matrix_call_args)s) + DO I=1,NCOMB T=TS(I) DO JJ=1,nincoming IF(POL(JJ).NE.1d0.AND.NHEL(JJ,I).EQ.INT(SIGN(1d0,POL(JJ)))) THEN @@ -167,11 +170,13 @@ c Set right sign for ANS, based on sign of chosen helicity ELSE FLAVOR_FOR_SYM(:) = FLAVOR(:) ENDIF - ANS=ANS/DBLE(IDEN)*BROKEN_SYM%(proc_id)s(FLAVOR_FOR_SYM) +%(smatrix_me_iden_line)s +%(xg_jamp2_pub)s call select_color(rcol, jamp2, iconfig,%(proc_id)s, icol, ivec) END +%(crossing_routines_me)s SUBROUTINE GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR_OUT) @@ -187,7 +192,7 @@ C Returns the flavor array for a given flavor index IFLAV END -Subroutine MATRIX%(proc_id)s(P,IFLAV, TS, AMP2, JAMP2, IVEC) +Subroutine MATRIX%(proc_id)s(P,%(hel_matrix_ic_param)sIFLAV, TS, AMP2, JAMP2, IVEC) C %(info_lines)s C @@ -227,6 +232,7 @@ C ARGUMENTS C REAL*8 P(0:3,NEXTERNAL) INTEGER IFLAV +%(me_matrix_ic_decl)s INTEGER FLAVOR(NEXTERNAL) REAL*8 TS(NCOMB) INTEGER IVEC diff --git a/madgraph/iolibs/template_files/matrix_standalone_crossing_v4.inc b/madgraph/iolibs/template_files/matrix_standalone_crossing_v4.inc new file mode 100644 index 000000000..dd193f970 --- /dev/null +++ b/madgraph/iolibs/template_files/matrix_standalone_crossing_v4.inc @@ -0,0 +1,279 @@ + SUBROUTINE %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX_IN, PERM, SGN, + & FLAV_IDX) +C Decode the crossing carried by FLAV_IDX_IN into a slot permutation. +C +C CROSS = (FLAV_IDX_IN-1) / NFLAV +C FLAV_IDX = mod(FLAV_IDX_IN-1, NFLAV) + 1 ! used for masking/... +C I = CROSS / (NEXTERNAL+1) ! partner of particle 1 +C J = mod(CROSS, NEXTERNAL+1) ! partner of particle 2 +C +C Particle 1 is swapped with particle I and particle 2 with particle J; 0 +C means "leave that particle alone", so FLAV_IDX_IN in [1,NFLAV] gives +C CROSS=0 and the identity, keeping old callers untouched. The base is +C NEXTERNAL+1 rather than NEXTERNAL so that I and J run over 0..NEXTERNAL +C and can designate the last particle as well. +C +C Swapping moves the momentum and the helicity between the two slots and +C flips their NSF/NSV flag through IC. Flipping that flag is what actually +C crosses the leg: helas stores the momentum as p*nsf and uses nhel*nsf, +C so the momentum sign change and the helicity flip both follow from it. +C Momenta therefore stay physical (positive energy), which matters because +C the helas spinors take dsqrt(p(0)+pp). +C +C The crossing is nothing but a fixed relabelling of slots, so it is +C decoded once into +C PERM(K) : the input slot whose content lands in crossed slot K, +C SGN(K) : the NSF/NSV sign flip applied to crossed slot K, +C and the callers reuse it for as many momenta/helicity rows as they like +C (see APPLY_CROSSING_TABLE) instead of decoding per matrix element call. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) +C ARGUMENTS + INTEGER FLAV_IDX_IN + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL) + INTEGER FLAV_IDX +C LOCAL + INTEGER CROSS, XI, XJ, XK + + FLAV_IDX = MOD(FLAV_IDX_IN-1, NFLAV) + 1 + CROSS = (FLAV_IDX_IN-1) / NFLAV + XI = CROSS / (NEXTERNAL+1) + XJ = MOD(CROSS, NEXTERNAL+1) + + DO XK = 1, NEXTERNAL + PERM(XK) = XK + SGN(XK) = 1 + ENDDO + +C XI==1 (resp. XJ==2) would swap a particle with itself: degenerate, so +C treated as "no crossing" just like 0. + IF (XI.NE.0 .AND. XI.NE.1) THEN + CALL %(proc_prefix)sSWAP_LEGS(1, XI, PERM, SGN) + ENDIF + IF (XJ.NE.0 .AND. XJ.NE.2) THEN + CALL %(proc_prefix)sSWAP_LEGS(2, XJ, PERM, SGN) + ENDIF + + RETURN + END + + + SUBROUTINE %(proc_prefix)sSWAP_LEGS(SLOT_A, SLOT_B, PERM, SGN) +C Exchange two legs in the permutation being built and flip their NSF/NSV +C sign (see GET_CROSS_PERM). SGN is swapped along with PERM before being +C negated, so that two overlapping swaps compose exactly as they would if +C the momentum/helicity/IC arrays themselves were swapped in turn. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER SLOT_A, SLOT_B + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL) + INTEGER ITMP + + ITMP = PERM(SLOT_A) + PERM(SLOT_A) = PERM(SLOT_B) + PERM(SLOT_B) = ITMP + ITMP = SGN(SLOT_A) + SGN(SLOT_A) = SGN(SLOT_B) + SGN(SLOT_B) = ITMP + SGN(SLOT_A) = -SGN(SLOT_A) + SGN(SLOT_B) = -SGN(SLOT_B) + + RETURN + END + + + SUBROUTINE %(proc_prefix)sAPPLY_CROSSING_TABLE(FLAV_IDX_IN, NROW, + & P_IN, NHEL_IN, IC_IN, P, NHEL, IC, FLAV_IDX) +C Apply the crossing carried by FLAV_IDX_IN to one set of momenta / NSF +C flags and to NROW helicity rows at once (see GET_CROSS_PERM). +C +C SMATRIX uses this to permute its whole NHEL table in a single sweep +C before the helicity loop: the permutation does not depend on the row, so +C decoding it once per SMATRIX call rather than once per helicity is both +C cheaper and the reason MATRIX/GET_AMP can stay pure. +C P/NHEL/IC must not alias P_IN/NHEL_IN/IC_IN. + IMPLICIT NONE + INCLUDE 'nexternal.inc' +C ARGUMENTS + INTEGER FLAV_IDX_IN, NROW + REAL*8 P_IN(0:3,NEXTERNAL) + INTEGER NHEL_IN(NEXTERNAL,NROW), IC_IN(NEXTERNAL) + REAL*8 P(0:3,NEXTERNAL) + INTEGER NHEL(NEXTERNAL,NROW), IC(NEXTERNAL) + INTEGER FLAV_IDX +C LOCAL + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL) + INTEGER XK, XR + + CALL %(proc_prefix)sGET_CROSS_PERM(FLAV_IDX_IN, PERM, SGN, FLAV_IDX) + + DO XK = 1, NEXTERNAL + P(0,XK) = P_IN(0,PERM(XK)) + P(1,XK) = P_IN(1,PERM(XK)) + P(2,XK) = P_IN(2,PERM(XK)) + P(3,XK) = P_IN(3,PERM(XK)) + IC(XK) = SGN(XK)*IC_IN(PERM(XK)) + ENDDO + DO XR = 1, NROW + DO XK = 1, NEXTERNAL + NHEL(XK,XR) = NHEL_IN(PERM(XK),XR) + ENDDO + ENDDO + + RETURN + END + + + SUBROUTINE %(proc_prefix)sAPPLY_CROSSING(FLAV_IDX_IN, P_IN, NHEL_IN, + & IC_IN, P, NHEL, IC, FLAV_IDX) +C Single helicity row flavour of APPLY_CROSSING_TABLE; kept public so that +C an external (f2py) caller holding an extended FLAV_IDX can pre-apply the +C crossing before calling GET_AMP, which is pure and rejects an extended +C index (see its contract). + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER FLAV_IDX_IN + REAL*8 P_IN(0:3,NEXTERNAL) + INTEGER NHEL_IN(NEXTERNAL), IC_IN(NEXTERNAL) + REAL*8 P(0:3,NEXTERNAL) + INTEGER NHEL(NEXTERNAL), IC(NEXTERNAL) + INTEGER FLAV_IDX + + CALL %(proc_prefix)sAPPLY_CROSSING_TABLE(FLAV_IDX_IN, 1, P_IN, + & NHEL_IN, IC_IN, P, NHEL, IC, FLAV_IDX) + + RETURN + END + + + INTEGER FUNCTION %(proc_prefix)sGET_SPINCOL_CROSS(CROSS) +C Initial state spin*color average of the crossed process. +C +C Crossing changes which particles sit in the initial state (pulling a +C gluon in takes the color average from 3 to 8), but every particle of a +C flavor group shares its spin and color, so this half of the denominator +C depends on CROSS only and is tabulated at generation time. The other +C half, the identical final state factor, is flavor dependent: see +C GET_IDENT_CROSS. A 0 entry marks a crossing that cannot be applied. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER NCROSS + PARAMETER (NCROSS=(NEXTERNAL+1)*(NEXTERNAL+1)) + INTEGER CROSS, I +C The three tables are emitted together; each routine keeps its own copy +C rather than sharing a COMMON, which would need a BLOCK DATA unit to be +C initialised by DATA. + INTEGER SPINCOL_CROSS_TABLE(0:NCROSS-1) + INTEGER BASEPID_CROSS_TABLE(0:NCROSS*NEXTERNAL-1) + INTEGER SRC_CROSS_TABLE(0:NCROSS*NEXTERNAL-1) +%(iden_cross_lines)s + + IF (CROSS .LT. 0 .OR. CROSS .GT. NCROSS-1) THEN + %(proc_prefix)sGET_SPINCOL_CROSS = 0 + ELSE + %(proc_prefix)sGET_SPINCOL_CROSS = SPINCOL_CROSS_TABLE(CROSS) + ENDIF + + RETURN + END + + + INTEGER FUNCTION %(proc_prefix)sGET_IDENT_CROSS(CROSS, FLAVOR) +C Identical final state factor (product of n!) of the crossed process. +C +C Flavor dependent, hence computed here rather than tabulated on CROSS: +C d d~ > g u u~ crossed gives d g > d u u~ with nothing identical, while +C d d~ > g d d~ crossed gives d g > d d d~ with two identical d. BROKEN_SYM +C cannot be reused for this: its tables describe the uncrossed final state. +C +C Two crossed final legs are identical when they carry the same flavor +C group (same representative PDG, conjugated already when the leg swapped +C side) and the same position inside it. FLAVOR is not permuted by the +C crossing, so slot K reads the position of the original leg that moved +C into it, via SRC_CROSS_TABLE. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER NCROSS + PARAMETER (NCROSS=(NEXTERNAL+1)*(NEXTERNAL+1)) + INTEGER CROSS + INTEGER FLAVOR(NEXTERNAL) + INTEGER SPINCOL_CROSS_TABLE(0:NCROSS-1) + INTEGER BASEPID_CROSS_TABLE(0:NCROSS*NEXTERNAL-1) + INTEGER SRC_CROSS_TABLE(0:NCROSS*NEXTERNAL-1) +%(iden_cross_lines)s + INTEGER K, L, N, FACT, OFF, I + LOGICAL USED(NEXTERNAL) + + OFF = CROSS*NEXTERNAL + DO K = 1, NEXTERNAL + USED(K) = .FALSE. + ENDDO + FACT = 1 + DO K = NINCOMING+1, NEXTERNAL + IF (USED(K)) CYCLE + N = 1 + DO L = K+1, NEXTERNAL + IF (USED(L)) CYCLE + IF (BASEPID_CROSS_TABLE(OFF+K-1).EQ.BASEPID_CROSS_TABLE(OFF+L + $ -1) .AND. FLAVOR(SRC_CROSS_TABLE(OFF+K-1)) + $ .EQ.FLAVOR(SRC_CROSS_TABLE(OFF+L-1))) THEN + USED(L) = .TRUE. + N = N + 1 + FACT = FACT * N + ENDIF + ENDDO + ENDDO + %(proc_prefix)sGET_IDENT_CROSS = FACT + + RETURN + END + + SUBROUTINE %(proc_prefix)sCROSS_GHIDX(CROSS, PERM, SGN, NHELCOL, + & GHIDX) +C Runtime good-helicity remap: send a crossed helicity row (given by its +C BASE-table config NHELCOL and the crossing's PERM/SGN from GET_CROSS_PERM) +C to the identity row whose shared GOODHEL bit gates it. This replaces the +C baked GHREMAP(NCROSS*NCOMB) table: the map is a fixed permutation, so it is +C cheaper to recompute it from the config than to store it -- permute and +C sign-flip the config, then re-encode it in the canonical mixed-radix order +C (the same STATES/NHSTATE the encoder/decoder use). +C GHIDX=0 when the crossing is not filterable (GHFILT flag: initial-initial +C swap, inapplicable, or a non-bijection); the caller then computes every +C helicity and never trains. For CROSS=0 (PERM identity, SGN +1) this returns +C IHEL, so the uncrossed path is unchanged. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER MAXHEL + PARAMETER (MAXHEL=%(maxhel)d) + INTEGER NCROSS + PARAMETER (NCROSS=(NEXTERNAL+1)*(NEXTERNAL+1)) + INTEGER CROSS, PERM(NEXTERNAL), SGN(NEXTERNAL) + INTEGER NHELCOL(NEXTERNAL), GHIDX + INTEGER I, K, D, TGT(NEXTERNAL) + INTEGER GHFILT(0:NCROSS-1) + INTEGER NHSTATE(NEXTERNAL), STATES(MAXHEL,NEXTERNAL) +%(ghfilt_data)s +%(nhstate_data)s +%(states_data)s + IF (GHFILT(CROSS).EQ.0) THEN + GHIDX = 0 + RETURN + ENDIF + DO K=1,NEXTERNAL + TGT(PERM(K)) = SGN(K)*NHELCOL(K) + ENDDO + GHIDX = 0 + DO K=1,NEXTERNAL + DO D=1,NHSTATE(K) + IF (STATES(D,K).EQ.TGT(K)) GOTO 7 + ENDDO + D = 1 + 7 CONTINUE + GHIDX = GHIDX*NHSTATE(K) + (D-1) + ENDDO + GHIDX = GHIDX + 1 + + RETURN + END diff --git a/madgraph/iolibs/template_files/matrix_standalone_f2py.inc b/madgraph/iolibs/template_files/matrix_standalone_f2py.inc index 9f9c015d4..0f37f611b 100644 --- a/madgraph/iolibs/template_files/matrix_standalone_f2py.inc +++ b/madgraph/iolibs/template_files/matrix_standalone_f2py.inc @@ -225,6 +225,8 @@ C undefined, corrupting memory when the density matrix is written. RETURN END +%(f2py_flav_idx_wrappers)s + LOGICAL FUNCTION PY_%(proc_prefix)sIS_BORN_HEL_SELECTED(HELID) IMPLICIT NONE C diff --git a/madgraph/iolibs/template_files/matrix_standalone_f2py_flav_idx.inc b/madgraph/iolibs/template_files/matrix_standalone_f2py_flav_idx.inc new file mode 100644 index 000000000..7755e57b1 --- /dev/null +++ b/madgraph/iolibs/template_files/matrix_standalone_f2py_flav_idx.inc @@ -0,0 +1,143 @@ +C ================================================================== +C f2py entry points taking the flavor index (FLAV_IDX) directly. +C +C These exist because the FLAVOR(NEXTERNAL) array cannot express a +C crossing: it holds unsigned group positions and is resolved through +C GET_FLAVOR_INDEX, which only ever returns 1..NFLAV. An *extended* +C FLAV_IDX = cross*NFLAV + flav carries both, so every entry point a +C crossing-aware caller needs must take the index, not the array. +C +C Only emitted for matrix_standalone_v4.inc, the one template that has +C GET_DENSITY_IDX / GET_ALL_INTER_IDX / GET_PDG_FOR_FLAVOR at all: the +C other standalone templates (matchbox, msP/msF, splitOrders) would +C fail to link against routines they never generate. +C ================================================================== + + SUBROUTINE PY_%(proc_prefix)sGET_PDG_FOR_FLAVOR(FLAV_IDX, PDGS) +C Per-leg signed PDG codes of the process FLAV_IDX selects, crossing +C included (legs permuted, and conjugated where they swapped between +C the initial and the final state). +C +C This is what lets a python caller work in PDG codes at all: it can +C enumerate the candidate FLAV_IDX values, ask each one what process +C it evaluates, and keep the one matching the request. All-zero means +C the index names no valid flavor/crossing. + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) +CF2PY INTENT(IN) :: FLAV_IDX +CF2PY INTENT(OUT) :: PDGS(NEXTERNAL) + INTEGER FLAV_IDX + INTEGER PDGS(NEXTERNAL) + CALL %(proc_prefix)sGET_PDG_FOR_FLAVOR(FLAV_IDX, PDGS) + RETURN + END + + SUBROUTINE PY_%(proc_prefix)sGET_FLAVOR_LAYOUT(NFLAV_OUT, + & NEXTERNAL_OUT, NCROSS_OUT) +C The three constants a caller needs to build an extended FLAV_IDX at +C all: FLAV_IDX = cross*NFLAV + flav with cross in [0,NCROSS-1], and +C cross = I*(NEXTERNAL+1)+J. Without NFLAV the encoding is simply not +C expressible, and parsing it out of matrix.f (as the tests must) is +C not something a caller should have to do. + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) +CF2PY INTENT(OUT) :: NFLAV_OUT +CF2PY INTENT(OUT) :: NEXTERNAL_OUT +CF2PY INTENT(OUT) :: NCROSS_OUT + INTEGER NFLAV_OUT, NEXTERNAL_OUT, NCROSS_OUT + NFLAV_OUT = NFLAV + NEXTERNAL_OUT = NEXTERNAL + NCROSS_OUT = %(ncross)d + RETURN + END + + SUBROUTINE PY_%(proc_prefix)sGET_NHEL_IDX(FLAV_IDX, IDEN_STAR, + & NHEL_STAR) +C Crossing-aware twin of PY_GET_NHEL. +C +C GET_NHEL reports the static IDEN, which is the averaging denominator +C of the *uncrossed* representative flavor only. SMATRIX itself does +C not use it that way -- it divides by IDEN/BROKEN_SYM(FLAVOR) when +C uncrossed and by GET_SPINCOL_CROSS*GET_IDENT_CROSS when crossed -- +C so a caller reading GET_NHEL and reconstructing ANS*IDEN would get a +C wrong answer for any crossed (or merely non-representative) flavor. +C This entry reports the denominator SMATRIX actually applied for this +C FLAV_IDX. GET_NHEL keeps its signature and its meaning for existing +C uncrossed callers. + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER NCOMB + PARAMETER ( NCOMB=%(ncomb)d) +CF2PY INTENT(IN) :: FLAV_IDX +CF2PY INTENT(OUT) :: IDEN_STAR +CF2PY INTENT(OUT) :: NHEL_STAR(NEXTERNAL,NCOMB) + INTEGER FLAV_IDX + INTEGER IDEN_STAR + INTEGER NHEL_STAR(NEXTERNAL,NCOMB) + CALL %(proc_prefix)sGET_NHEL_IDX(FLAV_IDX, IDEN_STAR, NHEL_STAR) + RETURN + END + + SUBROUTINE PY_%(proc_prefix)sGET_DENSITY_IDX(P, POS, N_CHANGING, + & ALLOW_HEL, N_COMB, FLAV_IDX, ALPHAS, SCALE2, INTER) +C Density matrix for an extended FLAV_IDX. PY_GET_DENSITY takes the +C FLAVOR array and so can only ever ask for an uncrossed density +C matrix; this is the only way to request a crossed one through f2py. +C Same CF2PY-before-declarations layout and same explicit INTER sizing +C as PY_GET_DENSITY -- see the comments there, both matter. + IMPLICIT NONE +CF2PY double precision, intent(in), dimension(0:3,%(nexternal)d) :: P +CF2PY integer, intent(in), dimension(*) :: POS +CF2PY integer, intent(in) :: N_CHANGING +CF2PY integer, intent(in), dimension(N_CHANGING*N_COMB) :: ALLOW_HEL +CF2PY integer, intent(in) :: N_COMB +CF2PY integer, intent(in) :: FLAV_IDX +CF2PY double precision, intent(in) :: ALPHAS +CF2PY double precision, intent(in) :: SCALE2 +CF2PY double complex, intent(out), dimension(N_COMB*(N_COMB+1)/2) :: INTER + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + REAL*8 P(0:3,NEXTERNAL) + INTEGER N_CHANGING, N_COMB + INTEGER POS(*) + INTEGER ALLOW_HEL(*) + INTEGER FLAV_IDX + DOUBLE PRECISION ALPHAS, SCALE2 + DOUBLE COMPLEX INTER(N_COMB*(N_COMB+1)/2) + CALL %(proc_prefix)sGET_DENSITY_IDX(P, POS, N_CHANGING, ALLOW_HEL, + & N_COMB, FLAV_IDX, ALPHAS, SCALE2, INTER) + RETURN + END + + SUBROUTINE PY_%(proc_prefix)sGET_ALL_INTER_IDX(P, NHEL, POS, + & N_CHANGING, ALLOW_HEL, N_COMB, FLAV_IDX, INTER) +C The un-normalised interference terms behind GET_DENSITY_IDX, for a +C caller supplying its own helicity configuration. Exposed for the +C same reason: its FLAVOR-array twin cannot carry a crossing. + IMPLICIT NONE +CF2PY double precision, intent(in), dimension(0:3,%(nexternal)d) :: P +CF2PY integer, intent(in), dimension(%(nexternal)d) :: NHEL +CF2PY integer, intent(in), dimension(*) :: POS +CF2PY integer, intent(in) :: N_CHANGING +CF2PY integer, intent(in), dimension(N_CHANGING*N_COMB) :: ALLOW_HEL +CF2PY integer, intent(in) :: N_COMB +CF2PY integer, intent(in) :: FLAV_IDX +CF2PY double complex, intent(out), dimension(N_COMB*(N_COMB+1)/2) :: INTER + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + REAL*8 P(0:3,NEXTERNAL) + INTEGER NHEL(NEXTERNAL) + INTEGER N_CHANGING, N_COMB + INTEGER POS(*) + INTEGER ALLOW_HEL(*) + INTEGER FLAV_IDX + DOUBLE COMPLEX INTER(N_COMB*(N_COMB+1)/2) + CALL %(proc_prefix)sGET_ALL_INTER_IDX(P, NHEL, POS, N_CHANGING, + & ALLOW_HEL, N_COMB, FLAV_IDX, INTER) + RETURN + END diff --git a/madgraph/iolibs/template_files/matrix_standalone_splitOrders_v4.inc b/madgraph/iolibs/template_files/matrix_standalone_splitOrders_v4.inc index c2806387d..1f176c9b9 100644 --- a/madgraph/iolibs/template_files/matrix_standalone_splitOrders_v4.inc +++ b/madgraph/iolibs/template_files/matrix_standalone_splitOrders_v4.inc @@ -668,6 +668,8 @@ c INTEGER I,J,SOL,N INTEGER FLAV_IDX INTEGER %(proc_prefix)sGET_FLAVOR_INDEX + INTEGER %(proc_prefix)sBROKEN_SYM + DOUBLE PRECISION RESCALE C ---------- C BEGIN CODE @@ -689,11 +691,17 @@ C ---------- call %(proc_prefix)sGET_JAMP(AMP,JAMP(1,1,I)) enddo +C GET_INTER only sees JAMPs, so it normalises with the bare static IDEN +C and cannot apply any flavor dependent factor. SMATRIX does +C ANS/IDEN*BROKEN_SYM(FLAVOR); the density matrix must use the same +C normalisation or the sum of its diagonal stops matching SMATRIX. + RESCALE = DBLE(%(proc_prefix)sBROKEN_SYM(FLAVOR)) SOL = 0 DO I = 1, N_COMB DO J= I, N_COMB SOL = SOL +1 call %(proc_prefix)sGET_INTER(JAMP(1,1,I), JAMP(1,1,J), INTER(1,SOL)) + INTER(:,SOL) = INTER(:,SOL) * RESCALE ENDDO ENDDO diff --git a/madgraph/iolibs/template_files/matrix_standalone_v4.inc b/madgraph/iolibs/template_files/matrix_standalone_v4.inc index ada2fd0eb..0411de3c7 100644 --- a/madgraph/iolibs/template_files/matrix_standalone_v4.inc +++ b/madgraph/iolibs/template_files/matrix_standalone_v4.inc @@ -86,6 +86,9 @@ C For a 1>N process, them BEAMTWO_HELAVGFACTOR would be set to 1. PARAMETER (NGOODHEL_FLAV=NCOMB*NFLAV) INTEGER FLAV_IDX INTEGER %(proc_prefix)sGET_FLAVOR_INDEX +C FLAV_USE is the flavor part of FLAV_IDX. + INTEGER FLAV_USE +%(smatrix_cross_decl)s INTEGER NTRY(NFLAV) LOGICAL GOODHEL(NCOMB,NFLAV) DATA NTRY/NNTRY_FLAV*0/ @@ -101,7 +104,10 @@ C common/%(proc_prefix)shelreset/HELRESET data HELRESET/.true./ -%(helicity_lines)s +C Allowed canonical helicity codes (mixed-radix over the per-leg states). +C The explicit NHEL config table is materialized at runtime by FILL_NHEL. + INTEGER HELALLOW(NCOMB) +%(hel_allow_data)s %(den_factor_line)s INTEGER POLARIZATIONS(0:NEXTERNAL,0:5) @@ -131,19 +137,25 @@ endif C ---------- C BEGIN CODE C ---------- -C FLAV_IDX=0 (or out of range) means GET_FLAVOR_INDEX could not resolve -C the requested flavor: it is not an allowed combination, so its matrix -C element is identically zero. Short-circuit before touching the -C 1..NFLAV GOODHEL/NTRY arrays. - IF (FLAV_IDX.LT.1 .OR. FLAV_IDX.GT.NFLAV) THEN +C FLAV_USE = mod(FLAV_IDX-1, NFLAV) + 1 is the flavor used for masking. +C FLAV_IDX<1 means GET_FLAVOR_INDEX could not resolve the requested flavor: +C it is not an allowed combination, so its matrix element is identically +C zero. Short-circuit before touching the 1..NFLAV GOODHEL/NTRY arrays. + IF (FLAV_IDX.LT.1) THEN ANS = 0D0 RETURN ENDIF - CALL %(proc_prefix)sGET_FLAVOR(FLAV_IDX, FLAVOR) - IF(USERHEL.EQ.-1) NTRY(FLAV_IDX)=NTRY(FLAV_IDX)+1 + FLAV_USE = MOD(FLAV_IDX-1, NFLAV) + 1 +%(smatrix_cross_decode)s +C The helicity filter is deliberately shared by every crossing of a given +C flavor, so it is indexed by FLAV_USE rather than by the full FLAV_IDX. + CALL %(proc_prefix)sGET_FLAVOR(FLAV_USE, FLAVOR) + CALL %(proc_prefix)sFILL_NHEL() + IF(USERHEL.EQ.-1) NTRY(FLAV_USE)=NTRY(FLAV_USE)+1 DO IHEL=1,NEXTERNAL JC(IHEL) = +1 ENDDO +%(smatrix_cross_apply)s C When spin-2 particles are involved, the Helicity filtering is dangerous for the 2->1 topology. C This is because depending on the MC setup the initial PS points have back-to-back initial states C for which some of the spin-2 helicity configurations are zero. But they are no longer zero @@ -159,22 +171,22 @@ C For this reason, we simply remove the filterin when there is only three ex ENDIF ANS = 0D0 DO IHEL=1,NCOMB - IF (USERHEL.EQ.-1.OR.USERHEL.EQ.IHEL) THEN - IF (GOODHEL(IHEL,FLAV_IDX) .OR. NTRY(FLAV_IDX) .LT. 20.OR.USERHEL.NE.-1) THEN - IF(NTRY(FLAV_IDX).GE.2.AND.POLARIZATIONS(0,0).ne.-1.and.(.not.%(proc_prefix)sIS_BORN_HEL_SELECTED(IHEL))) THEN + IF (USERHEL.EQ.-1.OR.USERHEL.EQ.HELALLOW(IHEL)) THEN +%(smatrix_goodhel_gate)s + IF(NTRY(FLAV_USE).GE.2.AND.POLARIZATIONS(0,0).ne.-1.and.(.not.%(proc_prefix)sIS_BORN_HEL_SELECTED(IHEL))) THEN CYCLE ENDIF - T=%(proc_prefix)sMATRIX(P ,NHEL(1,IHEL),JC(1),FLAV_IDX) +C MATRIX/GET_AMP get already crossed arrays and the reduced +C flavor index: the crossing was applied once, above. +%(smatrix_matrix_call)s IF(POLARIZATIONS(0,0).eq.-1.or.%(proc_prefix)sIS_BORN_HEL_SELECTED(IHEL)) THEN ANS=ANS+T ENDIF - IF (T .NE. 0D0 .AND. .NOT. GOODHEL(IHEL,FLAV_IDX)) THEN - GOODHEL(IHEL,FLAV_IDX)=.TRUE. - ENDIF +%(smatrix_goodhel_train)s ENDIF ENDIF ENDDO - ANS=ANS/DBLE(IDEN)*%(proc_prefix)sBROKEN_SYM(FLAVOR) +%(smatrix_iden_line)s IF(USERHEL.NE.-1) THEN ANS=ANS*HELAVGFACTOR ELSE @@ -196,6 +208,10 @@ C C Returns amplitude squared -- no average over initial state/symmetry factor c for the point with external lines W(0:6,NEXTERNAL) C +C CONTRACT: P, NHEL and IC must ALREADY be crossed and FLAV_IDX must ALREADY +C be reduced to [1,NFLAV] (see GET_AMP). SMATRIX applies the crossing once, +C before its helicity loop; this routine never decodes anything. +C %(process_lines)s C use aloha_object @@ -265,20 +281,90 @@ CF2PY INTENT(OUT) :: IDEN_STAR INTEGER NCOMB PARAMETER ( NCOMB=%(ncomb)d) - INTEGER NHEL(NEXTERNAL,NCOMB),NHEL_STAR(NEXTERNAL,NCOMB) + INTEGER NHEL(NEXTERNAL,NCOMB) + COMMON/%(proc_prefix)sPROCESS_NHEL/NHEL + INTEGER NHEL_STAR(NEXTERNAL,NCOMB) INTEGER IDEN,IDEN_STAR - -%(helicity_lines)s %(den_factor_line)s + CALL %(proc_prefix)sFILL_NHEL() IDEN_STAR = IDEN NHEL_STAR = NHEL END + SUBROUTINE %(proc_prefix)sGET_NHEL_IDX(FLAV_IDX_IN,IDEN_STAR, + & NHEL_STAR) +C Same as GET_NHEL, but reporting the denominator SMATRIX ACTUALLY +C divides by for FLAV_IDX_IN, rather than the static IDEN. +C +C The static IDEN is the averaging/symmetry factor of the uncrossed +C *representative* flavor. SMATRIX never uses it bare: it applies +C IDEN/BROKEN_SYM(FLAVOR) uncrossed (BROKEN_SYM correcting the +C identical-particle count of the representative to that of the actual +C flavor) and GET_SPINCOL_CROSS*GET_IDENT_CROSS when crossed. A caller +C that reads GET_NHEL and multiplies ANS by it to recover the raw +C helicity/color sum -- the natural thing to do, and what makes two +C crossings comparable -- is therefore wrong for every crossed flavor +C and for every non-representative one. +C +C GET_NHEL is deliberately left alone: its signature and its value are +C what existing uncrossed callers expect. +%(nhel_idx_decl)s + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER NCOMB + PARAMETER ( NCOMB=%(ncomb)d) + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) +CF2PY INTENT(IN) :: FLAV_IDX_IN +CF2PY INTENT(OUT) :: NHEL_STAR +CF2PY INTENT(OUT) :: IDEN_STAR + INTEGER FLAV_IDX_IN + INTEGER NHEL_STAR(NEXTERNAL,NCOMB) + INTEGER IDEN_STAR + INTEGER NHI_FLAV, NHI_CROSS + INTEGER FLAVOR(NEXTERNAL) + INTEGER %(proc_prefix)sBROKEN_SYM + + CALL %(proc_prefix)sGET_NHEL(IDEN_STAR, NHEL_STAR) +C An index naming no valid flavor gives a zero matrix element; report a +C 0 denominator rather than a plausible-looking one. + IF (FLAV_IDX_IN .LT. 1) THEN + IDEN_STAR = 0 + RETURN + ENDIF + NHI_FLAV = MOD(FLAV_IDX_IN-1, NFLAV) + 1 + NHI_CROSS = (FLAV_IDX_IN-1) / NFLAV + CALL %(proc_prefix)sGET_FLAVOR(NHI_FLAV, FLAVOR) +%(nhel_idx_body)s + RETURN + END + SUBROUTINE %(proc_prefix)sGET_AMP(P,NHEL,IC,FLAV_IDX,AMP) use model_object C %(process_lines)s C +C CONTRACT (this routine is pure: it decodes nothing). +C +C P(0:3,NEXTERNAL) : momenta, ALREADY crossed. +C NHEL(NEXTERNAL) : helicities, ALREADY crossed (permuted, NOT negated: +C helas uses nh=nhel*nsf, so the sign follows IC). +C IC(NEXTERNAL) : NSF/NSV flag per leg, ALREADY crossed (-1 on a leg +C the crossing moved across). +C FLAV_IDX : flavor index ALREADY reduced to [1,NFLAV]; it must +C NOT be an extended index carrying a crossing. +C +C When the crossing machinery is written out (see %(proc_prefix)sGET_CROSS_PERM below; +C it is left out when the process was generated with --use_crossing=False, +C in which case FLAV_IDX is never extended), the callers inside this file +C (SMATRIX via MATRIX, GET_ALL_INTER_CROSSED) apply the crossing ONCE per +C entry point with %(proc_prefix)sAPPLY_CROSSING / %(proc_prefix)sAPPLY_CROSSING_TABLE and then call +C this routine in their inner loop. An external (f2py) caller holding an +C extended FLAV_IDX must call the public %(proc_prefix)sAPPLY_CROSSING itself first; +C passing the extended index here would otherwise silently return the +C UNCROSSED amplitude, so the range is checked below and violations are +C reported and return AMP=0. +C CF2PY INTENT(OUT) :: AMP CF2PY INTENT(IN) :: NHEL CF2PY INTENT(IN) :: P(0:3,NEXTERNAL) @@ -296,6 +382,8 @@ C PARAMETER (NEXTERNAL=%(nexternal)d) INTEGER NWAVEFUNCS, NCOLOR PARAMETER (NWAVEFUNCS=%(nwavefuncs)d, NCOLOR=%(ncolor)d) + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) REAL*8 ZERO PARAMETER (ZERO=0D0) C @@ -304,11 +392,15 @@ C REAL*8 P(0:3,NEXTERNAL) INTEGER NHEL(NEXTERNAL), IC(NEXTERNAL) INTEGER FLAV_IDX - INTEGER FLAVOR(NEXTERNAL) + COMPLEX*16 AMP(NGRAPHS) C C LOCAL VARIABLES C - COMPLEX*16 AMP(NGRAPHS) +C FLAVOR is rebuilt from FLAV_IDX below and is NOT permuted by any +C crossing: each slot keeps its own flavor-group position, which is what +C the mask indexes. + INTEGER FLAVOR(NEXTERNAL) + INTEGER AMP_I type(aloha) W(NWAVEFUNCS) COMPLEX*16 DUM0,DUM1 DATA DUM0, DUM1/(0d0, 0d0), (1d0, 0d0)/ @@ -322,6 +414,18 @@ C C C bwcutoff=15 ! use if $ syntax is defined in the process +C Contract guard: an extended FLAV_IDX (one carrying a crossing) reaching +C this routine would be silently truncated to its flavor part and give the +C uncrossed amplitude. Fail loudly and return zero instead. + IF (FLAV_IDX.LT.1 .OR. FLAV_IDX.GT.NFLAV) THEN + WRITE(*,*) 'ERROR: GET_AMP got FLAV_IDX', FLAV_IDX, 'NFLAV', NFLAV + WRITE(*,*) 'GET_AMP needs a reduced index and crossed P/NHEL/IC.' + WRITE(*,*) 'Returning AMP=0.' + DO AMP_I = 1, NGRAPHS + AMP(AMP_I) = (0D0, 0D0) + ENDDO + RETURN + ENDIF %(flavor_mask_setup)s %(helas_calls)s %(amp2_lines)s @@ -433,12 +537,53 @@ C ZTEMP = DCONJG(JAMP_2(I)) SUBROUTINE %(proc_prefix)sGET_DENSITY(P, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAVOR, ALPHAS, SCALE2, INTER) +C Entry point taking the full FLAVOR(NEXTERNAL) array (back-compat): +C resolve it to FLAV_IDX and forward to GET_DENSITY_IDX. A crossing can +C only be requested through GET_DENSITY_IDX: an extended FLAV_IDX carries +C a crossing code, which no FLAVOR array can express (GET_FLAVOR_INDEX +C only ever returns 1..NFLAV). + implicit none + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) +CF2PY INTENT(IN) :: P(0:3,%(nexternal)d) +CF2PY INTENT(IN) :: POS(N_CHANGING) +CF2PY INTENT(IN) :: N_CHANGING +CF2PY INTENT(IN) :: ALLOW_HEL(N_CHANGING*N_COMB) +CF2PY INTENT(IN) :: N_COMB +CF2PY INTENT(IN) :: FLAVOR(%(nexternal)d) +CF2PY INTENT(IN) :: ALPHAS +CF2PY INTENT(IN) :: SCALE2 +CF2PY INTENT(OUT) :: INTER(N_COMB*(N_COMB+1)/2) + REAL*8 P(0:3,NEXTERNAL) + INTEGER N_CHANGING, N_COMB + INTEGER POS(*) + INTEGER ALLOW_HEL(*) + INTEGER FLAVOR(NEXTERNAL) + DOUBLE PRECISION ALPHAS, SCALE2 + DOUBLE COMPLEX INTER(*) + INTEGER %(proc_prefix)sGET_FLAVOR_INDEX + + CALL %(proc_prefix)sGET_DENSITY_IDX(P, POS, N_CHANGING, ALLOW_HEL, + & N_COMB, %(proc_prefix)sGET_FLAVOR_INDEX(FLAVOR), ALPHAS, SCALE2, + & INTER) + RETURN + END + + + SUBROUTINE %(proc_prefix)sGET_DENSITY_IDX(P, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAV_IDX, ALPHAS, SCALE2, INTER) c P momenta c NHEL base of helicity that are not changing c POS(N_CHNGING): position of the changing helicity c n_changing: number of changing helicity c ALLOW_HEL(NCOMB, N_CHANGING): combination of helicity to consider (all jamp computed) c INTER(NCOMB*(NCOMB+1)/2): all interference term (not the symmetric one) +c FLAV_IDX may carry a crossing. It is decoded and applied ONCE here (the +c whole NHEL table, the momenta and the NSF flags in one go) and only +c crossed arrays plus the reduced flavor index travel further down. POS and +c the helicity labels refer to the UNCROSSED (source process) leg ordering +c and are mapped through the crossing permutation here. No helicity flip is +c needed on top: helas folds it into nh=nhel*nsf when the NSF flag of a +c crossed leg is flipped. use model_object implicit none CF2PY INTENT(IN) :: P(0:3,%(nexternal)d) @@ -446,7 +591,7 @@ CF2PY INTENT(IN) :: POS(N_CHANGING) CF2PY INTENT(IN) :: N_CHANGING CF2PY INTENT(IN) :: ALLOW_HEL(N_CHANGING*N_COMB) CF2PY INTENT(IN) :: N_COMB -CF2PY INTENT(IN) :: FLAVOR(%(nexternal)d) +CF2PY INTENT(IN) :: FLAV_IDX CF2PY INTENT(IN) :: ALPHAS CF2PY INTENT(IN) :: SCALE2 CF2PY INTENT(OUT) :: INTER(N_COMB*(N_COMB+1)/2) @@ -462,7 +607,7 @@ C INTEGER N_CHANGING, N_COMB INTEGER POS(*) INTEGER ALLOW_HEL(*) - INTEGER FLAVOR(NEXTERNAL) + INTEGER FLAV_IDX DOUBLE PRECISION ALPHAS, SCALE2 DOUBLE COMPLEX INTER(*) INTEGER NINTER @@ -472,6 +617,13 @@ C c LOCAL INTEGER I,IHEL,IPART DOUBLE PRECISION PI +C Crossed copies, built once (see the crossing block below). + REAL*8 PUSE(0:3,NEXTERNAL) + INTEGER NHELUSE(NEXTERNAL,NB_NHEL) + INTEGER IC(NEXTERNAL), ICUSE(NEXTERNAL) + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL), CPOS(NEXTERNAL) + INTEGER FLAV_USE, DUMFLAV + DOUBLE PRECISION RESCALE C INTEGER NHEL(NEXTERNAL,NB_NHEL) C put in common block to expose this variable to python interface @@ -494,13 +646,30 @@ C G = 2* DSQRT(ALPHAS*pi) call UPDATE_AS_PARAM() ENDIF +C Unresolved flavor (GET_FLAVOR_INDEX miss): the matrix element and hence +C every interference term is identically zero. Guarded after the alphas +C update so that side effect is unchanged. + IF (FLAV_IDX.LT.1) THEN + return + ENDIF +C Decode and apply the crossing ONCE for the whole density matrix: the +C permutation is the same for every helicity row, so the NHEL table is +C permuted in one sweep. RESCALE carries the flavor / crossing dependent +C part of the normalisation (RESCALE=0 = impossible crossing). + IC(:) = 1 + CALL %(proc_prefix)sGET_INTER_RESCALE(FLAV_IDX, FLAV_USE, RESCALE) + IF (RESCALE.EQ.0D0) THEN + return + ENDIF + CALL %(proc_prefix)sFILL_NHEL() +%(density_cross_apply)s DO IHEL =1, NB_NHEL - THISNHEL(:) = NHEL(:, IHEL) + THISNHEL(:) = NHELUSE(:, IHEL) DO IPART=1,N_CHANGING - if(THISNHEL(POS(IPART)).NE.ALLOW_HEL(IPART)) GOTO 10 !BYPASS COMPUTATION FOR HELICITY + if(THISNHEL(CPOS(IPART)).NE.ALLOW_HEL(IPART)) GOTO 10 !BYPASS COMPUTATION FOR HELICITY ENDDO TMP_INTER(:) = 0 - call %(proc_prefix)sGET_ALL_INTER(P, THISNHEL, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAVOR, TMP_INTER) + call %(proc_prefix)sGET_ALL_INTER_CROSSED(PUSE, THISNHEL, ICUSE, CPOS, N_CHANGING, ALLOW_HEL, N_COMB, FLAV_USE, RESCALE, TMP_INTER) do I = 1, N_COMB*(N_COMB+1)/2 INTER(I) = INTER(I) + TMP_INTER(I) enddo @@ -510,12 +679,45 @@ C end SUBROUTINE %(proc_prefix)sGET_ALL_INTER(P, NHEL, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAVOR, INTER) +C Entry point taking the full FLAVOR(NEXTERNAL) array (back-compat); see +C GET_DENSITY. Use GET_ALL_INTER_IDX to request a crossing. + implicit none + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) +CF2PY INTENT(IN) :: P(0:3,%(nexternal)d) +CF2PY INTENT(IN) :: NHEL(%(nexternal)d) +CF2PY INTENT(IN) :: POS(N_CHANGING) +CF2PY INTENT(IN) :: N_CHANGING +CF2PY INTENT(IN) :: ALLOW_HEL(N_CHANGING*N_COMB) +CF2PY INTENT(IN) :: N_COMB +CF2PY INTENT(IN) :: FLAVOR(%(nexternal)d) +CF2PY INTENT(OUT) :: INTER(NCOMB*(NCOMB+1)/2) + REAL*8 P(0:3,NEXTERNAL) + INTEGER NHEL(NEXTERNAL) + INTEGER N_CHANGING, N_COMB + INTEGER POS(*) + INTEGER ALLOW_HEL(*) + INTEGER FLAVOR(NEXTERNAL) + DOUBLE COMPLEX INTER(*) + INTEGER %(proc_prefix)sGET_FLAVOR_INDEX + + CALL %(proc_prefix)sGET_ALL_INTER_IDX(P, NHEL, POS, N_CHANGING, + & ALLOW_HEL, N_COMB, %(proc_prefix)sGET_FLAVOR_INDEX(FLAVOR), INTER) + RETURN + END + + + SUBROUTINE %(proc_prefix)sGET_ALL_INTER_IDX(P, NHEL, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAV_IDX, INTER) c P momenta c NHEL base of helicity that are not changing c POS(N_CHNGING): position of the changing helicity c n_changing: number of changing helicity c ALLOW_HEL(NCOMB, N_CHANGING): combination of helicity to consider (all jamp computed) c INTER((NCOMB*NCOMB+1)/2: all interference term (not the symmetric one) +c FLAV_IDX may carry a crossing: it is decoded and applied ONCE here, and +c GET_ALL_INTER_CROSSED below then only sees crossed arrays. POS is given +c in the UNCROSSED (source process) slot numbering and is mapped through +c the crossing permutation here. implicit none CF2PY INTENT(IN) :: P(0:3,%(nexternal)d) CF2PY INTENT(IN) :: NHEL(%(nexternal)d) @@ -523,7 +725,7 @@ CF2PY INTENT(IN) :: POS(N_CHANGING) CF2PY INTENT(IN) :: N_CHANGING CF2PY INTENT(IN) :: ALLOW_HEL(N_CHANGING*N_COMB) CF2PY INTENT(IN) :: N_COMB -CF2PY INTENT(IN) :: FLAVOR(%(nexternal)d) +CF2PY INTENT(IN) :: FLAV_IDX CF2PY INTENT(OUT) :: INTER(NCOMB*(NCOMB+1)/2) c C @@ -536,7 +738,97 @@ C INTEGER N_CHANGING, N_COMB INTEGER POS(*) INTEGER ALLOW_HEL(*) + INTEGER FLAV_IDX + DOUBLE COMPLEX INTER(*) +c +c LOCAL +c + INTEGER I, IPART + INTEGER IC(NEXTERNAL), ICUSE(NEXTERNAL) + REAL*8 PUSE(0:3,NEXTERNAL) + INTEGER NHELUSE(NEXTERNAL) + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL), CPOS(NEXTERNAL) + INTEGER FLAV_USE, DUMFLAV + DOUBLE PRECISION RESCALE +C ---------- +C BEGIN CODE +C ---------- +C Unresolved flavor (not an allowed combination): the matrix element and +C therefore all interference terms are zero. + IF (FLAV_IDX.LT.1) THEN + DO I = 1, N_COMB*(N_COMB+1)/2 + INTER(I) = (0d0, 0d0) + ENDDO + RETURN + ENDIF +C RESCALE carries everything flavor / crossing dependent in the +C normalisation; RESCALE=0 marks a crossing that cannot be applied. + CALL %(proc_prefix)sGET_INTER_RESCALE(FLAV_IDX, FLAV_USE, RESCALE) + IF (RESCALE.EQ.0D0) THEN + DO I = 1, N_COMB*(N_COMB+1)/2 + INTER(I) = (0d0, 0d0) + ENDDO + RETURN + ENDIF +%(allinter_cross_apply)s + CALL %(proc_prefix)sGET_ALL_INTER_CROSSED(PUSE, NHELUSE, ICUSE, CPOS, + & N_CHANGING, ALLOW_HEL, N_COMB, FLAV_USE, RESCALE, INTER) + RETURN + END + + + SUBROUTINE %(proc_prefix)sGET_INTER_RESCALE(FLAV_IDX, FLAV_USE, + & RESCALE) +C Split an extended FLAV_IDX and return the factor by which GET_INTER's +C output must be multiplied. +C +C GET_INTER only ever sees JAMPs, so it cannot know the flavor: it +C normalises with the bare static IDEN and everything flavor dependent has +C to be applied by its caller. That is also what keeps the density matrix +C consistent with SMATRIX. RESCALE=0 marks a crossing that cannot be +C applied (zero matrix element). + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) + INTEGER FLAV_IDX, FLAV_USE + DOUBLE PRECISION RESCALE INTEGER FLAVOR(NEXTERNAL) + INTEGER %(proc_prefix)sBROKEN_SYM +%(inter_rescale_decl)s + + FLAV_USE = MOD(FLAV_IDX-1, NFLAV) + 1 + CALL %(proc_prefix)sGET_FLAVOR(FLAV_USE, FLAVOR) +%(inter_rescale_body)s + + RETURN + END + + + SUBROUTINE %(proc_prefix)sGET_ALL_INTER_CROSSED(P, NHEL, IC, POS, N_CHANGING, ALLOW_HEL, N_COMB, FLAV_IDX, RESCALE, INTER) +c Inner worker of the density machinery. +c +c CONTRACT: P, NHEL and IC are ALREADY crossed, POS is expressed in the +c CROSSED slot numbering, FLAV_IDX is ALREADY reduced to [1,NFLAV] and +c RESCALE already accounts for BROKEN_SYM / the crossed denominator. The +c callers (GET_ALL_INTER_IDX, GET_DENSITY_IDX) decode and apply the +c crossing once, so nothing is decoded per GET_AMP call here. +c NHEL is overwritten at the POS slots. + implicit none +C +C ARGUMENTS +C + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + REAL*8 P(0:3,NEXTERNAL) + INTEGER NHEL(NEXTERNAL) + INTEGER IC(NEXTERNAL) + INTEGER N_CHANGING, N_COMB + INTEGER POS(*) + INTEGER ALLOW_HEL(*) + INTEGER FLAV_IDX + DOUBLE PRECISION RESCALE DOUBLE COMPLEX INTER(*) c c Intermediate array @@ -545,18 +837,14 @@ c PARAMETER (NGRAPHS=%(ngraphs)d) INTEGER NCOLOR PARAMETER (NCOLOR=%(ncolor)d) - INTEGER IC(NEXTERNAL) DOUBLE COMPLEX AMP(NGRAPHS) DOUBLE COMPLEX, ALLOCATABLE, SAVE :: JAMP(:,:) INTEGER, SAVE :: S_NCOMB = 0 - c c LOCAL c INTEGER I,J,SOL,N - INTEGER FLAV_IDX - INTEGER %(proc_prefix)sGET_FLAVOR_INDEX if (allocated(jamp) .and. S_NCOMB.ne.N_COMB) then deallocate(jamp) @@ -569,16 +857,6 @@ c C ---------- C BEGIN CODE C ---------- - IC(:)=1 - FLAV_IDX = %(proc_prefix)sGET_FLAVOR_INDEX(FLAVOR) -C Unresolved flavor (not an allowed combination): the matrix element and -C therefore all interference terms are zero. - IF (FLAV_IDX.EQ.0) THEN - DO I = 1, N_COMB*(N_COMB+1)/2 - INTER(I) = (0d0, 0d0) - ENDDO - RETURN - ENDIF do I = 1, N_COMB do N = 1, N_CHANGING NHEL(POS(N)) = ALLOW_HEL((I-1)*N_CHANGING+N) @@ -592,6 +870,7 @@ C therefore all interference terms are zero. DO J= I, N_COMB SOL = SOL +1 call %(proc_prefix)sGET_INTER(JAMP(1,I), JAMP(1,J), INTER(SOL)) + INTER(SOL) = INTER(SOL)*RESCALE ENDDO ENDDO @@ -761,6 +1040,89 @@ C ---------- END + SUBROUTINE %(proc_prefix)sDECODE_HEL(CODE, THISNHEL) +C Decode a canonical mixed-radix helicity CODE (1..NCOMBFULL) into the +C per-leg helicity values THISNHEL(NEXTERNAL). The last external leg is the +C least-significant digit, matching the itertools.product ordering used to +C build the allowed-code list HELALLOW. + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER MAXHEL + PARAMETER (MAXHEL=%(maxhel)d) + INTEGER CODE, THISNHEL(NEXTERNAL) + INTEGER I, K, R, D + INTEGER NHSTATE(NEXTERNAL), STATES(MAXHEL,NEXTERNAL) +%(nhstate_data)s +%(states_data)s + R = CODE - 1 + DO K=NEXTERNAL,1,-1 + D = MOD(R, NHSTATE(K)) + THISNHEL(K) = STATES(D+1, K) + R = R / NHSTATE(K) + ENDDO + RETURN + END + + SUBROUTINE %(proc_prefix)sENCODE_HEL(THISNHEL, CODE) +C Inverse of DECODE_HEL: encode per-leg helicity values THISNHEL into the +C canonical mixed-radix code (used by the crossing-aware routines). + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER MAXHEL + PARAMETER (MAXHEL=%(maxhel)d) + INTEGER THISNHEL(NEXTERNAL), CODE + INTEGER I, K, D + INTEGER NHSTATE(NEXTERNAL), STATES(MAXHEL,NEXTERNAL) +%(nhstate_data)s +%(states_data)s + CODE = 0 + DO K=1,NEXTERNAL + DO D=1,NHSTATE(K) + IF (STATES(D,K).EQ.THISNHEL(K)) GOTO 5 + ENDDO + D = 1 + 5 CONTINUE + CODE = CODE*NHSTATE(K) + (D-1) + ENDDO + CODE = CODE + 1 + RETURN + END + + SUBROUTINE %(proc_prefix)sFILL_NHEL() +C Materialize the PROCESS_NHEL config table by decoding the list of allowed +C canonical helicity codes (HELALLOW). Runs once; the table is a runtime +C cache of the encoder/decoder representation, kept for the density-matrix +C and python (f2py) interfaces. + IMPLICIT NONE + INTEGER NEXTERNAL + PARAMETER (NEXTERNAL=%(nexternal)d) + INTEGER NCOMB + PARAMETER ( NCOMB=%(ncomb)d) + INTEGER NHEL(NEXTERNAL,NCOMB) + COMMON/%(proc_prefix)sPROCESS_NHEL/NHEL + INTEGER HELALLOW(NCOMB) + INTEGER I, K, THIS(NEXTERNAL) + LOGICAL DONE + SAVE DONE +%(hel_allow_data)s + DATA DONE /.FALSE./ + IF (DONE) RETURN + DO I=1,NCOMB + CALL %(proc_prefix)sDECODE_HEL(HELALLOW(I), THIS) + DO K=1,NEXTERNAL + NHEL(K,I) = THIS(K) + ENDDO + ENDDO + DONE = .TRUE. + RETURN + END + + +%(crossing_routines)s + + %(broken_sym_function)s @@ -768,3 +1130,6 @@ C ---------- %(flavor_array_function)s + + +%(flavor_pdg_function)s diff --git a/madgraph/madevent/gen_ximprove.py b/madgraph/madevent/gen_ximprove.py index 455cec077..c26f0cd43 100755 --- a/madgraph/madevent/gen_ximprove.py +++ b/madgraph/madevent/gen_ximprove.py @@ -271,8 +271,22 @@ def get_helicity(self, to_submit=True, clean=True): fsock.write(data) + # Cross-group crossing (Track B): in a base directory, bake the optim + # over the UNION good-hel of the crossing class so a single compiled + # optim can be shared by every crossing. crossgroup_helunion.dat gives, + # per base matrix index, base->base helicity permutations: the + # dependent for that crossing is good at helicity h iff perm[h] is good + # for the base. + helunion = collections.defaultdict(list) + hu_file = pjoin(Pdir, 'crossgroup_helunion.dat') + if os.path.exists(hu_file): + for line in open(hu_file): + vals = line.split() + if vals: + helunion[vals[0]].append([int(x) for x in vals[1:]]) + for matrix_file in misc.glob('matrix*orig.f', Pdir): - + split_file = matrix_file.split('/') me_index = split_file[-1][len('matrix'):-len('_orig.f')] @@ -286,7 +300,19 @@ def get_helicity(self, to_submit=True, clean=True): # Convert to sorted list for reproducibility #good_hels = sorted(list(good_hels)) - good_hels = [str(x) for x in sorted(all_good_hels[me_index])] + good_set = set(all_good_hels[me_index]) + # Cross-group base: the shared optim is also evaluated with each + # dependent's CROSSED helicity configs, but the recycled MATRIX + # bakes the base's helicity configs (it takes no runtime NHEL). + # The full helicity SUM is invariant under the crossing's helicity + # permutation, whereas the base's own good-hel SUBSET is not the + # dependent's -- dropping configs here biases a crossed dependent. + # So keep EVERY config for a base of a crossing class; wavefunction + # recycling is retained, only the good-hel config filter is off. + perms = helunion.get(me_index, []) + if perms: + good_set = set(range(1, len(perms[0]) + 1)) + good_hels = [str(x) for x in sorted(good_set)] if self.run_card['hel_zeroamp']: bad_amps = [str(x) for x in sorted(all_bad_amps[me_index])] diff --git a/madgraph/various/process_checks.py b/madgraph/various/process_checks.py index 423ec5d60..bce65fc5e 100755 --- a/madgraph/various/process_checks.py +++ b/madgraph/various/process_checks.py @@ -3906,6 +3906,810 @@ def output_flavor(comparison_results, output='text'): return fail_proc +#=============================================================================== +# check_crossing +#=============================================================================== +# Driver script run in a *fresh* interpreter for every compiled matrix2py +# module. Importing an f2py .so pollutes the importing interpreter (the module +# name 'matrix2py' can only be bound once and its Fortran COMMON blocks leak +# globally), so each module has to be probed in its own subprocess; the request +# and the answer are exchanged as JSON through files/stdout. +_CROSSING_DRIVER = r''' +import sys, json +import numpy as np +req = json.load(open(sys.argv[1])) +sys.path.insert(0, req["pdir"]) +import matrix2py +from flavor_dispatch import FlavorDispatch +me = FlavorDispatch(matrix2py) +me.initialisemodel(req["card"]) +out = {} +if req["mode"] == "enumerate": + nflav, nexternal, ncross = me.flavor_layout() + out["layout"] = [nflav, nexternal, ncross] + entries = [] + for cross in range(ncross): + for flav in range(1, nflav + 1): + idx = cross * nflav + flav + pdg = me.pdg_for_index(idx) + if pdg is not None: + entries.append([idx, cross, flav, list(pdg)]) + out["entries"] = entries +elif req["mode"] == "evaluate": + values = [] + for item in req["items"]: + P = np.asfortranarray(np.array(item["momenta"], dtype=float).T) + values.append(float(me.smatrix(P, int(item["index"])))) + out["values"] = values +sys.stdout.write("CROSSJSON:" + json.dumps(out) + "\n") +''' + + +def _crossing_build_env(): + """Environment for building/running the f2py module. + + numpy>=1.26 drives f2py through the meson backend, whose ``meson`` and + ``ninja`` executables normally sit next to the running interpreter. Prepend + that directory to PATH so ``make matrix2py.so`` finds them even when they are + not on the ambient PATH. + """ + env = dict(os.environ) + bindir = os.path.dirname(os.path.abspath(sys.executable)) + env['PATH'] = bindir + os.pathsep + env.get('PATH', '') + return env + + +def _crossing_build_f2py(pdir, env): + """Compile ``matrix2py.so`` in *pdir*; return True on success. + + The system ``f2py`` is unusable on some setups (dangling interpreter, or the + distutils backend removed on numpy>=1.26), so the makefile is driven with + ``F2PY=" -m numpy.f2py"`` which always resolves to the running + interpreter's f2py. A plain ``make matrix2py.so`` is tried first so a + working system f2py is still honoured. + """ + for f2py in (None, '%s -m numpy.f2py' % sys.executable): + for stale in glob.glob(pjoin(pdir, 'matrix2py*.so')): + try: + os.remove(stale) + except OSError: + pass + cmd = ['make', 'matrix2py.so'] + if f2py is not None: + cmd.append('F2PY=%s' % f2py) + with open(os.devnull, 'w') as devnull: + ret = subprocess.call(cmd, cwd=pdir, stdout=devnull, + stderr=devnull, env=env) + if ret == 0 and glob.glob(pjoin(pdir, 'matrix2py*.so')): + return True + return False + + +def _crossing_run_driver(pdir, request, env): + """Run the JSON driver against the module in *pdir*; return the answer dict + (or None on failure).""" + import json + import tempfile + request = dict(request) + request['pdir'] = pdir + script = pjoin(pdir, '_crossing_driver.py') + with open(script, 'w') as fsock: + fsock.write(_CROSSING_DRIVER) + fd, req_path = tempfile.mkstemp(suffix='.json', dir=pdir) + with os.fdopen(fd, 'w') as fsock: + json.dump(request, fsock) + try: + proc = subprocess.Popen([sys.executable, script, req_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=pdir, env=env) + output = proc.communicate()[0].decode() + finally: + try: + os.remove(req_path) + except OSError: + pass + for line in output.split('\n'): + if line.startswith('CROSSJSON:'): + return json.loads(line[len('CROSSJSON:'):]) + logger.debug("Crossing driver produced no answer in %s:\n%s" + % (pdir, output)) + return None + + +# The three standalone backends that decode an extended (crossing-carrying) +# flavor index. 'standalone' is the fortran default (f2py); the other two are +# the C++ / cudacpp-CPU-SIMD standalones. +CROSSING_EXPORTERS = ('standalone', 'standalone_cpp', 'standalone_mg7') + +# Vectorisation (SIMD) choices for the standalone_mg7 (cudacpp) backend; each +# maps to the madmatrix.mk 'BACKEND=cpp' build variant. 'auto' lets +# madmatrix pick the widest instruction set the host CPU supports. Only used by +# the standalone_mg7 crossing backend; ignored by the others. +MG7_SIMD_CHOICES = ('auto', 'none', 'sse4', 'avx2', '512y', '512z') + +# Floating-point precision choices for the standalone_mg7 (cudacpp) backend, each +# mapping to the madmatrix.mk 'FPTYPE=' build variant: 'd' double, 'f' float, +# 'm' mixed (double elsewhere, float in the colour algebra -- the madmatrix +# default). Only used by the standalone_mg7 crossing backend. +MG7_PRECISION_CHOICES = ('f', 'm', 'd') + + +def _crossing_pdg_entries(matrix_element, identity_only=False): + """Python enumeration of a matrix element's reachable extended flavor ids. + + Returns ``[(index, cross, flav0, pdg_tuple), ...]`` with a 0-based index + (``cross*NFLAV+flav0``) -- the encoding the C++/mg7 sigmaKin decodes. This + is the crossing twin of the fortran runtime GET_PDG_FOR_FLAVOR, used for the + backends that have no runtime PDG accessor. See + ProcessExporterFortran.compute_crossing_pdg_entries. + """ + if matrix_element is None: + # Correlation to a P* directory failed; the caller skips this module. + return None + import madgraph.iolibs.export_v4 as export_v4 + entries = export_v4.ProcessExporterFortran.compute_crossing_pdg_entries( + None, matrix_element, zero_based=True) + if identity_only: + entries = [e for e in entries if e[1] == 0] + return entries + + +# ── C++ standalone (standalone_cpp) ───────────────────────────────────────── +# A tiny driver that evaluates sigmaKin at the requested flavor_id and momenta. +# Each item gets a FRESH CPPProcess so the good-helicity cache (indexed by the +# reduced flavor) cannot carry a warmed-up crossing's helicity pattern into a +# different crossing of the same flavor -- exactly the recipe the acceptance +# test TestStandaloneCppCrossSymmetry uses. The request file holds, on the first +# line the number of items, then per item a flavor_id followed by 4*nexternal +# momentum components (E, px, py, pz per leg, in the leg order the crossed index +# expects them). +_CROSSING_CPP_DRIVER = r''' +#include +#include +#include +#include +#include "CPPProcess.h" +int main(int argc, char** argv){ + std::ifstream in(argv[1]); + int nitems; in >> nitems; + std::cout << std::setprecision(17); + for(int it = 0; it < nitems; it++){ + int fid; in >> fid; + CPPProcess process("../../Cards/param_card.dat"); + int npar = process.nexternal; + std::vector p; + for(int i = 0; i < npar; i++){ + double* m = new double[4]; + for(int j = 0; j < 4; j++) in >> m[j]; + p.push_back(m); + } + process.setMomenta(p); + double me = process.sigmaKin(fid); + std::cout << "ITEM " << it << " " << me << std::endl; + for(int i = 0; i < npar; i++) delete[] p[i]; + } + return 0; +} +''' + + +class _FortranCrossingBackend(object): + """The fortran standalone (f2py) crossing backend -- the historical path. + + Enumeration and evaluation both go through the compiled matrix2py module in + a fresh subprocess (see _CROSSING_DRIVER); the matrix element python object + is not needed because GET_PDG_FOR_FLAVOR resolves the crossed PDG at + runtime. + """ + output_format = 'standalone' + needs_matrix_element = False + + def __init__(self, options=None): + # options accepted for a uniform backend signature; --simd only applies + # to standalone_mg7. + pass + + def build(self, pdir, env): + return _crossing_build_f2py(pdir, env) + + def enumerate(self, pdir, matrix_element, card, env, identity_only): + answer = _crossing_run_driver( + pdir, {'mode': 'enumerate', 'card': card}, env) + if not answer: + return None + entries = [] + for idx, cross, flav, pdg in answer['entries']: + if identity_only and cross != 0: + continue + entries.append((idx, cross, flav, tuple(pdg))) + return entries + + def evaluate(self, pdir, items, card, env): + answer = _crossing_run_driver( + pdir, {'mode': 'evaluate', 'card': card, 'items': items}, env) + return answer['values'] if answer else [None] * len(items) + + +class _CppCrossingBackend(object): + """The C++ standalone (standalone_cpp) crossing backend. + + (`options` is accepted for a uniform backend constructor signature; the + SIMD/vectorisation choice only applies to standalone_mg7.) + + The crossed PDG of an extended flavor_id is computed in python (there is no + runtime accessor); evaluation compiles a small driver that news a fresh + CPPProcess per item and calls sigmaKin(flavor_id). + """ + output_format = 'standalone_cpp' + needs_matrix_element = True + + def __init__(self, options=None): + self.compiler = os.environ.get('CXX', 'g++') + + def build(self, pdir, env): + if not shutil.which(self.compiler): + return False + with open(os.devnull, 'w') as devnull: + rc = subprocess.call(['make'], cwd=pdir, stdout=devnull, + stderr=subprocess.STDOUT, env=env) + return rc == 0 and os.path.isfile(pjoin(pdir, 'CPPProcess.o')) + + def enumerate(self, pdir, matrix_element, card, env, identity_only): + return _crossing_pdg_entries(matrix_element, identity_only=identity_only) + + def evaluate(self, pdir, items, card, env): + with open(pjoin(pdir, 'driver_cross.cpp'), 'w') as fsock: + fsock.write(_CROSSING_CPP_DRIVER) + cxxflags = ['-O3', '-ffast-math', '-I../../src', '-I.', '-fPIC'] + libflags = ['-L../../lib', '-lmodel_sm'] + with open(os.devnull, 'w') as devnull: + rc = subprocess.call( + [self.compiler] + cxxflags + ['-c', '-o', 'driver_cross.o', + 'driver_cross.cpp'], + cwd=pdir, stdout=devnull, stderr=subprocess.STDOUT, env=env) + if rc != 0: + return [None] * len(items) + rc = subprocess.call( + [self.compiler, '-o', 'driver_cross', 'CPPProcess.o', + 'driver_cross.o'] + libflags, + cwd=pdir, stdout=devnull, stderr=subprocess.STDOUT, env=env) + if rc != 0: + return [None] * len(items) + req = pjoin(pdir, 'driver_cross.in') + with open(req, 'w') as fsock: + fsock.write('%d\n' % len(items)) + for item in items: + comps = ['%d' % int(item['index'])] + for leg in item['momenta']: + comps.extend('%.17e' % float(c) for c in leg) + fsock.write(' '.join(comps) + '\n') + try: + out = subprocess.check_output(['./driver_cross', 'driver_cross.in'], + cwd=pdir, env=env).decode() + except subprocess.CalledProcessError: + return [None] * len(items) + values = [None] * len(items) + for match in re.finditer(r'ITEM\s+(\d+)\s+([-\d.eE+]+)', out): + values[int(match.group(1))] = float(match.group(2)) + return values + + +# ── cudacpp CPU-SIMD standalone (standalone_mg7) ───────────────────────────── +# check_sa.exe generates its own RAMBO momenta, so to evaluate at a prescribed +# phase-space point the shipped check_sa.cc is patched (as the acceptance test +# TestStandaloneMg7CrossSymmetry does): its flavorID cap is lifted so the +# extended crossing ids pass validation, and, when MG_MOMFILE is set, the +# momenta read from that file are written into every event of the SIMD page +# before the matrix element is computed. +_MG7_CAP_FROM = 'if( flavorID >= CPPProcess::nmaxflavor )' +_MG7_CAP_TO = ('if( flavorID >= CPPProcess::nmaxflavor * ' + '(unsigned)((CPPProcess::npar+1)*(CPPProcess::npar+1)) )') +_MG7_MOM_FROM = ' prsk->getMomentaFinal();' +_MG7_MOM_TO = ( + ' prsk->getMomentaFinal();\n' + ' if( const char* mgmf = getenv("MG_MOMFILE") ) {\n' + ' std::ifstream mgin( mgmf );\n' + ' std::vector mgbuf( (std::size_t)CPPProcess::npar*4 );\n' + ' for( std::size_t mgk = 0; mgk < mgbuf.size(); mgk++ ) mgin >> mgbuf[mgk];\n' + ' for( unsigned int mgie = 0; mgie < nevt; mgie++ )\n' + ' for( int mgip = 0; mgip < CPPProcess::npar; mgip++ )\n' + ' for( int mgi4 = 0; mgi4 < 4; mgi4++ )\n' + ' MemoryAccessMomenta::ieventAccessIp4Ipar( hstMomenta.data(), mgie, mgi4, mgip ) = mgbuf[mgip*4+mgi4];\n' + ' }') + + +class _Mg7CrossingBackend(object): + """The cudacpp CPU-SIMD standalone (standalone_mg7) crossing backend. + + The vectorisation width is selectable via options['simd'] (see + MG7_SIMD_CHOICES): it is passed to the madmatrix build as + 'BACKEND=cpp', so the same crossing self-check can run on scalar + (none), SSE4, AVX2 or AVX-512 code, or let madmatrix auto-detect ('auto'). + The floating-point precision is selectable via options['precision'] (see + MG7_PRECISION_CHOICES): it is passed as 'FPTYPE=' (f/m/d). + """ + output_format = 'standalone_mg7' + needs_matrix_element = True + + def __init__(self, options=None): + self.compiler = os.environ.get('CXX', 'g++') + simd = (options or {}).get('simd', 'auto') + if simd not in MG7_SIMD_CHOICES: + raise InvalidCmd( + "Unknown --simd '%s' for standalone_mg7; choose one of %s." + % (simd, ', '.join(MG7_SIMD_CHOICES))) + self.simd = simd + precision = (options or {}).get('precision', 'm') + if precision not in MG7_PRECISION_CHOICES: + raise InvalidCmd( + "Unknown --precision '%s' for standalone_mg7; choose one of %s." + % (precision, ', '.join(MG7_PRECISION_CHOICES))) + self.precision = precision + + def build(self, pdir, env): + if not shutil.which(self.compiler): + return False + check = pjoin(pdir, 'check_sa.cc') + try: + with open(check) as fsock: + src = fsock.read() + except IOError: + return False + src = src.replace(_MG7_CAP_FROM, _MG7_CAP_TO) + src = src.replace(_MG7_MOM_FROM, _MG7_MOM_TO, 1) + with open(check, 'w') as fsock: + fsock.write(src) + make_cmd = ['make', '-j2', 'BACKEND=cpp%s' % self.simd, + 'FPTYPE=%s' % self.precision, 'check_sa.exe'] + with open(os.devnull, 'w') as devnull: + rc = subprocess.call(make_cmd, cwd=pdir, + stdout=devnull, stderr=subprocess.STDOUT, + env=env) + return rc == 0 and os.path.isfile(pjoin(pdir, 'check_sa.exe')) + + def enumerate(self, pdir, matrix_element, card, env, identity_only): + return _crossing_pdg_entries(matrix_element, identity_only=identity_only) + + def evaluate(self, pdir, items, card, env): + values = [] + for item in items: + momfile = pjoin(pdir, 'mom_cross.dat') + with open(momfile, 'w') as fsock: + for leg in item['momenta']: + fsock.write(' '.join('%.17e' % float(c) for c in leg) + '\n') + run_env = dict(env) + run_env['MG_MOMFILE'] = momfile + try: + out = subprocess.check_output( + ['./check_sa.exe', 'perf', '-v', '-f', + str(int(item['index'])), '1', '8', '1'], + cwd=pdir, env=run_env, stderr=subprocess.STDOUT).decode() + except subprocess.CalledProcessError: + values.append(None) + continue + mes = re.findall(r'Matrix element =\s*([-\d.eE+]+)', out) + values.append(float(mes[0]) if mes else None) + return values + + +_CROSSING_BACKENDS = { + 'standalone': _FortranCrossingBackend, + 'standalone_cpp': _CppCrossingBackend, + 'standalone_mg7': _Mg7CrossingBackend, +} + + +def _crossing_dir_name(matrix_element): + """The SubProcesses/P* directory name generated for this matrix element. + + Both the C++ and the mg7 exporters name the directory ``P`` + the process + shell string (export_cpp uses P_ and export_mg7 uses + P, and shell_string already is ``_``), so this + single reconstruction correlates a matrix element to its output directory + for either backend without instantiating a throwaway exporter. + """ + return 'P' + matrix_element.get('processes')[0].shell_string() + + +def check_crossing(process_definition, param_card=None, options=None, + cmd=FakeInterface()): + """Compare the crossing-enabled and crossing-disabled standalone output. + + The process is generated twice and output to the standalone backend picked + by ``options['exporter']`` (one of :data:`CROSSING_EXPORTERS`; default + ``'standalone'``, the fortran/f2py path): + + * ``--use_crossing=False`` — the crossing machinery is *off*; each generated + matrix element is self-contained and reachable only as its own identity. + This is the independent, per-diagram reference (``value_direct``). + * ``--use_crossing=True`` — the crossing machinery is *on*; a single matrix + element reaches many physical processes through the extended flavor index + (leg permutation + NSF flip + per-crossing denominator). + + For every physical subprocess of the reference, the same signed-PDG process + is located in the crossing output and evaluated *through a genuine crossing* + (a non-identity extended index reproducing that PDG signature, when one + exists) at the very same phase-space point, giving ``value_crossed``. The + two must agree: this exercises the crossing (leg permutation / dynamic NSF / + crossed averaging denominator) against a value computed with none of them. + + The backend abstraction (:data:`_CROSSING_BACKENDS`) parametrises the three + steps that differ per exporter -- the ``output`` format, the build, and how + an extended index is evaluated -- while the generate/match/momenta logic is + shared. ``'standalone'`` enumerates the crossed PDG at runtime via f2py + (GET_PDG_FOR_FLAVOR); ``'standalone_cpp'`` / ``'standalone_mg7'`` have no + runtime accessor and compute it in python from the same crossing tables + (:func:`_crossing_pdg_entries`), then evaluate through a compiled driver. + + Processes whose crossing is auto-disabled by an s-channel constraint (e.g. + ``u u~ > z > e+ e-``: what is s-channel in one arrangement is not in its + crossings) reach nothing but their own identity, so ``value_crossed`` falls + back to the identity and the result is flagged 'crossing not applicable'. + + Returns a list of result dicts consumed by :func:`output_crossing`. + """ + import tempfile + import madgraph.interface.master_interface as master_interface + + if options is None: + options = {} + energy = float(options.get('energy', 1000.0)) + + exporter = options.get('exporter', 'standalone') + if exporter not in _CROSSING_BACKENDS: + raise InvalidCmd( + "Unknown crossing exporter '%s'; choose one of %s." + % (exporter, ', '.join(CROSSING_EXPORTERS))) + backend = _CROSSING_BACKENDS[exporter](options) + + model = process_definition.get('model') + proc_line = options.get('proc_line') + if proc_line is None: + # Fall back to a regenerable string; the caller normally supplies the + # verbatim line via options so s-channel/forbidden constraints survive. + proc_line = process_definition.nice_string().split(':', 1)[-1].strip() + modelname = model.get('modelpath') or model.get('name') + + ninitial = len([leg for leg in process_definition.get('legs') + if not leg.get('state')]) + + tmproot = tempfile.mkdtemp(prefix='mg5_crosscheck_') + + def _generate(use_crossing, name): + """Generate + output the backend format; return + ``(outdir, pdirs, me_by_pdir)``. + + ``me_by_pdir`` maps each P* directory to its matrix element (only built + when the backend needs it -- the C++/mg7 backends compute the crossed + PDG in python and so need the matrix element object; the fortran backend + resolves it at runtime and leaves the map empty).""" + mgcmd = master_interface.MasterCmd() + mgcmd.no_notification() + mgcmd.exec_cmd('set automatic_html_opening False', printcmd=False) + mgcmd.exec_cmd('set group_subprocesses False', printcmd=False) + mgcmd.exec_cmd('set apply_flavor_grouping True', printcmd=False) + mgcmd.exec_cmd('import model %s' % modelname, printcmd=False) + # Carry over any user-defined multiparticle labels (e.g. a custom + # 'define x = g u u~'); the built-in ones (p, j, ...) are recreated by + # 'import model', but user labels only live in the caller's session. + user_mp = getattr(cmd, '_multiparticles', None) + if user_mp and hasattr(mgcmd, '_multiparticles'): + mgcmd._multiparticles.update(user_mp) + mgcmd.exec_cmd('generate %s --use_crossing=%s' + % (proc_line, use_crossing), printcmd=False) + outdir = pjoin(tmproot, name) + mgcmd.exec_cmd('output %s %s -f' % (backend.output_format, outdir), + printcmd=False) + subroot = pjoin(outdir, 'SubProcesses') + pdirs = [pjoin(subroot, d) for d in sorted(os.listdir(subroot)) + if d.startswith('P') and os.path.isdir(pjoin(subroot, d))] + me_by_pdir = {} + if backend.needs_matrix_element: + by_name = {} + try: + for me in mgcmd._curr_matrix_elements.get_matrix_elements(): + by_name[_crossing_dir_name(me)] = me + except Exception as err: + logger.debug("Could not read matrix elements for the crossing " + "check (%s): %s" % (backend.output_format, err)) + for pdir in pdirs: + me_by_pdir[pdir] = by_name.get(os.path.basename(pdir)) + # If the user supplied a param_card, use it in place of the model + # default for both evaluation and momenta generation. + if param_card: + shutil.copy(param_card, pjoin(outdir, 'Cards', 'param_card.dat')) + return outdir, pdirs, me_by_pdir + + def _pdg_label(pdg): + try: + names = [] + for code in pdg: + part = model.get_particle(code) + names.append(part.get_name() if part else str(code)) + return (' '.join(names[:ninitial]) + ' > ' + + ' '.join(names[ninitial:])) + except Exception: + return str(tuple(pdg)) + + results = [] + env = _crossing_build_env() + try: + ref_out, ref_pdirs, ref_me = _generate('False', 'reference') + cross_out, cross_pdirs, cross_me = _generate('True', 'crossing') + ref_card = pjoin(ref_out, 'Cards', 'param_card.dat') + cross_card = pjoin(cross_out, 'Cards', 'param_card.dat') + + # ── build every module ────────────────────────────────────────────── + built = {} + for pdir in ref_pdirs + cross_pdirs: + built[pdir] = backend.build(pdir, env) + if not any(built.get(pdir) for pdir in ref_pdirs) or \ + not any(built.get(pdir) for pdir in cross_pdirs): + # Nothing usable on either side: signal a skip rather than a fail. + return [{'status': 'build_failed', 'exporter': exporter}] + + # ── enumerate the crossing output: pdg-tuple -> (pdir, index, cross) ─ + # Two-stage matching so the crossing code path is exercised *safely*: + # * within a module keep the lowest-cross index per PDG (this is what + # find_pdg does). A module owning the process as its identity gives + # cross==0; a module reaching it only by crossing gives cross>0. The + # dedup is essential -- a *shadowed* higher-cross index can report the + # same PDG yet evaluate to a different (wrong) value, so it must never + # be picked over the identity of the module that owns the process. + # * across modules prefer a genuine crossing (cross>0) from a module + # that does not own the process, so the comparison exercises the + # crossing rather than a plain identity when the process line spans + # crossable subprocesses. + cross_map = {} + for pdir in cross_pdirs: + if not built.get(pdir): + continue + entries = backend.enumerate(pdir, cross_me.get(pdir), cross_card, + env, identity_only=False) + if not entries: + continue + module_map = {} # find_pdg semantics: lowest cross per PDG + for idx, cross, _flav, pdg in entries: + key = tuple(pdg) + if key not in module_map: + module_map[key] = (idx, cross) + for key, (idx, cross) in module_map.items(): + existing = cross_map.get(key) + # Prefer a genuine crossing (cross>0) over an identity match. + if existing is None or (existing[2] == 0 and cross > 0): + cross_map[key] = (pdir, idx, cross) + + # ── enumerate the reference identities and generate momenta ───────── + # (ref_pdir, ref_idx, pdg) for every reference subprocess (cross==0). + ref_subprocs = [] + momenta_by_pdg = {} + for pdir in ref_pdirs: + if not built.get(pdir): + continue + entries = backend.enumerate(pdir, ref_me.get(pdir), ref_card, env, + identity_only=True) + if not entries: + continue + for idx, cross, _flav, pdg in entries: + if cross != 0: + continue # reference has no genuine crossing anyway + key = tuple(pdg) + ref_subprocs.append((pdir, idx, key)) + if key not in momenta_by_pdg: + momenta_by_pdg[key] = _crossing_momenta( + key, ninitial, model, param_card, energy, cmd) + + # ── batch the evaluations per module ──────────────────────────────── + # value_direct: reference module at its own identity index. + direct_jobs = {} + for pdir, idx, key in ref_subprocs: + direct_jobs.setdefault(pdir, []).append((idx, key)) + direct_val = {} + for pdir, jobs in direct_jobs.items(): + items = [{'index': idx, 'momenta': momenta_by_pdg[key]} + for idx, key in jobs if momenta_by_pdg[key] is not None] + values = backend.evaluate(pdir, items, ref_card, env) + vi = 0 + for idx, key in jobs: + if momenta_by_pdg[key] is None: + continue + direct_val[(pdir, idx, key)] = values[vi] + vi += 1 + + # value_crossed: crossing module at the (preferably crossed) index. + crossed_jobs = {} + for _pdir, _idx, key in ref_subprocs: + match = cross_map.get(key) + if match is None or momenta_by_pdg[key] is None: + continue + cpdir, cidx, _ccross = match + crossed_jobs.setdefault(cpdir, []).append((cidx, key)) + crossed_val = {} + for cpdir, jobs in crossed_jobs.items(): + items = [{'index': cidx, 'momenta': momenta_by_pdg[key]} + for cidx, key in jobs] + values = backend.evaluate(cpdir, items, cross_card, env) + for (cidx, key), value in zip(jobs, values): + crossed_val[(cpdir, cidx, key)] = value + + # ── assemble the per-subprocess results ───────────────────────────── + for pdir, idx, key in ref_subprocs: + value_direct = direct_val.get((pdir, idx, key)) + match = cross_map.get(key) + value_crossed = None + cross_code = None + # crossing_matched records whether a crossing reproducing this + # subprocess was *located* in the crossing build, so the report can + # tell "no crossing reaches this here" apart from "a crossing was + # found but its matrix element could not be evaluated". + crossing_matched = match is not None + if match is not None and momenta_by_pdg[key] is not None: + cpdir, cidx, ccross = match + value_crossed = crossed_val.get((cpdir, cidx, key)) + cross_code = ccross + results.append({ + 'process': _pdg_label(key), + 'pdg': key, + 'value_direct': value_direct, + 'value_crossed': value_crossed, + 'cross_code': cross_code, + 'crossing_matched': crossing_matched, + 'exporter': exporter, + 'status': 'ok', + }) + finally: + shutil.rmtree(tmproot, ignore_errors=True) + + return results + + +def _crossing_momenta(pdg, ninitial, model, param_card, energy, cmd): + """A seeded phase-space point for the leg ordering *pdg* (signed codes). + + Uses the same RAMBO seed as the check_sa templates so the point is + reproducible. Returns a list of ``[E, px, py, pz]`` per leg, or None. + """ + try: + legs = base_objects.LegList() + for i, code in enumerate(pdg): + legs.append(base_objects.Leg({'id': int(code), + 'state': (i >= ninitial), + 'number': i + 1})) + proc = base_objects.Process({'legs': legs, 'model': model}) + evaluator = MatrixElementEvaluator(model, param_card, cmd=cmd, + auth_skipping=False, reuse=False) + momenta = _get_seeded_python_momenta(proc, evaluator, energy) + if momenta is None: + return None + return [list(map(float, p)) for p in momenta] + except Exception as err: + logger.debug("Could not build momenta for %s: %s" % (tuple(pdg), err)) + return None + + +def output_crossing(comparison_results, output='text'): + """Present the results of a crossing check in a table. + + Compares ``value_direct`` (the crossing-disabled build, evaluating the + subprocess with its own diagrams) against ``value_crossed`` (the + crossing-enabled build, evaluating the same signed-PDG process through the + extended flavor index). ``output='fail'`` returns the number of failures + instead of the formatted string. + """ + exporter = None + for data in comparison_results: + if data.get('exporter'): + exporter = data['exporter'] + break + + if len(comparison_results) == 1 and \ + comparison_results[0].get('status') == 'build_failed': + if exporter in ('standalone', None): + reason = ("f2py matrix2py module (f2py / numpy build backend " + "unavailable)") + else: + reason = "%s output (C++ compiler / build toolchain unavailable)" \ + % exporter + msg = ("Could not build the %s; the crossing check cannot run here." + % reason) + return 0 if output == 'fail' else msg + + proc_col_size = 17 + process_header = "Process" + for data in comparison_results: + # Leave room for the ' (identity)' tag that may be appended below. + proc = data['process'] + ' (identity)' + if len(proc) + 1 > proc_col_size: + proc_col_size = len(proc) + 1 + col_size = 20 + + pass_proc = 0 + fail_proc = 0 + no_check_proc = 0 + failed_proc_list = [] + no_check_proc_list = [] + any_crossed = False + + res_str = '' + if exporter: + res_str += "Exporter: %s\n" % exporter + res_str += fixed_string_length(process_header, proc_col_size) + \ + fixed_string_length("Direct", col_size) + \ + fixed_string_length("Crossed", col_size) + \ + fixed_string_length("Relative diff.", col_size) + \ + "Result" + + for one_comp in comparison_results: + proc = one_comp['process'] + val_d = one_comp['value_direct'] + val_c = one_comp['value_crossed'] + + if val_d is None or val_c is None: + no_check_proc += 1 + no_check_proc_list.append(proc) + if val_d is None: + reason = "reference matrix element could not be evaluated" + elif one_comp.get('crossing_matched'): + # A crossing reproducing this process WAS found, but evaluating + # its matrix element failed -- a build/run problem of this + # backend, not a missing crossing. + reason = ("crossing found but its matrix element could not be " + "evaluated with this exporter") + else: + # No crossing in the *generated* output reaches this exact + # subprocess. A crossing may still exist from a process line + # not spanned here (e.g. d d~ > g d d~ for g d > d d d~), or + # the backend groups flavors so this ordering is not produced. + reason = ("no crossing in the generated output reproduces this " + "subprocess") + res_str += '\n' + fixed_string_length(proc, proc_col_size) + \ + " * Not checked: %s *" % reason + continue + + cross_code = one_comp.get('cross_code') + crossed = bool(cross_code) + any_crossed = any_crossed or crossed + + ref = abs(val_d) if val_d != 0 else abs(val_c) + if ref == 0: + diff = 0.0 + else: + diff = abs(val_d - val_c) / ref + + tag = '' if crossed else ' (identity)' + res_str += '\n' + fixed_string_length(proc + tag, proc_col_size) + \ + fixed_string_length("%1.10e" % val_d, col_size) + \ + fixed_string_length("%1.10e" % val_c, col_size) + \ + fixed_string_length("%1.10e" % diff, col_size) + + if diff < 1e-6: + pass_proc += 1 + res_str += "Passed" + else: + fail_proc += 1 + failed_proc_list.append(proc) + res_str += "Failed" + + res_str += "\nSummary: %i/%i passed, %i/%i failed" % ( + pass_proc, pass_proc + fail_proc, + fail_proc, pass_proc + fail_proc) + if fail_proc: + res_str += "\nFailed processes: %s" % ', '.join(failed_proc_list) + if no_check_proc: + res_str += "\nNot checked processes: %s" % ', '.join(no_check_proc_list) + if not any_crossed and (pass_proc or fail_proc): + res_str += ("\nNote: every subprocess was matched at the identity, so " + "this compares the crossing-enabled build against the " + "crossing-disabled one at cross=0. No non-identity crossing " + "was reached -- either the process line spans no crossable " + "subprocesses or a constrained s-channel forbids crossing.") + + if output == 'text': + return res_str + else: + return fail_proc + + #=============================================================================== # Marsaglia-Zaman RNG matching the check_sa Fortran/C++ template seed #=============================================================================== diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index b4d1bac78..382d7324d 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -1741,6 +1741,10 @@ def get_process_function_definitions(self, write=True): export_v4.ProcessExporterFortran._fill_broken_sym_replace_dict( replace_dict, sym_data) + # Crossing-symmetry holes (identity fills when use_crossing is off -> + # byte-identical output). See get_madmatrix_crossing_dict. + replace_dict.update(self.get_madmatrix_crossing_dict(self.matrix_elements[0])) + file = self.read_template_file(self.process_definition_template) % replace_dict # HACK! ignore write=False case if len(params) == 0: # remove cIPD from OpenMP pragma (issue #349) file_lines = file.split('\n') @@ -1771,6 +1775,9 @@ def get_sigmaKin_lines(self, color_amplitudes, write=True): replace_dict['nb_channel'] = len(self.multi_channel_map) replace_dict['nb_color'] = max(1, len(self.matrix_elements[0].get('color_basis'))) + # Crossing-symmetry hole (per-event denominator); identity fill when off. + replace_dict.update(self.get_madmatrix_crossing_dict(self.matrix_elements[0])) + if write: file = self.read_template_file(self.process_sigmaKin_function_template) % replace_dict file = strip_banner(file, banner_mark = "!") # skip first 8 lines in process_sigmaKin_function.inc (copyright) @@ -1785,11 +1792,19 @@ def get_all_sigmaKin_lines(self, color_amplitudes, class_name): if self.single_helicities: ###misc.sprint(type(self.helas_call_writer)) ###misc.sprint( 'before get_matrix_element_calls', self.matrix_elements[0].get_number_of_wavefunctions() ) # WRONG value of nwf, eg 7 for gg_tt - helas_calls = self.helas_call_writer.get_matrix_element_calls(\ + # Crossing symmetry: tell the helas writer to emit the per-event + # momentum-permutation preamble + NSF-blended external calls. Read at + # emission time and reset afterwards (the writer is reused across + # outputs, per the fortran/standalone_cpp lesson). + self.helas_call_writer.use_crossing_ic = getattr(self, 'use_crossing', False) + try: + helas_calls = self.helas_call_writer.get_matrix_element_calls(\ self.matrix_elements[0], color_amplitudes[0], multi_channel_map = self.multi_channel_map ) + finally: + self.helas_call_writer.use_crossing_ic = False ###misc.sprint( 'after get_matrix_element_calls', self.matrix_elements[0].get_number_of_wavefunctions() ) # CORRECT value of nwf, eg 5 for gg_tt assert len(self.matrix_elements) == 1 or len(self.matrix_elements) == 2 # how to handle if this is not true? self.couplings2order = self.helas_call_writer.couplings2order @@ -1930,7 +1945,16 @@ def get_all_sigmaKin_lines(self, color_amplitudes, class_name): file_extend.append( file ) assert i == 0, "more than one ME in get_all_sigmaKin_lines" # AV sanity check (added for color_sum.cc but valid independently) ret_lines.extend( file_extend ) - return '\n'.join(ret_lines) + result = '\n'.join(ret_lines) + if getattr(self, 'use_crossing', False): + # (A) Per-lane crossing: calculate_jamps takes the per-lane helicity + # rows (host only), read by the external block. Gated so a + # non-crossing build keeps the historical signature byte-for-byte. + result = result.replace( + 'const int ievt00 // input: first event number in current C++ event page (for CUDA, ievt depends on threadid)\n#endif', + 'const int ievt00, // input: first event number in current C++ event page (for CUDA, ievt depends on threadid)\n' + ' const int _ighel = -1 // crossing: good-hel index; the external block derives the per-lane helicity per page (>=0 = crossing, -1 = scalar ihel)\n#endif', 1) + return result # AV - modify export_cpp.OneProcessExporterCPP method (replace '# Process' by '// Process') def get_process_info_lines(self, matrix_element): @@ -1952,10 +1976,77 @@ def generate_process_files(self): self.edit_memorybuffers() # AV new file (NB this is generic in Subprocesses and then linked in Sigma-specific) self.edit_memoryaccesscouplings() # AV new file (NB this is generic in Subprocesses and then linked in Sigma-specific) super().generate_process_files() + self.edit_crossing_demo() # per-process folded-crossing flavor ids for check_sa # NB: symlink of cudacpp.mk to makefile is overwritten by madevent makefile if this exists (#480) # NB: this relies on the assumption that cudacpp code is generated before madevent code files.ln(pjoin(self.path, "..", "makefile"), self.path, "makefile") + def _folded_crossing_flavorids(self, matrix_element): + """Extended flavor ids of the crossed subprocesses folded into this base + ME (merge_crossing='record'). One id per asked crossing direction + (mirror pairs collapsed), matched LABEL-AWARE against the reachable + (index, cross, flav, pdg) enumeration so a merged _quark leg matches any + same-sign flavor -- the same selection check_sa.f's crossing demo uses. + The index IS the mg7 flavor id (cross*nflav+flav0), so flavorPDG(id, k) + gives the crossed PDG at runtime.""" + crossed = matrix_element.get('crossed_processes') + if not crossed: + return [] + import madgraph.iolibs.export_v4 as export_v4 + Fort = export_v4.ProcessExporterFortran + merged = matrix_element.get('processes')[0].get('model').get( + 'merged_particles') + entries = Fort.compute_crossing_pdg_entries(self, matrix_element) + pdg_to_id = {} + for (index, _cross, _flav0, pdg) in entries: + pdg_to_id.setdefault(pdg, index) + reach = [pdg for (_i, _c, _f, pdg) in entries] + + def leg_matches(leg_id, pdg): + a = abs(leg_id) + if a in merged: + return (leg_id > 0) == (pdg > 0) and abs(pdg) in merged[a] + return pdg == leg_id + + ninitial = matrix_element.get_nexternal_ninitial()[1] + ids, seen = [], set() + for (proc, _bp, _xp) in crossed: + legs = [l.get('id') for l in proc.get('legs')] + orients = [legs] + if ninitial == 2: + orients.append([legs[1], legs[0]] + legs[2:]) + hit = None + for orient in orients: + for r in reach: + if len(r) == len(orient) and \ + all(leg_matches(L, P) for L, P in zip(orient, r)): + hit = r + break + if hit is not None: + break + if hit is None: + continue + mirror = (hit[1], hit[0]) + hit[2:] if ninitial == 2 else hit + if hit in seen or mirror in seen: + continue + seen.add(hit) + seen.add(mirror) + ids.append(pdg_to_id[hit]) + return ids + + def edit_crossing_demo(self): + """Write crossing_demo.dat (the folded-crossing flavor ids) into the P* + directory so the shared check_sa.exe can demonstrate each crossed + subprocess at its own RAMBO point. Nothing is written when the ME has no + folded crossings (check_sa then just shows the base flavors).""" + if not getattr(self, 'use_crossing', False): + return + ids = self._folded_crossing_flavorids(self.matrix_elements[0]) + if not ids: + return + with open(pjoin(self.path, 'crossing_demo.dat'), 'w') as fsock: + fsock.write(' '.join(str(i) for i in ids) + '\n') + # AV - replace the export_cpp.OneProcessExporterCPP method (add debug printouts and multichannel handling #473) def edit_mgonGPU(self): """Generate mgOnGpuConfig.h""" @@ -2069,6 +2160,31 @@ def edit_coloramps(self): icolamp_text += text % (iconfigc+1, iconfig_to_diag[iconfigc+1]-1) # diag - 1 is to follow MadSpace indexing icolamp.append(icolamp_text) replace_dict['is_LC'] = '\n'.join(icolamp) + + # Canonical colour-flow code of each colour flow -- baked so the ME can + # return the self-describing code (the MG7 colour encoding) instead of a + # raw flow index. Same encoding as the fortran output / subprocesses.json + # (get_color_code_tables); valid==false leaves the flows to the fallback. + codes = None + if self.color_basis: + n_initial = self.matrix_element.get_nexternal_ninitial()[1] + legs = self.process.get_legs_with_decays() + repr_dict = {leg.get("number"): + self.model.get_particle(leg.get("id")).get_color() + * (-1) ** (1 + leg.get("state")) for leg in legs} + color_flow_dicts = self.color_basis.color_flow_decomposition( + repr_dict, n_initial) + codes, _slots = self.get_color_code_tables(color_flow_dicts, legs) + if codes is None: + replace_dict['colorflowcode_valid'] = 'false' + replace_dict['colorflowcode_lines'] = '\n'.join( + ' 0, // colour flow %d (no usable code -- use the tag table)' + % i for i in range(nb_color)) + else: + replace_dict['colorflowcode_valid'] = 'true' + replace_dict['colorflowcode_lines'] = '\n'.join( + ' %d, // colour flow %d' % (c, i) + for i, c in enumerate(codes)) ff.write(template % replace_dict) ff.close() @@ -2227,6 +2343,256 @@ def get_reset_jamp_lines(self, color_amplitudes): ret_lines = "" return ret_lines + # ------------------------------------------------------------------ + # Crossing symmetry (extended flavor id) for the madmatrix / cudacpp + # CPU-SIMD backend. Mirrors export_cpp.get_crossing_replace_dict and the + # fortran path but adapted to the SIMD structure of this backend: the + # per-event momentum permutation lives in calculate_jamps (emitted by the + # helas writer, gated by use_crossing_ic), while the crossing-aware + # good-helicity union, the per-event denominator and the crossed flavorPDG + # accessor are filled here. When self.use_crossing is False every hole gets + # the historical code so the output is byte-for-byte the old one. + # ------------------------------------------------------------------ + def get_madmatrix_crossing_dict(self, matrix_element): + plain = { + 'crossing_decl': '', + 'goodhel_scan_count': 'nmaxflavor', + 'goodhel_scan_skip': '', + 'sigmakin_denominator': + ' MEs_sv = MEs_sv * broken_symmetry_factor(iflavorVec[ievt0]) / helcolDenominators[0];', + 'flavorpdg_body': ' return flavorPDGs[iflavor][ipar];', + # No crossing: the selected helicity is the base row, unchanged. + 'selected_hel_code_1': 'cGoodHel[ighel] + 1', + 'selected_hel_code_2': 'cGoodHel[ighel] + 1', + # No crossing: union good-hel loop, scalar helicity (historical). + 'goodhel_percross_statics': '', + 'goodhel_percross_decl': '', + 'goodhel_percross_record': '', + 'goodhel_percross_build': '', + 'sigmakin_hel_bound': 'cNGoodHel', + 'sigmakin_perlane_decl': '', + 'sigmakin_ihel_expr': 'cGoodHel[ighel]', + 'calc_jamps_ihlane_arg': '', + } + if not getattr(self, 'use_crossing', False): + return plain + + import madgraph.iolibs.export_v4 as export_v4 + Fort = export_v4.ProcessExporterFortran + me = matrix_element + tables = Fort.compute_crossing_tables(self, me) + nexternal = tables['nexternal'] + ninitial = tables['ninitial'] + ncross = (nexternal + 1) * (nexternal + 1) + nflav = len(me.get_external_flavors_with_iden()) + spincol = tables['spincol'] + basepid = tables['basepid'] + source = tables['source'] + perm = tables['perm'] + ic = tables['ic'] + + # Crossed per-leg signed PDG for every extended flavor id (physical PDG, + # conjugated where the leg swapped side; 0 for an invalid crossing). + n_flavors, pdg_flat, antipdg_flat = Fort._build_flav_pdg_tables(self, me) + fpdg = [] + for cross in range(ncross): + for flav0 in range(nflav): + for k in range(nexternal): + if spincol[cross] == 0: + fpdg.append(0) + continue + src = perm[cross * nexternal + k] + if ic[cross * nexternal + k] == 1: + fpdg.append(pdg_flat[flav0 * nexternal + src]) + else: + fpdg.append(antipdg_flat[flav0 * nexternal + src]) + + def arr(vals): + return '{ ' + ', '.join(str(v) for v in vals) + ' }' + + crossing_decl = ( + " // ---- Crossing symmetry tables (extended id = cross*nmaxflavor + flav) ----\n" + " // Initial-state spin*color average per crossing (0 = crossing that\n" + " // must not be applied: out of range, impossible, or overlapping swap).\n" + " static const int spincol_cross[%(ncross)d] = %(spincol)s;\n" + " // Crossed physical signed PDG per (extended id, leg); 0 if invalid.\n" + " static const int flavorPDGs_cross[%(nfpdg)d] = %(fpdg)s;\n" + " // Identical-final-state factor of the crossed process (flavor\n" + " // dependent -> runtime). FLAVOR is not permuted, so slot k reads the\n" + " // original leg that moved into it via src_cross.\n" + " __device__ int ident_cross( int cross, int iflavor )\n" + " {\n" + " static const int basepid_cross[%(ncrossN)d] = %(basepid)s;\n" + " static const int src_cross[%(ncrossN)d] = %(source)s;\n" + " const int off = cross * npar;\n" + " bool used[npar];\n" + " for ( int k = 0; k < npar; k++ ) used[k] = false;\n" + " int fact = 1;\n" + " for ( int k = %(ninitial)d; k < npar; k++ )\n" + " {\n" + " if ( used[k] ) continue;\n" + " int n = 1;\n" + " for ( int l = k + 1; l < npar; l++ )\n" + " {\n" + " if ( used[l] ) continue;\n" + " if ( basepid_cross[off + k] == basepid_cross[off + l] &&\n" + " cFlavors[iflavor][src_cross[off + k]] == cFlavors[iflavor][src_cross[off + l]] )\n" + " {\n" + " used[l] = true;\n" + " n = n + 1;\n" + " fact = fact * n;\n" + " }\n" + " }\n" + " }\n" + " return fact;\n" + " }\n" + ) % {'ncross': ncross, 'spincol': arr(spincol), + 'nfpdg': ncross * nflav * nexternal, 'fpdg': arr(fpdg), + 'ncrossN': ncross * nexternal, 'basepid': arr(basepid), + 'source': arr(source), 'ninitial': ninitial} + + # Per-leg helicity states in the cHel (allow_reverse=False) order, used + # to re-encode a crossed helicity config into its canonical code. + pdict = me.get('processes')[0].get('model').get('particle_dict') + hstates = [pdict[wf.get('pdg_code')].get_helicity_states(False) + for wf in me.get_external_wavefunctions()] + hnstate = [len(s) for s in hstates] + maxhel = max(hnstate) if hnstate else 1 + states_flat = [] + for k in range(nexternal): + states_flat.extend(hstates[k][i] if i < hnstate[k] else 0 + for i in range(maxhel)) + # Crossed-event selected helicity (allselhel). See the CAUTION below: + # this transform is compile-checked only, NOT validated at runtime. + crossing_decl = crossing_decl + ( + " // ---- Crossed-event selected helicity code (allselhel) ----\n" + " // For a crossed event the reported per-event helicity must be the\n" + " // CROSSED code, not the base row: mirror the fortran\n" + " // APPLY_CROSSING_TABLE, which permutes the base NHEL config by the\n" + " // crossing slot permutation (NHEL(k)=NHEL_IN(perm(k)), no sign flip\n" + " // -- the NSF sign lives in IC), then ENCODE_HEL it into the\n" + " // canonical mixed-radix code over the base per-leg helicity states.\n" + " // cross 0 is the identity (base row+1), so the non-crossing path is\n" + " // unchanged.\n" + " //\n" + " // !!! CAUTION: COMPILE-CHECKED ONLY, NOT VALIDATED AT RUNTIME. The\n" + " // |M|^2 path evaluates each row with the helicity read by\n" + " // DESTINATION slot (cHel[ihel][s]) and the permutation absorbed by\n" + " // the good-helicity union sum, so whether the SELECTED row needs\n" + " // this perm digit-permute, an NSF sign flip, both, or nothing must\n" + " // be confirmed by a cudacpp event-level run that checks the reported\n" + " // crossed-event helicity against the fortran backend. Until then do\n" + " // NOT rely on allselhel for crossed events (the |M|^2 is correct).\n" + " __device__ inline int selected_hel_code( int base_ihel, unsigned int flavor_id )\n" + " {\n" + " const int xcross = (int)( flavor_id / nmaxflavor );\n" + " if ( xcross == 0 ) return base_ihel + 1;\n" + " constexpr int maxhel = %(maxhel)d;\n" + " static const int xhel_perm[( npar + 1 ) * ( npar + 1 ) * npar] = %(xperm)s;\n" + " static const int xhel_nhstate[npar] = %(xnhstate)s;\n" + " static const int xhel_states[npar * maxhel] = %(xstates)s;\n" + " int code = 0;\n" + " for ( int k = 0; k < npar; k++ )\n" + " {\n" + " const int val = (int)cHel[base_ihel][xhel_perm[xcross * npar + k]];\n" + " int d = 0;\n" + " for ( int dd = 0; dd < xhel_nhstate[k]; dd++ )\n" + " {\n" + " if ( xhel_states[k * maxhel + dd] == val )\n" + " {\n" + " d = dd;\n" + " break;\n" + " }\n" + " }\n" + " code = code * xhel_nhstate[k] + d;\n" + " }\n" + " return code + 1;\n" + " }\n" + ) % {'xperm': arr(perm), 'xnhstate': arr(hnstate), + 'maxhel': maxhel, 'xstates': arr(states_flat)} + + sigmakin_denominator = ( + " // Per-event crossing-aware denominator: cross may differ per event.\n" + " // cross==0 keeps the historical IDEN/BROKEN_SYM path; a genuine\n" + " // crossing rebuilds it from the crossed initial-state spin*color\n" + " // times the identical-final-state factor of the actual flavors.\n" + " fptype_sv denom_sv;\n" + " for ( int ieppV = 0; ieppV < neppV; ++ieppV )\n" + " {\n" + " const unsigned int fid = iflavorVec[ievt0 + ieppV];\n" + " const int dcr = (int)( fid / nmaxflavor );\n" + " const int dfl = (int)( fid % nmaxflavor );\n" + " fptype f;\n" + " if ( dcr == 0 )\n" + " f = (fptype)broken_symmetry_factor( dfl ) / helcolDenominators[0];\n" + " else if ( spincol_cross[dcr] == 0 )\n" + " f = (fptype)0.; // invalid crossing (out of range / overlapping swap) -> ME 0\n" + " else\n" + " f = (fptype)1. / ( (fptype)spincol_cross[dcr] * (fptype)ident_cross( dcr, dfl ) );\n" + " reinterpret_cast( &denom_sv )[ieppV] = f;\n" + " }\n" + " MEs_sv = MEs_sv * denom_sv;" + ) + + flavorpdg_body = ( + " const int ncross = ( npar + 1 ) * ( npar + 1 );\n" + " if ( iflavor < 0 || iflavor >= ncross * nmaxflavor ) return 0;\n" + " return flavorPDGs_cross[iflavor * npar + ipar];" + ) + + return { + 'crossing_decl': crossing_decl, + # Good-helicity UNION now also spans crossings: sample every valid + # extended flavor id (skip spincol==0) so cGoodHel covers the crossed + # helicity rows too. A helicity that vanishes for a given event's + # crossing simply contributes 0 at run time. + 'goodhel_scan_count': str(ncross * nflav), + 'goodhel_scan_skip': + ' if ( spincol_cross[iflav / nmaxflavor] == 0 ) continue;\n ', + 'sigmakin_denominator': sigmakin_denominator, + 'flavorpdg_body': flavorpdg_body, + # Reported per-event helicity: the crossed code for the event's + # crossing (unvalidated at runtime, see selected_hel_code). + 'selected_hel_code_1': + 'selected_hel_code( cGoodHel[ighel], iflavorVec[ievt] )', + 'selected_hel_code_2': + 'selected_hel_code( cGoodHel[ighel], iflavorVec[ievt2] )', + # (A) Per-lane helicity: the C++ good-hel loop runs once over the + # per-crossing good-hel count; each lane uses its crossing's ighel-th + # good helicity (the union is never materialised on the hot path). + # Host only -- GPU + mixed-precision stay on the union (untested here). + # Validated byte-identical on sse4 with divergent lanes (see + # [[mg7-perlane-helicity]]). + 'goodhel_percross_statics': + '#ifndef MGONGPUCPP_GPUIMPL\n' + ' static constexpr int cNcross = ( npar + 1 ) * ( npar + 1 );\n' + ' static int cGoodHelOfCross[cNcross][ncomb]; // per-crossing good-hel rows\n' + ' static int cNGoodPerCross[cNcross]; // #good hel per crossing\n' + ' static int cNGoodMaxCross; // max over crossings\n' + '#endif', + 'goodhel_percross_decl': + ' static bool _gpc[cNcross][ncomb];\n' + ' for( int _c = 0; _c < cNcross; _c++ ) for( int _h = 0; _h < ncomb; _h++ ) _gpc[_c][_h] = false;\n', + 'goodhel_percross_record': + ' _gpc[iflav / nmaxflavor][ihel] = true;\n', + 'goodhel_percross_build': + ' for( int _c = 0; _c < cNcross; _c++ ) {\n' + ' int _n = 0;\n' + ' for( int _h = 0; _h < ncomb; _h++ ) if( _gpc[_c][_h] ) { cGoodHelOfCross[_c][_n] = _h; _n++; }\n' + ' cNGoodPerCross[_c] = _n;\n' + ' }\n' + ' cNGoodMaxCross = 0;\n' + ' for( int _c = 0; _c < cNcross; _c++ ) if( cNGoodPerCross[_c] > cNGoodMaxCross ) cNGoodMaxCross = cNGoodPerCross[_c];\n', + 'sigmakin_hel_bound': 'cNGoodMaxCross', + # No per-page precompute in sigmaKin: pass the good-hel index ighel + # and let the external block derive the per-lane helicity per page + # (so mixed precision's second page is handled). The scalar ihel arg + # is unused when crossing (a dummy 0). + 'sigmakin_perlane_decl': '', + 'sigmakin_ihel_expr': '0', + 'calc_jamps_ihlane_arg': ', ighel', + } + #------------------------------------------------------------------------------------ import madgraph.core.helas_objects as helas_objects @@ -2501,13 +2867,13 @@ def super_get_matrix_element_calls(self, matrix_element, color_amplitudes, multi // for GPU it is an int // for SIMD it is also an int, since it is constant across the SIMD vector #ifdef MGONGPUCPP_GPUIMPL - const unsigned int iflavor = F_ACCESS::kernelAccessConst( iflavorVec ); + const unsigned int iflavor = F_ACCESS::kernelAccessConst( iflavorVec )""" + self._crossing_flav_reduce() + """; #else const unsigned int* iflavor_rec = F_ACCESS::ieventAccessRecordConst( iflavorVec, ievt0 ); const uint_sv iflavor_sv = F_ACCESS::kernelAccessConst( iflavor_rec ); - const unsigned int iflavor = reinterpret_cast(&iflavor_sv)[0]; + const unsigned int iflavor = reinterpret_cast(&iflavor_sv)[0]""" + self._crossing_flav_reduce() + """; #endif -""") +""" + (self._crossing_preamble(matrix_element) if getattr(self, 'use_crossing_ic', False) else '')) diagrams = matrix_element.get('diagrams') diag_to_config = {} for config in sorted(multi_channel_map.keys()): @@ -2648,13 +3014,181 @@ def get_matrix_element_calls(self, matrix_element, color_amplitudes, multi_chann if not item.startswith('\n') and not item.startswith('#'): res[i]=' '+item return res + # ------------------------------------------------------------------ + # Crossing-symmetry helpers (only active when self.use_crossing_ic). + # When off, every path below is a no-op and the emitted code is + # byte-identical to the historical (no-crossing) output. + # ------------------------------------------------------------------ + def _crossing_flav_reduce(self): + """Reduce the extended flavor id to the flavor group index (flav_use). + The runtime iflavorVec entry is cross*nmaxflavor+flav_use; flav_use is + what indexes cFlavors/masks (constant across the SIMD page).""" + return ' % nmaxflavor' if getattr(self, 'use_crossing_ic', False) else '' + + @staticmethod + def _crossing_int_2d(flat, ncols): + """Format a flat int list as a C++ 2-D initializer { {...}, {...} }.""" + rows = [] + for start in range(0, len(flat), ncols): + rows.append('{ ' + ', '.join(str(v) for v in flat[start:start+ncols]) + ' }') + return '{\n ' + ',\n '.join(rows) + ' }' + + def _crossing_tables(self, matrix_element): + import madgraph.iolibs.export_v4 as export_v4 + return export_v4.ProcessExporterFortran.compute_crossing_tables( + self, matrix_element) + + def _crossing_preamble(self, matrix_element): + """Per-event momentum permutation for crossing symmetry (C++/SIMD). + + All events in a SIMD page share flav_use but may carry DIFFERENT + crossings, so this gather is genuinely per-event (NOT vectorized): for + each event we permute its momenta into the crossed slot order (xmom, + positive energy preserved) and record the per-event NSF sign flips + (icsign). The momentum sign flip of a swapped leg is applied through the + NSF flag inside the HELAS routines (see _crossing_external_block).""" + tables = self._crossing_tables(matrix_element) + nexternal = tables['nexternal'] + ncross = (nexternal + 1) * (nexternal + 1) + perm = self._crossing_int_2d(tables['perm'], nexternal) + ic = self._crossing_int_2d(tables['ic'], nexternal) + return """#ifndef MGONGPUCPP_GPUIMPL + // === CROSSING SYMMETRY: per-event momentum permutation (NOT vectorized) === + constexpr int ncross = ( npar + 1 ) * ( npar + 1 ); + static const int cross_perm[ncross][npar] = %(perm)s; + static const int cross_ic[ncross][npar] = %(ic)s; + alignas( mgOnGpu::cppAlign ) fptype xmom[npar * np4 * neppV]; + fptype_sv icsign[npar]; + // 2 scratch external wavefunctions for the per-event NSF-sign blend + fptype_sv pvec_x[2][np4]; + cxtype_sv w_x[2][nw6]; + ALOHAOBJ aloha_x[2]; + aloha_x[0] = ALOHAOBJ{ pvec_x[0], w_x[0] }; + aloha_x[1] = ALOHAOBJ{ pvec_x[1], w_x[1] }; + for( int ieppV = 0; ieppV < neppV; ++ieppV ) + { + const int xcr = (int)( iflavorVec[ievt0 + ieppV] / nmaxflavor ); + for( int s = 0; s < npar; ++s ) + { + const int src = cross_perm[xcr][s]; + for( int ip4 = 0; ip4 < np4; ++ip4 ) + xmom[s * np4 * neppV + ip4 * neppV + ieppV] = + MemoryAccessMomenta::ieventAccessIp4IparConst( momenta, ieppV, ip4, src ); + reinterpret_cast( &icsign[s] )[ieppV] = (fptype)cross_ic[xcr][s]; + } + } +#endif +""" % {'perm': perm, 'ic': ic} + + @staticmethod + def _hel_state_values(spin, mass): + """Helicity values of an external leg (matching Particle.get_helicity_ + states) so the per-lane blend can loop over exactly the states cHel + holds. Scalars (spin 1) have none. Massive vectors add the 0 state.""" + massless = mass in ('ZERO', 'zero') + if spin == 2: # fermion + return [-1, 1] + if spin == 3: # vector + return [-1, 1] if massless else [-1, 0, 1] + if spin == 5: # spin-2 + return [-2, 2] if massless else [-2, -1, 0, 1, 2] + return None # spin 1 scalar (no helicity) + + def _crossing_external_block(self, wf, argument): + """External HELAS call under crossing symmetry (C++/SIMD). + + Reads the per-event permuted momenta (xmom, in crossed slot order) and + applies the per-event NSF sign flip by computing the wavefunction twice + (nsf = +base and -base) and blending lane-wise through icsign. + + Helicity is PER-LANE: each lane's helicity row is _ihlane[lane] (set by + sigmaKin from the event's crossing; nullptr -> the scalar ihel, used by + getGoodHel). For a helicity-carrying leg the wavefunction is built for + each of the leg's helicity states and accumulated weighted by a per-lane + mask (does this lane want state _v?), so a single pass computes each + lane's own good helicity. get_amp downstream stays fully SIMD. Scalars + carry no helicity, so their block is the plain NSF blend. GPU unchanged.""" + routine = helas_call_writers.HelasCallWriter.mother_dict[ + argument.get_spin_state_number()].lower() + routine = routine + 'x' * (6 - len(routine)) + routine = routine + '' + s = wf.get('number_external') - 1 + me = wf.get('me_id') - 1 + spin = argument.get('spin') + if spin == 1: + nsf = (-1) ** (wf.get('state') == 'initial') + elif argument.is_boson(): + nsf = (-1) ** (wf.get('state') == 'initial') + else: + nsf = - (-1) ** wf.get_with_flow('is_part') + mass = wf.get('mass') + states = self._hel_state_values(spin, mass) + + def one_call(sign, obj, hel=None): + if spin == 1: + call = '%s( xmom, %+d, cFlavors[iflavor][%d], %s, %d );' % \ + (routine, sign, s, obj, s) + else: + call = '%s( xmom, m_pars->%s, %s, %+d, cFlavors[iflavor][%d], %s, %d );' % \ + (routine, mass, hel, sign, s, obj, s) + return self.format_coupling(call) + + lines = ['#ifndef MGONGPUCPP_GPUIMPL'] + if states is None: + # Scalar: no helicity, plain NSF blend (unchanged). + lines.append(' ' + one_call(nsf, 'aloha_x[0]')) + lines.append(' ' + one_call(-nsf, 'aloha_x[1]')) + lines.append(' { const fptype_sv _sp = ( icsign[%d] + (fptype)1. ) * (fptype)0.5;' % s) + lines.append(' const fptype_sv _sm = ( (fptype)1. - icsign[%d] ) * (fptype)0.5;' % s) + lines.append(' for( int _k = 0; _k < np4; _k++ ) pvec_sv[%d][_k] = _sp * pvec_x[0][_k] + _sm * pvec_x[1][_k];' % me) + lines.append(' for( int _k = 0; _k < nw6; _k++ ) w_sv[%d][_k] = _sp * w_x[0][_k] + _sm * w_x[1][_k];' % me) + lines.append(' aloha_obj[%d].flv_index = aloha_x[0].flv_index; }' % me) + else: + stlist = ', '.join(str(v) for v in states) + lines.append(' { static const int _st%d[%d] = { %s };' % (s, len(states), stlist)) + lines.append(' bool _first%d = true;' % s) + lines.append(' for( int _vi = 0; _vi < %d; _vi++ ) {' % len(states)) + lines.append(' const int _v = _st%d[_vi];' % s) + lines.append(' ' + one_call(nsf, 'aloha_x[0]', '_v')) + lines.append(' ' + one_call(-nsf, 'aloha_x[1]', '_v')) + lines.append(' const fptype_sv _sp = ( icsign[%d] + (fptype)1. ) * (fptype)0.5;' % s) + lines.append(' const fptype_sv _sm = ( (fptype)1. - icsign[%d] ) * (fptype)0.5;' % s) + lines.append(' fptype_sv _hm{};') + # Per-lane helicity row, derived PER PAGE (ievt0 = this iParity page's + # first event) so mixed precision (nParity=2) picks the right page. + # _ighel<0 -> scalar ihel (getGoodHel scan / non-crossing). + lines.append(' for( int _ie = 0; _ie < neppV; _ie++ ) {') + lines.append(' int _hr;') + lines.append(' if( _ighel < 0 ) { _hr = ihel; }') + lines.append(' else { const int _cr = (int)( iflavorVec[ievt0 + _ie] / nmaxflavor ); _hr = ( _ighel < cNGoodPerCross[_cr] ) ? cGoodHelOfCross[_cr][_ighel] : -1; }') + lines.append(' reinterpret_cast( &_hm )[_ie] = ( _hr >= 0 && (int)cHel[_hr][%d] == _v ) ? (fptype)1. : (fptype)0.;' % s) + lines.append(' }') + lines.append(' if( _first%d ) {' % s) + lines.append(' for( int _k = 0; _k < np4; _k++ ) pvec_sv[%d][_k] = _hm * ( _sp * pvec_x[0][_k] + _sm * pvec_x[1][_k] );' % me) + lines.append(' for( int _k = 0; _k < nw6; _k++ ) w_sv[%d][_k] = _hm * ( _sp * w_x[0][_k] + _sm * w_x[1][_k] );' % me) + lines.append(' _first%d = false;' % s) + lines.append(' } else {') + lines.append(' for( int _k = 0; _k < np4; _k++ ) pvec_sv[%d][_k] += _hm * ( _sp * pvec_x[0][_k] + _sm * pvec_x[1][_k] );' % me) + lines.append(' for( int _k = 0; _k < nw6; _k++ ) w_sv[%d][_k] += _hm * ( _sp * w_x[0][_k] + _sm * w_x[1][_k] );' % me) + lines.append(' } }') + lines.append(' aloha_obj[%d].flv_index = aloha_x[0].flv_index; }' % me) + lines.append('#else') + # GPU: crossing not implemented; emit the plain (identity) external call + # so the file still compiles for GPU (only CPU/SIMD is validated). + gpu = self.get_external(wf, argument, _no_crossing=True) + lines.append(gpu.rstrip('\n')) + lines.append('#endif\n') + return '\n'.join(lines) + # AV - replace helas_call_writers.GPUFOHelasCallWriter method (improve formatting) # [GPUFOHelasCallWriter.format_coupling is called by GPUFOHelasCallWriter.get_external_line/generate_helas_call] # [GPUFOHelasCallWriter.get_external_line is called by GPUFOHelasCallWriter.get_external] # [=> GPUFOHelasCallWriter.get_external is called by GPUFOHelasCallWriter.generate_helas_call] # [GPUFOHelasCallWriter.generate_helas_call is called by UFOHelasCallWriter.get_wavefunction_call/get_amplitude_call] first_get_external = True - def get_external(self, wf, argument): + def get_external(self, wf, argument, _no_crossing=False): + if getattr(self, 'use_crossing_ic', False) and not _no_crossing: + return self._crossing_external_block(wf, argument) line = self.get_external_line(wf, argument) split_line = line.split(',') split_line = [ str.lstrip(' ').rstrip(' ') for str in split_line] # AV diff --git a/madmatrix/output.py b/madmatrix/output.py index 1820b10e9..9d8ee4bc2 100644 --- a/madmatrix/output.py +++ b/madmatrix/output.py @@ -70,6 +70,12 @@ class ProcessExporterMadMatrix(export_cpp.ProcessExporterMG7): # AV - use a custom OneProcessExporter oneprocessclass = model_handling.OneProcessExporterMadMatrix + # Crossing symmetry (extended flavor id) is supported by the madmatrix / + # cudacpp CPU-SIMD backend (gated by --use_crossing, default on). The MG7 + # (pure-cpp mg7_v5) exporter keeps supports_crossing=False. When + # --use_crossing=False the generated output is byte-identical to before. + supports_crossing = True + # Information to find the template file that we want to include from madgraph # you can include additional file from the plugin directory as well # AV - use template files from PLUGINDIR instead of MG5DIR and add gpu/mgOnGpuVectors.h diff --git a/tests/acceptance_tests/test_standalone_cross_symmetry.py b/tests/acceptance_tests/test_standalone_cross_symmetry.py new file mode 100644 index 000000000..9b22a054e --- /dev/null +++ b/tests/acceptance_tests/test_standalone_cross_symmetry.py @@ -0,0 +1,2265 @@ +################################################################################ +# +# Copyright (c) 2009 The MadGraph5_aMC@NLO Development team and Contributors +# +# This file is a part of the MadGraph5_aMC@NLO project, an application which +# automatically generates Feynman diagrams and matrix elements for arbitrary +# high-energy processes in the Standard Model and beyond. +# +# It is subject to the MadGraph5_aMC@NLO license which should accompany this +# distribution. +# +# For more information, visit madgraph.phys.ucl.ac.be and amcatnlo.web.cern.ch +# +################################################################################ +"""Check the crossing-symmetry support of the fortran standalone output. + +The standalone SMATRIX takes a flavor index (IFLAV / FLAV_IDX). Its range is +extended so that a single value carries both the flavor and a crossing to +apply, decoded as:: + + cross = (IFLAV-1) / NFLAV + flav = mod(IFLAV-1, NFLAV) + 1 ! the index used for masking/... + I = cross / (NEXTERNAL+1) + J = mod(cross, NEXTERNAL+1) + +I and J are the crossing partners of particle 1 and particle 2 respectively: +particle 1 is swapped with particle I and particle 2 with particle J, with 0 +meaning "leave that particle alone". IFLAV in [1,NFLAV] gives cross=0, i.e. the +identity, so existing callers are unaffected. The base is NEXTERNAL+1 rather +than NEXTERNAL so that I and J run over 0..NEXTERNAL and can designate the last +particle as well. + +Swapping a particle across the initial/final state flips its NSF/NSV helas flag +(which is what negates the momentum stored in the wavefunction) and flips its +helicity, so the crossed call evaluates the same analytic amplitude in a +different kinematic region. + +The processes u u~ > g g and u g > u g are exactly each other's crossing under +(I=0, J=3): swapping particle 2 with particle 3 turns the incoming u~ into an +outgoing u and the outgoing g into an incoming g. Because the swap also +reorders the legs, the crossed call takes the *other* process's natural +momentum layout, so this test feeds both codes the very same momenta. + +Crossing preserves the raw sum over helicities and colors of |M|^2, not the +averaged matrix element: the two processes have different averaging/symmetry +denominators (IDEN=72 for u u~ > g g, IDEN=96 for u g > u g, since crossing a +gluon into the initial state changes the color average and un-identifies the +two final state gluons). SMATRIX divides by the IDEN of the *crossed* process, +so a crossed call returns the properly averaged matrix element of the process +it crosses into and can be compared directly against the other code. +""" + +from __future__ import absolute_import + +import json +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +import logging + +logger = logging.getLogger('madgraph.stdout.cross_symmetry') + +import madgraph +import madgraph.interface.master_interface as cmd_interface + +pjoin = os.path.join + +# The two processes are each other's crossing under (I=0, J=3). +PROC_QQ_GG = 'u u~ > g g' +PROC_QG_QG = 'u g > u g' + +# A CHIRAL pair: the W+ couples only to a left-handed u and a right-handed d~, so +# every external quark is 100% polarized and the per-leg density matrix diagonal +# is fully asymmetric ((++) empty, (--) full, or vice versa). That is what makes +# a crossed-fermion helicity FLIP detectable: on u u~ > g g the fermion density +# is (++)==(--), so a flip would be invisible; here it would swap a full entry +# with an empty one. u d~ > w+ g is mapped onto u g > w+ d by (I=0, J=NEXTERNAL): +# the incoming d~ becomes the outgoing d of the last slot (the crossed, still +# 100%-polarized fermion), the outgoing g becomes incoming. +PROC_UDX_WPG = 'u d~ > w+ g' +PROC_UG_WPD = 'u g > w+ d' + +# q q~ > g q q~ is likewise mapped onto q g > q q q~ by the same (I=0, J=3) +# crossing: the incoming q~ becomes the outgoing q of slot 3 and the outgoing g +# becomes an incoming one, leaving the legs ordered as (q, g, q, q, q~). +# Repeated over the quark flavors to exercise the flavor tables / masks and the +# BROKEN_SYM factor, which sees two identical final u's on the crossed side. +PROC_QQX_GQQX = '%(q)s %(q)s~ > g %(q)s %(q)s~' +PROC_QG_QQQX = '%(q)s g > %(q)s %(q)s %(q)s~' +QUARK_FLAVORS = ['u', 'd', 's', 'c'] + +# The merged (multi-flavor) form of the same pair. Generated with the group +# labels so that flavor grouping keeps every quark combination in a single +# matrix element, which is the only way to get NFLAV>1 and a non-trivial mask. +PROC_MERGED_QQX_GQQX = '_quark _anti_quark > g _quark _anti_quark' +PROC_MERGED_QG_QQQX = '_quark g > _quark _quark _anti_quark' + +# The same merged process constrained to a single squared coupling order. A +# squared-order constraint is what sets the process' 'split_orders', which is +# what makes write_matrix_element_v4 pick matrix_standalone_splitOrders_v4.inc +# instead of the default template. Same final state, so BROKEN_SYM is still 2 +# on the rows where the two final quarks differ. +PROC_MERGED_QG_QQQX_SO = '_quark g > _quark _quark _anti_quark QED^2==0' + +# Processes constraining an s-channel propagator. A crossing moves legs between +# the initial and the final state, so what is s-channel in the generated process +# is not s-channel in its crossings: `> z >` (required) and `$$ z` (forbidden, +# diagram removed) must therefore disable the crossing machinery on their own. +# A single `$ z` only forbids the on-shell *region* of a kept diagram, which +# survives the crossing, so it must NOT disable anything. +PROC_REQUIRED_S = 'u u~ > z > e+ e-' +PROC_FORBIDDEN_S = 'u u~ > e+ e- $$ z' +PROC_FORBIDDEN_ONSH_S = 'u u~ > e+ e- $ z' +PROC_UNCONSTRAINED = 'u u~ > e+ e-' + +# Every routine/table that only exists to decode an extended FLAV_IDX. +CROSSING_MACHINERY_NAMES = [ + 'APPLY_CROSSING', 'APPLY_CROSSING_TABLE', 'GET_CROSS_PERM', + 'GET_SPINCOL_CROSS', 'GET_IDENT_CROSS', 'SWAP_LEGS', + 'SPINCOL_CROSS_TABLE', 'BASEPID_CROSS_TABLE', 'SRC_CROSS_TABLE'] + +# cross = I*(NEXTERNAL+1) + J = 0*5 + 3 = 3. Both processes have NFLAV=1, so +# IFLAV = cross*NFLAV + flav = 3*1 + 1 = 4. +NEXTERNAL = 4 +CROSS_2_3 = 0 * (NEXTERNAL + 1) + 3 +# Same crossing for the 2->3 pair, where the base is NEXTERNAL+1 = 6. +NEXTERNAL_5 = 5 +CROSS_2_3_5 = 0 * (NEXTERNAL_5 + 1) + 3 +# Crossing particle 2 with the *last* particle. Only expressible because the +# base is NEXTERNAL+1: with base NEXTERNAL, mod(cross, NEXTERNAL) could never +# yield NEXTERNAL. +CROSS_2_LAST = 0 * (NEXTERNAL + 1) + NEXTERNAL +IFLAV_IDENTITY = 1 + + +def _iflav(cross, flav, nflav): + """Encode a crossing code and a flavor index into the extended IFLAV.""" + return cross * nflav + flav + + +# Subprocess probe for the good-helicity remap (GHREMAP) relation. Run against +# a compiled matrix2py module: for every DERIVABLE crossing (active partners all +# final), the crossed good-helicity set -- the rows where py_smatrixhel_idx is +# non-zero, unioned over many phase-space points -- must equal the identity +# good-helicity set mapped through the crossing's own row permutation sigma +# (config h -> (ic[k]*nhel[perm[k],h])_k). This is the invariant the generated +# GHREMAP encodes, so a wrong table (or a wrong derivability condition) breaks +# the fix. Run in a subprocess: importing an f2py .so into the test interpreter +# would leak a compiled module and clash across tests. +# +# GOTCHA locked in by this probe: 3 phase-space points are NOT enough -- for +# u u~ > g g, cross=23 then showed 6 non-zero rows instead of 8 (an accidental +# zero at the probed points). NPTS is deliberately >= 12. +_GOODHEL_PROBE = r''' +import sys, math +import numpy as np +sys.path.insert(0, %(pdir)r) +import matrix2py as m + +NINITIAL = %(ninitial)d +NPTS = %(npts)d + +def get_crossing_permutation(cross, nexternal): + base = nexternal + 1 + i_part, j_part = cross // base, cross %% base + perm = list(range(nexternal)); ic = [1] * nexternal + def swap(a, b): + perm[a], perm[b] = perm[b], perm[a]; ic[a] = -ic[a]; ic[b] = -ic[b] + valid = not (i_part not in (0, 1) and j_part not in (0, 2) + and (i_part == 2 or j_part == 1 or i_part == j_part)) + if i_part not in (0, 1): swap(0, i_part - 1) + if j_part not in (0, 2): swap(1, j_part - 1) + return perm, ic, valid + +def rambo(nf, ecm, rng): + q = np.zeros((4, nf)) + for i in range(nf): + c = 2 * rng.random() - 1 + s = math.sqrt(1 - c * c) + phi = 2 * math.pi * rng.random() + r1, r2 = rng.random(), rng.random() + q[0, i] = -math.log(r1 * r2) + q[3, i] = q[0, i] * c + q[2, i] = q[0, i] * s * math.cos(phi) + q[1, i] = q[0, i] * s * math.sin(phi) + Q = q.sum(axis=1) + M = math.sqrt(Q[0]**2 - Q[1]**2 - Q[2]**2 - Q[3]**2) + b = -Q[1:] / M; g = Q[0] / M; a = 1.0 / (1.0 + g); x = ecm / M + p = np.zeros((4, nf)) + for i in range(nf): + bq = b @ q[1:, i] + p[1:, i] = x * (q[1:, i] + b * (q[0, i] + a * bq)) + p[0, i] = x * (g * q[0, i] + bq) + return p + +def momenta(nexternal, ninitial, npts, seed): + rng = np.random.default_rng(seed) + ecm = 1000.0; nf = nexternal - ninitial; ps = [] + for _ in range(npts): + P = np.zeros((4, nexternal)) + P[0, 0] = ecm / 2; P[3, 0] = ecm / 2 + if ninitial >= 2: + P[0, 1] = ecm / 2; P[3, 1] = -ecm / 2 + P[:, ninitial:] = rambo(nf, ecm, rng) + ps.append(np.asfortranarray(P)) + return ps + +m.py_initialisemodel(%(card)r) +nflav, nexternal_l, ncross = m.py_get_flavor_layout() +_iden, nhel = m.py_get_nhel_idx(1) +nhel = np.array(nhel) # (nexternal, ncomb) +nexternal, ncomb = nhel.shape +ps = momenta(nexternal, NINITIAL, NPTS, seed=20260721) +row_of = {tuple(nhel[:, h]): h + 1 for h in range(ncomb)} + +def good_set(flav_idx): + good = set() + for P in ps: + for h in range(1, ncomb + 1): + if abs(m.py_smatrixhel_idx(P, h, flav_idx)) > 1e-30: + good.add(h) + return good + +g_id = good_set(1) +assert g_id, 'identity has no good helicity -- probe is broken' +base = nexternal + 1 +checked = genuine = 0 +for cross in range(1, base * base): + perm, ic, valid = get_crossing_permutation(cross, nexternal) + if not valid: + continue + I, J = cross // base, cross %% base + # DERIVABLE = the crossing's active partners are all final particles. + final_only = ((I in (0, 1) or I > NINITIAL) and (J in (0, 2) or J > NINITIAL)) + if not final_only: + continue + flav_idx = cross * nflav + 1 + # Skip a crossing that is not evaluable (spincol==0 -> SMATRIX returns 0). + tot = sum(abs(m.py_smatrixhel_idx(ps[0], h, flav_idx)) + for h in range(1, ncomb + 1)) + if tot == 0: + continue + sigma = {} + for h in range(ncomb): + cfg = tuple(ic[k] * nhel[perm[k], h] for k in range(nexternal)) + hp = row_of.get(cfg) + assert hp is not None, 'cross %%d: sigma is not a row bijection' %% cross + sigma[h + 1] = hp + expected = {sigma[h] for h in g_id} + g_cr = good_set(flav_idx) + assert g_cr == expected, ( + 'cross %%d (I=%%d,J=%%d): crossed good-hel %%s != sigma(identity) %%s' + %% (cross, I, J, sorted(g_cr), sorted(expected))) + checked += 1 + if perm != list(range(nexternal)): + genuine += 1 +assert genuine >= 1, 'no genuine (non-identity) derivable crossing was checked' +print('GHREMAP_RELATION_OK checked=%%d genuine=%%d points=%%d' %% + (checked, genuine, NPTS)) +''' + + +# Subprocess probe for the CROSSED spin-density matrix through the f2py wrapper +# PY_GET_DENSITY_IDX -- the only path by which a python caller can request a +# crossed density matrix (the FLAVOR-array PY_GET_DENSITY resolves through +# GET_FLAVOR_INDEX, which only returns 1..NFLAV and so cannot carry a crossing). +# Prints, per external leg, the three interference terms (++),(+-),(--) of that +# leg's density matrix, so the parent can compare a crossed evaluation against a +# natively generated reference term by term. Run in a subprocess because an +# f2py .so leaks into the importing interpreter and clashes across dirs/tests. +_DENSITY_PROBE = r''' +import sys, json +import numpy as np +sys.path.insert(0, %(pdir)r) +import matrix2py as m +m.py_initialisemodel(%(card)r) +momenta = %(momenta)s # [[E,px,py,pz], ...] per leg +P = np.asfortranarray(np.array(momenta, dtype=float).T) # (4, nexternal) +flav_idx = %(flav_idx)d +allow_hel = np.array([1, -1], dtype=np.int32) +out = {} +for leg in %(legs)s: + pos = np.array([leg], dtype=np.int32) + inter = np.asarray(m.py_get_density_idx( + P, pos, 1, allow_hel, 2, flav_idx, 0.0, 0.0)).ravel() + out[str(leg)] = [[float(z.real), float(z.imag)] for z in inter] +print('DENSITY_JSON ' + json.dumps(out)) +''' + + +class TestStandaloneCrossSymmetry(unittest.TestCase): + """u u~ > g g and u g > u g must reproduce each other under crossing.""" + + # A crossing swaps a leg between the initial and final state, so it probes + # a genuinely different kinematic region of the same analytic amplitude. + # Compare at a few scattering angles rather than a single point. + cos_thetas = [0.3, -0.62, 0.85] + energy = 1000.0 + tolerance = 1e-11 + + debugging = getattr(unittest, 'debug', False) + + def setUp(self): + self.cmd = cmd_interface.MasterCmd() + self.cmd.no_notification() + prefix = 'cross_debug_' if self.debugging else 'cross_' + self.tmpdir = tempfile.mkdtemp(prefix=prefix) + + def tearDown(self): + if not self.debugging and os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + # ------------------------------------------------------------------ + # generation / build helpers + # ------------------------------------------------------------------ + def _generate(self, process, name, options='', split_orders=False): + """Generate the standalone output for `process`, return its P* dir. + + `options` is appended to the generate command (e.g. --use_crossing=False). + `split_orders` selects the driver for the split-orders template, whose + density entry point takes the FLAVOR array rather than a FLAV_IDX. + """ + pdir = self._output_standalone(process, name, options) + self._write_driver(pdir, split_orders=split_orders) + self._build(pdir) + return pdir + + def _output_standalone(self, process, name, options=''): + """Write the standalone output for `process` and return its P* dir. + + Split out of _generate for the tests that only inspect the emitted + fortran and so have no reason to pay for a compile. + """ + outdir = pjoin(self.tmpdir, name) + self.cmd.exec_cmd('set automatic_html_opening False') + self.cmd.exec_cmd('set group_subprocesses False') + self.cmd.exec_cmd('set apply_flavor_grouping True') + self.cmd.exec_cmd('import model sm') + self.cmd.exec_cmd(('generate %s %s' % (process, options)).strip()) + self.cmd.exec_cmd('output standalone %s -f' % outdir) + + subproc_root = pjoin(outdir, 'SubProcesses') + pdirs = [pjoin(subproc_root, name) for name in sorted(os.listdir(subproc_root)) + if name.startswith('P') and os.path.isdir(pjoin(subproc_root, name))] + self.assertEqual(len(pdirs), 1, + 'Expected a single subprocess directory for %s, got %s' + % (process, pdirs)) + return pdirs[0] + + def _matrix_code(self, pdir): + """The emitted matrix.f with comment lines stripped. + + Only definitions/uses must be matched, not the prose: a comment may + legitimately still mention the machinery to explain its absence. + """ + with open(pjoin(pdir, 'matrix.f')) as fsock: + source = fsock.read() + return '\n'.join(line for line in source.split('\n') + if not line.lstrip().upper().startswith('C')) + + def _write_driver(self, pdir, split_orders=False): + """Replace check_sa.f by a driver reading momenta+IFLAV from a file. + + Reading the input rather than hardcoding it lets each process be + compiled once and then probed at many points / flavor indices. + + The split-orders template has no crossing machinery and hence no + GET_DENSITY_IDX: its density entry point takes the FLAVOR array, so the + driver resolves the index through GET_FLAVOR first. Everything else + (SMATRIX, GET_FLAVOR, GET_FLAVOR_INDEX) has the same interface, so only + that one call differs. + """ + if split_orders: + density_call = ''' CALL GET_FLAVOR(FLAV_IDX, FLAVOR) + CALL GET_DENSITY(P, DPOS, 1, ALLOW_HEL, 2, FLAVOR, + & 0D0, 0D0, INTER)''' + else: + density_call = ''' CALL GET_DENSITY_IDX(P, DPOS, 1, ALLOW_HEL, 2, FLAV_IDX, + & 0D0, 0D0, INTER)''' + # GET_NHEL_IDX / GET_PDG_FOR_FLAVOR only exist in matrix_standalone_v4; + # the split-orders template lacks them, so its driver must not reference + # them or it will not link. + if split_orders: + nhel_idx_call = ''' WRITE(*,*) 'IDEN= ', -1 + WRITE(*,*) 'PDG= ', 0''' + else: + nhel_idx_call = ''' CALL GET_NHEL_IDX(FLAV_IDX, IDEN_STAR, NHEL_STAR) + CALL GET_PDG_FOR_FLAVOR(FLAV_IDX, PDGS) + WRITE(*,*) 'IDEN= ', IDEN_STAR + WRITE(*,*) 'PDG= ', (PDGS(I),I=1,NEXTERNAL)''' + # GET_NHEL writes NEXTERNAL*NCOMB entries into NHEL_STAR using its own + # NCOMB; an oversized array in the caller is safe and avoids parsing + # NCOMB out of matrix.f. + driver = ''' PROGRAM DRIVER + use model_object + IMPLICIT NONE + INCLUDE "coupl.inc" + INCLUDE "nexternal.inc" + INTEGER NCOMB_MAX + PARAMETER (NCOMB_MAX=4096) + REAL*8 P(0:3,NEXTERNAL), MATELEM + INTEGER FLAV_IDX, I, J, MODE + INTEGER FLAVOR(NEXTERNAL) + INTEGER GET_FLAVOR_INDEX + INTEGER NHEL_STAR(NEXTERNAL,NCOMB_MAX), IDEN_STAR + INTEGER DPOS(1), ALLOW_HEL(2) + INTEGER PDGS(NEXTERNAL) + DOUBLE COMPLEX INTER(3) + call setpara('param_card.dat') + OPEN(UNIT=42,FILE='cross_input.dat',STATUS='OLD') + READ(42,*) MODE + IF (MODE.EQ.1) THEN + READ(42,*) FLAV_IDX + CALL GET_FLAVOR(FLAV_IDX, FLAVOR) + WRITE(*,*) 'POS= ', (FLAVOR(I),I=1,NEXTERNAL) + ELSEIF (MODE.EQ.2) THEN + READ(42,*) (FLAVOR(I),I=1,NEXTERNAL) + WRITE(*,*) 'IDX= ', GET_FLAVOR_INDEX(FLAVOR) + ELSEIF (MODE.EQ.4) THEN +C Density matrix: interference between the helicity states of one leg. +C GET_DENSITY_IDX takes the index directly, so it can carry a crossing; +C the FLAVOR-array entry point cannot express one. + READ(42,*) FLAV_IDX + READ(42,*) DPOS(1) + DO I=1,NEXTERNAL + READ(42,*) (P(J,I),J=0,3) + ENDDO + ALLOW_HEL(1) = +1 + ALLOW_HEL(2) = -1 +%(density_call)s + DO I=1,3 + WRITE(*,*) 'INTER= ', DREAL(INTER(I)), DIMAG(INTER(I)) + ENDDO + ELSEIF (MODE.EQ.5) THEN +C The f2py-facing crossing accessors: GET_NHEL_IDX returns the crossed +C averaging denominator (unlike GET_NHEL, which only knows the static +C uncrossed one), and GET_PDG_FOR_FLAVOR returns the per-leg signed PDG +C of the process the extended FLAV_IDX selects (crossed and conjugated). + READ(42,*) FLAV_IDX +%(nhel_idx_call)s + ELSE + READ(42,*) FLAV_IDX + DO I=1,NEXTERNAL + READ(42,*) (P(J,I),J=0,3) + ENDDO + CALL SMATRIX(P,FLAV_IDX,MATELEM) + CALL GET_NHEL(IDEN_STAR,NHEL_STAR) + WRITE(*,*) 'ANS= ', MATELEM + WRITE(*,*) 'IDEN= ', IDEN_STAR + ENDIF + CLOSE(42) + END +''' + with open(pjoin(pdir, 'check_sa.f'), 'w') as fsock: + fsock.write(driver % {'density_call': density_call, + 'nhel_idx_call': nhel_idx_call}) + + def _build(self, pdir): + retcode = self._call(['make', 'check'], pdir) + self.assertEqual(retcode, 0, 'Failed to compile standalone check in %s' % pdir) + + def _build_f2py(self, pdir): + """Build the f2py matrix2py module in `pdir`, or skip the test. + + f2py needs a working numpy build backend (meson on numpy>=1.26 / + python>=3.12), which is not guaranteed in every environment. When it is + missing this raises SkipTest rather than a failure: the wrapper logic is + also covered by a mock-backed test that has no toolchain dependency. + """ + env = dict(os.environ) + with open(os.devnull, 'w') as devnull: + retcode = subprocess.call(['make', 'matrix2py.so'], cwd=pdir, + stdout=devnull, stderr=devnull, env=env) + modules = [name for name in os.listdir(pdir) + if name.startswith('matrix2py') and name.endswith('.so')] + if retcode != 0 or not modules: + raise unittest.SkipTest( + 'Could not build the f2py module in %s (f2py/numpy build ' + 'backend unavailable); skipping the compiled-module test.' + % pdir) + + def _call(self, command, cwd): + if logger.isEnabledFor(logging.INFO): + return subprocess.call(command, cwd=cwd) + with open(os.devnull, 'w') as devnull: + return subprocess.call(command, stdout=devnull, stderr=devnull, cwd=cwd) + + # ------------------------------------------------------------------ + # running + # ------------------------------------------------------------------ + def _probe(self, pdir, lines): + """Feed the driver an input block and return its stdout.""" + with open(pjoin(pdir, 'cross_input.dat'), 'w') as fsock: + fsock.write('\n'.join(lines) + '\n') + return subprocess.Popen(['./check'], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=pdir).communicate()[0].decode() + + def _flavor_positions(self, pdir, flav): + """GET_FLAVOR: the per-leg flavor-group positions of a flavor index.""" + output = self._probe(pdir, ['1', '%d' % flav]) + match = re.search(r'POS=\s*(.*)', output) + self.assertTrue(match, 'No POS from %s, got:\n%s' % (pdir, output)) + return tuple(int(token) for token in match.group(1).split()) + + def _flavor_index(self, pdir, positions): + """GET_FLAVOR_INDEX: flavor index of a position vector, 0 if absent.""" + output = self._probe(pdir, ['2', ' '.join(str(p) for p in positions)]) + match = re.search(r'IDX=\s*(-?\d+)', output) + self.assertTrue(match, 'No IDX from %s, got:\n%s' % (pdir, output)) + return int(match.group(1)) + + def _nhel_idx(self, pdir, iflav): + """(crossed IDEN, per-leg signed PDG) an extended FLAV_IDX selects. + + Exercises the two f2py-facing accessors GET_NHEL_IDX / + GET_PDG_FOR_FLAVOR that a python caller working in PDG codes relies on. + """ + output = self._probe(pdir, ['5', '%d' % iflav]) + iden = re.search(r'IDEN=\s*(-?\d+)', output) + pdg = re.search(r'PDG=\s*(.*)', output) + self.assertTrue(iden and pdg, + 'No IDEN/PDG from %s, got:\n%s' % (pdir, output)) + return int(iden.group(1)), tuple(int(t) for t in pdg.group(1).split()) + + def _density(self, pdir, momenta, iflav, leg): + """Return the 3 interference terms of the density matrix of `leg`. + + (++), (+-) and (--) for the two helicity states of that single leg, + each as a complex number. + """ + lines = ['4', '%d' % iflav, '%d' % leg] + for mom in momenta: + lines.append(' '.join('%.17e' % component for component in mom)) + output = self._probe(pdir, lines) + values = re.findall(r'INTER=\s*(\S+)\s+(\S+)', output) + self.assertEqual(len(values), 3, + 'Expected 3 interference terms from %s, got:\n%s' + % (pdir, output)) + return [complex(float(re.sub('[dD]', 'e', real)), + float(re.sub('[dD]', 'e', imag))) + for real, imag in values] + + def _density_f2py(self, pdir, momenta, iflav, legs): + """The same per-leg density matrix as _density, but obtained through the + compiled f2py module's PY_GET_DENSITY_IDX. Returns {leg: [c++, c+-, c--]}. + + Requires the module already built (_build_f2py). Runs in a subprocess so + the f2py .so does not leak into the test interpreter and clash with the + other process' module. + """ + card = pjoin(pdir, os.pardir, os.pardir, 'Cards', 'param_card.dat') + script = _DENSITY_PROBE % { + 'pdir': pdir, 'card': card, + 'momenta': repr([list(mom) for mom in momenta]), + 'flav_idx': iflav, 'legs': repr(tuple(legs))} + script_path = pjoin(pdir, 'density_probe.py') + with open(script_path, 'w') as fsock: + fsock.write(script) + output = subprocess.Popen( + [sys.executable, script_path], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=pdir).communicate()[0].decode() + match = re.search(r'DENSITY_JSON (.*)', output) + self.assertTrue(match, 'No density from f2py probe in %s:\n%s' + % (pdir, output)) + raw = json.loads(match.group(1)) + return {int(leg): [complex(re_, im_) for re_, im_ in terms] + for leg, terms in raw.items()} + + def _run(self, pdir, momenta, iflav): + """Return the averaged matrix element SMATRIX gives for this IFLAV.""" + lines = ['3', '%d' % iflav] + for mom in momenta: + lines.append(' '.join('%.17e' % component for component in mom)) + output = self._probe(pdir, lines) + ans = re.search(r'ANS=\s*(?P[\d\.eEdD\+-]+)', output) + self.assertTrue(ans, + 'Could not read the matrix element from %s, got:\n%s' + % (pdir, output)) + return float(ans.group('value').replace('D', 'E').replace('d', 'e')) + + def _phase_space(self, cos_theta): + """A massless 2->2 point: (leg1_in, leg2_in, leg3_out, leg4_out). + + Every parton here (u, u~, g) is massless, so one point serves both + processes; only the interpretation of each slot differs. + """ + halfe = 0.5 * self.energy + sin_theta = math.sqrt(1.0 - cos_theta ** 2) + return [(halfe, 0.0, 0.0, halfe), + (halfe, 0.0, 0.0, -halfe), + (halfe, halfe * sin_theta, 0.0, halfe * cos_theta), + (halfe, -halfe * sin_theta, 0.0, -halfe * cos_theta)] + + def _read_nflav(self, pdir): + """NFLAV of a generated process, needed to encode the extended IFLAV. + + IFLAV = cross*NFLAV + flav, so the crossing code cannot be turned into + an index without it. Read it rather than assume 1: if flavor grouping + ever merges several flavors here, a hardcoded 1 would silently probe + the wrong flavor instead of failing. + """ + with open(pjoin(pdir, 'matrix.f')) as fsock: + match = re.search(r'PARAMETER\s*\(NFLAV=(\d+)\)', fsock.read()) + self.assertTrue(match, 'Could not read NFLAV from %s' % pdir) + return int(match.group(1)) + + @staticmethod + def _solve3(matrix, rhs): + """Solve a 3x3 system by Cramer's rule (avoids a numpy dependency).""" + def det(m): + return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) + - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])) + base = det(matrix) + solution = [] + for col in range(3): + replaced = [[rhs[row] if c == col else matrix[row][c] + for c in range(3)] for row in range(3)] + solution.append(det(replaced) / base) + return solution + + def _phase_space_2to3(self, phis_deg=(0.0, 130.0, 245.0), alpha_deg=35.0): + """A massless 2->3 point: (leg1_in, leg2_in, leg3_out, .., leg5_out). + + Three massless momenta summing to zero are always coplanar, so the + final state is built in a plane as a closed triangle -- the direction + angles fix the energies up to the overall scale -- and then rotated out + of the beam-transverse plane by alpha so the point is not degenerate + with respect to the beam axis. + """ + phis = [math.radians(phi) for phi in phis_deg] + cosines = [math.cos(phi) for phi in phis] + sines = [math.sin(phi) for phi in phis] + # sum E*cos = 0, sum E*sin = 0, sum E = energy + energies = self._solve3([cosines, sines, [1.0, 1.0, 1.0]], + [0.0, 0.0, self.energy]) + for energy in energies: + self.assertGreater(energy, 0.0, + 'Unphysical phase-space point: energies=%s' + % energies) + alpha = math.radians(alpha_deg) + halfe = 0.5 * self.energy + momenta = [(halfe, 0.0, 0.0, halfe), (halfe, 0.0, 0.0, -halfe)] + for index, energy in enumerate(energies): + momenta.append((energy, + energy * cosines[index], + energy * sines[index] * math.cos(alpha), + energy * sines[index] * math.sin(alpha))) + return momenta + + def _assert_crossing(self, crossed_dir, crossed_iflav, reference_dir, label, + reference_perm=None): + """The crossed call on one process must match the other one, plain. + + reference_perm reorders the momenta for the reference code when the + crossing lands the legs in a different order than the reference + process expects; None means both take the very same array. + """ + for cos_theta in self.cos_thetas: + momenta = self._phase_space(cos_theta) + crossed = self._run(crossed_dir, momenta, crossed_iflav) + if reference_perm is None: + reference_momenta = momenta + else: + reference_momenta = [momenta[index] for index in reference_perm] + reference = self._run(reference_dir, reference_momenta, + IFLAV_IDENTITY) + scale = max(abs(crossed), abs(reference), 1e-99) + self.assertLessEqual( + abs(crossed - reference) / scale, self.tolerance, + '%s disagrees at cos(theta)=%s: crossed=%r reference=%r' + % (label, cos_theta, crossed, reference)) + + # ------------------------------------------------------------------ + # tests + # ------------------------------------------------------------------ + def test_crossing_gives_back_identity(self): + """cross=0 must leave the existing behaviour untouched.""" + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + momenta = self._phase_space(self.cos_thetas[0]) + plain = self._run(qq_gg, momenta, IFLAV_IDENTITY) + self.assertNotEqual(plain, 0.0, + 'Sanity check failed: %s gives a null matrix element' + % PROC_QQ_GG) + # IFLAV = cross*NFLAV + flav with cross=0 is just flav: same answer. + self.assertEqual(plain, self._run(qq_gg, momenta, + _iflav(0, 1, nflav=1))) + + def test_qq_gg_crossed_gives_qg_qg(self): + """u u~ > g g with particle 2 <-> 3 crossed must give u g > u g.""" + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + qg_qg = self._generate(PROC_QG_QG, 'Proc_qg_qg') + self._assert_crossing( + crossed_dir=qq_gg, crossed_iflav=_iflav(CROSS_2_3, 1, nflav=1), + reference_dir=qg_qg, label='%s crossed (I=0,J=3) vs %s' + % (PROC_QQ_GG, PROC_QG_QG)) + + def test_qq_gg_crossed_with_last_particle(self): + """Particle 2 must be crossable with the last particle (J=NEXTERNAL). + + This is the case the NEXTERNAL+1 base exists for: with base NEXTERNAL, + J could only reach NEXTERNAL-1 and this crossing was unreachable. + Swapping particle 2 with particle 4 in u u~ > g g turns the incoming u~ + into an outgoing u sitting in slot 4 and the outgoing g of slot 4 into + an incoming one, so the legs come out ordered as u g > g u: the same + physics as u g > u g with the two final legs exchanged. + """ + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + qg_qg = self._generate(PROC_QG_QG, 'Proc_qg_qg') + self._assert_crossing( + crossed_dir=qq_gg, crossed_iflav=_iflav(CROSS_2_LAST, 1, nflav=1), + reference_dir=qg_qg, reference_perm=(0, 1, 3, 2), + label='%s crossed (I=0,J=4) vs %s with final legs swapped' + % (PROC_QQ_GG, PROC_QG_QG)) + + def test_qqx_gqqx_crossed_gives_qg_qqqx(self): + """q q~ > g q q~ crossed (2<->3) must give q g > q q q~, for each q. + + A 2->3 pair, so the crossing has to survive a real flavor table (each + leg carries its own flavor-group position) and a BROKEN_SYM / + identical-particle factor that only exists on the crossed side: the + crossed final state has two identical quarks, which the uncrossed + q q~ > g q q~ does not. That shows up as IDEN 36 -> 192. + + Repeated over u/d/s/c: up- and down-type quarks sit in different + flavor groups, so their flavor tables and masks differ. + """ + for quark in QUARK_FLAVORS: + with self.subTest(quark=quark): + qqx_gqqx = self._generate(PROC_QQX_GQQX % {'q': quark}, + 'Proc_qqx_gqqx_%s' % quark) + qg_qqqx = self._generate(PROC_QG_QQQX % {'q': quark}, + 'Proc_qg_qqqx_%s' % quark) + nflav = self._read_nflav(qqx_gqqx) + momenta = self._phase_space_2to3() + + crossed = self._run(qqx_gqqx, momenta, + _iflav(CROSS_2_3_5, 1, nflav=nflav)) + reference = self._run(qg_qqqx, momenta, IFLAV_IDENTITY) + + self.assertNotEqual( + reference, 0.0, + 'Sanity check failed: %s gives a null matrix element' + % (PROC_QG_QQQX % {'q': quark})) + scale = max(abs(crossed), abs(reference), 1e-99) + self.assertLessEqual( + abs(crossed - reference) / scale, self.tolerance, + '%s crossed (I=0,J=3) disagrees with %s: ' + 'crossed=%r reference=%r' + % (PROC_QQX_GQQX % {'q': quark}, + PROC_QG_QQQX % {'q': quark}, crossed, reference)) + + def test_merged_flavor_crossing_every_flavor(self): + """Every flavor of the merged q q~ > g q q~ must cross onto q g > q q q~. + + The single-flavor tests above only ever exercise the rows where all + quarks share one flavor, and those are exactly the rows for which the + denominator happens to be flavor independent. This one sweeps the whole + merged table (NFLAV=28 against NFLAV=16), which is what catches a + denominator built from the process's representative flavor instead of + the actual one: d d~ > g u u~ crosses to d g > d u u~ (nothing + identical) while d d~ > g d d~ crosses to d g > d d d~ (two identical + d), and getting that wrong shows up as a clean factor 2. + + Flavors are matched through the generated GET_FLAVOR / + GET_FLAVOR_INDEX rather than by index: the two processes do not have + the same NFLAV, so equal indices mean nothing. + """ + merged_a = self._generate(PROC_MERGED_QQX_GQQX, 'Proc_merged_a') + merged_b = self._generate(PROC_MERGED_QG_QQQX, 'Proc_merged_b') + nflav_a = self._read_nflav(merged_a) + self.assertGreater(nflav_a, 1, + 'Expected a merged multi-flavor matrix element, got ' + 'NFLAV=%s: this test would not probe the flavor ' + 'dependence of the denominator' % nflav_a) + momenta = self._phase_space_2to3() + + unmapped = [] + for flav in range(1, nflav_a + 1): + positions = self._flavor_positions(merged_a, flav) + # Caller slot 2 holds leg 3 (the gluon) and slot 3 holds leg 2. + crossed = (positions[0], positions[2], positions[1], + positions[3], positions[4]) + reference_perm = None + target = self._flavor_index(merged_b, crossed) + if target < 1: + # Slots 3 and 4 are both _quark, so the target keeps only one + # ordering of each unordered pair. Try the other one, swapping + # the momenta along with the flavors. + swapped = (crossed[0], crossed[1], crossed[3], + crossed[2], crossed[4]) + target = self._flavor_index(merged_b, swapped) + reference_perm = (0, 1, 3, 2, 4) + if target < 1: + unmapped.append((flav, positions, crossed)) + continue + + with self.subTest(flav=flav, positions=positions): + crossed_value = self._run(merged_a, momenta, + _iflav(CROSS_2_3_5, flav, + nflav=nflav_a)) + reference_momenta = momenta if reference_perm is None else \ + [momenta[index] for index in reference_perm] + reference = self._run(merged_b, reference_momenta, target) + scale = max(abs(crossed_value), abs(reference), 1e-99) + self.assertLessEqual( + abs(crossed_value - reference) / scale, self.tolerance, + 'flavor %s (positions %s) crossed disagrees: crossed=%r ' + 'reference=%r (ratio %r)' + % (flav, positions, crossed_value, reference, + reference / crossed_value if crossed_value else None)) + + self.assertFalse(unmapped, + 'Crossed flavors with no counterpart in %s: %s' + % (PROC_MERGED_QG_QQQX, unmapped)) + + def test_merged_flavor_reverse_crossing_covers_every_flavor(self): + """The reverse crossing must reach every flavor of q q~ > g q q~. + + q g > q q q~ has fewer flavors (16) than q q~ > g q q~ (28), which + looks like the reverse mapping cannot be onto. It is: the crossing + partner J is the missing degree of freedom. J=3 and J=4 cross particle + 2 with one or the other of the two final quarks, and those land on + different flavors of the target. The two coincide only when the two + final quarks already share a flavor, so the count works out exactly: + + 16 flavors x 2 crossings - 4 degenerate = 28 + + J=4 leaves the legs ordered (q, q~, q, g, q~) instead of the target's + (q, q~, g, q, q~), hence the momentum swap of slots 3 and 4. + """ + merged_a = self._generate(PROC_MERGED_QQX_GQQX, 'Proc_merged_a') + merged_b = self._generate(PROC_MERGED_QG_QQQX, 'Proc_merged_b') + nflav_a = self._read_nflav(merged_a) + nflav_b = self._read_nflav(merged_b) + momenta = self._phase_space_2to3() + + covered = {} + for flav_b in range(1, nflav_b + 1): + positions = self._flavor_positions(merged_b, flav_b) + variants = ( + # J=3: legs already come out in the target's order. + (3, (positions[0], positions[2], positions[1], + positions[3], positions[4]), None), + # J=4: cross the other final quark, then reorder slots 3/4. + (4, (positions[0], positions[3], positions[1], + positions[2], positions[4]), (0, 1, 3, 2, 4)), + ) + for j_part, target_positions, perm in variants: + flav_a = self._flavor_index(merged_a, target_positions) + self.assertGreaterEqual( + flav_a, 1, + 'Crossed flavor %s (from %s flavor %s, J=%s) has no ' + 'counterpart in %s' + % (target_positions, PROC_MERGED_QG_QQQX, flav_b, j_part, + PROC_MERGED_QQX_GQQX)) + covered.setdefault(flav_a, []).append((flav_b, j_part)) + + with self.subTest(flav_b=flav_b, j_part=j_part): + cross = 0 * (NEXTERNAL_5 + 1) + j_part + crossed_momenta = momenta if perm is None else \ + [momenta[index] for index in perm] + crossed_value = self._run(merged_b, crossed_momenta, + _iflav(cross, flav_b, + nflav=nflav_b)) + reference = self._run(merged_a, momenta, flav_a) + scale = max(abs(crossed_value), abs(reference), 1e-99) + self.assertLessEqual( + abs(crossed_value - reference) / scale, self.tolerance, + '%s flavor %s crossed (J=%s) disagrees with %s flavor ' + '%s: crossed=%r reference=%r' + % (PROC_MERGED_QG_QQQX, flav_b, j_part, + PROC_MERGED_QQX_GQQX, flav_a, crossed_value, + reference)) + + self.assertEqual( + len(covered), nflav_a, + 'The reverse crossing covers %s of the %s flavors of %s; missing ' + '%s' % (len(covered), nflav_a, PROC_MERGED_QQX_GQQX, + sorted(set(range(1, nflav_a + 1)) - set(covered)))) + + def test_crossed_density_matrix(self): + """The density matrix must survive the crossing, helicity by helicity. + + Every other test here sums over helicities, which makes them blind to + how a crossed leg's helicity is labelled: a spurious flip would just + permute the terms of the sum and cancel out. The density matrix is + resolved per helicity, so it is the one probe that pins that down. + + The expectation is that NO extra flip is needed. Helas builds the + wavefunction with nh=nhel*nsf, so flipping the NSF flag of a crossed + leg already flips its effective helicity; the caller's label therefore + carries over unchanged through the slot permutation. If a flip were + missing (or applied twice) the diagonal terms would swap and the + off-diagonal one would conjugate, which this comparison would catch. + + Probed on the gluon of u g > u g, which is leg 2 there and comes from + the crossing on the u u~ > g g side. + """ + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + qg_qg = self._generate(PROC_QG_QG, 'Proc_qg_qg') + for cos_theta in self.cos_thetas: + momenta = self._phase_space(cos_theta) + with self.subTest(cos_theta=cos_theta): + crossed = self._density(qq_gg, momenta, + _iflav(CROSS_2_3, 1, nflav=1), leg=2) + reference = self._density(qg_qg, momenta, IFLAV_IDENTITY, + leg=2) + self.assertTrue(any(abs(term) > 1e-99 for term in reference), + 'Sanity check failed: null density matrix for ' + '%s' % PROC_QG_QG) + for index, (got, want) in enumerate(zip(crossed, reference)): + scale = max(abs(got), abs(want), 1e-99) + self.assertLessEqual( + abs(got - want) / scale, self.tolerance, + 'Density matrix term %s disagrees at cos(theta)=%s: ' + 'crossed=%r reference=%r' % (index, cos_theta, got, want)) + + def test_density_matrix_diagonal_matches_smatrix(self): + """Summing the density matrix diagonal must reproduce SMATRIX. + + The diagonal terms are |M|^2 for each helicity of the probed leg, so + summing them has to give back what SMATRIX returns for that flavor. + This pins the normalisation of the density path, which GET_INTER cannot + get right on its own: it only sees JAMPs, so it divides by the bare + static IDEN and can apply neither BROKEN_SYM nor a crossed denominator. + + Probed on the merged q g > q q q~, whose two final quarks live in the + same flavor group: BROKEN_SYM is 2 exactly when they differ, and those + are the rows that were coming out a factor 2 low. A single-flavor + process would have BROKEN_SYM=1 throughout and prove nothing. + """ + merged_b = self._generate(PROC_MERGED_QG_QQQX, 'Proc_merged_b') + nflav_b = self._read_nflav(merged_b) + momenta = self._phase_space_2to3() + for flav in range(1, nflav_b + 1): + with self.subTest(flav=flav): + density = self._density(merged_b, momenta, flav, leg=1) + diagonal = density[0] + density[2] + reference = self._run(merged_b, momenta, flav) + self.assertNotEqual(reference, 0.0, + 'Sanity check failed: null matrix element ' + 'for flavor %s' % flav) + scale = max(abs(diagonal), abs(reference), 1e-99) + self.assertLessEqual( + abs(diagonal.real - reference) / scale, self.tolerance, + 'Density diagonal does not sum to SMATRIX for flavor %s: ' + 'diagonal=%r smatrix=%r (ratio %r)' + % (flav, diagonal.real, reference, + reference / diagonal.real if diagonal.real else None)) + + def _assert_chiral_crossed_density(self, crossed, reference): + """Every leg's crossed density matrix must match the native one, AND the + crossed fermion (last leg) must be fully polarized so the check actually + discriminates a helicity flip. + + `crossed` / `reference` are {leg: [c++, c+-, c--]} for legs 1..4 of + u g > w+ d. Leg 4 is the d that swapped initial<->final on the + u d~ > w+ g side; the W+ makes it 100% one-handed, so (++) and (--) are + one full / one empty. A missing or doubled crossing flip would swap them, + which the term-by-term comparison then catches. + """ + pol_pp, pol_mm = abs(reference[4][0]), abs(reference[4][2]) + self.assertGreater(max(pol_pp, pol_mm), 1e-3, + 'Reference crossed-fermion density is null; the probe ' + 'is broken (%r)' % reference[4]) + self.assertLess(min(pol_pp, pol_mm), 1e-9 * max(pol_pp, pol_mm), + 'Crossed fermion is not fully polarized, so a helicity ' + 'flip would NOT be discriminated: (++)=%r (--)=%r' + % (reference[4][0], reference[4][2])) + for leg in (1, 2, 3, 4): + self.assertTrue(any(abs(term) > 1e-99 for term in reference[leg]), + 'Null reference density for leg %s' % leg) + for index, (got, want) in enumerate(zip(crossed[leg], + reference[leg])): + scale = max(abs(got), abs(want), 1e-99) + self.assertLessEqual( + abs(got - want) / scale, self.tolerance, + 'Crossed density term %s of leg %s disagrees: crossed=%r ' + 'reference=%r' % (index, leg, got, want)) + + def test_crossed_density_matrix_chiral_fortran(self): + """The crossed spin-density matrix of a CHIRAL process, via the compiled + Fortran GET_DENSITY_IDX (no f2py). + + u d~ > w+ g crossed by (I=0, J=NEXTERNAL) is u g > w+ d; its outgoing d + is the incoming d~ that swapped sides, still 100% polarized by the W. The + density matrix is per helicity, so it is the probe that pins how that + crossed leg's helicity is LABELLED -- the same no-flip convention the + madevent cross-group event helicity (DSIG_XGHEL) depends on. Every leg, + crossed vs natively generated, must agree term by term. + """ + udx_wpg = self._generate(PROC_UDX_WPG, 'Proc_udx_wpg') + ug_wpd = self._generate(PROC_UG_WPD, 'Proc_ug_wpd') + crossed_iflav = _iflav(CROSS_2_LAST, 1, nflav=1) + for cos_theta in self.cos_thetas: + momenta = self._phase_space(cos_theta) + with self.subTest(cos_theta=cos_theta): + crossed = {leg: self._density(udx_wpg, momenta, crossed_iflav, + leg=leg) for leg in (1, 2, 3, 4)} + reference = {leg: self._density(ug_wpd, momenta, IFLAV_IDENTITY, + leg=leg) for leg in (1, 2, 3, 4)} + self._assert_chiral_crossed_density(crossed, reference) + + def test_crossed_density_matrix_chiral_f2py(self): + """Same chiral crossed-density-matrix check, but through the f2py + PY_GET_DENSITY_IDX wrapper -- the only way a python caller can ask for a + crossed density matrix. Skips if the f2py build backend is unavailable. + """ + udx_wpg = self._output_standalone(PROC_UDX_WPG, 'Proc_udx_wpg_f2py') + ug_wpd = self._output_standalone(PROC_UG_WPD, 'Proc_ug_wpd_f2py') + self._build_f2py(udx_wpg) + self._build_f2py(ug_wpd) + crossed_iflav = _iflav(CROSS_2_LAST, 1, nflav=1) + for cos_theta in self.cos_thetas: + momenta = self._phase_space(cos_theta) + with self.subTest(cos_theta=cos_theta): + crossed = self._density_f2py(udx_wpg, momenta, crossed_iflav, + (1, 2, 3, 4)) + reference = self._density_f2py(ug_wpd, momenta, IFLAV_IDENTITY, + (1, 2, 3, 4)) + self._assert_chiral_crossed_density(crossed, reference) + + def test_split_orders_density_diagonal_matches_smatrix(self): + """The same invariant on the split-orders template. + + matrix_standalone_splitOrders_v4.inc is a separate template with its own + copy of the density code, and it had the very same missing-BROKEN_SYM + bug as the default one: SMATRIX applies BROKEN_SYM(FLAVOR) while + GET_INTER normalises with the bare static IDEN and cannot, so the + diagonal came out a factor BROKEN_SYM low. Fixing one template does not + fix the other, hence this test next to + test_density_matrix_diagonal_matches_smatrix. + + Uses the merged q g > q q q~ for the same reason: its two final quarks + share a flavor group, so BROKEN_SYM=2 on the rows where they differ. A + single-flavor process has BROKEN_SYM=1 everywhere and would pass even + with the rescaling removed entirely. + """ + merged = self._generate(PROC_MERGED_QG_QQQX_SO, 'Proc_merged_so', + split_orders=True) + # Guard the premise: if the squared-order syntax ever stopped setting + # split_orders, this would silently retest the default template. + self.assertIn('SMATRIX_SPLITORDERS', self._matrix_code(merged), + 'Expected %s to be written with the split-orders ' + 'template; this test would otherwise just retest the ' + 'default one' % PROC_MERGED_QG_QQQX_SO) + nflav = self._read_nflav(merged) + self.assertGreater(nflav, 1, + 'Expected a merged multi-flavor matrix element, got ' + 'NFLAV=%s: BROKEN_SYM would be 1 throughout and this ' + 'test could not fail' % nflav) + momenta = self._phase_space_2to3() + for flav in range(1, nflav + 1): + with self.subTest(flav=flav): + density = self._density(merged, momenta, flav, leg=1) + diagonal = density[0] + density[2] + reference = self._run(merged, momenta, flav) + self.assertNotEqual(reference, 0.0, + 'Sanity check failed: null matrix element ' + 'for flavor %s' % flav) + scale = max(abs(diagonal), abs(reference), 1e-99) + self.assertLessEqual( + abs(diagonal.real - reference) / scale, self.tolerance, + 'Split-orders density diagonal does not sum to SMATRIX for ' + 'flavor %s: diagonal=%r smatrix=%r (ratio %r)' + % (flav, diagonal.real, reference, + reference / diagonal.real if diagonal.real else None)) + + def test_use_crossing_false_drops_the_machinery(self): + """--use_crossing=False must emit no crossing code, same ME otherwise. + + The extended FLAV_IDX only makes sense when the crossed subprocesses + are *not* generated separately, which is exactly what --use_crossing + drives. With it off, none of the decoding routines nor the tables they + read may reach matrix.f (they would be dead code, and GET_AMP's IC + would carry a crossing that can never be requested), while the plain + uncrossed matrix element must be untouched: the crossing-off path goes + through ANS/IDEN*BROKEN_SYM instead of the per-crossing denominator, + and those two must agree for CROSS=0. + """ + default = self._generate(PROC_QQ_GG, 'Proc_qq_gg_default') + no_cross = self._generate(PROC_QQ_GG, 'Proc_qq_gg_nocross', + options='--use_crossing=False') + + code = self._matrix_code(no_cross) + for name in CROSSING_MACHINERY_NAMES: + self.assertNotIn(name, code, + '%s is still emitted with --use_crossing=False' + % name) + # Sanity: the very same assertion must fail on the default output, + # otherwise this test would pass on a matrix.f that never had any. + self.assertIn('GET_SPINCOL_CROSS', self._matrix_code(default), + 'Default output has no crossing machinery either: ' + 'this test proves nothing') + + for cos_theta in self.cos_thetas: + momenta = self._phase_space(cos_theta) + with self.subTest(cos_theta=cos_theta): + plain = self._run(no_cross, momenta, IFLAV_IDENTITY) + reference = self._run(default, momenta, IFLAV_IDENTITY) + self.assertNotEqual(reference, 0.0, + 'Sanity check failed: %s gives a null ' + 'matrix element' % PROC_QQ_GG) + self.assertEqual(plain, reference, + '--use_crossing=False changes the uncrossed ' + 'matrix element at cos(theta)=%s: %r vs %r' + % (cos_theta, plain, reference)) + + def _assert_machinery(self, process, name, expected): + """Assert the crossing machinery is (not) emitted for `process`.""" + code = self._matrix_code(self._output_standalone(process, name)) + if expected: + # One representative name is enough to prove the machinery is there; + # the full list matters only for the "must be absent" direction, + # where any single leftover would be dead code reading a crossing + # that can never be requested. + self.assertIn('GET_SPINCOL_CROSS', code, + 'Crossing machinery is missing for %s, which does ' + 'not constrain any s-channel' % process) + else: + for routine in CROSSING_MACHINERY_NAMES: + self.assertNotIn(routine, code, + '%s is emitted for %s, whose s-channel ' + 'constraint no crossing preserves' + % (routine, process)) + return code + + def test_required_s_channel_disables_crossing(self): + """`> z >` must drop the machinery; the same process without it keeps it. + + A required s-channel names a propagator that is only s-channel in this + arrangement of the legs, so it cannot survive a crossing and the + machinery must not be emitted. The unconstrained twin is generated too: + without it, the test would pass on any matrix.f that never had the + machinery at all (e.g. if e+e- output stopped emitting it for an + unrelated reason). + """ + self._assert_machinery(PROC_REQUIRED_S, 'Proc_required_s', + expected=False) + self._assert_machinery(PROC_UNCONSTRAINED, 'Proc_unconstrained_req', + expected=True) + + def test_forbidden_s_channel_disables_crossing(self): + """`$$ z` removes a diagram by s-channel, so it must drop the machinery. + + Paired with the unconstrained twin for the same anti-vacuity reason as + test_required_s_channel_disables_crossing. + """ + self._assert_machinery(PROC_FORBIDDEN_S, 'Proc_forbidden_s', + expected=False) + self._assert_machinery(PROC_UNCONSTRAINED, 'Proc_unconstrained_forb', + expected=True) + + def test_forbidden_onshell_s_channel_keeps_crossing(self): + """A single `$ z` must NOT disable crossing: the diagram is kept. + + `$` only forbids the on-shell region of a propagator, it does not pin + the topology, so the crossing machinery stays. This is the test that + stops the fix from being over-broad and disabling crossing for every + process carrying any `$`-like constraint. + """ + self._assert_machinery(PROC_FORBIDDEN_ONSH_S, 'Proc_forbidden_onsh_s', + expected=True) + + def test_f2py_flavor_index_accessors(self): + """GET_NHEL_IDX / GET_PDG_FOR_FLAVOR must describe the crossed process. + + These are the f2py-facing accessors that let a python caller work in + PDG codes: they turn an extended FLAV_IDX into (crossed denominator, + crossed+conjugated PDG list). Two failure modes they must not have, + both invisible to the |M|^2 tests: + * GET_NHEL_IDX returning the static uncrossed IDEN (the historical + GET_NHEL bug) rather than the crossed one, and + * GET_PDG_FOR_FLAVOR forgetting to conjugate a leg that swapped + between the initial and the final state. + For u u~ > g g the identity (IFLAV=1) is itself, and the (I=0,J=3) + crossing (IFLAV=4) is u g > u g: leg 2's u~ (pdg -2) becomes an + outgoing u (pdg +2) in slot 3, and IDEN goes 72 -> 96. + """ + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + + iden_id, pdg_id = self._nhel_idx(qq_gg, IFLAV_IDENTITY) + self.assertEqual(iden_id, 72, + 'Identity IDEN wrong: %s' % iden_id) + self.assertEqual(pdg_id, (2, -2, 21, 21), + 'Identity PDG wrong: %s' % (pdg_id,)) + + iden_cr, pdg_cr = self._nhel_idx(qq_gg, _iflav(CROSS_2_3, 1, nflav=1)) + self.assertEqual(iden_cr, 96, + 'Crossed IDEN should be 96 (u g > u g), got %s. A 72 ' + 'here is the GET_NHEL static-IDEN bug.' % iden_cr) + self.assertEqual(pdg_cr, (2, 21, 2, 21), + 'Crossed PDG should be u g > u g with leg 2 conjugated,' + ' got %s' % (pdg_cr,)) + + def test_f2py_pdg_wrapper(self): + """The python PDG wrapper must find the crossing and call the right ME. + + End-to-end through the compiled f2py module: build it, then drive + flavor_dispatch.FlavorDispatch. A caller who knows only the physical + process as a signed-PDG list must get back the extended FLAV_IDX (via + find_pdg) and the correct crossed matrix element (via + matrix_element_pdg). For a u u~ > g g module the identity is itself and + the (I=0,J=3) crossing is u g > u g. Skips if f2py cannot build here. + """ + pdir = self._output_standalone(PROC_QQ_GG, 'Proc_qq_gg_f2py') + self._build_f2py(pdir) + + # Run in a subprocess: importing an f2py .so into the test interpreter + # would leak a compiled module and clash across tests. + script = ''' +import sys, math, numpy as np +sys.path.insert(0, %(pdir)r) +import matrix2py +from flavor_dispatch import FlavorDispatch +me = FlavorDispatch(matrix2py) +me.initialisemodel(%(card)r) +assert me.flavor_layout() == (1, 4, 25), me.flavor_layout() +assert me.pdg_for_index(1) == (2, -2, 21, 21), me.pdg_for_index(1) +assert me.pdg_for_index(4) == (2, 21, 2, 21), me.pdg_for_index(4) +assert me.find_pdg([2, -2, 21, 21]) == 1 +assert me.find_pdg([2, 21, 2, 21]) == 4 +assert me.find_pdg([6, -6, 21, 21]) is None # unreachable process +E = 500.0; c = 0.3; s = math.sqrt(1.0 - c * c) +P = np.asfortranarray(np.array([[E, 0, 0, E], [E, 0, 0, -E], + [E, E * s, 0, E * c], [E, -E * s, 0, -E * c]]).T) +direct = me.smatrix(P, 4) +via = me.matrix_element_pdg(P, [2, 21, 2, 21]) +assert abs(direct - via) <= 1e-11 * abs(direct), (direct, via) +assert direct > 0.0 +print("F2PY_PDG_OK") +''' % {'pdir': pdir, + 'card': pjoin(pdir, os.pardir, os.pardir, 'Cards', 'param_card.dat')} + script_path = pjoin(pdir, 'pdg_wrapper_probe.py') + with open(script_path, 'w') as fsock: + fsock.write(script) + proc = subprocess.Popen([sys.executable, script_path], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cwd=pdir) + output = proc.communicate()[0].decode() + self.assertIn('F2PY_PDG_OK', output, + 'PDG wrapper probe failed:\n%s' % output) + + def _assert_goodhel_relation(self, process, name, ninitial, npts=16): + """Compiled-module check of the GHREMAP good-helicity relation. + + Builds the f2py module for `process` and, for every DERIVABLE crossing, + asserts the crossed good-helicity set equals the identity's mapped + through the crossing permutation sigma (the invariant GHREMAP encodes). + Skips if the f2py toolchain is unavailable, exactly like the other + compiled-module tests. + """ + pdir = self._output_standalone(process, name) + self._build_f2py(pdir) + card = pjoin(pdir, os.pardir, os.pardir, 'Cards', 'param_card.dat') + script = _GOODHEL_PROBE % {'pdir': pdir, 'card': card, + 'ninitial': ninitial, 'npts': npts} + script_path = pjoin(pdir, 'goodhel_relation_probe.py') + with open(script_path, 'w') as fsock: + fsock.write(script) + proc = subprocess.Popen([sys.executable, script_path], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cwd=pdir) + output = proc.communicate()[0].decode() + self.assertIn('GHREMAP_RELATION_OK', output, + 'good-helicity relation probe failed for %s:\n%s' + % (process, output)) + + def test_goodhel_relation_qq_gg(self): + """The crossed good-helicity set of u u~ > g g must be the identity's + mapped through sigma, for every derivable crossing (>=12 points, so the + cross=23 accidental-zero undercount cannot mask a bug).""" + self._assert_goodhel_relation(PROC_QQ_GG, 'Proc_qq_gg_goodhel', + ninitial=2) + + def test_goodhel_relation_qq_ggg(self): + """Same relation on a 2->3 (u u~ > g g g): more crossings, and the + initial-initial swaps that break the relation are correctly excluded + from the derivable set the probe checks.""" + self._assert_goodhel_relation('u u~ > g g g', 'Proc_qq_ggg_goodhel', + ninitial=2) + + def test_qg_qg_crossed_gives_qq_gg(self): + """u g > u g with particle 2 <-> 3 crossed must give u u~ > g g.""" + qq_gg = self._generate(PROC_QQ_GG, 'Proc_qq_gg') + qg_qg = self._generate(PROC_QG_QG, 'Proc_qg_qg') + self._assert_crossing( + crossed_dir=qg_qg, crossed_iflav=_iflav(CROSS_2_3, 1, nflav=1), + reference_dir=qq_gg, label='%s crossed (I=0,J=3) vs %s' + % (PROC_QG_QG, PROC_QQ_GG)) + + +class TestCheckCrossingCommand(unittest.TestCase): + """The `check crossing` MG5 subcommand end-to-end. + + Drives the same code path as ``check crossing ``: + ``process_checks.check_crossing`` regenerates the process to fortran + standalone twice (crossing on and off), builds the f2py ``matrix2py`` + module in every P* directory, and compares each subprocess evaluated + through the crossing-enabled build against its crossing-disabled value. + Skips (rather than fails) when the f2py/numpy build backend is missing. + """ + + # x = u u~, x x > x x is the smallest line that puts a subprocess of the + # crossing-disabled reference (u u > u u) behind a *genuine* crossing in the + # crossing-enabled build: the two modes pick different representatives, so + # u u > u u is reached there only by a non-identity FLAV_IDX. That makes the + # comparison exercise APPLY_CROSSING rather than a plain identity, and it is + # small enough (no external gluon) to build quickly. + def setUp(self): + import madgraph.interface.master_interface as cmd_interface + self.cmd = cmd_interface.MasterCmd() + self.cmd.no_notification() + self.cmd.exec_cmd('set automatic_html_opening False', printcmd=False) + self.cmd.exec_cmd('import model sm', printcmd=False) + self.cmd.exec_cmd('define xq = u u~', printcmd=False) + + def _run_check(self, proc_line, exporter='standalone'): + import madgraph.various.process_checks as process_checks + # The C++/mg7 backends need a working C++ compiler + build toolchain; + # the fortran one needs f2py. Skip (do not fail) when unavailable. + if exporter != 'standalone': + compiler = os.environ.get('CXX', 'g++') + if not shutil.which(compiler): + raise unittest.SkipTest('no C++ compiler (%s) available for ' + 'exporter %s' % (compiler, exporter)) + procdef = self.cmd.extract_process(proc_line) + results = process_checks.check_crossing( + procdef, param_card=None, + options={'energy': 1000.0, 'proc_line': proc_line, + 'exporter': exporter}, + cmd=self.cmd) + if any(r.get('status') == 'build_failed' for r in results): + raise unittest.SkipTest( + 'Could not build the %s crossing output (build backend ' + 'unavailable); skipping the check crossing test.' % exporter) + return results, process_checks + + def _assert_all_pass_with_crossing(self, results, process_checks, + require_crossing=True): + """Shared assertions: every subprocess agrees (Passed), at least one is + reached through a genuine (non-identity) crossing, and the rendered + report is failure-free.""" + self.assertTrue(results, 'check crossing returned no comparison') + checked = 0 + crossed = 0 + for res in results: + self.assertEqual(res['status'], 'ok', res) + vd = res['value_direct'] + vc = res['value_crossed'] + self.assertIsNotNone(vd, 'no direct value for %s' % res['process']) + self.assertIsNotNone(vc, 'no crossed value for %s' % res['process']) + self.assertGreater(abs(vd), 0.0, + 'null matrix element for %s' % res['process']) + scale = max(abs(vd), abs(vc), 1e-99) + self.assertLessEqual( + abs(vd - vc) / scale, 1e-6, + '%s disagrees between crossing on/off: direct=%r crossed=%r' + % (res['process'], vd, vc)) + checked += 1 + if res.get('cross_code'): + crossed += 1 + self.assertGreater(checked, 0, 'no subprocess was checked') + if require_crossing: + # Non-vacuity: the comparison must genuinely go through the crossing + # machinery for at least one subprocess, not only identity matches. + self.assertGreater( + crossed, 0, + 'No subprocess was reached through a non-identity crossing; the ' + 'test would then only compare the two builds at cross=0') + + # The rendered report must show the Passed verdict, as the other check + # subcommands do. + text = process_checks.output_crossing(results) + self.assertIn('Passed', text) + self.assertIn('Summary:', text) + self.assertEqual(process_checks.output_crossing(results, 'fail'), 0, + 'output_crossing reported a failure:\n%s' % text) + return crossed + + def test_check_crossing_command(self): + """standalone (fortran): every subprocess must agree between the two + modes, with a Passed verdict, and at least one must be reached through a + real crossing.""" + results, process_checks = self._run_check('xq xq > xq xq') + self._assert_all_pass_with_crossing(results, process_checks) + + def test_check_crossing_command_cpp(self): + """standalone_cpp backend: u u > u u reached through a genuine crossing + of a different subprocess must agree with its independent value.""" + results, process_checks = self._run_check( + 'xq xq > xq xq', exporter='standalone_cpp') + self._assert_all_pass_with_crossing(results, process_checks) + + def test_check_crossing_command_mg7(self): + """standalone_mg7 (cudacpp CPU-SIMD) backend: same genuine-crossing + agreement, evaluated at a prescribed phase-space point injected into the + SIMD momenta buffer.""" + results, process_checks = self._run_check( + 'xq xq > xq xq', exporter='standalone_mg7') + self._assert_all_pass_with_crossing(results, process_checks) + + def test_check_crossing_invalid_exporter(self): + """An unknown --exporter must raise a clear InvalidCmd, not run.""" + import madgraph + import madgraph.various.process_checks as process_checks + procdef = self.cmd.extract_process('g u > g u') + with self.assertRaises(madgraph.InvalidCmd) as ctx: + process_checks.check_crossing( + procdef, param_card=None, + options={'energy': 1000.0, 'proc_line': 'g u > g u', + 'exporter': 'not_a_backend'}, + cmd=self.cmd) + self.assertIn('not_a_backend', str(ctx.exception)) + + def test_check_crossing_invalid_simd(self): + """An unknown standalone_mg7 --simd must raise a clear InvalidCmd. + + No build: constructing the mg7 backend validates the choice up front. + """ + import madgraph + import madgraph.various.process_checks as process_checks + procdef = self.cmd.extract_process('g u > g u') + with self.assertRaises(madgraph.InvalidCmd) as ctx: + process_checks.check_crossing( + procdef, param_card=None, + options={'energy': 1000.0, 'proc_line': 'g u > g u', + 'exporter': 'standalone_mg7', 'simd': 'not_a_simd'}, + cmd=self.cmd) + self.assertIn('not_a_simd', str(ctx.exception)) + + def test_check_crossing_s_channel_graceful(self): + """A required s-channel disables crossing; the check must still pass. + + `u u~ > z > e+ e-` is only s-channel in this arrangement of the legs, so + no crossing preserves it and the crossing machinery is not emitted. The + command must handle this gracefully: every subprocess is matched at the + identity and passes (the crossing-enabled and crossing-disabled builds + agree), rather than erroring. + """ + results, process_checks = self._run_check('u u~ > z > e+ e-') + self.assertTrue(results, 'check crossing returned no comparison') + for res in results: + self.assertEqual(res['status'], 'ok', res) + self.assertIsNotNone(res['value_direct']) + self.assertIsNotNone(res['value_crossed']) + self.assertFalse(res.get('cross_code'), + 'a constrained-s-channel process should not be ' + 'reached by any non-identity crossing: %s' % res) + self.assertEqual(process_checks.output_crossing(results, 'fail'), 0) + + +class TestCrossingUnsupportedOutput(unittest.TestCase): + """Outputs that cannot cross must refuse a process generated with crossing. + + --use_crossing is on by default and tells the generation *not* to write the + crossed subprocesses out separately, because the matrix element is supposed + to reach them through an extended FLAV_IDX. The fortran standalone and the + (grouped) madevent output decode one; an output that cannot would quietly + produce a matrix element missing those subprocesses, so it has to raise + instead. + """ + + # Outputs reached through ExportV4Factory that have no crossing machinery + # (madevent is no longer here: the grouped exporter shares a base matrix + # element through the crossing router, see TestCrossingPartition). + UNSUPPORTED_FORMATS = ['matchbox'] + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='cross_unsupported_') + + def tearDown(self): + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + def _output(self, fmt, name, options=''): + """Run generate+output for `fmt`, returning nothing or raising.""" + cmd = cmd_interface.MasterCmd() + cmd.no_notification() + cmd.exec_cmd('set automatic_html_opening False') + cmd.exec_cmd('import model sm') + cmd.exec_cmd(('generate %s %s' % (PROC_QG_QG, options)).strip()) + cmd.exec_cmd('output %s %s -f' % (fmt, pjoin(self.tmpdir, name))) + + def test_unsupported_output_raises_with_crossing(self): + """The default (crossing on) must be refused, and say how to fix it.""" + for fmt in self.UNSUPPORTED_FORMATS: + with self.subTest(format=fmt): + with self.assertRaises(madgraph.InvalidCmd) as ctx: + self._output(fmt, 'raise_%s' % fmt) + message = str(ctx.exception) + # An error that does not name the way out would just leave the + # user stuck, so the remedy is part of the requirement. + self.assertIn('--use_crossing=False', message, + 'The %s error does not name the fix: %s' + % (fmt, message)) + + def test_unsupported_output_accepted_without_crossing(self): + """--use_crossing=False must let the very same output through. + + Without this the test above would be satisfied by an exporter that is + simply broken, rather than by one gating on the crossing request. + """ + for fmt in self.UNSUPPORTED_FORMATS: + with self.subTest(format=fmt): + self._output(fmt, 'ok_%s' % fmt, + options='--use_crossing=False') + + def test_supported_outputs_accept_crossing(self): + """Outputs that DO implement crossing must not be caught. + + Anchors the gate against being over-broad: a check that refused every + output would pass both tests above. The fortran standalone decodes the + extended FLAV_IDX directly; the grouped madevent output reaches the + crossed subprocesses through the crossing router. + """ + for fmt in ('standalone', 'madevent'): + with self.subTest(format=fmt): + self._output(fmt, 'ok_%s' % fmt) + + +# The C++ standalone driver: take a fixed RAMBO phase space point once +# (all-massless, so the momenta are identical between the two P directories) and +# print sigmaKin at each flavor_id passed on the command line. Each flavor_id is +# evaluated in a FRESH CPPProcess so the good-helicity cache starts empty: that +# cache is indexed by the reduced flavor (flav_use), so different crossings of +# one flavor would otherwise share it and, once it kicks in, a later crossing +# would be filtered by an earlier one's non-zero-helicity pattern (the deferred +# open question of keying the cache on the full flavor_id). The momenta are +# generated once and reused so every process sees the very same point. +# The shipped check_sa.cpp only ever loops over its own maxflavor identities, so +# a purpose-built driver is needed to request a crossed flavor_id. +_CPP_DRIVER = r""" +#include +#include +#include +#include "CPPProcess.h" +#include "rambo.h" + +int main(int argc, char** argv){ + double energy = 1000.0; + double weight; + CPPProcess seed("../../Cards/param_card.dat"); + vector p = get_momenta(seed.ninitial, energy, + seed.getMasses(), weight); + std::cout << std::setprecision(17); + for(int a = 1; a < argc; a++){ + int fid = atoi(argv[a]); + CPPProcess process("../../Cards/param_card.dat"); + process.setMomenta(p); + double me = process.sigmaKin(fid); + std::cout << "sigmaKin(" << fid << ") = " << me << std::endl; + } + return 0; +} +""" + + +class TestStandaloneCppCrossSymmetry(unittest.TestCase): + """standalone_cpp must reproduce the crossing the fortran standalone does. + + Mirror of TestStandaloneCrossSymmetry for the C++ backend: u u~ > g g and + u g > u g are each other's crossing under (I=0, J=3). In the 0-based C++ + flavor_id encoding cross = flavor_id / nflav and flav = flavor_id % nflav, + so with NFLAV=1 the crossing (I=0, J=3) -> cross = 0*(NEXTERNAL+1)+3 = 3 is + reached by flavor_id = 3. sigmaKin already divides by the crossed + denominator, so a crossed call returns the properly averaged matrix element + of the process it crosses into and can be compared directly. + + Skipped when no C++ compiler is available (the whole check needs to build + and run real C++). + """ + + energy = 1000.0 + tolerance = 1e-9 + # cross = I*(NEXTERNAL+1)+J = 0*5+3 = 3, flavor_id = cross*NFLAV+flav (NFLAV=1) + CROSS_2_3 = 3 + IDENTITY = 0 + + debugging = getattr(unittest, 'debug', False) + + def setUp(self): + self.compiler = os.environ.get('CXX', 'g++') + if not shutil.which(self.compiler): + self.skipTest('no C++ compiler (%s) available' % self.compiler) + self.tmpdir = tempfile.mkdtemp(prefix='cross_cpp_') + + def tearDown(self): + if not self.debugging and os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + # ------------------------------------------------------------------ + def _output_standalone_cpp(self, process, name, options=''): + """Write the standalone_cpp output for `process`, return its P* dir.""" + outdir = pjoin(self.tmpdir, name) + cmd = cmd_interface.MasterCmd() + cmd.no_notification() + cmd.exec_cmd('set automatic_html_opening False') + cmd.exec_cmd('set group_subprocesses False') + cmd.exec_cmd('set apply_flavor_grouping True') + cmd.exec_cmd('import model sm') + cmd.exec_cmd(('generate %s %s' % (process, options)).strip()) + cmd.exec_cmd('output standalone_cpp %s -f' % outdir) + + subproc_root = pjoin(outdir, 'SubProcesses') + pdirs = [pjoin(subproc_root, d) for d in sorted(os.listdir(subproc_root)) + if d.startswith('P') and os.path.isdir(pjoin(subproc_root, d))] + self.assertEqual(len(pdirs), 1, + 'Expected a single subprocess directory for %s, got %s' + % (process, pdirs)) + return pdirs[0] + + def _cpp_source(self, pdir): + with open(pjoin(pdir, 'CPPProcess.cc')) as fsock: + return fsock.read() + + def _build_and_run(self, pdir, flavor_ids): + """Build the driver in `pdir` and return {flavor_id: sigmaKin}.""" + # 'make' compiles CPPProcess.o and links the shipped check; it also + # proves the generated code compiles. + with open(os.devnull, 'w') as devnull: + rc = subprocess.call(['make'], cwd=pdir, stdout=devnull, + stderr=subprocess.STDOUT) + self.assertEqual(rc, 0, 'make failed in %s' % pdir) + + with open(pjoin(pdir, 'driver_cross.cpp'), 'w') as fsock: + fsock.write(_CPP_DRIVER) + cxxflags = ['-O3', '-ffast-math', '-I../../src', '-I.', '-fPIC'] + libflags = ['-L../../lib', '-lmodel_sm'] + with open(os.devnull, 'w') as devnull: + rc = subprocess.call( + [self.compiler] + cxxflags + ['-c', '-o', 'driver_cross.o', + 'driver_cross.cpp'], + cwd=pdir, stdout=devnull, stderr=subprocess.STDOUT) + self.assertEqual(rc, 0, 'driver compile failed in %s' % pdir) + rc = subprocess.call( + [self.compiler, '-o', 'driver_cross', 'CPPProcess.o', + 'driver_cross.o'] + libflags, + cwd=pdir, stdout=devnull, stderr=subprocess.STDOUT) + self.assertEqual(rc, 0, 'driver link failed in %s' % pdir) + + out = subprocess.check_output( + ['./driver_cross'] + [str(f) for f in flavor_ids], + cwd=pdir).decode() + values = {} + for match in re.finditer(r'sigmaKin\((\d+)\)\s*=\s*([-\d.eE+]+)', out): + values[int(match.group(1))] = float(match.group(2)) + self.assertEqual(set(values), set(flavor_ids), + 'driver output did not cover every flavor_id: %s' % out) + return values + + # ------------------------------------------------------------------ + def test_qq_gg_crossed_gives_qg_qg(self): + """u u~ > g g crossed by (I=0,J=3) must equal u g > u g at the same + momenta, and the crossed value must differ from the identity one so the + check is non-vacuous.""" + crossed_dir = self._output_standalone_cpp(PROC_QQ_GG, 'qqgg') + reference_dir = self._output_standalone_cpp(PROC_QG_QG, 'qgqg') + + crossed = self._build_and_run(crossed_dir, + [self.IDENTITY, self.CROSS_2_3]) + reference = self._build_and_run(reference_dir, [self.IDENTITY]) + + self.assertAlmostEqual( + crossed[self.CROSS_2_3], reference[self.IDENTITY], + delta=self.tolerance * abs(reference[self.IDENTITY]), + msg='u u~ > g g crossed (%r) != u g > u g identity (%r)' + % (crossed[self.CROSS_2_3], reference[self.IDENTITY])) + # Non-vacuous: the crossing must move the answer, not return the + # identity value. + self.assertNotAlmostEqual( + crossed[self.CROSS_2_3], crossed[self.IDENTITY], places=6, + msg='crossed value equals the identity value; crossing had no ' + 'effect, so the test would pass trivially') + + def test_qg_qg_crossed_gives_qq_gg(self): + """The reverse: u g > u g crossed by (I=0,J=3) must equal u u~ > g g.""" + crossed_dir = self._output_standalone_cpp(PROC_QG_QG, 'qgqg_rev') + reference_dir = self._output_standalone_cpp(PROC_QQ_GG, 'qqgg_rev') + + crossed = self._build_and_run(crossed_dir, + [self.IDENTITY, self.CROSS_2_3]) + reference = self._build_and_run(reference_dir, [self.IDENTITY]) + + self.assertAlmostEqual( + crossed[self.CROSS_2_3], reference[self.IDENTITY], + delta=self.tolerance * abs(reference[self.IDENTITY]), + msg='u g > u g crossed (%r) != u u~ > g g identity (%r)' + % (crossed[self.CROSS_2_3], reference[self.IDENTITY])) + + def test_invalid_overlapping_swap_returns_zero(self): + """An overlapping-swap crossing code (I=2, J=1 here) is marked invalid; + sigmaKin must short-circuit to 0 for it.""" + pdir = self._output_standalone_cpp(PROC_QQ_GG, 'qqgg_inv') + # cross = I*(NEXTERNAL+1)+J = 2*5+1 = 11, flavor_id = 11 (NFLAV=1). + overlapping = 2 * (NEXTERNAL + 1) + 1 + values = self._build_and_run(pdir, [overlapping]) + self.assertEqual(values[overlapping], 0.0, + 'an overlapping-swap code must give a zero matrix ' + 'element, got %r' % values[overlapping]) + + def test_use_crossing_false_drops_the_machinery(self): + """--use_crossing=False must compile and emit no crossing machinery, + while still giving the same uncrossed matrix element.""" + with_dir = self._output_standalone_cpp(PROC_QQ_GG, 'qqgg_on') + without_dir = self._output_standalone_cpp(PROC_QQ_GG, 'qqgg_off', + options='--use_crossing=False') + + on_src = self._cpp_source(with_dir) + off_src = self._cpp_source(without_dir) + for token in ('spincol_cross', 'cross_perm', 'cross_ic', + 'ident_cross', 'flav_use', 'const int ic[]'): + self.assertIn(token, on_src, + '%s should be emitted with crossing on' % token) + self.assertNotIn(token, off_src, + '%s must NOT be emitted with --use_crossing=False' + % token) + + on = self._build_and_run(with_dir, [self.IDENTITY]) + off = self._build_and_run(without_dir, [self.IDENTITY]) + self.assertAlmostEqual( + on[self.IDENTITY], off[self.IDENTITY], + delta=self.tolerance * abs(on[self.IDENTITY]), + msg='the uncrossed matrix element changed when the crossing ' + 'machinery was emitted: %r vs %r' + % (on[self.IDENTITY], off[self.IDENTITY])) + + +class TestStandaloneMg7CrossSymmetry(unittest.TestCase): + """standalone_mg7 (madmatrix / cudacpp CPU-SIMD) must reproduce the crossing. + + Mirror of TestStandaloneCppCrossSymmetry for the data-parallel madmatrix + backend. The extended flavor id encodes cross = id / nflav and flav = id % + nflav (0-based, NFLAV=1 here), so (I=0, J=3) -> cross = 3 -> id = 3. The key + extra check versus the scalar C++ backend is that DIFFERENT events in the + SAME SIMD page may carry DIFFERENT crossings while sharing the reduced + flavor: the per-event momentum permutation must not be vectorized. + + The whole check needs to build and run real C++/SIMD code; skipped (not + failed) if the compiler or the madmatrix build toolchain is unavailable. + """ + + CROSS_2_3 = 3 # cross = I*(NEXTERNAL+1)+J = 0*5+3 = 3, id = cross*NFLAV+flav + IDENTITY = 0 + OVERLAP = 2 * (NEXTERNAL + 1) + 1 # cross=11 (I=2,J=1): overlapping swap -> invalid + tolerance = 1e-9 + + debugging = getattr(unittest, 'debug', False) + + def setUp(self): + self.compiler = os.environ.get('CXX', 'g++') + if not shutil.which(self.compiler): + self.skipTest('no C++ compiler (%s) available' % self.compiler) + self.tmpdir = tempfile.mkdtemp(prefix='cross_mg7_') + + def tearDown(self): + if not self.debugging and os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + # ------------------------------------------------------------------ + def _output_standalone_mg7(self, process, name, options=''): + """Write the standalone_mg7 output for `process`, return its P* dir.""" + outdir = pjoin(self.tmpdir, name) + cmd = cmd_interface.MasterCmd() + cmd.no_notification() + cmd.exec_cmd('set automatic_html_opening False') + cmd.exec_cmd('set group_subprocesses False') + cmd.exec_cmd('set apply_flavor_grouping True') + cmd.exec_cmd('import model sm') + cmd.exec_cmd(('generate %s %s' % (process, options)).strip()) + cmd.exec_cmd('output standalone_mg7 %s -f' % outdir) + + subproc_root = pjoin(outdir, 'SubProcesses') + pdirs = [pjoin(subproc_root, d) for d in sorted(os.listdir(subproc_root)) + if d.startswith('P') and os.path.isdir(pjoin(subproc_root, d))] + self.assertEqual(len(pdirs), 1, + 'Expected a single subprocess directory for %s, got %s' + % (process, pdirs)) + return pdirs[0] + + def _cpp_source(self, pdir): + with open(pjoin(pdir, 'CPPProcess.cc')) as fsock: + return fsock.read() + + def _patch_and_build(self, pdir): + """Patch the shipped check_sa.cc so it can (a) evaluate the EXTENDED + flavor ids the crossing needs (the shipped cap stops at nmaxflavor) and + (b) demonstrate a per-event mixed-crossing page (env MG_FLVMIX/MG_SAMEMOM), + then build check_sa.exe. Skip if the madmatrix toolchain cannot build.""" + check = pjoin(pdir, 'check_sa.cc') + with open(check) as fsock: + src = fsock.read() + src = src.replace( + 'if( flavorID >= CPPProcess::nmaxflavor )', + 'if( flavorID >= CPPProcess::nmaxflavor * ' + '(unsigned)((CPPProcess::npar+1)*(CPPProcess::npar+1)) )') + src = src.replace( + ' std::vector flvVec( nevt, flavorID );', + ' std::vector flvVec( nevt, flavorID );\n' + ' if( const char* mix = getenv("MG_FLVMIX") ) { unsigned int a=0,b=0; ' + 'sscanf(mix,"%u,%u",&a,&b); for(unsigned int i=0;igetMomentaFinal();', + ' prsk->getMomentaFinal();\n' + ' if( getenv("MG_SAMEMOM") ) for( unsigned int ie=1; ie g g crossed by (I=0,J=3) equals u g > u g at the same momenta + (both 2->2 massless -> identical RAMBO momenta for the same seed).""" + crossed = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg') + reference = self._output_standalone_mg7(PROC_QG_QG, 'qgqg') + self._patch_and_build(crossed) + self._patch_and_build(reference) + + crossed_val = self._me(crossed, self.CROSS_2_3) + identity_val = self._me(crossed, self.IDENTITY) + reference_val = self._me(reference, self.IDENTITY) + + self.assertAlmostEqual( + crossed_val, reference_val, + delta=self.tolerance * abs(reference_val), + msg='u u~ > g g crossed (%r) != u g > u g identity (%r)' + % (crossed_val, reference_val)) + # Non-vacuous: the crossing must move the answer. + self.assertNotAlmostEqual( + crossed_val, identity_val, places=6, + msg='crossed value equals the identity value; crossing had no effect') + + def test_qg_qg_crossed_gives_qq_gg(self): + """The reverse: u g > u g crossed by (I=0,J=3) equals u u~ > g g.""" + crossed = self._output_standalone_mg7(PROC_QG_QG, 'qgqg_rev') + reference = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg_rev') + self._patch_and_build(crossed) + self._patch_and_build(reference) + self.assertAlmostEqual( + self._me(crossed, self.CROSS_2_3), self._me(reference, self.IDENTITY), + delta=self.tolerance * abs(self._me(reference, self.IDENTITY)), + msg='u g > u g crossed != u u~ > g g identity') + + def test_per_event_different_cross(self): + """THE point of the SIMD port: within ONE SIMD page, events carrying + DIFFERENT crossings (but the same reduced flavor) each get their own + crossed matrix element. Feed identical momenta to every event, alternate + the crossing per event (even -> identity, odd -> cross 2<->3) and check + each lane independently.""" + pdir = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg_perevent') + self._patch_and_build(pdir) + identity_val = self._me(pdir, self.IDENTITY) + crossed_val = self._me(pdir, self.CROSS_2_3) + self.assertNotAlmostEqual(identity_val, crossed_val, places=6, + msg='degenerate: identity == crossed') + mixed = self._event_mes( + pdir, self.IDENTITY, + env={'MG_SAMEMOM': '1', + 'MG_FLVMIX': '%d,%d' % (self.IDENTITY, self.CROSS_2_3)}) + self.assertGreaterEqual(len(mixed), 4, + 'need several events to prove per-event crossing') + for i, me in enumerate(mixed): + expected = identity_val if i % 2 == 0 else crossed_val + self.assertAlmostEqual( + me, expected, delta=self.tolerance * abs(expected) + 1e-12, + msg='event %d (cross %s) got %r, expected %r' + % (i, 'id' if i % 2 == 0 else '2<->3', me, expected)) + + def test_invalid_overlapping_swap_returns_zero(self): + """An overlapping-swap crossing code (I=2, J=1 -> cross 11) is invalid; + the per-event denominator must short-circuit its matrix element to 0.""" + pdir = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg_inv') + self._patch_and_build(pdir) + self.assertEqual(self._me(pdir, self.OVERLAP), 0.0, + 'an overlapping-swap code must give a zero ME') + + def test_use_crossing_false_byte_identical(self): + """--use_crossing=False must emit NO crossing machinery (every crossing + token absent from the generated source) and still give the same + uncrossed matrix element as the crossing-on build. (A full byte-identical + `diff -r` against the pre-feature output was checked by hand; here we + assert the token absence and the numerical invariance.)""" + on_dir = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg_on') + off_dir = self._output_standalone_mg7(PROC_QQ_GG, 'qqgg_off', + options='--use_crossing=False') + on_src = self._cpp_source(on_dir) + off_src = self._cpp_source(off_dir) + for token in ('spincol_cross', 'cross_perm', 'cross_ic', 'ident_cross', + 'xmom', 'flavorPDGs_cross'): + self.assertIn(token, on_src, + '%s should be emitted with crossing on' % token) + self.assertNotIn(token, off_src, + '%s must NOT be emitted with --use_crossing=False' + % token) + self._patch_and_build(on_dir) + self._patch_and_build(off_dir) + self.assertAlmostEqual( + self._me(on_dir, self.IDENTITY), self._me(off_dir, self.IDENTITY), + delta=self.tolerance * abs(self._me(off_dir, self.IDENTITY)), + msg='the uncrossed ME changed when the crossing machinery was emitted') + + +class TestCrossingPartition(unittest.TestCase): + """partition_crossing_classes routes each subprocess flavor to a base matrix + element via crossing. A module drops its own matrix.f only when every one + of its flavors is a crossing of a base module's flavor; the basis for sharing + one matrix.f across a base and its crossings in the madevent output.""" + + def _groups(self, proc): + import madgraph.iolibs.group_subprocs as group_subprocs + import madgraph.iolibs.export_v4 as export_v4 + cmd = cmd_interface.MasterCmd() + cmd.run_cmd('import model sm') + cmd.run_cmd('define j = g u u~') + # partition_crossing_classes operates on the FULL (unmerged) matrix-element + # list -- exactly what the madevent output reconstructs from the recorded + # crossings before grouping. Generate unmerged here so the routing has the + # crossed modules to eliminate (the default merge_crossing='record' would + # fold them away at generation, leaving nothing to route). + old = os.environ.get('MG_MERGE_CROSSING') + os.environ['MG_MERGE_CROSSING'] = 'off' + try: + cmd.run_cmd('generate %s' % proc) + finally: + if old is None: + os.environ.pop('MG_MERGE_CROSSING', None) + else: + os.environ['MG_MERGE_CROSSING'] = old + groups = group_subprocs.SubProcessGroup.group_amplitudes( + cmd._curr_amps, 'madevent') + for g in groups: + g.generate_matrix_elements() + return groups, export_v4.ProcessExporterFortran() + + def test_partition_pp_jj(self): + groups, exp = self._groups('p p > j j') + eliminated_any = False + for g in groups: + mes = g.get('matrix_elements') + bases, routing = exp.partition_crossing_classes(mes) + self.assertEqual(len(routing), len(mes)) + for i in range(len(mes)): + self.assertTrue(routing[i], 'a module with no flavors') + for (b, iflav) in routing[i]: + self.assertIn(b, bases) # routes to a real base + self.assertGreaterEqual(iflav, 1) # 1-based FLAV_IDX + if i not in bases: + # an eliminated module never routes back to itself + self.assertNotEqual(b, i) + if len(bases) < len(mes): + eliminated_any = True + self.assertTrue(eliminated_any, + 'no module was eliminated by crossing in p p > j j') + + +class TestMadeventCrossingHelicity(unittest.TestCase): + """End-to-end regression for the crossed-helicity label written to the LHE. + + The madevent helicity path is the phase-4 GET_NHEL decoder plus the phase-5 + runtime crossing encode (the base-selected helicity code is relabelled into + the dependent's canonical code by permuting its mixed-radix digits with the + crossing permutation, replacing the old DSIG_XGHEL / router HELMAP tables). + p p > w+ j is the sharp test: its crossed subprocesses (u g > w+ d, ...) put + the W+ -- a massive vector with THREE helicity states -- in a leg the + crossing moved, so a bug in the relabel scrambles the W+ helicity. The W+ + polarisation is physically CHIRAL (asymmetric transverse states) with a + populated longitudinal (0) state; a scrambled relabel typically reads a + quark leg's +-1 into the W+ slot and destroys that structure. + + This runs a full (small) madevent generation, so it is a slow test. + """ + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='cross_mev_hel_') + + def tearDown(self): + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + def test_w_helicity_asymmetry_ppwj(self): + from madgraph import MG5DIR + from madgraph.various import lhe_parser + outdir = pjoin(self.tmpdir, 'ppwj') + card = pjoin(self.tmpdir, 'cmd.txt') + with open(card, 'w') as f: + f.write('generate p p > w+ j\n' + 'output madevent %s -f\n' + 'launch\n' + 'set nevents 1000\n' + 'set iseed 777\n' % outdir) + subprocess.call([sys.executable, pjoin(MG5DIR, 'bin', 'mg5_aMC'), card]) + + lhe = pjoin(outdir, 'Events', 'run_01', 'unweighted_events.lhe.gz') + self.assertTrue(os.path.isfile(lhe), + 'madevent produced no LHE file (%s)' % lhe) + + counts = {-1: 0, 0: 0, 1: 0} + nevt = 0 + for event in lhe_parser.EventFile(lhe): + for part in event: + if part.pid == 24 and part.status == 1: # the final-state W+ + hel = int(round(part.helicity)) + self.assertIn(hel, (-1, 0, 1), + 'W+ has undefined/non-physical helicity %r -- ' + 'helicity output off or scrambled' + % part.helicity) + counts[hel] += 1 + nevt += 1 + total = sum(counts.values()) + self.assertGreater(nevt, 100, 'too few events generated (%d)' % nevt) + self.assertEqual(total, nevt, 'expected exactly one final-state W+ per ' + 'event (got %d W+ in %d events)' % (total, nevt)) + + fm, f0, fp = (counts[-1] / total, counts[0] / total, counts[1] / total) + # All three W+ helicity states populated, incl. the longitudinal 0. + for hel in (-1, 0, 1): + self.assertGreater(counts[hel], 0, + 'W+ helicity %d not populated: %s' % (hel, counts)) + # The two transverse states are chirally asymmetric. + self.assertGreater(abs(fm - fp), 0.05, + 'W+ transverse helicities not chirally asymmetric: %s' + % counts) + # The longitudinal fraction sits in a physical window (a scrambled + # relabel collapses or inflates it out of this range). + self.assertTrue(0.02 < f0 < 0.45, + 'W+ longitudinal fraction unphysical: %.3f (%s)' + % (f0, counts)) + + +class TestColorFlowCode(unittest.TestCase): + """The canonical COLOUR-FLOW code, the colour analogue of the canonical + helicity code. + + A colour flow is labelled by its connectivity once the INITIAL-state legs + swap their colour/anticolour roles (the LHE convention runs initial-state + colour lines 'through', so without that flip a label sits in the same slot + on two legs and the flow is not a colour<->anticolour bijection). Ordering + the colour and anticolour slots by leg, digit i is the anticolour slot that + colour slot i connects to and code = sum_i digit_i * N^i. + + Two properties make it usable as an event label and make crossing + transparent (both verified here): + (a) every basis flow is a clean bijection, i.e. it encodes at all; + (b) the code is INJECTIVE over a process's colour basis, so the code + identifies the flow and no per-process flow table is needed. + Crossing-covariance (relabelling legs by the crossing permutation carries a + base flow's code onto the crossed process's own flow code) is exercised by + the crossing machinery itself: _router_colmap matches flows through the + same _color_flow_canon helper. + """ + + # (process, expected number of colour flows) -- includes g g > g g g, whose + # 24 flows over 5 colour slots is the widest case that stays quick. + PROCS = [('u u~ > g g', 2), ('g g > g g', 6), ('u u~ > u u~', 2), + ('g g > t t~', 2), ('u u~ > g g g', 6), ('g g > g g g', 24)] + + def test_color_flow_code_bijective_and_injective(self): + import madgraph.core.helas_objects as helas_objects + import madgraph.iolibs.export_v4 as export_v4 + exp = export_v4.ProcessExporterFortranMEGroup.__new__( + export_v4.ProcessExporterFortranMEGroup) + checked = 0 + for proc, nflow_exp in self.PROCS: + cmd = cmd_interface.MasterCmd() + cmd.exec_cmd('generate %s' % proc, printcmd=False) + me = helas_objects.HelasMultiProcess(cmd._curr_amps) + for m in me.get('matrix_elements'): + if not m.get('color_basis'): + continue + codes = exp._color_flow_codes(m) + # (a) every flow is a clean colour<->anticolour bijection + self.assertIsNotNone( + codes, '%s: a colour flow is not a clean bijection -- the ' + 'initial-state colour/anticolour flip is required' % proc) + self.assertEqual(len(codes), nflow_exp, + '%s: expected %d colour flows, got %d' + % (proc, nflow_exp, len(codes))) + # (b) the code identifies the flow + self.assertEqual(len(set(codes)), len(codes), + '%s: colour-flow codes collide: %s' + % (proc, codes)) + checked += 1 + self.assertTrue(checked, 'no coloured matrix element was checked') + + def test_color_flow_code_round_trip(self): + """decode(code(flow)) reproduces the flow's canonical connectivity, and + the slot structure is FLOW-INDEPENDENT (it is process data, fixed by the + colour representations). Together these are what allow the colour tags + to be rebuilt from the code alone instead of read out of the generated + ICOLUP table -- the step this encoding is aiming at. + """ + import madgraph.core.helas_objects as helas_objects + import madgraph.iolibs.export_v4 as export_v4 + exp = export_v4.ProcessExporterFortranMEGroup.__new__( + export_v4.ProcessExporterFortranMEGroup) + checked = 0 + for proc, _nflow in self.PROCS: + cmd = cmd_interface.MasterCmd() + cmd.exec_cmd('generate %s' % proc, printcmd=False) + me = helas_objects.HelasMultiProcess(cmd._curr_amps) + for m in me.get('matrix_elements'): + if not m.get('color_basis'): + continue + states = [l.get('state') for l in + m.get('processes')[0].get_legs_with_decays()] + slots = None + for flow in exp._module_color_flows(m): + conns = exp._color_flow_canon(flow, states) + this = exp._color_flow_slots(conns) + if slots is None: + slots = this + # the slot structure must not depend on the flow + self.assertEqual(this, slots, + '%s: slot structure varies between flows ' + '(%s vs %s)' % (proc, this, slots)) + code = exp._color_flow_code(conns) + self.assertIsNotNone(code, '%s: flow did not encode' % proc) + back = exp._color_flow_decode(code, slots[0], slots[1]) + self.assertEqual(back, conns, + '%s: code %d does not round-trip\n got %s' + '\n want %s' + % (proc, code, sorted(back), sorted(conns))) + checked += 1 + self.assertTrue(checked, 'no colour flow was round-tripped') + + +class TestMadeventColorFlowRatio(unittest.TestCase): + """End-to-end guard on the COLOUR written to the LHE, for u u~ > u u~. + + Every event's colour tags must form a clean colour<->anticolour bijection + once the initial-state legs swap roles (the canonical form the colour-flow + code is built on): each colour label is matched by exactly one anticolour + label. That is the colour analogue of "the helicity is one of the physical + states", and it is what breaks first if the colour flow written to the event + is ever rebuilt wrongly -- e.g. when the tags start being decoded from the + canonical colour-flow code instead of read from the ICOLUP table. + + u u~ > u u~ is chosen deliberately: its two colour flows are STRONGLY + asymmetric (~98/2), so the test can pin down WHICH flow is which. A process + whose flows are related by a symmetry -- g g > t t~ splits 50/50 -- would + pass just as happily with the two flow labels SWAPPED, which is exactly the + bug this is meant to catch. Here a swap inverts 98/2 into 2/98. + + The dominant flow is identified topologically (do the colour connections + stay inside the initial/final groups, or cross between them?) rather than by + raw leg indices, so the check does not depend on leg ordering. Runs a small + madevent generation, so it is a slow test. + """ + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='cross_col_ratio_') + + def tearDown(self): + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + @staticmethod + def _canon(parts): + """{colour label: [legs]}, {anticolour label: [legs]} with initial-state + legs swapping the two roles.""" + col, anti = {}, {} + for i, p in enumerate(parts): + c, a = int(p.color1), int(p.color2) + if p.status == -1: + c, a = a, c + if c: + col.setdefault(c, []).append(i) + if a: + anti.setdefault(a, []).append(i) + return col, anti + + def test_color_flow_ratio_uux_uux(self): + from madgraph import MG5DIR + from madgraph.various import lhe_parser + outdir = pjoin(self.tmpdir, 'uux') + card = pjoin(self.tmpdir, 'cmd.txt') + with open(card, 'w') as f: + f.write('generate u u~ > u u~\n' + 'output madevent %s -f\n' + 'launch\n' + 'set nevents 2000\n' + 'set iseed 909\n' % outdir) + subprocess.call([sys.executable, pjoin(MG5DIR, 'bin', 'mg5_aMC'), card]) + + lhe = pjoin(outdir, 'Events', 'run_01', 'unweighted_events.lhe.gz') + self.assertTrue(os.path.isfile(lhe), + 'madevent produced no LHE file (%s)' % lhe) + + nevt = 0 + sigs = {} + for event in lhe_parser.EventFile(lhe): + parts = [p for p in event] + nevt += 1 + col, anti = self._canon(parts) + # (1) structure: a perfect colour <-> anticolour matching + self.assertEqual(set(col), set(anti), + 'colour labels do not pair with anticolour labels ' + '(event %d): %s vs %s' % (nevt, sorted(col), + sorted(anti))) + self.assertTrue(col, 'event %d carries no colour at all' % nevt) + for lbl, legs in col.items(): + self.assertEqual(len(legs), 1, + 'colour label %s appears on %d legs (event %d)' + % (lbl, len(legs), nevt)) + self.assertEqual(len(anti[lbl]), 1, + 'anticolour label %s appears on %d legs ' + '(event %d)' % (lbl, len(anti[lbl]), nevt)) + # topological signature of the flow: does each colour connection + # stay inside the initial / final group, or cross between them? + ini = set(i for i, p in enumerate(parts) if p.status == -1) + sig = tuple(sorted(('I' if c in ini else 'F') + + ('I' if a in ini else 'F') + for c, a in ((col[l][0], anti[l][0]) + for l in col))) + sigs[sig] = sigs.get(sig, 0) + 1 + + self.assertGreater(nevt, 100, 'too few events generated (%d)' % nevt) + # (2) exactly the two expected colour topologies + self.assertEqual(set(sigs), {('FF', 'II'), ('FI', 'IF')}, + 'unexpected colour-flow topologies: %s' % sigs) + same = sigs[('FF', 'II')] / nevt # connections inside each group + cross = sigs[('FI', 'IF')] / nevt # connections crossing the groups + # (3) the asymmetry, and crucially WHICH topology dominates: swapping + # the two flow labels would invert this and fail here. + self.assertGreater(same, 0.9, + 'the initial-initial / final-final colour topology ' + 'should dominate u u~ > u u~ (measured ~0.98), got ' + '%.3f (cross=%.3f)' % (same, cross)) + self.assertTrue(0.002 < cross < 0.1, + 'the crossing colour topology should be present but ' + 'strongly suppressed (measured ~0.02), got %.4f' + % cross) diff --git a/tests/acceptance_tests/test_standalone_madevent_consistency.py b/tests/acceptance_tests/test_standalone_madevent_consistency.py index 0c9dd0198..66efca10c 100644 --- a/tests/acceptance_tests/test_standalone_madevent_consistency.py +++ b/tests/acceptance_tests/test_standalone_madevent_consistency.py @@ -181,6 +181,14 @@ def _call_with_optional_redirection(self, command, cwd): def _extract_standalone_flavors(self, output, subproc_dir): lines = output.splitlines() + # The standalone driver may append a crossing-symmetry demonstration + # (its own 'PDG ... / Matrix element = ...' lines for crossed + # processes). Those are not the primary per-flavor output this test + # compares against madevent, so stop at that section's header. + for cut, line in enumerate(lines): + if 'Crossing-symmetry example' in line: + lines = lines[:cut] + break standalone_rows = [] for index, line in enumerate(lines): stripped = line.strip() diff --git a/tests/unit_tests/core/test_diagram_generation.py b/tests/unit_tests/core/test_diagram_generation.py index 729c50ce3..8913d3b11 100755 --- a/tests/unit_tests/core/test_diagram_generation.py +++ b/tests/unit_tests/core/test_diagram_generation.py @@ -53,7 +53,8 @@ class AmplitudeTest(unittest.TestCase): def setUp(self): self.mydict = {'diagrams':self.mydiaglist, 'process':self.myprocess, - 'has_mirror_process': False} + 'has_mirror_process': False, + 'crossed_processes': []} self.myamplitude = diagram_generation.Amplitude(self.mydict) @@ -124,7 +125,8 @@ def test_representation(self): goal = "{\n" goal = goal + " \'process\': %s,\n" % repr(self.myprocess) goal = goal + " \'diagrams\': %s,\n" % repr(self.mydiaglist) - goal = goal + " \'has_mirror_process\': False\n}" + goal = goal + " \'has_mirror_process\': False,\n" + goal = goal + " \'crossed_processes\': []\n}" self.assertEqual(goal, str(self.myamplitude)) diff --git a/tests/unit_tests/iolibs/test_export_cpp.py b/tests/unit_tests/iolibs/test_export_cpp.py index d621b4dd5..6658239fe 100755 --- a/tests/unit_tests/iolibs/test_export_cpp.py +++ b/tests/unit_tests/iolibs/test_export_cpp.py @@ -920,7 +920,8 @@ def test_cpp_export_decay_chain_broken_symmetry_metadata(self): 'broken_sym_component_old_factors': ",".join(str(v) for v in sym_data['component_old_factors']), 'broken_sym_pid_list': ",".join(str(v) for v in sym_data['pid_list']), 'broken_sym_block_starts': ",".join(str(v) for v in sym_data['block_starts']), - 'broken_sym_block_lengths': ",".join(str(v) for v in sym_data['block_lengths']) + 'broken_sym_block_lengths': ",".join(str(v) for v in sym_data['block_lengths']), + 'ident_cross_function': '' } template_path = pjoin(MG5DIR, 'madgraph', 'iolibs', 'template_files', 'cpp_process_function_definitions.inc')