diff --git a/.github/workflows/acceptancetest.yml b/.github/workflows/acceptancetest.yml index 3c4fa29029..b3326d16fb 100644 --- a/.github/workflows/acceptancetest.yml +++ b/.github/workflows/acceptancetest.yml @@ -1389,3 +1389,69 @@ jobs: cd $GITHUB_WORKSPACE ./tests/test_manager.py test_density_mode_vs_standalone_LI1 -pA -t0 -l INFO + + # --------------------------------------------------------------------------- + # Crossing symmetry (tests/acceptance_tests/test_standalone_cross_symmetry.py) + # + # Split by toolchain and cost. NOTE: test_manager.py exits 1 when a test is + # SKIPPED (not only when it fails), and the crossing tests self-skip when + # gfortran / f2py / g++ are missing -- so the toolchain steps below are not + # optional, they are what keeps these jobs green. + # --------------------------------------------------------------------------- + acceptancetest_crossing_static: + # The parts that only inspect generated code / python objects: the crossing + # partition of a multi-flavor process, the canonical colour-flow code, and + # the outputs that cannot decode a crossing (they get the folded crossings + # expanded back instead). No compilation, so this is the fast smoke test. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: crossing partition / colour-flow code / unsupported outputs + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py -pA TestCrossingPartition TestColorFlowCode TestCrossingUnsupportedOutput -t0 -l INFO + + + acceptancetest_crossing_fortran: + # The fortran standalone reference suite: a crossed SMATRIX call must + # reproduce the process it crosses into (2->2 and 2->3, every merged flavor, + # split orders), the density matrix must follow the crossing, an s-channel + # constraint must disable the machinery, and the good-helicity sets must obey + # the crossing's row permutation. TestGoodHelCParityDedup rides along (same + # backend, ~10s): the C-parity de-duplication of the helicity sum must hold + # where it engages and refuse itself where the pairs do not match. + # Needs gfortran AND f2py (restore-pip-cache provides meson/ninja and sets + # f2py_compiler); the f2py-backed tests skip without it, which fails the job. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: crossing symmetry + C-parity good-helicity dedup, fortran standalone + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py -pA TestStandaloneCrossSymmetry TestGoodHelCParityDedup -t0 -l INFO + + + acceptancetest_crossing_cpp: + # The two C++ backends (standalone_cpp and standalone_mg7/madmatrix, incl. + # the per-event mixed-crossing SIMD page) plus the `check crossing` + # subcommand end to end over all three exporters. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: crossing symmetry, C++ / madmatrix backends and `check crossing` + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py -pA TestStandaloneCppCrossSymmetry TestStandaloneMg7CrossSymmetry TestCheckCrossingCommand -t0 -l INFO + diff --git a/.github/workflows/acceptancetest_madevent.yml b/.github/workflows/acceptancetest_madevent.yml index d173796936..918732c96a 100644 --- a/.github/workflows/acceptancetest_madevent.yml +++ b/.github/workflows/acceptancetest_madevent.yml @@ -996,3 +996,81 @@ jobs: run: | cd $GITHUB_WORKSPACE ./tests/test_manager.py test_madevent_mssm_gogo -pA -t0 -l INFO + + + # --------------------------------------------------------------------------- + # Crossing symmetry, madevent end-to-end. Each of these runs one or more full + # (small) madevent generations, hence one job apiece. + # --------------------------------------------------------------------------- + acceptancetest_crossing_madevent_labels: + # The event LABELS a crossed subprocess writes to the LHE: the W+ helicity of + # p p > w+ j (the crossed leg is a massive vector, so a bad relabel scrambles + # it) and the colour flow of u u~ > u u~ (98/2 asymmetric, so a swapped flow + # label is detectable). + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: crossed helicity and colour-flow labels written to the LHE + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py -pA TestMadeventCrossingHelicity TestMadeventColorFlowRatio -t0 -l INFO + + + acceptancetest_crossing_madevent_xsec: + # The cross-section-level guards on the crossing router: each process is + # integrated twice (crossing-routed vs --use_crossing=False, same seed) and + # the two results must agree. + # p p > w+ j, w+ > j j decay chain riding on a crossed production + # p p > t t~ j j inclusive, and the only test that integrates the + # cross-GROUP (Track B) router + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: crossing cross-section regressions (decay chain + inclusive) + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py -pA TestMadeventDecayChainCrossing TestMadeventInclusiveCrossingXsec -t0 -l INFO + + + acceptancetest_crossing_reweight: + # The folded-subprocess layout of the default (crossing on) standalone output, + # and the reweighting of a sample whose events sit in a grouped subprocess + # where every merged leg is an anti-particle (the -81 label regression). + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: folded subprocesses + merged anti-particle labels in reweight + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_standalone_crossing_folds_qqx_subprocess test_reweight_merged_antiparticle_labels -pA -t0 -l INFO + + + acceptancetest_density_multicore: + # do_reweight splits the event file across jobs in multicore mode; each job + # writes the average density matrix of its own chunk and the mother interface + # has to recombine them into the canonical Average_density_matrix_*.txt. + # Generates 3000 events (> nevt_job) so the file is really split, then + # reweights twice (1 core vs 2) -- the slowest of the new jobs. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + + - name: test one of the test test_density_mode_multicore + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_density_mode_multicore -pA -t0 -l INFO diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 7712ca4eda..8c79c2ba7d 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -733,3 +733,25 @@ jobs: run: | cd $GITHUB_WORKSPACE ./tests/test_manager.py test_generate_ewsud_ttbar test_generate_ewsud_ww test_generate_ewsud_zz -t0 + + + unittest_34: + # Unit tests added with the crossing-symmetry feature: + # tests/unit_tests/interface/test_reweight_density.py + # (TestAverageDensityMatrix: the average-density-matrix helpers shared by + # DensityInterface and CommonRunCmd.do_reweight's multicore recombination) + # tests/unit_tests/various/test_reweight_interface.py + # (TestPdgForMeCall: merged-particle labels, both signs, resolved to the + # event's concrete PDGs before the fortran call) + # Pure python, no toolchain: the whole job is a couple of seconds. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/restore-pip-cache + + - name: reweight density / merged-label unit tests + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py TestAverageDensityMatrix TestPdgForMeCall -t0 diff --git a/MadSpin/decay.py b/MadSpin/decay.py index ca9ab1edc0..93c1969e77 100755 --- a/MadSpin/decay.py +++ b/MadSpin/decay.py @@ -66,6 +66,30 @@ logger = logging.getLogger('decay.stdout') # -> stdout logger_stderr = logging.getLogger('decay.stderr') # ->stderr +import contextlib + +@contextlib.contextmanager +def _no_merge_crossing(): + """Temporarily disable crossing-symmetry folding (merge_crossing='record') + for the enclosed MG5 generation. + + MadSpin's legacy full_decay_chain (madspin_v1) and onshell_v1 paths evaluate + the production matrix element through a PDG-dispatch interface (the + standalone_msP fortran driver / smatrixhel) that cannot reach a crossing- + folded subprocess, so their generation must keep every subprocess on its own + (as before the crossing feature). The density path is crossing-aware and + keeps crossing on. MG_MERGE_CROSSING=off is the documented escape hatch + (see madgraph_interface.do_add).""" + saved = os.environ.get('MG_MERGE_CROSSING') + os.environ['MG_MERGE_CROSSING'] = 'off' + try: + yield + finally: + if saved is None: + os.environ.pop('MG_MERGE_CROSSING', None) + else: + os.environ['MG_MERGE_CROSSING'] = saved + import random import math from madgraph import MG5DIR, MadGraph5Error @@ -3097,11 +3121,12 @@ def generate_all_matrix_element(self): commandline = commandline.replace('add process', 'generate',1) logger.info(commandline) - - mgcmd.exec_cmd(commandline, precmd=True) + + with _no_merge_crossing(): + mgcmd.exec_cmd(commandline, precmd=True) commandline = 'output standalone_msP %s %s' % \ - (pjoin(path_me,'production_me'), ' '.join(list(self.list_branches.keys()))) - mgcmd.exec_cmd(commandline, precmd=True) + (pjoin(path_me,'production_me'), ' '.join(list(self.list_branches.keys()))) + mgcmd.exec_cmd(commandline, precmd=True) logger.info('Done %.4g' % (time.time()-start)) # 3. Create all_ME + topology objects ---------------------------------- @@ -3181,7 +3206,8 @@ def generate_all_matrix_element(self): commandline += self.get_proc_with_decay(proc, one_decay, mgcmd._curr_model, self.options) commandline = commandline.replace('add process', 'generate',1) logger.info(commandline) - mgcmd.exec_cmd(commandline, precmd=True) + with _no_merge_crossing(): + mgcmd.exec_cmd(commandline, precmd=True) # remove decay with 0 branching ratio. mgcmd.remove_pointless_decay(self.banner.param_card) commandline = 'output standalone_msF %s %s' % (pjoin(path_me,'full_me'), @@ -5123,7 +5149,17 @@ def generate_all_matrix_element(self): commandline = commandline.replace('add process', 'generate',1) mgcmd = self.mgcmd - mgcmd.exec_cmd(commandline, precmd=True) + # The legacy onshell_v1 path (mode=='onshell') evaluates the production + # ME through smatrixhel, which dispatches on concrete PDGs and returns 0 + # for a crossing-folded subprocess (-> production_me==0 -> ZeroDivision). + # Keep every subprocess on its own for it, exactly like full_decay_chain. + # The density path (mode=='density') is crossing-aware (GET_DENSITY_IDX + # via _resolve_crossed) and keeps crossing on. + if self.mode == 'onshell': + with _no_merge_crossing(): + mgcmd.exec_cmd(commandline, precmd=True) + else: + mgcmd.exec_cmd(commandline, precmd=True) # remove decay with 0 branching ratio. #mgcmd.remove_pointless_decay(self.banner.param_card) # @@ -5135,16 +5171,52 @@ def generate_all_matrix_element(self): # store information about matrix element for matrix_element in mgcmd._curr_matrix_elements.get_matrix_elements(): me_string = matrix_element.get('processes')[0].shell_string() - for me in matrix_element.get('processes'): - dirpath = pjoin(path_me, ms_me_subdir, 'SubProcesses', "P%s" % me_string) - # get the orignal order: - initial = [] - final = [l.get('id') for l in me.get_legs_with_decays()\ - if l.get('state') or initial.append(l.get('id'))] + + def register(leg_ids, _pdir="P%s" % me_string): + # ``leg_ids`` is the ordered list of external PDG ids (initial + # then final, in leg order). Build the (sorted) lookup tag and + # keep the natural leg order for momentum extraction. A base + # process always wins over a crossed one, so never overwrite. + initial = [i for i, l in zip(leg_ids, order_state) if not l] + final = [i for i, l in zip(leg_ids, order_state) if l] order = (tuple(initial), tuple(final)) - initial.sort(), final.sort() - tag = (tuple(initial), tuple(final)) - self.all_me[tag] = {'pdir': "P%s" % me_string, 'order': order} + tag = (tuple(sorted(initial)), tuple(sorted(final))) + self.all_me.setdefault(tag, {'pdir': _pdir, 'order': order}) + + for me in matrix_element.get('processes'): + legs = me.get_legs_with_decays() + order_state = [l.get('state') for l in legs] + register([l.get('id') for l in legs]) + + # Crossed subprocesses folded into this matrix element with crossing + # symmetry on (merge_crossing='record') were NOT generated as their + # own directory: only the representative partner is on disk. Its + # crossing-aware SMATRIX (smatrixhel dispatches on the signed PDGs, + # see interface_madspin.calculate_matrix_element) can still compute + # them, so register each crossed process against the SAME pdir. + # Without this the onshell/density lookup misses e.g. the recorded + # q q~ > t t~ g (partner of the kept q g > t t~ q) and raises + # KeyError. The decays never cross (they ride on their production + # leg), so re-attach the base decay chains before expanding, exactly + # as the exporter does for the check_sa crossing demo. + try: + crossed_processes = matrix_element.get('crossed_processes') + except Exception: + crossed_processes = [] + base_decays = matrix_element.get('processes')[0].get('decay_chains') + for cross_entry in crossed_processes: + proc = cross_entry[0] + if base_decays: + proc = copy.copy(proc) + proc.set('decay_chains', base_decays) + # empty LegList (same class as proc's legs) forces + # get_legs_with_decays to recompute with the re-attached decays + proc.set('legs_with_decays', proc.get('legs').__class__()) + legs = proc.get_legs_with_decays() + else: + legs = proc.get('legs') + order_state = [l.get('state') for l in legs] + register([l.get('id') for l in legs]) return self.all_me diff --git a/MadSpin/interface_madspin.py b/MadSpin/interface_madspin.py index 426251540f..b6bae2416e 100755 --- a/MadSpin/interface_madspin.py +++ b/MadSpin/interface_madspin.py @@ -2311,6 +2311,7 @@ def calculate_matrix_element_from_density(self, production, decays, decay_dict, for i, pdg in enumerate(all_pdg): pdg = tuple([x for x in pdg if x != 0]) self.pdg2prefix[pdg] = (str(all_prefix[i].decode()).strip(), i) + self._build_cross_resolve() if self.model_init: self.model_init = False @@ -2559,12 +2560,12 @@ def _decay_signature(dec_evt): #print(f"production = {production}") #print(f"decays = {decays}") if MEdenom_prod is None: - prod_diag = density_prod.trace().real - else: + prod_diag = density_prod.trace().real + else: prod_diag = MEdenom_prod prod_diag /= (iden_p * sym_factor_prod_ident) if MEdenom_decay is not None: - dec_diag *= MEdenom_decay + dec_diag *= MEdenom_decay return me, density_prod, prod_diag, dec_diag @@ -2603,20 +2604,32 @@ def get_allowed_hel(self, list_hels): def get_density(self, event, position, allow_hel, ncomb, dimension): orig_order = getattr(event, '_ms_orig_order_for_density', None) + cross_info = getattr(event, '_ms_cross_info', None) if orig_order is None: - _, orig_order, _, _ = self.get_pdir(event) + _, orig_order, prefix, pos = self.get_pdir(event) event._ms_orig_order_for_density = orig_order + # Crossed subprocess: remember (prefix, FLAV_IDX) so the density is + # evaluated through the crossing-aware GET_DENSITY_IDX rather than + # the base py_get_density (which dispatches on the base PDGs only). + if isinstance(pos, tuple) and pos and pos[0] == 'CROSS': + cross_info = (prefix, pos[1]) + else: + cross_info = False + event._ms_cross_info = cross_info # Fast path: single-point momentum extraction without permutation # construction. With apply_flavor_grouping, orig_order contains the # merged-particle PDGs (e.g. 81) while the event carries the raw # PDGs (e.g. 1, 2, 3): get_momenta must be told about the merge so - # that final.index(pdg) succeeds. + # that final.index(pdg) succeeds. A crossed orig_order is already in + # signed *physical* PDGs (from PY_GET_PDG_FOR_FLAVOR), so it matches the + # event PDGs directly and must NOT be merge-reverted. + merged_map = None if cross_info else (self._revert_merged or None) try: - p = event.get_momenta(orig_order, merged_map=self._revert_merged or None) + p = event.get_momenta(orig_order, merged_map=merged_map) except Exception: # Safety fallback for unusual event structures. - all_p = event.get_all_momenta(orig_order, merged_map=self._revert_merged or None) + all_p = event.get_all_momenta(orig_order, merged_map=merged_map) assert len(all_p) == 1, "Error: get_density can only be called for a single phase-space point" p = all_p[0] P = rwgt_interface.ReweightInterface.invert_momenta(p) @@ -2638,18 +2651,36 @@ def get_density(self, event, position, allow_hel, ncomb, dimension): raise ValueError("Error in get_density: 'position' must contain at least one position index") if len(allow_hel) % n_changing != 0: raise ValueError("Error in get_density: inconsistent 'allow_hel' and 'position' lengths") - # PY_GET_DENSITY(PDGS, PROCID, P, POS, ALLOW_HEL, ALPHAS, SCALE2) - density_array = self.f2py_module.py_get_density(pdgs=pdgs, - procid=-1, - p=P, - pos=position, - allow_hel=allow_hel, - alphas=event.aqcd, - scale2=event.scale**2) - #print(f"density_array = {density_array}") - density_matrix = madspin.DensityMatrix(density_array, - n_changing, - allow_hel, + if cross_info: + # Crossed subprocess: PY_GET_DENSITY_IDX(P, POS, N_CHANGING, + # ALLOW_HEL, N_COMB, FLAV_IDX, ALPHAS, SCALE2) -> INTER. Momenta P + # are already in the crossed leg order (orig_order == the order + # PY_GET_PDG_FOR_FLAVOR reports for this FLAV_IDX); the routine + # applies the crossing permutation/conjugation internally. + prefix, flav_idx = cross_info + density_array = getattr( + self.f2py_module, 'py_%sget_density_idx' % prefix.lower())( + p=P, + pos=position, + n_changing=n_changing, + allow_hel=allow_hel, + n_comb=ncomb, + flav_idx=flav_idx, + alphas=event.aqcd, + scale2=event.scale**2) + else: + # PY_GET_DENSITY(PDGS, PROCID, P, POS, ALLOW_HEL, ALPHAS, SCALE2) + density_array = self.f2py_module.py_get_density(pdgs=pdgs, + procid=-1, + p=P, + pos=position, + allow_hel=allow_hel, + alphas=event.aqcd, + scale2=event.scale**2) + #print(f"density_array = {density_array}") + density_matrix = madspin.DensityMatrix(density_array, + n_changing, + allow_hel, dimension) return density_matrix @@ -2727,7 +2758,15 @@ def get_iden(self, event): # END REMOVE # get_pdir returns (pdir, orig_order, prefix, pos) - _, _, _, pos = self.get_pdir(event) + _, _, prefix, pos = self.get_pdir(event) + if isinstance(pos, tuple) and pos and pos[0] == 'CROSS': + # Crossed subprocess: the static IDEN (get_idens) is the averaging + # denominator of the uncrossed representative only. GET_NHEL_IDX + # reports the denominator SMATRIX actually applies for this + # (crossed) FLAV_IDX -- e.g. colour 1/9 for q q~ vs 1/24 for q g. + iden_star, _nhel = getattr( + self.f2py_module, 'py_%sget_nhel_idx' % prefix.lower())(pos[1]) + return int(iden_star) idens = self.f2py_module.get_idens() #print(f"idens = {idens} , pos = {pos}") return idens[pos] @@ -2741,6 +2780,61 @@ def get_mymod(self,pdir,MODE): + def _build_cross_resolve(self): + """Physical-PDG resolver for CROSSED subprocesses folded in with crossing + symmetry (merge_crossing='record'). + + The base combined wrapper (pdg2prefix / py_get_density / get_idens) only + enumerates the generated representative processes, so a crossed event + (e.g. q q~ > t t~ g, kept only as its partner q g > t t~ q) is not found + there. The per-process crossing-aware f2py entry points + PY_GET_FLAVOR_LAYOUT / GET_PDG_FOR_FLAVOR / GET_NHEL_IDX let us + enumerate every (crossing, flavor) the representative can evaluate; we key + them by the sorted signed-physical-PDG multiset so get_pdir can resolve a + crossed event to (representative prefix, extended FLAV_IDX) and reach the + *_IDX routines (which apply the correct crossed averaging denominator).""" + mod = self.f2py_module + self.cross_resolve = {} + prefixes = set() + for b in mod.get_prefix(): + p = b.decode().strip() if isinstance(b, bytes) else str(b).strip() + if p: + prefixes.add(p) + for pfx in prefixes: + lp = pfx.lower() + layout = getattr(mod, 'py_%sget_flavor_layout' % lp, None) + get_pdg = getattr(mod, 'py_%sget_pdg_for_flavor' % lp, None) + if layout is None or get_pdg is None: + continue # non-crossing template: no extended-FLAV_IDX entries + nflav, nexternal, ncross = (int(x) for x in layout()) + for cross in range(ncross): + for flav in range(1, nflav + 1): + flav_idx = cross * nflav + flav + pdgs = tuple(int(x) for x in get_pdg(flav_idx)) + if not any(pdgs): + continue # index names no valid flavor/crossing + key = tuple(sorted(pdgs)) + self.cross_resolve.setdefault(key, []).append( + (pfx, flav_idx, pdgs)) + + def _resolve_crossed(self, event, pdir): + """Resolve a crossed production event to + (pdir, crossed_order, prefix, ('CROSS', flav_idx)) via cross_resolve, or + None when the event is not a recorded crossing. ``crossed_order`` is the + signed-physical leg order the momenta must be supplied in (the order + PY_GET_PDG_FOR_FLAVOR reports for this flav_idx).""" + if not getattr(self, 'cross_resolve', None): + return None + phys_tag, _ = event.get_tag_and_order(None) + ninit = len(phys_tag[0]) + key = tuple(sorted(list(phys_tag[0]) + list(phys_tag[1]))) + for (pfx, flav_idx, pdgs) in self.cross_resolve.get(key, []): + cand = (tuple(sorted(pdgs[:ninit])), tuple(sorted(pdgs[ninit:]))) + if cand == phys_tag: + crossed_order = (tuple(pdgs[:ninit]), tuple(pdgs[ninit:])) + return pdir, crossed_order, pfx, ('CROSS', flav_idx) + return None + def get_pdir(self,event): # Use the merged-PDG tag (same as calculate_matrix_element). MadSpin's # all_me is keyed by the merged-particle representation when @@ -2760,7 +2854,17 @@ def get_pdir(self,event): tag = (init, final) orig_order = self.all_me[tag]['order'] pdir = self.all_me[tag]['pdir'] - prefix, pos = self.pdg2prefix[tuple(list(orig_order[0]) + list(orig_order[1]))] + try: + prefix, pos = self.pdg2prefix[tuple(list(orig_order[0]) + list(orig_order[1]))] + except KeyError: + # The signature is a crossed subprocess that the base pdg2prefix does + # not enumerate (crossing symmetry folded it into a representative). + # Resolve it to the representative prefix + extended FLAV_IDX so the + # crossing-aware *_IDX f2py routines can evaluate it. + resolved = self._resolve_crossed(event, pdir) + if resolved is None: + raise + return resolved #misc.sprint(f"get_pdir: pdir = {pdir} , orig_order = {orig_order} , prefix = {prefix}") return pdir,orig_order, prefix, pos @@ -2781,6 +2885,22 @@ def calculate_matrix_element(self, event): tag = (init, final) orig_order = self.all_me[tag]['order'] pdir = self.all_me[tag]['pdir'] + # A crossing-folded subprocess (merge_crossing='record') is absent from + # the base pdg2prefix, so the combined smatrixhel dispatches on its + # concrete PDGs and returns 0. Resolve it to (representative prefix, + # extended FLAV_IDX) plus the physical crossed leg order, so a zero + # smatrixhel result can fall back to the crossing-aware + # PY_SMATRIX_IDX -- the same folded channel the density path + # reaches through GET_DENSITY_IDX. Base events also resolve here but + # keep their non-zero smatrixhel value, so the fallback never fires for + # them. Without this, MEdenom_prod (the reshuffle denominator in the + # density path) is 0 for a crossed production event -> divide-by-zero. + cross_resolved = self._resolve_crossed(event, pdir) + cross_info = None + if cross_resolved is not None: + _, cross_order, cross_prefix, cross_pos = cross_resolved + if isinstance(cross_pos, tuple) and cross_pos and cross_pos[0] == 'CROSS': + cross_info = (cross_prefix.lower(), cross_pos[1], cross_order) if pdir in self.all_f2py: all_p = event.get_all_momenta(orig_order, merged_map=self._revert_merged or None) if self.options['identical_particle_in_prod_and_decay'] == "crash" and\ @@ -2804,6 +2924,18 @@ def calculate_matrix_element(self, event): new_value = self.all_f2py[pdir](pdg_for_call, p_inv, 0.113, 0) else: new_value = self.all_f2py[pdir](pdg_for_call, p_inv, event.aqcd, event.scale, -1) + if new_value == 0 and cross_info: + # Folded crossed subprocess: evaluate through the + # crossing-aware SMATRIX_IDX. Momenta go in the physical + # crossed leg order (no merge revert), matching the order + # PY_GET_PDG_FOR_FLAVOR reports for this FLAV_IDX; the + # smatrixhel call above already applied the event's alphas + # (UPDATE_AS_PARAM), which SMATRIX_IDX reuses. + cross_prefix, flav_idx, cross_order = cross_info + cross_p = event.get_momenta(cross_order, merged_map=None) + cross_P = rwgt_interface.ReweightInterface.invert_momenta(cross_p) + new_value = float(getattr(self.f2py_module, + 'py_%ssmatrix_idx' % cross_prefix)(p=cross_P, flav_idx=flav_idx)) if self.options['identical_particle_in_prod_and_decay'] == "average": out += new_value else: @@ -2838,6 +2970,7 @@ def calculate_matrix_element(self, event): for i, pdg in enumerate(all_pdg): pdg = tuple([x for x in pdg if x != 0]) self.pdg2prefix[tuple(pdg)] = (str(all_prefix[i].decode()).strip(), i) + self._build_cross_resolve() if self.model_init: self.model_init = False diff --git a/Template/LO/SubProcesses/makefile b/Template/LO/SubProcesses/makefile index fbce3dfade..6122052f43 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 c9605e375e..90d934a277 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 c9ec6da6c6..1081d45558 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: @@ -1533,8 +1550,10 @@ def myHash(target_string): addon = '' else: name = short_name - if unknown_tag: + if unknown_tag and outgoing: addon += '%(propa)s' + elif unknown_tag: + addon += '%(tags)s' # if outgoing is not None: # return '_'.join((name,) + tuple(other_names)) + addon + '_%s' % outgoing @@ -2050,8 +2069,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 +2613,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/aloha/template_files/Makefile_F b/aloha/template_files/Makefile_F index 6858c11c5f..6af13888c5 100644 --- a/aloha/template_files/Makefile_F +++ b/aloha/template_files/Makefile_F @@ -36,5 +36,14 @@ shared: $(LIBDIR)$(LIBRARY_SHARED) clean: $(RM) *.o $(LIBDIR)$(LIBRARY) - + all: $(LIBDIR)$(LIBRARY) + +# aloha_functions.o provides the aloha_object F90 module (module ALOHA_OBJECT) +# that every ALOHA routine does "use aloha_object". Build it first so a parallel +# (-j) sub-make cannot compile a routine before aloha_object.mod exists. +# Order-only (|) so the routines are not recompiled when it merely rebuilds. +# NB: keep this AFTER the first (default-goal) target above -- a bare rule for +# $(ALOHARoutine) placed first would hijack the default goal and libdhelas would +# never be built. +$(ALOHARoutine): | $(BASIC_OBJS) diff --git a/madgraph/core/base_objects.py b/madgraph/core/base_objects.py index a43e58fad7..0fdb19709d 100755 --- a/madgraph/core/base_objects.py +++ b/madgraph/core/base_objects.py @@ -1108,12 +1108,18 @@ def check_flavor(self, map_flavor, model): """ pdgs = [p.get_pdg_code() for p in self.get('particles')] + # 'merged_particles' is keyed by the positive merged code only + # ({81: [1,2,3,4], 82: [11,13], ...}) while pdgs holds the *signed* code: + # a leg that is an antiparticle instance reports -82, not 82. Membership + # must therefore always be tested through abs() -- a bare `pdg in ...` + # silently misses an interaction whose merged legs are all antiparticles + # (e.g. the l+ pair of a lepton-number-violating H-- l+ l+ vertex). + positions = [i for i in range(len(pdgs)) if abs(pdgs[i]) in model.get('merged_particles')] flavor = [map_flavor[pdg].pop() if abs(pdg) in model.get('merged_particles') else 0 for pdg in pdgs] for coupling in self.get('couplings').values(): if isinstance(coupling, str): # if no PDG in merge range -> return True - if any([pdg in model['merged_particles'] for pdg in pdgs]): - positions = [i for i in range(len(pdgs)) if abs(pdgs[i]) in model['merged_particles']] + if positions: if len(positions) != 2: raise Exception elif flavor[positions[0]] == flavor[positions[1]]: diff --git a/madgraph/core/diagram_generation.py b/madgraph/core/diagram_generation.py index 1f239abf5b..fffacd77e7 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""" @@ -1377,24 +1388,55 @@ def default_setup(self): self['amplitudes'] = AmplitudeList() self['decay_chains'] = DecayChainAmplitudeList() + @staticmethod + def _decays_break_crossing(process_definition): + """True if any decay (recursively) pins a specific s-channel propagator. + + Crossing acts at the production level and lets the force-onshell decays + ride along, so a plain decay chain keeps crossing (see + export_v4.breaks_crossing_symmetry). But a decay that names a required or + forbidden s-channel does break it, and the production generator cannot + see that constraint (the core process it builds has the decays stripped + off). Detect it here so the production is recorded with merge_crossing off + in that case, keeping generation and the crossing-machinery emission + (which tests the full process) in agreement. + """ + for decay in process_definition.get('decay_chains'): + if decay.get('required_s_channels') or \ + decay.get('forbidden_s_channels') or \ + DecayChainAmplitude._decays_break_crossing(decay): + return True + return False + def __init__(self, argument = None, collect_mirror_procs = False, - ignore_six_quark_processes = False, loop_filter=None, diagram_filter=False): + ignore_six_quark_processes = False, loop_filter=None, + diagram_filter=False, merge_crossing=False): """Allow initialization with Process and with ProcessDefinition""" - + if isinstance(argument, base_objects.Process): super(DecayChainAmplitude, self).__init__() from madgraph.loop.loop_diagram_generation import LoopMultiProcess if argument['perturbation_couplings']: MultiProcessClass=LoopMultiProcess else: - MultiProcessClass=MultiProcess + MultiProcessClass=MultiProcess + # Record the production's crossings onto the base amplitude (so the + # decay-chain matrix element inherits them and the crossed + # subprocesses are not generated separately), UNLESS a decay pins an + # s-channel -- then the crossing machinery is not emitted downstream + # and the crossed subprocesses must stay fully generated. + prod_merge_crossing = merge_crossing + if isinstance(argument, base_objects.ProcessDefinition) and \ + self._decays_break_crossing(argument): + prod_merge_crossing = False if isinstance(argument, base_objects.ProcessDefinition): self['amplitudes'].extend(\ MultiProcessClass.generate_multi_amplitudes(argument, collect_mirror_procs, ignore_six_quark_processes, loop_filter=loop_filter, - diagram_filter=diagram_filter)) + diagram_filter=diagram_filter, + merge_crossing=prod_merge_crossing)) else: self['amplitudes'].append(\ MultiProcessClass.get_amplitude_from_proc(argument, @@ -1672,7 +1714,8 @@ def get(self, name): DecayChainAmplitude(process_def, self.get('collect_mirror_procs'), self.get('ignore_six_quark_processes'), - diagram_filter=self['diagram_filter'])) + diagram_filter=self['diagram_filter'], + merge_crossing=self['merge_crossing'])) else: self['amplitudes'].extend(\ self.generate_multi_amplitudes(process_def, @@ -1914,11 +1957,18 @@ def get_flavor(id, fsleg): continue # Check for successful crossings, unless we have specified - # properties that break crossing symmetry + # properties that break crossing symmetry. Crossing is a + # tree-level construction: a perturbative process (anything with + # the [...] syntax -- NLO, loop-induced, loop) must NOT be + # crossed, not even its tree-level Born/real sub-amplitudes, so + # its output stays byte-identical to a no-crossing build. (The + # 'loop_diagrams' guard below only catches an actual loop + # amplitude; the Born of an NLO process is an ordinary tree.) if not process.get('required_s_channels') and \ not process.get('forbidden_onsh_s_channels') and \ not process.get('forbidden_s_channels') and \ - not process.get('is_decay_chain') and not diagram_filter: + not process.get('is_decay_chain') and not diagram_filter and \ + not process.get('perturbation_couplings'): try: crossed_index = success_procs.index(sorted_legs) # The relabeling of legs for loop amplitudes is cumbersome @@ -1944,6 +1994,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 46d3b015e7..ec7b619723 100755 --- a/madgraph/core/helas_objects.py +++ b/madgraph/core/helas_objects.py @@ -871,6 +871,12 @@ def get(self, name): self['lcut_size'] = self.get_lcut_size() if name in ['spin', 'mass', 'width', 'self_antipart']: + # onshell_zero_width (a runtime annotation set post-generation on an + # internal propagator whose particle is also an external/on-shell + # state) forces a ZERO width there -- see + # HelasMatrixElement.set_onshell_particles_width_to_zero. + if name == 'width' and getattr(self, 'onshell_zero_width', False): + return 'ZERO' return self['particle'].get(name) elif name == 'pdg_code': return self['particle'].get_pdg_code() @@ -3984,6 +3990,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 +4034,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 +4044,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 +4070,10 @@ 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 'crossed_processes' in amplitude and \ + 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() @@ -5334,6 +5353,79 @@ def get_all_mass_widths(self): return set([(d.get('mass'),d.get('width')) for d in self.get_all_wavefunctions()]) + def set_onshell_particles_width_to_zero(self): + """Drop the width of any internal propagator whose particle is also an + external (initial/final) state of the process. + + An external particle is an on-shell asymptotic state, so treating an + internal propagator of the same field as an unstable resonance + (the i*M*Gamma in its denominator) is inconsistent: e.g. the s/u-channel + top in t a > t a. This mirrors the T-channel width drop but is keyed on + the particle being external rather than on the propagator momentum being + spacelike. It is applied by setting the propagator wavefunction's width + to ZERO, which every UFO backend reads for the propagator's W argument + (see HelasWavefunction.get_helas_call_dict); in the complex-mass scheme + the same ZERO makes that propagator use the real mass. Controlled by the + zerowidth_external option; returns True if any width was dropped.""" + # The asymptotic external states are the decay LEAVES: in a decay chain + # (d d~ > z z, z > e+ e-) the core final z is not asymptotic, it is a + # resonance decaying to e+ e-, so expand the decays (a no-op without + # them). Using the core legs would wrongly flag the resonance's field. + external_pdgs = set() + offshell_pdgs = set() + for proc in self.get('processes'): + legs = proc.get_legs_with_decays() \ + if hasattr(proc, 'get_legs_with_decays') else proc.get('legs') + for leg in legs: + external_pdgs.add(abs(leg.get('id'))) + # A leg tagged off-shell (the "particle*" syntax -> leg['offshell']) + # is deliberately NOT treated as an asymptotic on-shell state: it + # stands for an off-shell resonance whose Breit-Wigner width must be + # kept (e.g. p p > t* t~*). Exclude its field so no propagator of it + # has the width dropped. + for leg in proc.get('legs'): + if leg.get('offshell'): + offshell_pdgs.add(abs(leg.get('id'))) + external_pdgs -= offshell_pdgs + # A would-be Goldstone boson is eaten by -- and shares the mass of -- its + # gauge boson (G+ <-> W+, G0 <-> Z). In Feynman/FD gauge the Goldstone + # propagates explicitly, so if the gauge boson is an external on-shell + # state the Goldstone's internal propagator must drop its width too: + # otherwise the vector propagator carries ZERO width while its Goldstone + # keeps i*M*Gamma, an inconsistency that breaks the gauge-boson/Goldstone + # Ward identity. Add the Goldstone PDGs whose (shared) mass matches an + # external massive gauge boson. No-op in unitary gauge (no Goldstones) + # and for processes without an external vector boson. + model = self.get('processes')[0].get('model') if self.get('processes') \ + else None + if model is not None and external_pdgs: + ext_vector_masses = set() + for pdg in external_pdgs: + part = model.get_particle(pdg) + if part and part.get('spin') == 3 \ + and str(part.get('mass')).lower() != 'zero': + ext_vector_masses.add(part.get('mass')) + if ext_vector_masses: + for part in model.get('particles'): + if part.get('goldstone') \ + and part.get('mass') in ext_vector_masses: + external_pdgs.add(abs(part.get('pdg_code'))) + dropped = False + for wf in self.get_all_wavefunctions(): + # a wavefunction with no mothers is an external leg (no propagator, + # hence no width); only internal propagators carry the i*M*Gamma. + # A decay-chain resonance (onshell is True: produced on shell then + # decayed) MUST keep its Breit-Wigner width even if the same field + # also appears as an asymptotic external leg (e.g. one top decays + # while the other is final). + if wf.get('mothers') and wf.get('width') != 'ZERO' \ + and wf.get('onshell') is not True \ + and abs(wf.get_pdg_code()) in external_pdgs: + wf.onshell_zero_width = True + dropped = True + return dropped + + def get_coupling_for_flv(self, flv, model): """Return the coupling constant for a specific flavor""" diff --git a/madgraph/interface/amcatnlo_interface.py b/madgraph/interface/amcatnlo_interface.py index f2c6e2b8d8..9527a1cd99 100755 --- a/madgraph/interface/amcatnlo_interface.py +++ b/madgraph/interface/amcatnlo_interface.py @@ -65,6 +65,12 @@ logger = logging.getLogger('cmdprint') # -> stdout logger_stderr = logging.getLogger('fatalerror') # ->stderr +# Options baked into the matrix element at 'output' time (generation-time only, +# e.g. the T-channel width treatment): they can appear in the MG5 history but +# are rejected by the run interface's check_set (common_run_interface), so any +# replay of generation-time 'set' commands into a run interface must skip them. +NON_RUNTIME_SET_OPTIONS = ('zerowidth_tchannel',) + # a new function for the improved NLO generation glob_directories_map = [] def generate_directories_fks_async(i): @@ -1035,9 +1041,18 @@ def do_launch(self, line): else: ME = run_interface.aMCatNLOCmd(me_dir=argss[0],options=self.options) ME.pass_in_web_mode() - # transfer interactive configuration + # transfer interactive configuration. Generation-time-only options + # (e.g. zerowidth_tchannel, whose T-channel-width treatment is baked + # into the matrix element at 'output' time) appear in the MG5 history + # but are NOT valid run-time 'set' options -- replaying them would + # raise in the run interface's check_set. Skip them here; a genuine + # run-time 'set zerowidth_tchannel' typed at the run prompt still + # goes straight to the run interface and correctly crashes. config_line = [l for l in self.history if l.strip().startswith('set')] for line in config_line: + opt = line.split()[1] if len(line.split()) > 1 else '' + if opt in NON_RUNTIME_SET_OPTIONS: + continue ME.exec_cmd(line) stop = self.define_child_cmd_interface(ME) return stop diff --git a/madgraph/interface/common_run_interface.py b/madgraph/interface/common_run_interface.py index c8217cad3d..8162653539 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' % \ @@ -2395,6 +2403,14 @@ def check_multicore(self): for key, value in cross_sections.items(): cross_sections[key] = value / (nb_event+1) lhe.remove() + if reweight_mode == 'density': + # each job has written the average density matrix of its own + # chunk of events (and named the file after that chunk). Now + # that the chunks are recombined, re-compute the average over + # the full file --each event carries its own tag-- + # and clean up the per chunk files. + reweight_interface.combine_density_matrix(new_args[0], all_lhe, + reweight_card=pjoin(self.me_dir, 'Cards', 'reweight_card.dat')) for key in cross_sections: if key == 'orig' or (key.isdigit() and not (key[0] == '2')): continue @@ -6860,7 +6876,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 d7e1ef7fd0..7946719e97 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") @@ -935,7 +950,10 @@ def help_set(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("auto_convert_model ",'$MG:color:GREEN') + logger.info("zerowidth_external ",'$MG:color:GREEN') + logger.info(" > (default: True) [tree-level output] drop the width of an internal") + logger.info(" > propagator whose particle is also an external (initial/final) state") + logger.info("auto_convert_model ",'$MG:color:GREEN') logger.info(" > (default: False) If set on True any python2 UFO model will be automatically converted to pyton3 format") logger.info("nlo_mixed_expansion ",'$MG:color:GREEN') logger.info("deactivates mixed expansion support at NLO, goes back to MG5aMCv2 behavior") @@ -1088,7 +1106,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 +2320,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 +2418,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 +2428,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): @@ -3122,7 +3170,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', @@ -3141,6 +3190,14 @@ class MadGraphCmd(HelpToCmd, CheckValidForCmd, CompleteForCmd, CmdExtended): _export_formats = _v4_export_formats + ['standalone_cpp', 'aloha', 'matchbox_cpp', 'matchbox', 'mg7_v5', 'mg7', 'standalone_mg7'] + # Formats that CONSUME the recorded crossings (merge_crossing='record') + # instead of needing them expanded back into separate subprocesses: they fold + # each crossed subprocess into its base directory and reach it through the + # base's crossing-aware SMATRIX/sigmaKin at an extended flavor index. + # 'standalone_rw' is the reweight's own output: its python driver resolves a + # crossed event through the generated GET_PDG_FOR_FLAVOR entry points (see + # reweight_interface.ReweightInterface.build_cross_resolve). + _crossing_folding_formats = ('standalone', 'standalone_mg7', 'standalone_rw') _set_options = ['group_subprocesses', 'ignore_six_quark_processes', 'stdout_level', @@ -3154,6 +3211,7 @@ class MadGraphCmd(HelpToCmd, CheckValidForCmd, CompleteForCmd, CmdExtended): 'max_npoint_for_channel', 'max_t_for_channel', 'zerowidth_tchannel', + 'zerowidth_external', 'default_unset_couplings', 'nlo_mixed_expansion' ] @@ -3237,6 +3295,7 @@ class MadGraphCmd(HelpToCmd, CheckValidForCmd, CompleteForCmd, CmdExtended): 'default_unset_couplings': 99, # 99 means infinity 'max_t_for_channel': 99, # means no restrictions 'zerowidth_tchannel': True, + 'zerowidth_external': True, 'nlo_mixed_expansion':True, 'apply_flavor_grouping': True } @@ -3256,6 +3315,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 @@ -3378,20 +3439,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'): @@ -4315,6 +4417,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"+ @@ -4568,6 +4700,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 "+ @@ -5011,6 +5161,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 @@ -9132,19 +9284,46 @@ 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 help_set2_zerowidth_external(self): + logger.info("zerowidth_external ",'$MG:color:GREEN') + logger.info(" > (default: True) [generation/output-time option for tree-level output]") + logger.info(" > drop the width in the propagator denominator of any internal propagator") + logger.info(" > whose particle also appears as an external (initial/final) state -- an") + logger.info(" > external particle is an on-shell asymptotic state, so its internal") + logger.info(" > propagator (e.g. the s/u-channel top in t a > t a) must not carry the") + logger.info(" > i*M*Gamma resonance term. In the complex-mass scheme the real mass is") + logger.info(" > used there too. External legs themselves have no width argument.") + + def set2_zerowidth_external(self, args, log=True): + """Set whether the width should be dropped for internal propagators whose + particle is also an external state. Default True. Applied at output time + per matrix element (HelasMatrixElement.set_onshell_particles_width_to_zero), + so it is a code-generation option like zerowidth_tchannel. + Example: set zerowidth_external False + """ + args = ['zerowidth_external'] + args + self.check_set(args) + self.options[args[0]] = banner_module.ConfigFile.format_variable(args[1], bool, 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 @@ -9797,28 +9976,142 @@ def do_output(self, line): # Reset _export_dir, so we don't overwrite by mistake later self._export_dir = None + def _crossing_needs_expansion(self, amps): + """True if `amps` carry folded crossings the current output cannot read. + + Only the folding-capable standalone backends consume the recorded + crossings directly (they reach them through the base's crossing-aware + SMATRIX/sigmaKin). Every other output needs them back as explicit + subprocesses, and expanding is always safe: it just reproduces the + complete unmerged (--use_crossing=False) output. + """ + if self._export_format in self._crossing_folding_formats: + return False + return any(amp.get('crossed_processes') for amp in amps + if 'crossed_processes' in amp) + + def _expand_recorded_crossings(self, amps): + """Expand each amplitude's recorded crossings back into separate + (mirror-folded) amplitudes, reproducing a merge_crossing=False + generation. The crossed diagrams are reused (cross_amplitude), not + regenerated. 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); the beam-swap is + folded back into has_mirror_process here, exactly as + generate_matrix_elements would. + + Shared by the grouped and the ungrouped paths so that an output which + cannot read folded crossings gets them expanded automatically, without + the user having to pass --use_crossing=False. + """ + 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')) + + originals = [(amp, amp.get('crossed_processes') + if 'crossed_processes' in amp else []) + for amp in amps] + expanded = diagram_generation.AmplitudeList() + seen = {} # fast_proc -> amplitude, for mirror fold + for amp, _crossed in originals: + amp.set('crossed_processes', []) + expanded.append(amp) + seen[_fastproc(amp)] = amp + 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 + return expanded + + def _expand_crossings_for_ungrouped_output(self): + """Put folded crossings back for an output that cannot read them. + + Counterpart of the grouped path's expansion, for the ungrouped one. A + plain amplitude carries its crossings in `crossed_processes` and is + expanded in place; a decay chain records them on its inner amplitudes + instead, and its grouping does not survive a partial expansion, so the + affected chains are regenerated whole with merge_crossing=False (the + base diagrams are still reused by cross_amplitude). Either way the + result is exactly the complete unmerged output. + """ + dc_amps = [amp for amp in self._curr_amps + if isinstance(amp, diagram_generation.DecayChainAmplitude)] + non_dc_amps = diagram_generation.AmplitudeList( + [amp for amp in self._curr_amps + if not isinstance(amp, diagram_generation.DecayChainAmplitude)]) + + dc_crossed = self._export_format not in \ + self._crossing_folding_formats and \ + any(a.get('crossed_processes') + for dc in dc_amps for a in dc.get('amplitudes') + if 'crossed_processes' in a) + expand_non_dc = self._crossing_needs_expansion(non_dc_amps) + if not dc_crossed and not expand_non_dc: + return + + if expand_non_dc: + non_dc_amps = self._expand_recorded_crossings(non_dc_amps) + + if dc_crossed: + ign6 = self.options.get('ignore_six_quark_processes', []) or [] + if self.options['group_subprocesses'] == 'Auto': + collect_mirror = True + else: + collect_mirror = self.options['group_subprocesses'] + dc_amps = [diagram_generation.DecayChainAmplitude( + procdef, collect_mirror, ign6, merge_crossing=False) + for procdef in self._curr_proc_defs + if procdef.get('decay_chains')] + + new_amps = diagram_generation.AmplitudeList() + new_amps.extend(non_dc_amps) + new_amps.extend(dc_amps) + new_amps.sort(key=lambda x: x.get_number_of_diagrams(), reverse=True) + self._curr_amps = new_amps + # Export a matrix element - def export(self, nojpeg = False, main_file_name = "", group_processes=True, + def export(self, nojpeg = False, main_file_name = "", group_processes=True, args=[]): """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') @@ -9883,6 +10176,79 @@ 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 \ + self._crossing_folding_formats: + # DecayAmplitude / DecayChainAmplitude are Amplitude + # subclasses that override default_setup with their own + # key set and do NOT carry crossed_processes (e.g. the + # compute_widths and MadSpin decay paths reach here), so + # guard on the dict key rather than the amplitude type + # (_crossing_needs_expansion does that). + if self._crossing_needs_expansion(non_dc_amps): + non_dc_amps = \ + self._expand_recorded_crossings(non_dc_amps) + + # Decay chains: the crossing dedup (folding the crossed + # decay-chain subprocesses into the base's crossing-aware + # SMATRIX) is implemented for the standalone backends only. + # For the summation backends each crossed decay-chain + # subprocess must stay its own integration unit; rather + # than reconstruct-and-route them (whose grouping does not + # reproduce the historical layout), regenerate the affected + # decay chains fully (merge_crossing=False), giving exactly + # the pre-dedup output. cross_amplitude reuse still avoids + # regenerating the diagrams of the base subprocess. + if any(a.get('crossed_processes') + for dc in dc_amps for a in dc.get('amplitudes') + if 'crossed_processes' in a): + ign6 = self.options.get( + 'ignore_six_quark_processes', []) or [] + if self.options['group_subprocesses'] == 'Auto': + collect_mirror = True + else: + collect_mirror = \ + self.options['group_subprocesses'] + regenerated = \ + diagram_generation.DecayChainAmplitudeList() + for procdef in self._curr_proc_defs: + if not procdef.get('decay_chains'): + continue + regenerated.append( + diagram_generation.DecayChainAmplitude( + procdef, collect_mirror, ign6, + merge_crossing=False)) + # Regenerating walks _curr_proc_defs, so the + # diagram-count ordering _curr_amps was sorted into + # above is lost -- and that ordering decides the + # subprocess group numbering (P1_/P2_...). Restore it + # so the output is named exactly as an uncrossed + # generation would name it. + regenerated.sort( + key=lambda x: x.get_number_of_diagrams(), + reverse=True) + dc_amps = regenerated + if non_dc_amps: subproc_groups.extend(\ group_subprocs.SubProcessGroup.group_amplitudes(\ @@ -9919,8 +10285,15 @@ def generate_matrix_elements(self, group_processes=True): if uid == 0 and last_error: raise last_error else: # Not grouped subprocesses + # Same automatic expansion as the grouped path above: an + # ungrouped output (e.g. the ungrouped madevent) cannot read + # the folded crossings, so put them back as explicit + # subprocesses instead of forcing the user to regenerate with + # --use_crossing=False. Without this the crossings would be + # silently missing from the output. + self._expand_crossings_for_ungrouped_output() mode = {} - if self._export_format in [ 'standalone_msP' , + if self._export_format in [ 'standalone_msP' , 'standalone_msF', 'standalone_rw']: mode['mode'] = 'MadSpin' # The conditional statement tests whether we are dealing @@ -9970,6 +10343,24 @@ def generate_matrix_elements(self, group_processes=True): ndiags, cpu_time = generate_matrix_elements(self,group_processes) + # zerowidth_external: an external (initial/final) particle is an on-shell + # asymptotic state, so an internal propagator of the same field must not + # carry the i*M*Gamma resonance term (e.g. the s/u-channel top in + # t a > t a). Drop that width per matrix element before any backend + # writes the propagator calls (all UFO backends read the wavefunction + # width). Tree-level only; complex-mass scheme then uses the real mass. + if self.options.get('zerowidth_external', True) and \ + self._curr_matrix_elements.get_matrix_elements(): + n_dropped = 0 + for me in self._curr_matrix_elements.get_matrix_elements(): + if me.set_onshell_particles_width_to_zero(): + n_dropped += 1 + if n_dropped: + logger.info("Some on-shell (external) particle widths have been " + "set to zero in their internal propagators [new]\n if " + "you want to keep them set \"zerowidth_external\" to " + "False", '$MG:BOLD') + calls = 0 @@ -10494,7 +10885,7 @@ def do_compute_widths(self, line, model=None, do2body=True, decaymodel=None): logger_mg.info('More info in temporary files:\n %s/index.html' % (decay_dir)) with misc.MuteLogger(['madgraph','ALOHA','cmdprint','madevent'], [40,40,40,40]): self.exec_cmd('output madevent %s -f' % decay_dir,child=False) - + #modify some parameter of the default run_card run_card = banner_module.RunCard(pjoin(decay_dir,'Cards','run_card.dat')) if run_card['ickkw']: diff --git a/madgraph/interface/master_interface.py b/madgraph/interface/master_interface.py index b18de52528..20191b5e10 100755 --- a/madgraph/interface/master_interface.py +++ b/madgraph/interface/master_interface.py @@ -629,6 +629,9 @@ def help_set2_output_dependencies(self, *args, **opts): def help_set2_zerowidth_tchannel(self, *args, **opts): return self.cmd.help_set2_zerowidth_tchannel(self, *args, **opts) + + def help_set2_zerowidth_external(self, *args, **opts): + return self.cmd.help_set2_zerowidth_external(self, *args, **opts) def help_tutorial(self, *args, **opts): diff --git a/madgraph/interface/reweight_interface.py b/madgraph/interface/reweight_interface.py index a2ddef2c4f..75d484ac50 100755 --- a/madgraph/interface/reweight_interface.py +++ b/madgraph/interface/reweight_interface.py @@ -118,7 +118,12 @@ def __init__(self, event_path=None, allow_madspin=False, mother=None, *completek self.use_eventid = False self.inc_sudakov = False self.event_path = event_path - self.path2prefix = {} # store the f2pyprefix associated to a library + self.path2prefix = {} # store the f2pyprefix associated to a library + # id_to_path-style tag -> folded crossed subprocesses reachable through + # a base matrix element (see build_cross_resolve); empty when crossing + # folded nothing. + self.cross_resolve = {} + self.cross_resolve_second = {} if event_path: logger.info("Extracting the banner ...") self.do_import(event_path, allow_madspin=allow_madspin) @@ -1596,6 +1601,28 @@ def _get_revert_merged_for(self, model): rm[val] = key return rm + def _pdg_for_me_call(self, event, orig_order, momenta, relevant_model): + """Return the PDG list to hand to the fortran for one permutation. + + orig_order holds the leg ids of the generated process, which under + flavor grouping are the merged codes (81 for jets, 82 for charged + leptons, ...). Those are SIGNED: a subprocess whose grouped legs are + anti-particles -- g q~ > w+ q~, whose get_pdg_order is + [21,-81,24,-81] -- carries the negative code. model['merged_particles'] + is keyed by the POSITIVE code only ({81: [1,2,3,4], ...}), hence the + abs(): without it the merged labels are handed to the fortran as-is, + the flavor mapping there resolves them to "no flavour", and both + SMATRIXHEL and GET_DENSITY return an exact zero. + + Kept in one place because both calculate_matrix_element implementations + (matrix element and density matrix) need exactly this.""" + pdg = list(orig_order[0]) + list(orig_order[1]) + merged = relevant_model.get('merged_particles') if relevant_model \ + else self.merged_particles + if merged and any(abs(p) in merged for p in pdg): + return event.get_pdg(momenta) + return pdg + def calculate_matrix_element(self, event, hypp_id, scale2=0): """routine to return the matrix element""" @@ -1635,19 +1662,33 @@ def calculate_matrix_element(self, event, hypp_id, scale2=0): #else: # base = "rw_me" + # A crossed subprocess folded in by crossing symmetry has no id_to_path + # entry of its own; it is reached through the base's crossing-aware + # SMATRIX at an extended flavor index (see build_cross_resolve). Stays + # None for every ordinary lookup. + flav_idx = procindex = None if (not self.second_model and not self.second_process and not self.dedicated_path) or hypp_id==0: if tag in self.id_to_path: orig_order, Pdir, hel_dict = self.id_to_path[tag] else: cross_tag = self.get_crossing_tag(tag) - orig_order, Pdir, hel_dict = self.id_to_path[cross_tag] + folded = None if cross_tag else self.resolve_folded_crossing( + tag, tag_orig, self.cross_resolve) + if folded: + orig_order, Pdir, hel_dict, procindex, flav_idx = folded + else: + orig_order, Pdir, hel_dict = self.id_to_path[cross_tag] else: try: orig_order, Pdir, hel_dict = self.id_to_path_second[tag] except KeyError: cross_tag = self.get_crossing_tag(tag) + folded = None if cross_tag else self.resolve_folded_crossing( + tag, tag_orig, self.cross_resolve_second) if cross_tag: orig_order, Pdir, hel_dict = self.id_to_path[cross_tag] + elif folded: + orig_order, Pdir, hel_dict, procindex, flav_idx = folded elif self.options['allow_missing_finalstate']: return 0.0 else: @@ -1679,10 +1720,7 @@ def calculate_matrix_element(self, event, hypp_id, scale2=0): else: nhel = -1 - pdg = list(orig_order[0])+list(orig_order[1]) - relevant_merged = relevant_model.get('merged_particles') if relevant_model else self.merged_particles - if relevant_merged and any(p in relevant_merged for p in pdg): - pdg = event.get_pdg(all_p[0]) + pdg = self._pdg_for_me_call(event, orig_order, all_p[0], relevant_model) #boosting the event all_p = self.method_boost_event(event, all_p, orig_order, hypp_id) @@ -1712,7 +1750,14 @@ def calculate_matrix_element(self, event, hypp_id, scale2=0): with misc.chdir(Pdir): with misc.stdchannel_redirected(sys.stdout, os.devnull): #misc.sprint(pdg, pid, p, event.aqcd, scale2, nhel) - new_value = module.smatrixhel(pdg, pid, p, event.aqcd, scale2, nhel) + if flav_idx is None: + new_value = module.smatrixhel(pdg, pid, p, event.aqcd, scale2, nhel) + else: + # folded crossing: name the matrix element by its slot + # and the process by the extended flavor index, the PDG + # dispatch cannot reach it. NPDG is f2py-derived from p. + new_value = module.smatrixhel_idx(procindex, flav_idx, p, + event.aqcd, scale2, nhel) #misc.sprint(new_value) if new_value == 0: raise Exception("Invalid matrix element") @@ -1741,14 +1786,25 @@ def get_crossing_tag(self,tag): """find if using crossing symmetry allow to find the correct tag and return the assoicated tag""" # get list of possible crossing tag - crossing_tag = [tuple([int(x) for x in sorted(list(t[0])+list(t[1]))]) for t in self.id_to_path.keys()] + # id_to_path is not uniformly keyed: the NLO path also stores the + # virtual matrix element under ((initial, final), 'V'), so t[1] can be a + # string rather than a list of PDGs. Only a plain (initial, final) pair + # can carry a crossing, so skip anything else instead of trying to sort + # a string against a tuple. + crossing_tag = [] + for t in self.id_to_path.keys(): + try: + crossing_tag.append( + tuple([int(x) for x in sorted(list(t[0]) + list(t[1]))])) + except (TypeError, ValueError): + continue mytag = list(tag[0])+list(tag[1]) if self.revert_merged: for i in range(len(mytag)): if mytag[i] in self.revert_merged: mytag[i] = self.revert_merged[mytag[i]] - if -mytag[i] in self.revert_merged: + elif -mytag[i] in self.revert_merged: mytag[i] = -self.revert_merged[-mytag[i]] mytag.sort() mytag=tuple(mytag) @@ -1893,10 +1949,30 @@ def create_standalone_tree_directory(self, data ,second=False): else: logger.info('generating the square matrix element for reweighting (second model and/or processes)') start = time.time() + # The reweight matches each event's flavor to a subprocess matrix + # element (id_to_path), and reaches a FOLDED crossed subprocess through + # the base's crossing-aware SMATRIX (see build_cross_resolve), so the + # crossings stay folded: the crossed subprocesses cost neither a + # generation nor a compilation. + # + # Two modes still want the crossed subprocesses back as separate entries: + # - 'keep_ordering' promises that the events are written in the matrix + # element's own leg order, which makes the id_to_path key + # order-sensitive; a folded crossing has no directory and hence no + # such order to promise, so a crossed event would miss the lookup. + # - the density mode evaluates GET_DENSITY, not SMATRIX, and only its + # FLAVOR-array entry point is wired up here; the FLAVOR array cannot + # express a crossing (GET_DENSITY_IDX would be needed, as MadSpin's + # density path does it). + # Perturbative (NLO / ewsudakov [...]) definitions are left untouched: + # they already skip crossing at generation, and the flag must not land + # inside their option-laden line. + xflag = ' --use_crossing=False' \ + if (self.keep_ordering or self.flag_density_matrix) else '' commandline='' for i,proc in enumerate(data['processes']): if '[' not in proc: - commandline += "add process %s ;" % proc + commandline += "add process %s%s ;" % (proc, xflag) else: has_nlo = True if self.banner.get('run_card','ickkw') == 3: @@ -1907,15 +1983,6 @@ 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 - # 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. - 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', 'generate',1) logger.info(commandline) try: @@ -2158,7 +2225,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 ------------------------------------------------ @@ -2382,6 +2449,8 @@ def load_module(self, metag=1): self.id_to_path = {} self.id_to_path_second = {} + self.cross_resolve = {} + self.cross_resolve_second = {} rwgt_dir_possibility = ['rw_me','rw_me_%s' % self.nb_library,'rw_mevirt','rw_mevirt_%s' % self.nb_library] fprefix = '' for onedir in rwgt_dir_possibility: @@ -2448,8 +2517,10 @@ def load_module(self, metag=1): data = self.id_to_path + cross_data = self.cross_resolve if onedir not in ["rw_me", "rw_mevirt"]: data = self.id_to_path_second + cross_data = self.cross_resolve_second # get all the information @@ -2521,8 +2592,218 @@ def load_module(self, metag=1): misc.sprint(order, pdir,) raise Exception( "two different matrix-element have the same initial/final state. Leading to an ambiguity. If your events are ALWAYS written in the correct-order (look at the numbering in the Feynman Diagram). Then you can add inside your reweight_card the line 'change keep_ordering True'." ) data[tag] = order, pdir, hel - - + + # The merged-particle convention of the model that built `data`: + # get_pdg_order (hence every id_to_path key) may speak merged codes, + # and the crossed subprocesses must be keyed the same way. + if onedir in ("rw_me", "rw_mevirt"): + cross_model = getattr(self, 'original_model', None) or self.model + else: + cross_model = self.model + if cross_model is not None: + merged_map = self._get_revert_merged_for(cross_model) + else: + # restored from a pickle without a model loaded: the saved map + merged_map = getattr(self, 'revert_merged', None) + self.build_cross_resolve(mymod, all_prefix, all_pdgs, hel_dict, + pdir, 'virt' in onedir, cross_data, + merged_map) + + def build_cross_resolve(self, mymod, all_prefix, all_pdgs, hel_dict, pdir, + is_virt, cross_data, merged_map): + """Add to `cross_data` every CROSSED subprocess folded into this + module's matrix elements, keyed exactly like id_to_path. + + With crossing on (merge_crossing='record') a crossed subprocess is not + generated as a directory of its own: the base's crossing-aware SMATRIX + evaluates it at an *extended* flavor index (FLAV_IDX = cross*NFLAV+flav), + so it has no get_pdg_order entry and id_to_path cannot see it -- a + crossed event would silently lose its weight. The per-process f2py entry + points PY_GET_FLAVOR_LAYOUT / GET_PDG_FOR_FLAVOR let us walk that + index space and ask each entry which process it evaluates, restricted to + the crossings the generation actually recorded (crossed_flavors.dat -- + the runtime space also holds crossings that are merely applicable, e.g. a + Z pulled into the initial state, and evaluating one of those for an event + would produce a wrong weight rather than no weight). + + A matrix element covers several subprocesses in two independent ways, and + the crossing has to be applied to each: as FLAVOR indices inside one + get_pdg_order entry (flavor grouping: 81 for jets), and as several + get_pdg_order entries sharing one prefix (the exporter combining + processes with an identical matrix element, e.g. g u > h u and g s > h s). + So each recorded crossing is applied to EVERY base entry of the prefix, + by permuting and conjugating its PDGs the way GET_PDG_FOR_FLAVOR did for + the representative -- which is what makes the tags come out in the same + vocabulary the base entries use. + + Each entry maps an id_to_path-style tag to a LIST of candidates + ``(order, pdir, hel, procindex, flav_idx, pdgs)``, one per flavor of the + tag's merged matrix element: the matrix element is NOT flavor blind + across those -- g d > z d and g u > z u differ by ~25% -- so the flavor is + picked per event from the signed PDGs (see resolve_folded_crossing). + `procindex` is the 1-based get_prefix slot the crossing-aware + SMATRIXHEL_IDX dispatch expects. + + The helicity dictionary is the base one, unchanged: SMATRIX applies the + crossing to its whole NHEL table before the helicity loop, which makes + the helicity configuration selected by row r the base row r read + positionally in the crossed leg order (verified against independently + generated crossed subprocesses, per helicity).""" + codes = self.get_recorded_crossings(pdir) + if not codes: + return + import madgraph.iolibs.export_v4 as export_v4 + get_perm = export_v4.ProcessExporterFortran.get_crossing_permutation + # merged codes (81, ...) as they appear in a base entry, i.e. the legs + # whose flavor a base entry leaves open and the flavor index resolves. + labels = set(merged_map.values()) if merged_map else set() + slots = {} + for i, (prefix, pdgs) in enumerate(zip(all_prefix, all_pdgs), 1): + slots.setdefault(prefix, []).append((i, [int(x) for x in pdgs])) + for prefix, entries in slots.items(): + if not codes.get(prefix): + continue + layout = getattr(mymod, 'py_%sget_flavor_layout' % prefix, None) + get_pdg = getattr(mymod, 'py_%sget_pdg_for_flavor' % prefix, None) + if layout is None or get_pdg is None: + # matrix element written without the crossing machinery + continue + nflav, nexternal, ncross = (int(x) for x in layout()) + entries = [e for e in entries if len(e[1]) == nexternal] + for cross in codes[prefix]: + if not 0 < cross < ncross: + continue # 0 is the base, already in id_to_path + perm, ic, valid = get_perm(cross, nexternal) + if not valid: + continue + for flav in range(1, nflav+1): + crossed = [int(x) for x in get_pdg(cross*nflav + flav)] + if not any(crossed): + continue # names no valid flavor/crossing + base = [int(x) for x in get_pdg(flav)] + # Which legs the crossing conjugated: those it moved between + # the initial and the final state, except a self-conjugate + # one (a gluon crossed to the other side is still a gluon). + # Read off the representative rather than from the model, so + # that this needs nothing but the generated entry points. + conj = [ic[k] == -1 and crossed[k] != base[perm[k]] + for k in range(nexternal)] + for (procindex, pdgs) in entries: + xpdgs = [-pdgs[perm[k]] if conj[k] else pdgs[perm[k]] + for k in range(nexternal)] + # The physical process this candidate evaluates: the + # crossed base entry, with the legs it leaves merged + # resolved by the flavor index. + phys = [crossed[k] if abs(xpdgs[k]) in labels + else xpdgs[k] for k in range(nexternal)] + tag, order = self.tag_from_pdgs(xpdgs) + if is_virt: + tag = (tag, 'V') + cross_data.setdefault(tag, []).append( + (order, pdir, hel_dict.get(prefix, {}), + procindex, cross*nflav + flav, phys)) + + def tag_from_pdgs(self, pdgs): + """(tag, order) of a subprocess given its per-leg PDG codes, in the same + convention load_module uses to key id_to_path.""" + if self.is_decay: + incoming, outgoing = [pdgs[0]], list(pdgs[1:]) + else: + incoming, outgoing = list(pdgs[0:2]), list(pdgs[2:]) + order = (list(incoming), list(outgoing)) + incoming.sort() + if not self.keep_ordering: + outgoing.sort() + return (tuple(incoming), tuple(outgoing)), order + + def get_recorded_crossings(self, pdir): + """{prefix: [cross codes]} of the crossed subprocesses folded into the + matrix elements of `pdir`, from the crossed_flavors.dat written at output + time (see export_v4.write_crossing_records). + + An absent file means an output produced before crossings were recorded, + hence one with nothing folded; an empty list for a prefix means that + matrix element folds no crossing.""" + path = pjoin(pdir, 'crossed_flavors.dat') + if not os.path.exists(path): + return {} + codes = {} + for line in open(path): + line = line.split('#', 1)[0].split() + if not line: + continue + prefix, complete = line[0].lower(), line[1] == '1' + codes[prefix] = [int(c) for c in line[2:]] + if not complete: + logger.warning('Crossing symmetry folded a subprocess into the ' + 'matrix element %s that could not be resolved ' + 'back to a flavor. An event of that flavor will ' + 'stop the reweighting rather than be given a ' + 'wrong weight; if that happens, rerun with ' + '"change keep_ordering True" (which keeps the ' + 'crossed subprocesses separate) and report it.', + prefix) + return codes + + def resolve_folded_crossing(self, tag, phys_tag, cross_data): + """(order, Pdir, hel, procindex, flav_idx) of the folded crossed + subprocess matching an event, or None. + + `tag` is the (merged) id_to_path key the event was looked up with and + `phys_tag` its signed *physical* PDG twin. All candidates under one tag + share the merged flavor pattern, so the physical PDGs pick which flavor + of the merged matrix element the event actually is -- the matrix element + is not flavor blind. A candidate leg that the flavor index does not + resolve keeps its merged label and matches any member of the group.""" + candidates = cross_data.get(tag) if cross_data else None + if not candidates: + return None + merged = self.revert_merged_groups() + ninitial = 1 if self.is_decay else 2 + for (order, Pdir, hel, procindex, flav_idx, pdgs) in candidates: + if self.pdgs_match_event(pdgs, ninitial, phys_tag, merged): + return order, Pdir, hel, procindex, flav_idx + return None + + def revert_merged_groups(self): + """{merged code: [member pdgs]} of the model the current lookup uses + (empty without flavor grouping).""" + if not self.revert_merged: + return {} + groups = {} + for pdg, code in self.revert_merged.items(): + groups.setdefault(code, []).append(pdg) + return groups + + @staticmethod + def pdgs_match_event(pdgs, ninitial, phys_tag, merged): + """Can `pdgs` (a subprocess' per-leg codes, possibly carrying merged + labels) be the event whose physical tag is `phys_tag`? Compared as + multisets per side, a merged label absorbing any one member flavor of the + same sign. Merged groups are disjoint, so the greedy assignment below is + exact.""" + sides = ((pdgs[:ninitial], phys_tag[0]), (pdgs[ninitial:], phys_tag[1])) + for legs, want in sides: + left = list(want) + labels = [] + for pdg in legs: + if abs(pdg) in merged: + labels.append(pdg) + elif pdg in left: + left.remove(pdg) + else: + return False + for pdg in labels: + hit = next((q for q in left if (q > 0) == (pdg > 0) + and abs(q) in merged[abs(pdg)]), None) + if hit is None: + return False + left.remove(hit) + if left: + return False + return True + + def load_model(self, name, use_mg_default, complex_mass=False, ew_scheme=None): """load the model""" @@ -2703,7 +2984,153 @@ def load_from_pickle(self, keep_name=False): - + +#=============================================================================== +# Helper functions for the average density matrix (density mode) +# +# Those are module level functions since the average density matrix is written +# either by DensityInterface itself (single core) or by the mother interface +# recombining the output of the various multicore jobs +# (common_run_interface.do_reweight). +#=============================================================================== +def parse_matrix_normalisation(value): + """interpret the argument of 'change matrix_normalisation'. + return (value, understood) where understood is False if the argument is + neither 'True' nor 'False' (in which case the normalisation is disabled)""" + + value = value.strip("[],()") + if value == 'True': + return True, True + elif value == 'False': + return False, True + return False, False + +def get_matrix_normalisation(card_path): + """return the matrix_normalisation option of a density mode reweight card. + The default (option absent from the card) is the one of DensityInterface.""" + + matrix_normalisation = True # default value of DensityInterface + if not card_path or not os.path.exists(card_path): + return matrix_normalisation + + with open(card_path) as card: + for line in card: + line = line.strip() + if not line or line[0] in ('#', '!'): + continue + split_line = line.split() + if len(split_line) < 3 or split_line[0] != 'change' or \ + split_line[1] != 'matrix_normalisation': + continue + # last occurence wins, as when the card is executed line by line + matrix_normalisation, _ = parse_matrix_normalisation(split_line[2]) + return matrix_normalisation + +def average_density_matrix_label(lhe_path): + """the label identifying an event file in the average density matrix output""" + + if lhe_path.endswith('.gz'): + lhe_path = lhe_path[:-3] + return os.path.basename(lhe_path)[:-4] + +def average_density_matrix_path(lhe_path, output_dir=None): + """the canonical path of the average density matrix associated to an event file""" + + if output_dir is None: + output_dir = os.path.dirname(lhe_path) + return pjoin(output_dir, + "Average_density_matrix_%s.txt" % average_density_matrix_label(lhe_path)) + +def write_average_density_matrix(rho_avg, lhe_path, output_dir=None): + """log the average density matrix rho_avg (line form) and write it in square + form next to the event file it has been computed from. return the path used.""" + + import madgraph.various.Density_functions as dens + + rho_avg_square = dens.DensityMatrixObservables(rho_avg).square_matrix() + + logger.info("Average density matrix:") + for i in range(len(rho_avg_square)): + print("\t",list(rho_avg_square[i])) + + path = average_density_matrix_path(lhe_path, output_dir) + file_density = open(path, 'w') + file_density.write(f'Average density matrix of LHE file {average_density_matrix_label(lhe_path)}:\n') + # Cast each entry to a plain Python ``complex`` so that the file is + # written in the legacy ``(re+imj)`` repr regardless of the underlying + # numpy dtype (newer numpy prints np.complex64 values with a + # ``np.complex64(...)`` wrapper which the consumer parser cannot read). + for i in range(len(rho_avg_square)): + row = [complex(v) for v in rho_avg_square[i]] + file_density.write('\t' + str(row) + '\n') + file_density.close() + return path + +def average_density_matrix_from_lhe(lhe_path, matrix_normalisation=True): + """re-compute the average density matrix from the tag of every + event of an already reweighted event file. This reproduces exactly what + DensityInterface.launch_actual_reweighting accumulates on the fly: + - matrix_normalisation True: the per event matrices stored in the file are + already normalised by their trace, the average is weighted by the weight + of the events. + - matrix_normalisation False: the per event matrices are the raw ones, the + average is a plain average over the events. + return the average density matrix in line form (None if no event of the file + carries a density matrix).""" + + average_rho = None + total_wgt = 0. + nb_event = 0 + + lhe = lhe_parser.EventFile(lhe_path) + lhe.parsing = "wgt_only" # we only need the weight and the tag + for event in lhe: + if not event.density: + continue + if matrix_normalisation: + contrib = [value * event.wgt for value in event.density] + total_wgt += event.wgt + else: + contrib = event.density + nb_event += 1 + if average_rho is None: + average_rho = list(contrib) + elif len(contrib) != len(average_rho): + raise Exception("Inconsistent size of the density matrices within %s" % lhe_path) + else: + for i in range(len(average_rho)): + average_rho[i] += contrib[i] + lhe.close() + + if average_rho is None: + return None + + norm = total_wgt if matrix_normalisation else nb_event + return [value / norm for value in average_rho] + +def combine_density_matrix(lhe_path, chunk_paths=(), reweight_card=None): + """write the canonical average density matrix of lhe_path after a multicore + reweighting: each job has written the average of its own chunk of events, so + the average of the full (recombined) file is re-computed here and the per + chunk files are removed. return the path of the file written (None if the + average could not be computed).""" + + matrix_normalisation = get_matrix_normalisation(reweight_card) + rho_avg = average_density_matrix_from_lhe(lhe_path, matrix_normalisation) + + if rho_avg is None: + # keep the per chunk files: they are the only output left in that case + logger.warning("No density matrix found in %s: the average density matrix is not written." % lhe_path) + return None + + path = write_average_density_matrix(rho_avg, lhe_path) + for chunk in chunk_paths: + chunk_path = average_density_matrix_path(chunk) + if chunk_path != path and os.path.exists(chunk_path): + os.remove(chunk_path) + return path + + class DensityInterface(ReweightInterface): """Basic interface for computing density matrix""" @@ -2927,15 +3354,9 @@ def do_change_matrix_normalisation(self,line): Choses if the production matrix should be normalised by its trace or not. Default = True """ - for i in range(len(line)): - line[i] = line[i].strip("[],()") - if line[0] == 'True': - self.matrix_normalisation = True - elif line[0] == 'False': - self.matrix_normalisation = False - else: + self.matrix_normalisation, understood = parse_matrix_normalisation(line[0]) + if not understood: logger.warning('Option matrix_normalisation not understood, set it to True. Please use the syntax: change matrix_normalisation True if you want to enable it.') - self.matrix_normalisation = False def do_change_particle_in_density_matrix(self, line): @@ -3115,22 +3536,8 @@ def launch_actual_reweighting(self, param_card_iterator, for i in range(len(rho_avg)): rho_avg[i] = self.average_rho[i] / self.nevents - rho_avg_instance = dens.DensityMatrixObservables(rho_avg) - rho_avg_square = rho_avg_instance.square_matrix() - - logger.info("Average density matrix:") - for i in range(len(rho_avg_square)): - print("\t",list(rho_avg_square[i])) - file_density = open(pjoin(os.path.dirname(self.event_path), f"Average_density_matrix_{os.path.basename(self.lhe_input.name)[:-4]}.txt"), 'w') - file_density.write(f'Average density matrix of LHE file {os.path.basename(self.lhe_input.name)[:-4]}:\n') - # Cast each entry to a plain Python ``complex`` so that the file is - # written in the legacy ``(re+imj)`` repr regardless of the underlying - # numpy dtype (newer numpy prints np.complex64 values with a - # ``np.complex64(...)`` wrapper which the consumer parser cannot read). - for i in range(len(rho_avg_square)): - row = [complex(v) for v in rho_avg_square[i]] - file_density.write('\t' + str(row) + '\n') - file_density.close() + write_average_density_matrix(rho_avg, self.lhe_input.name, + output_dir=os.path.dirname(self.event_path)) if self.output_type == "default": @@ -3238,10 +3645,7 @@ def calculate_matrix_element(self, event, hypp_id, scale2=0): else: nhel = -1 - pdg = list(orig_order[0])+list(orig_order[1]) - relevant_merged = relevant_model.get('merged_particles') if relevant_model else self.merged_particles - if relevant_merged and any(p in relevant_merged for p in pdg): - pdg = event.get_pdg(all_p[0]) + pdg = self._pdg_for_me_call(event, orig_order, all_p[0], relevant_model) #list_properties is the list of properties of the class FourMomentum that we can use to rank particles list_properties = [p for p in dir(lhe_parser.FourMomentum) if isinstance(getattr(lhe_parser.FourMomentum,p),property)] @@ -3334,6 +3738,21 @@ def calculate_matrix_element(self, event, hypp_id, scale2=0): rho_instance = dens.DensityMatrixObservables(production_matrix, self.number_combinations * (self.number_combinations + 1) / 2) new_value = rho_instance.density_matrix + # An identically-zero density matrix is not a physical answer for an + # event that is in the event file: either GET_DENSITY could not resolve + # the flavour of the legs it was handed (typically merged-particle + # labels 81/82/... reaching the fortran instead of the concrete PDGs), + # or 'allowed_helicities' selects a helicity configuration that carries + # no amplitude. Do not average it in: the trace is zero, so with + # matrix_normalisation on the normalisation is 0/0 and the average + # density matrix comes back as NaN; with it off the event silently + # dilutes the average. Refuse loudly, as the matrix-element path does. + if not any(new_value): + raise Exception("Invalid density matrix: only zeros returned for " + "event %s (pdg %s). Check that the flavour of every leg can be " + "resolved and that 'allowed_helicities' selects a contributing " + "helicity configuration." % (getattr(event, 'ievent', -1), list(pdg))) + return new_value diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index 354815b369..446bd172b0 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,286 @@ 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': '', + # No crossing: every call is the uncrossed process, so the C-parity + # de-duplication is always allowed. + 'csym_dedup_ok': 'true', + # 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) + + # Per-leg tables (one entry per external leg). The crossing's slot + # permutation and NSF sign flips are decoded from the crossing code at + # runtime (cross_perm_ic, mirroring the fortran GET_CROSS_PERM), and the + # two halves of the denominator are rebuilt from these -- so no + # cross-indexed table (spincol/basepid/src/perm/ic) is stored. + spincol_part_init = self._cpp_int_array(tables['spincol_part']) + ids_base_init = self._cpp_int_array(tables['ids_base']) + antipid_base_init = self._cpp_int_array(tables['antipid_base']) + # 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 slot permutation is a fixed\n" + "// relabelling decoded from the crossing code at runtime\n" + "// (cross_perm_ic), so no cross-indexed table is stored; the\n" + "// denominator splits into the crossing-dependent initial-state\n" + "// spin*color (spincol_cross) and the flavor-dependent identical-\n" + "// final-state factor (ident_cross), both rebuilt from per-leg data.\n" + "const int ncross = %(ncross)d;\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). Genuinely per-\n" + "// crossing (not derivable from per-leg data), so kept as a table --\n" + "// the fortran path tabulates it too. The gating identity row itself\n" + "// is recomputed per row at runtime (see the good-helicity loop).\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, 'ghfilt': ghfilt_init} + + cross_perm_block = ( + "int perm[nexternal];\n" + "int ic[nexternal];\n" + "cross_perm_ic(cross, perm, ic);") + + 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" + "// Runtime crossing decode (mirrors the fortran GET_CROSS_PERM/\n" + "// SWAP_LEGS): cross = i*(nexternal+1) + j swaps particle 1 with i\n" + "// and particle 2 with j (0 = leave alone; i==1 / j==2 are self-swaps,\n" + "// also no-ops). perm[k] is the input slot landing in crossed slot k\n" + "// and ic[k] its NSF sign flip. perm/ic are always left a valid\n" + "// permutation (identity for an inapplicable code) so a momentum\n" + "// gather never reads out of range; the return value flags an\n" + "// applicable crossing (false = overlapping swap / out of range).\n" + "bool CPPProcess::cross_perm_ic(int cross, int* perm, int* ic)\n" + "{\n" + " const int ncross = (nexternal + 1) * (nexternal + 1);\n" + " for (int k = 0; k < nexternal; k++) { perm[k] = k; ic[k] = 1; }\n" + " if (cross < 0 || cross >= ncross) return false;\n" + " const int xi = cross / (nexternal + 1);\n" + " const int xj = cross %% (nexternal + 1);\n" + " // Overlapping-swap codes compose into a 3-cycle the consumers\n" + " // read with opposite orientation: pure redundancy, invalid.\n" + " if (xi != 0 && xi != 1 && xj != 0 && xj != 2 &&\n" + " (xi == 2 || xj == 1 || xi == xj)) return false;\n" + " if (xi != 0 && xi != 1)\n" + " {\n" + " int t = perm[0]; perm[0] = perm[xi - 1]; perm[xi - 1] = t;\n" + " ic[0] = -ic[0]; ic[xi - 1] = -ic[xi - 1];\n" + " }\n" + " if (xj != 0 && xj != 2)\n" + " {\n" + " int t = perm[1]; perm[1] = perm[xj - 1]; perm[xj - 1] = t;\n" + " ic[1] = -ic[1]; ic[xj - 1] = -ic[xj - 1];\n" + " }\n" + " // A crossing may only conjugate a leg that CHANGES SIDE. Both\n" + " // legs of a same-side transposition are conjugated while\n" + " // neither moves across, which is no crossing at all: for a\n" + " // 2 -> N process that is the beam swap (xi==2 / xj==1), which\n" + " // must not conjugate anything; for a 1 -> N one it is every xj\n" + " // swap. Mirrors the fortran GET_CROSS_PERM.\n" + " for (int k = 0; k < nexternal; k++)\n" + " if (ic[k] == -1 &&\n" + " ((k < %(ninitial)d) == (perm[k] < %(ninitial)d)))\n" + " return false;\n" + " return true;\n" + "}\n" + "\n" + "//------------------------------------------------------------------\n" + "// Initial-state spin*color average of the crossed process: the\n" + "// product of the per-leg spin*color (spincol_part, conjugation\n" + "// invariant) over the legs the crossing puts in the initial state.\n" + "// 0 for a crossing that cannot be applied.\n" + "int CPPProcess::spincol_cross(int cross)\n" + "{\n" + " static const int spincol_part[nexternal] = %(spincol_part)s;\n" + " int perm[nexternal], ic[nexternal];\n" + " if (!cross_perm_ic(cross, perm, ic)) return 0;\n" + " int factor = 1;\n" + " for (int k = 0; k < %(ninitial)d; k++)\n" + " factor *= spincol_part[perm[k]];\n" + " return factor;\n" + "}\n" + "\n" + "//------------------------------------------------------------------\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 -- ids_base, conjugated to antipid_base\n" + "// when the leg swapped side) and the same actual flavor. FLAVOR is\n" + "// not permuted by the crossing, so slot k reads flavor[perm[k]].\n" + "int CPPProcess::ident_cross(int cross, const int* flavor)\n" + "{\n" + " static const int ids_base[nexternal] = %(ids_base)s;\n" + " static const int antipid_base[nexternal] = %(antipid_base)s;\n" + " int perm[nexternal], ic[nexternal];\n" + " cross_perm_ic(cross, perm, ic);\n" + " int bpid[nexternal];\n" + " for (int k = 0; k < nexternal; k++)\n" + " bpid[k] = (ic[k] == 1) ? ids_base[perm[k]] : antipid_base[perm[k]];\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 (bpid[k] == bpid[l] &&\n" + " flavor[perm[k]] == flavor[perm[l]])\n" + " {\n" + " used[l] = true;\n" + " n = n + 1;\n" + " fact = fact * n;\n" + " }\n" + " }\n" + " }\n" + " return fact;\n" + "}" + ) % {'spincol_part': spincol_part_init, 'ids_base': ids_base_init, + 'antipid_base': antipid_base_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': + ' bool cross_perm_ic(int cross, int* perm, int* ic);\n' + ' int spincol_cross(int cross);\n' + ' int ident_cross(int cross, const int* flavor);', + 'ident_cross_function': ident_cross_function, + # C-parity de-duplication only for the uncrossed process (cross 0): + # a crossing permutes/sign-flips the helicities so a base-row flip + # is not the crossed C-parity partner (crossed flavors: full sum). + 'csym_dedup_ok': 'cross == 0', + # 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 the runtime-decoded cross_perm_ic), + # 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 +1697,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 +1871,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 +2926,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 +3045,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 +3225,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 +3239,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 +3268,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 +3287,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 +3600,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 +3632,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 +3902,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 5ca4b9e287..a4289d4959 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 525478ac5c..4297cac098 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,89 @@ 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') + # compute_flavor_masks() is indexed by the FULL external legs, so for a + # decay chain (p p > w+ w-, w+ > j j, w- > j j) the flavor tuple spans the + # 6 decay leaves, not the 4 core legs of process.get('legs'). Expand the + # decays so leg_ids lines up with the flavor tuple (a no-op without decays). + legs = process.get_legs_with_decays() if hasattr(process, 'get_legs_with_decays') \ + else process.get('legs') + leg_ids = [leg.get('id') for leg in 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 +670,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 +732,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 +1135,35 @@ def write_matrix_element_v4(self): """ pass + def _check_crossing_support(self): + """Note that this output cannot read folded crossings. + + `--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 + folding-capable standalone backends implement that decoding. + + This used to refuse the export and ask the user to regenerate with + --use_crossing=False. It no longer does: the crossed subprocesses are + recorded as metadata at generation, so an output that cannot read them + gets them expanded back into explicit subprocesses automatically (see + MadGraphCmd._expand_recorded_crossings, applied on both the grouped and + the ungrouped path). Erroring out here would additionally be wrong for + the many processes that fold NO crossing at all -- nothing would be + missing from their output -- and it fired on the flag rather than on the + data. --use_crossing=False stays available, but is no longer needed just + to reach a non-folding output. + """ + + if self.supports_crossing: + return + if not self.opt.get('use_crossing', False): + return + logger.debug("The '%s' output does not read folded crossings; any " + "recorded crossed subprocess will be expanded back into " + "an explicit subprocess.", + 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 +1866,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 +1919,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 +2172,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 +2373,1285 @@ 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. A decay + chain itself does NOT break crossing: p p > t t~ j j, t > ... still + crosses at the production level (force-onshell decays ride along on the + legs they hang off), so crossing stays enabled -- the crossing tables + just have to be built over the full decay leaves (see + compute_crossing_tables) so the identical-particle/denominator factors + reflect the real final state. + """ + if process.get('required_s_channels') or \ + process.get('forbidden_s_channels'): + return True + # Crossing is a tree-level construction; a perturbative (loop / loop- + # induced) process must not go through it. Its matrix element has no + # flavor/PDG crossing tables (compute_crossing_pdg_entries would index + # past the end), so treat it as crossing-breaking to keep every + # crossing gate -- and the crossed-group detection -- clear of it. + if process.get('perturbation_couplings'): + 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['ident_resonance'] = \ + self.compute_crossing_tables(matrix_element)['ident_resonance'] + 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, xgrow_map=None): + """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). + + ``xgrow_map`` (Track-A bases only) is ``{cross: (dep_proc_id, cmap)}`` + for every within-group router flavor routed here: which subprocess the + crossed call is FOR, and that subprocess's diagram -> this module's + diagram map under the crossing. It drives the multi-channel row; see + the ``me_confsub_j`` fill below. + """ + 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': '', + # Multi-channel row: without crossing this matrix element is + # only ever called for its own subprocess, so its own CONFSUB + # row is the right one and AMP2 is already in its numbering. + 'me_confsub_j': 'CONFSUB(%s, I)' % pid, + # 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': '', + # No crossing: every call is the uncrossed base process, so the + # C-parity de-duplication is always applicable. + 'me_csym_cross_ok': '.TRUE.', + 'hel_csym_cross_ok': '.TRUE.', + }) + 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), + 'ident_resonance': + self.compute_crossing_tables(matrix_element)['ident_resonance'], + '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))} + # ---- multi-channel row for calls routed here by a within-group router. + # CHANNEL and AMP2 are both in THIS module's diagram numbering (the + # router already translated CHANNEL through the crossing), but the loop + # that builds XTOT must enumerate the configs of the subprocess the call + # is FOR: GET_CHANNEL_CUT(P, I) is evaluated on the DEPENDENT's momenta, + # so I has to be a config of the dependent's row, and the AMP2 slot + # paired with it is the dependent's diagram sent through the crossing. + # Walking our own row instead pairs each amplitude with a different + # config's cut -- a bijective relabel, so the weights still sum to 1 and + # the cross section is unchanged, but the importance sampling is + # mis-paired. Both lookups are resolved from CROSSUSE through baked + # tables rather than a common block set by the router: madevent runs + # vectorised (IVEC/warps) and mutable shared state would race. + # + # Safe to key on the crossing code alone: a base that serves a + # within-group router is never also a cross-group (Track B) base -- + # compute_crossgroup_routing skips any group that has within-group + # routing -- so no foreign crossing can reach these tables. + ngraphs_me = len(matrix_element.get('diagrams')) + nxc = (matrix_element.get_nexternal_ninitial()[0] + 1) ** 2 - 1 + xg_rows, xg_cols = {}, {} + xg_cfg = [list(range(0, ngraphs_me + 1))] # column 1 = identity + for cross in sorted(xgrow_map or {}): + dep_pid, cmap = xgrow_map[cross] + # Only a clean permutation of our own diagrams is usable: anything + # else (a fallback map, or a dependent with a different diagram + # count) keeps our own row, i.e. the historical behaviour. + if not 1 <= cross <= nxc or \ + sorted(cmap) != list(range(1, ngraphs_me + 1)): + continue + col = [0] + list(cmap) + if col not in xg_cfg: + xg_cfg.append(col) + xg_rows[cross] = dep_pid + xg_cols[cross] = xg_cfg.index(col) + 1 + if xg_rows: + def _data2d(name, icol, values, per_line=10): + out = [] + for s in range(0, len(values), per_line): + chunk = values[s:s + per_line] + out.append(' DATA (%s(I,%d),I=%d,%d) /%s/' + % (name, icol, s, s + len(chunk) - 1, + ','.join(str(v) for v in chunk))) + return out + xg_lines = [' INTEGER IXROW, IXR', + ' INTEGER XGROWT(0:%d), XGCOLT(0:%d)' % (nxc, nxc), + self.format_integer_data_lines( + 'XGROWT', [xg_rows.get(c, int(pid)) + for c in range(nxc + 1)]), + self.format_integer_data_lines( + 'XGCOLT', [xg_cols.get(c, 1) + for c in range(nxc + 1)]), + ' INTEGER XGCFG(0:%d,%d)' + % (ngraphs_me, len(xg_cfg))] + for icol, col in enumerate(xg_cfg): + xg_lines += _data2d('XGCFG', icol + 1, col) + xg_decl = '\n' + '\n'.join(xg_lines) + xg_decode = ('\n IXROW = XGROWT(CROSSUSE)' + '\n IXR = XGCOLT(CROSSUSE)') + # Slot 0 of every XGCFG column is 0, so a config this subprocess has + # no diagram for still reads back as 0 and is skipped as before. + confsub_j = 'XGCFG(CONFSUB(IXROW, I), IXR)' + elif id(matrix_element) in getattr(self, '_crossgroup_base_mes', ()): + # Cross-group (Track B) base: same defect, but this object is + # SYMLINKED into the dependent P directories (write_crossgroup_mk), + # so one binary serves them all and the row cannot be baked here -- + # a dependent's configs live in ITS directory's config_subproc_map, + # and GET_CHANNEL_CUT already resolves to the dependent's genps.o. + # Take the row from XGROW, which every directory defines for + # itself in its own auto_dsig.f (see write_xgrow_routines): the + # identity (our own CONFSUB row) where we are generated, the routed + # subprocess's row composed with the crossing map in a dependent's. + # LMAXCONFIGS is a single global maximum (Source/maxconfigs.inc, + # symlinked), so the loop bound is the same in every directory. + xg_decl = '\n INTEGER XGJROW(LMAXCONFIGS)' + xg_decode = '\n CALL XGROW%s(CROSSUSE, XGJROW)' % pid + confsub_j = 'XGJROW(I)' + else: + xg_decl, xg_decode = '', '' + confsub_j = 'CONFSUB(%s, I)' % pid + replace_dict.update({ + 'me_confsub_j': confsub_j, + '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' + '%(xg_decl)s' + ) % {'nflav': nflav, 'cp': cp, 'xg_decl': xg_decl}, + # 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' + '%(xg_decode)s' + ) % {'cp': cp, 'xg_decode': xg_decode}, + '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' + '%(xg_decl)s' + ) % {'nflav': nflav, 'cp': cp, 'xg_decl': xg_decl}, + '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' + '%(xg_decode)s' + ) % {'cp': cp, 'xg_decode': xg_decode}, + 'hel_matrix_call_args': 'PUSE ,IC, FLAV_USE, TS, AMP2, JAMP2, IVEC', + 'hel_matrix_ic_param': 'IC,', + # C-parity de-duplication only for the uncrossed base process + # (CROSSUSE 0): a crossing permutes/sign-flips the helicities, so a + # base-row FLIP is not the crossed C-parity partner. Crossed + # dependents keep the full helicity sum. + 'me_csym_cross_ok': 'CROSSUSE.EQ.0', + 'hel_csym_cross_ok': 'CROSSUSE.EQ.0', + }) + + # (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 + per-particle tables below. + + The per-slot representative PDG (BASEPID) and FLAVOR source slot (SRC) + GET_IDENT_CROSS needs are not tabulated per crossing: they follow from + the crossing's own PERM/IC (the same GET_SPINCOL_CROSS decodes) applied + to two NEXTERNAL-long base tables. IDS_BASE is the base process PDG of + each leg; ANTIPID_BASE is its charge conjugate (used for a leg that + swapped between the initial and the final state). Slot k of crossing + CROSS then reads leg PERM(k), conjugated when IC(k) flipped, and looks + up FLAVOR(PERM(k)); two crossed final legs are identical iff they share + both. This drops the two NCROSS*NEXTERNAL-long tables. + + 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) + + return '\n'.join([ + self.format_integer_data_lines('SPINCOL_PART', tables['spincol_part']), + self.format_integer_data_lines('IDS_BASE', tables['ids_base']), + self.format_integer_data_lines('ANTIPID_BASE', tables['antipid_base']), + self.format_integer_data_lines('COUNTABLE', tables['countable'])]) + + @staticmethod + def _leaf_block_sizes(process): + """Per core leg, the number of decay leaves it expands to. + + A decay chain's matrix element runs over the decay *leaves*, but a + crossing acts at the *production* level: it may only permute whole + production legs, and a decaying production leg carries its whole decay + block (all its leaves) as one unit. This returns a list parallel to + ``process.get('legs')`` giving each core leg's leaf count -- 1 for a + non-decaying leg (a single leaf that a crossing may move), >1 for a + decaying resonance (a block a crossing must never split or pull into the + initial state). Non-decay processes get all 1s, so every downstream use + is a no-op for them. Mirrors base_objects.get_legs_with_decays exactly: + decays are matched to final legs in leg order, first id-match wins. + """ + decays = list(process.get('decay_chains')) + sizes = [] + for leg in process.get('legs'): + if not leg.get('state') or not decays: + sizes.append(1) + continue + ids = [d.get('legs')[0].get('id') for d in decays] + if leg.get('id') in ids: + decay = decays.pop(ids.index(leg.get('id'))) + sizes.append(len(decay.get_legs_with_decays()) - 1) + else: + sizes.append(1) + return sizes + + 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') + # For a decay chain the crossing acts at the production level but the + # matrix element (and its NEXTERNAL) is over the decay *leaves*, so the + # crossing tables must span the leaves too: the two z of e+ e- > z z + # look like an identical pair on the core legs, yet z > mu+ mu- and + # z > e+ e- make the real final state non-identical (denominator 4, not + # 8). get_legs_with_decays() is the plain legs for a non-decay process. + legs = process.get_legs_with_decays() \ + if hasattr(process, 'get_legs_with_decays') else 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] + + # Per LEAF: the size of the production block it belongs to, and whether + # it is 'countable' for the identical-final factor. A crossing permutes + # production legs, so a decaying leg's whole block (its >1 leaves) moves + # as a unit; the CROSS codes can only transpose single leaves, so any + # crossing that would carry a block leaf into the initial state (splitting + # the block, or making a decaying resonance an initial particle) is + # rejected below. block_size is 1 for every leaf of a non-decay process, + # so decay chains are the only ones this constrains. + block_size = [] + # Referenced through the class, not self: the C++/mg7 exporters call + # compute_crossing_tables unbound with a non-Fortran self (see the + # get_iden_cross_lines docstring), which has no _leaf_block_sizes. + for size in ProcessExporterFortran._leaf_block_sizes(process): + block_size.extend([size] * size) + assert len(block_size) == nexternal, \ + 'leaf block sizes %s do not span NEXTERNAL %d' % (block_size, + nexternal) + # A block leaf (size > 1) is a decay product locked inside a resonance: + # it never counts toward the identical-final factor at the leaf level + # (that factor is resonance-level, see ident_resonance below). A single + # leaf (size 1) is a genuine external and does count. + countable = [1 if size == 1 else 0 for size in block_size] + + 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)] + + # Two codes that name no crossing, rejected exactly like an + # impossible one: a 0 spin*color makes SMATRIX and + # GET_PDG_FOR_FLAVOR both return a null result. slot_ids is + # still the permuted signature so the IDS_BASE/BASEPID + # rebuild sanity below stays consistent. GET_CROSS_PERM + # applies the same two rules at runtime. + # + # 1. A leg conjugated without changing side. The two legs of + # a same-side transposition are both conjugated while + # neither moves across, which is no crossing at all: for + # 2 -> N that is the beam swap (XI==2 / XJ==1), giving + # e.g. u~ g > e+ ve d, not even charge conserving; for + # 1 -> N it is every XJ swap. + # 2. A decay-block leaf carried across the initial/final + # line: it would split the block (pull one decay product + # into the initial state) or make a decaying resonance an + # initial particle. For a non-decay process every + # block_size is 1, so this one never fires. + if any(ic[slot] == -1 and + ((slot < ninitial) == (perm[slot] < ninitial) + or block_size[perm[slot]] > 1) + for slot in range(nexternal)): + spincol.append(0) + else: + # 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) + + # Per-particle spin*color (states * |color repr|), for every base leg. + # It is conjugation-invariant (a particle and its antiparticle share + # both), so a crossing's initial-state spin*color is just the product of + # these over the legs that land in the initial slots -- which is how + # GET_SPINCOL_CROSS recomputes SPINCOL_CROSS_TABLE at runtime from the + # NEXTERNAL-long SPINCOL_PART instead of the NCROSS-long table. + spincol_part = [] + for slot in range(nexternal): + pol = polarizations[slot] + nspin = len(pol) if pol else \ + len(particle(leg_ids[slot]).get_helicity_states()) + spincol_part.append(nspin * abs(particle(leg_ids[slot]).get('color'))) + + # Per-particle base PDG and its charge conjugate, one entry per base + # leg. GET_IDENT_CROSS rebuilds BASEPID_CROSS_TABLE / SRC_CROSS_TABLE at + # runtime from these two NEXTERNAL-long tables plus the crossing PERM/IC, + # instead of storing the two NCROSS*NEXTERNAL-long tables. + ids_base = list(leg_ids) + antipid_base = [particle(pid).get_anti_pdg_code() for pid in leg_ids] + + # ident_resonance: the part of the identical-final factor a crossing + # leaves untouched. A crossing only ever permutes the single-leaf + # (countable) legs -- decay blocks stay put -- so the crossed identical + # factor is (n! over the crossed countable final legs) times this + # constant. It collects everything a leaf-level count over the crossed + # legs cannot see: identical resonances decaying identically, and the + # identical particles locked inside each decay block. base_non_chain is + # the identical factor of the base's own countable final legs, so + # dividing it out of the resonance-level identical_particle_factor leaves + # exactly that constant. For a non-decay process every final leg is + # countable and there are no resonances, so base_non_chain equals the + # whole identical factor and ident_resonance is 1 -- GET_IDENT_CROSS then + # reduces to the historical plain leaf count. + final_countable = collections.defaultdict(int) + for slot in range(ninitial, nexternal): + if countable[slot]: + final_countable[(leg_ids[slot], + tuple(polarizations[slot] or []))] += 1 + base_non_chain = 1 + for count in final_countable.values(): + base_non_chain *= math.factorial(count) + identical = matrix_element.get('identical_particle_factor') + assert identical % base_non_chain == 0, \ + 'Countable identical factor %d does not divide the identical-' \ + 'particle factor %d' % (base_non_chain, identical) + ident_resonance = identical // base_non_chain + + # Sanity: for the identity crossing, spin*color times the identical + # factor must rebuild the static IDEN, else this and + # get_denominator_factor have drifted apart. A decay chain's identical + # factor is resonance-level (two z decaying the same way count once, + # differently not at all), so it is checked through + # identical_particle_factor rather than a leaf count; the initial + # spin*color (which may carry a sign from an antiparticle beam in + # get_denominator_factor but not in the abs-based spincol) is only + # required to divide IDEN. + if process.get('decay_chains'): + assert matrix_element.get_denominator_factor() % spincol[0] == 0, \ + 'Crossing initial spin*color does not divide IDEN: ' \ + '%s vs %s' % (spincol[0], + matrix_element.get_denominator_factor()) + else: + assert spincol[0] * identical == \ + matrix_element.get_denominator_factor(), \ + 'Crossing denominator disagrees with get_denominator_factor: ' \ + '%s*%s vs %s' % (spincol[0], identical, + matrix_element.get_denominator_factor()) + # Sanity: the small per-particle tables reproduce the per-crossing + # tables the runtime routines used to read. SPINCOL_PART -> the + # initial-state spin*color; IDS_BASE/ANTIPID_BASE plus the crossing + # PERM/IC -> BASEPID_CROSS_TABLE / SRC_CROSS_TABLE (checked for the + # applicable crossings, the only ones GET_IDENT_CROSS is ever asked). + for cross in range((nexternal + 1) * (nexternal + 1)): + perm, ic, valid = \ + ProcessExporterFortran.get_crossing_permutation(cross, nexternal) + expect = 0 if not valid else 1 + if valid: + for slot in range(ninitial): + expect *= spincol_part[perm[slot]] + assert expect == spincol[cross] or spincol[cross] == 0, \ + 'SPINCOL_PART product %s != SPINCOL_CROSS_TABLE %s at CROSS %d' \ + % (expect, spincol[cross], cross) + if not valid: + continue + for slot in range(nexternal): + bp = ids_base[perm[slot]] if ic[slot] == 1 \ + else antipid_base[perm[slot]] + assert bp == basepid[cross * nexternal + slot] and \ + perm[slot] == source[cross * nexternal + slot], \ + 'IDS_BASE/ANTIPID_BASE rebuild != BASEPID/SRC at CROSS ' \ + '%d slot %d' % (cross, slot) + + return {'spincol': spincol, 'spincol_part': spincol_part, + 'ids_base': ids_base, 'antipid_base': antipid_base, + 'basepid': basepid, 'source': source, + 'perm': perm_flat, 'ic': ic_flat, + 'countable': countable, 'ident_resonance': ident_resonance, + '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), + decoded 0-based as ``cross*NFLAV + flav0`` (``zero_based=False`` gives + the 1-based fortran form). **NFLAV here is the madevent / C++ / mg7 + one**, ``get_external_flavors_with_iden()`` -- the count those backends + size their flavor table by, deliberately not the STANDALONE fortran + NFLAV, which comes from _build_flav_table_flat (compute_flavor_masks) + and is a different, usually larger number: 1 vs 2 for + ``p p > w+ j, w+ > e+ ve``, 2 vs 4 for ``p p > z j``, 1/1/9 vs 1/4/12 + for ``p p > j j``. See the NFLAV comment in get_crossing_routines. + So ``index`` is meaningful to partition_crossing_classes (madevent + routing) and to the C++ demo_pdg table, and NOT to the standalone + fortran PY_GET_PDG_FOR_FLAVOR: a caller holding a standalone + module must take NFLAV from PY_GET_FLAVOR_LAYOUT and build the + index itself (reweight_interface.build_cross_resolve does). ``cross`` + and ``pdg_tuple`` carry no such convention and are good everywhere. + * ``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 *for + the signature*: 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). The backends agree on + which PDG tuple a (CROSS, flavor) names; they do NOT share one index + convention, see ``index`` above. 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') + # A group that breaks crossing (pinned s-channel, or a perturbative + # / loop-induced matrix element) has no crossing tables -- skip it + # before partition_crossing_classes, which would index past the end. + if any(self.breaks_crossing_symmetry(proc) + for me in mes_g for proc in me.get('processes')): + continue + 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 +4901,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 @@ -3371,6 +4923,10 @@ def __init__(self, *args,**opts): self.format = 'standalone' self.prefix_info = {} + # proc_prefix -> (list of recorded CROSS codes, complete flag), filled + # per subprocess directory and written out by write_f2py_splitter; see + # recorded_crossing_codes. + self.crossing_records = {} ProcessExporterFortran.__init__(self, *args, **opts) def copy_template(self, model): @@ -3652,6 +5208,23 @@ def write_f2py_splitter(self): flavor_index_decl = '\n'.join(' integer %sget_flavor_index' % prefix for prefix in sorted(smatrixhel_prefixes)) + # smatrixhel_idx: the same dispatch keyed on the 1-based matrix-element + # slot (the get_pdg_order / get_prefix index) instead of on the PDG + # codes, taking the extended FLAV_IDX as given. A FOLDED crossed + # subprocess has no PDG entry of its own -- that is the whole point of + # folding -- so the PDG dispatch cannot reach it; a caller that resolved + # the crossing itself (through GET_PDG_FOR_FLAVOR) holds a slot and an + # extended index instead. It shares f77_smatrixhel's alphas/scale2 + # handling so that a crossed and an uncrossed evaluation of the same + # event use the exact same running couplings. + idxtext = [] + for i, prefix in enumerate(allprefix, 1): + keyword = 'if' if i == 1 else 'else if' + idxtext.append(' %s (procindex.eq.%i) then' % (keyword, i)) + idxtext.append(' call %ssmatrixhel(p, nhel, flav_idx, ans)' % prefix) + if idxtext: + idxtext.append(' endif') + all_prefix = set([k[0] for k in self.prefix_info.values()]) setpara_for_each_matrix = '' for prefix in all_prefix: @@ -3687,11 +5260,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 = '' @@ -3717,8 +5297,9 @@ def write_f2py_splitter(self): all_iden += ' idens(%s) = %s \n' % (i, iden) #misc.sprint(all_iden) - formatting = {'python_information':'\n'.join(info), + formatting = {'python_information':'\n'.join(info), 'smatrixhel': '\n'.join(smtext), + 'smatrixhel_idx': '\n'.join(idxtext), 'flavor_index_decl': flavor_index_decl, 'maxpart': max_nexternal, 'nb_me': len(allids), @@ -3746,9 +5327,59 @@ def write_f2py_splitter(self): fsock.close() formatting['nhel'] = all_nhel_f2py text = template2 % formatting - fsock = writers.FortranWriter(pjoin(self.dir_path, 'SubProcesses', 'f2py_wrapper.f'),'w') + f2py_wrapper_path = pjoin(self.dir_path, 'SubProcesses', 'f2py_wrapper.f') + fsock = writers.FortranWriter(f2py_wrapper_path,'w') fsock.writelines(text) - fsock.close() + fsock.close() + + # Expose the per-process crossing-aware f2py entry points + # (PY_GET_PDG_FOR_FLAVOR / GET_FLAVOR_LAYOUT / GET_NHEL_IDX / + # GET_DENSITY_IDX, etc.) in the COMBINED all_matrix module. They live in + # each subprocess' self-contained f2py_matrix_wrapper.f and call the + # M_* routines already linked into liball...me; the combined wrapper is + # otherwise base-only, so a crossing-aware python caller (MadSpin's + # density path) could not reach a folded crossed subprocess through it. + # Concatenate rather than add the files to the f2py command line: f2py's + # multi-file build leaves the extra wrappers' symbols undefined at + # dlopen on some platforms, whereas a single scanned source links them. + wrappers = sorted(glob.glob(pjoin(self.dir_path, 'SubProcesses', + '*', 'f2py_matrix_wrapper.f'))) + if wrappers: + with open(f2py_wrapper_path, 'a') as fsock: + for wpath in wrappers: + fsock.write('\nC crossing-aware f2py wrappers from %s\n' + % os.path.relpath(wpath, + pjoin(self.dir_path, 'SubProcesses'))) + fsock.write(open(wpath).read()) + + self.write_crossing_records() + + def write_crossing_records(self): + """List the folded crossed subprocesses for the python consumers of the + combined f2py module (see recorded_crossing_codes). + + GET_PDG_FOR_FLAVOR tells a caller what process an extended FLAV_IDX + evaluates, but not whether that crossing is a subprocess the generation + asked for: its CROSS space is dense and also holds crossings that are + merely applicable (a Z or a decay product pulled into the initial state). + Only generation knows the difference, so it is recorded here, one line + per matrix element, + + ... + + with 0 when a recorded crossed process could not be matched + to a runtime crossing -- the consumer must then not trust the list to + cover every folded subprocess. The file is always written (empty lists + included) so that its absence means "produced before this existed", and + a consumer can tell that apart from "nothing was folded".""" + path = pjoin(self.dir_path, 'SubProcesses', 'crossed_flavors.dat') + with open(path, 'w') as fsock: + fsock.write('# folded crossed subprocesses, written by MG5aMC\n') + fsock.write('# ...\n') + for prefix in sorted(self.crossing_records): + codes, complete = self.crossing_records[prefix] + fsock.write('%s %d%s\n' % (prefix, 1 if complete else 0, + ''.join(' %d' % c for c in codes))) def get_model_parameter(self, model): """ returns all the model parameter @@ -3914,6 +5545,12 @@ def color_dim_from_particle(p): ids = [l.get('id') for l in proc.get('legs_with_decays')] iden = compute_iden_from_pdgs(ids, ninitial, self.model) self.prefix_info[(tuple(ids), proc.get('id'))] = [proc_prefix, proc.get_tag(), ncomb, iden] + # Which CROSS codes of this matrix element name a crossed subprocess + # this generation actually requested. Only a python caller holding + # them can walk the folded crossings without also evaluating the + # merely-applicable ones; write_f2py_splitter exports them. + self.crossing_records[proc_prefix] = \ + self.recorded_crossing_codes(matrix_element) template = open(pjoin(self.mgme_dir, 'madgraph', 'iolibs', 'template_files', 'makefile_sa_f_sp'),'r') text = template.read() @@ -4175,6 +5812,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 +5851,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 +5873,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 +5894,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 +6049,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 +6117,269 @@ def write_matrix_element_v4(self, writer, matrix_element, fortran_model, #=========================================================================== # write_check_sa #=========================================================================== + def _recorded_crossing_matches(self, matrix_element): + """(matches, complete): the reachable crossing each RECORDED crossed + subprocess of this matrix element corresponds to. + + Crossing records (merge_crossing='record') say which crossed processes + are real subprocesses of the generation; the runtime crossing space + (GET_PDG_FOR_FLAVOR / its python twin compute_crossing_pdg_entries) is a + dense enumeration of CROSS codes that also contains mathematically + applicable but unrequested crossings -- e.g. a Z pulled into the initial + state for p p > z j. Consumers that must not evaluate the latter (the + check_sa demo, the reweight's folded-crossing lookup) intersect the two + here. + + `matches` is a list of ``(pdg_signature, cross)`` in the recorded order, + matched LABEL-AWARE: a recorded process may carry merged multiparticle + labels (_quark = 81) and so may the reachable signature (a leg that does + not vary with the flavor index keeps its label), so a label matches any + member flavor of the same sign, and two labels match when equal. That is + also why a recorded process is matched as a whole rather than leg by leg: + the reachable set already encodes the correct flavor pairings, which + resolving each merged leg on its own would not (it would fabricate e.g. a + W coupling two same-flavor quarks). Both beam orientations are tried. + `complete` is False when a recorded process has NO reachable + instantiation, so a caller can fall back rather than hide a real + crossing.""" + crossed = matrix_element.get('crossed_processes') \ + if 'crossed_processes' in matrix_element else None + 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? Equal ids + # (two concrete particles, or two identical merged labels) always + # match; otherwise one of the two may be a merged label covering the + # other flavor, with the same sign. + if leg_id == pdg: + return True + a, b = abs(leg_id), abs(pdg) + if (leg_id > 0) != (pdg > 0): + return False + return (a in merged and b in merged[a]) or \ + (b in merged and a in merged[b]) + + ninitial = matrix_element.get_nexternal_ninitial()[1] + # signatures the runtime can actually reach (applicable crossings) + reachable = [(tuple(pdg), cross) for (_i, cross, _f, pdg) in + self.compute_crossing_pdg_entries(matrix_element)] + # A decay-chain base records its crossings at the PRODUCTION level, but + # the reachable signatures span the decay leaves (the ME's NEXTERNAL), so + # the recorded process must be expanded before it can match. The decays + # never cross (they ride along on their production leg), so re-attaching + # the base's decay chains and expanding gives the crossed leaf signature. + base_decays = matrix_element.get('processes')[0].get('decay_chains') + + def crossed_leg_ids(proc): + if not base_decays: + return [l.get('id') for l in proc.get('legs')] + expanded = copy.copy(proc) + expanded.set('decay_chains', base_decays) + expanded.set('legs_with_decays', base_objects.LegList()) + return [l.get('id') for l in expanded.get_legs_with_decays()] + + matches, complete = [], True + for (proc, _bp, _xp) in crossed: + legs = crossed_leg_ids(proc) + 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, cross) in reachable: + if len(r) == len(orient) and \ + all(leg_matches(L, P) for L, P in zip(orient, r)): + hit = (r, cross) + break + if hit is not None: + break + if hit is None: + complete = False + continue + if hit[1] == 0: + # The identity: a recorded process that is the base's own beam + # swap (mirror), not a crossing. Consumers show/reach the base + # through its own PDG entry, so drop it. + continue + matches.append(hit) + return matches, complete + + def recorded_crossing_codes(self, matrix_element): + """(cross codes, complete) of the crossed subprocesses folded into this + matrix element: the CROSS half of every extended FLAV_IDX that names a + crossing this generation actually requested. + + This is what a python consumer needs to walk the folded crossings + soundly: it can enumerate GET_PDG_FOR_FLAVOR over + ``cross*NFLAV + flav`` (getting the exact per-flavor signature, which + the flavor index and not the code determines) while skipping the codes + that are merely applicable. See _recorded_crossing_matches.""" + matches, complete = self._recorded_crossing_matches(matrix_element) + return sorted(set(cross for (_sig, cross) in matches)), complete + + 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. Matching on the PDG + rather than the extended index avoids the NFLAV-convention gap between + the crossing-PDG enumeration and the runtime flavor table. Mirror pairs + are collapsed (the chosen signature's beam swap is also marked seen). + See _recorded_crossing_matches for the matching itself.""" + matches, complete = self._recorded_crossing_matches(matrix_element) + ninitial = matrix_element.get_nexternal_ninitial()[1] + sigs, seen = [], set() + for (hit, _cross) in matches: + 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 +6452,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 +6512,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 +7238,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 +7786,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 @@ -6065,7 +8078,8 @@ def finalize(self, matrix_elements, history, mg5options, flaglist, second_export # write_matrix_element_v4 #=========================================================================== def write_matrix_element_v4(self, writer, matrix_element, fortran_model, - proc_id = "", config_map = [], subproc_number = ""): + proc_id = "", config_map = [], subproc_number = "", + xgrow_map = None): """Export a matrix element to a matrix.f file in MG4 madevent format""" if not matrix_element.get('processes') or \ @@ -6101,7 +8115,41 @@ 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, + xgrow_map=xgrow_map) + mask_decl, mask_setup, n_flavors, active_flavor_mask = \ self._get_flavor_mask_blocks(matrix_element) @@ -6111,12 +8159,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 +8407,420 @@ 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 crossing 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. + + Both crossing flavours feed this: a Track B cross-group dependent (whose + base lives in another P directory) and a Track A within-group router + (whose base is a matrix element of the same directory). Either way the + recycled optim is entered with crossed momenta, and it bakes its helicity + configs -- so pruning it to the base's own good-hel biases the crossed + caller.""" + 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. + + CAUTION: G_base U sigma(G_base) is the union in the *loop-index* space + of matrix_orig.f, which takes NHEL at run time. It is NOT a safe + helicity table for the recycled matrix_optim.f, which bakes its + configs and can only apply SGN via IC -- never PERM. gen_ximprove + therefore keeps EVERY config for a crossing base and only uses these + perms for their length; do not "optimise" it back to this union. + + * 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 +8874,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 +8972,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 +9012,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 +9789,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 +10241,275 @@ 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) + # Multi-channel config remap. CHANNEL arrives as THIS subprocess's AMP2 + # slot (SUBDIAG = CONFSUB(, iconf), a diagram number in this + # module's numbering), but the base SMATRIX enhances AMP2(CHANNEL) in + # ITS numbering: AMP2 is filled by the BASE's diagrams evaluated at the + # CROSSED momenta, so the slot holding |this subprocess's diagram m|^2 is + # the base diagram carrying m's topology *under the crossing*. Translate + # the slot with the same map the cross-group path uses for DSIG_XGCONFIG + # -- and for the same reason; walking the base's own CONFSUB row instead + # would name the base diagram that shares the topology with legs left in + # place, which is not the one the crossed momenta filled. Per flavor, + # since each routes to its own base/crossing. Emitted only when some + # flavor is non-identity (it usually is not, the diagram numbering being + # largely crossing-covariant), so most routers are unchanged. + # + # The base applies the same map to its multi-channel row (xgrow_map in + # fill_crossing_replace_dict_me) and accepts it only as a permutation of + # ITS diagrams, so a base with a different diagram count is left alone on + # both sides -- crossing partners always have the same count, this only + # keeps the two ends from disagreeing. + ngraphs = len(matrix_element.get('diagrams')) + ident_cfg = list(range(1, ngraphs + 1)) + cfg_cache = {} # (base_index, cross) -> map; flavors often share one + configmap = [] + for (b, iflav) in routing: + key = (b, (iflav - 1) // len(matrix_elements[b] + .get_external_flavors_with_iden())) + if key not in cfg_cache: + cmap = self._crossgroup_configmap( + matrix_element, matrix_elements[b], key[1]) + if len(matrix_elements[b].get('diagrams')) != ngraphs: + cmap = ident_cfg + cfg_cache[key] = cmap + configmap.append(cfg_cache[key]) + chan_name = None + if any(cm != ident_cfg for cm in configmap): + chan_name = 'XGCONF_%s' % proc_id + decl.append(' INTEGER XCHAN') + decl.append(' INTEGER %s(%d,%d)' + % (chan_name, ngraphs, len(configmap))) + decl.append(' DATA %s /%s/' % ( + chan_name, ','.join(str(x) for col in configmap for x in col))) + 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)) + chan = 'channel' + if chan_name: + # A config this subprocess has no diagram for gives CHANNEL=0; + # leave it alone (the base's own multi-channel block already + # handles what it gets) rather than indexing outside the table. + chan = 'XCHAN' + dispatch += [ + ' XCHAN = channel', + ' IF (channel.GE.1.AND.channel.LE.%d) XCHAN =' + ' %s(channel,%d)' % (ngraphs, chan_name, flav0 + 1)] + dispatch.append( + ' CALL SMATRIX%d(P, %d, RHEL, RCOL, %s, IVEC, ANS,' + ' IHEL, ICOL)' % (base_index + 1, iflav, chan)) + 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,16 +10581,180 @@ 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') + # Record each router's base->base helicity permutation, exactly as a + # cross-group dependent does (crossgroup_helunion.dat). A router sends + # its call into the base SMATRIX, and with helicity recycling that is + # the RECYCLED matrix_optim.f, whose helicity configs are baked + # into the HELAS calls -- it takes no runtime NHEL, so it cannot apply + # the crossing's helicity permutation the way matrix_orig.f does + # (CR_APPLY_CROSSING_TABLE permutes NHEL along with the momenta). + # The base's good-hel SUBSET is not closed under that permutation, so + # a pruned optim silently drops part of the routed process's helicity + # sum -- the whole cross section comes out low. Writing the perms here + # makes gen_ximprove bake this base over every config (and skip the + # C-parity de-duplication, whose |M|^2 identity is only established + # for cross 0), which is what the Track B path already does. + for idep, route in enumerate(crossing_routing or []): + if route is None or idep in crossing_bases: + continue + for (base_index, iflav) in route: + base_me = matrix_elements[base_index] + nflav_base = len(base_me.get_external_flavors_with_iden()) + identity = list(range( + 1, base_me.get_helicity_combinations() + 1)) + pi = self._crossgroup_base_helperm( + base_me, (iflav - 1) // nflav_base) + if pi == identity: + # This crossing leaves the helicity configs where they + # are, so the base's own good-hel set already covers it. + continue + if pi is None: + # Not a clean permutation (the crossing is not helicity + # bijective). matrix_orig.f has a run-time escape for + # that -- GHIDX=0 makes it compute every helicity -- but + # the recycled optim is baked and has none, and we cannot + # say which configs the router needs. Fall back to the + # identity purely as the length-NCOMB marker that makes + # gen_ximprove keep every config. + pi = identity + perms = self._crossgroup_helperms.setdefault( + subprocdir, {}).setdefault(base_index + 1, []) + if pi not in perms: + perms.append(pi) + else: + crossing_bases, crossing_routing = None, None + # Per base: {crossing -> (dependent proc_id, dep-diagram -> base-diagram + # map)}. The base's multi-channel loop needs both to weight a routed call + # correctly -- the row says which configs to enumerate (they pair with + # GET_CHANNEL_CUT on the dependent's momenta), the map turns each of that + # subprocess's diagrams into the AMP2 slot the crossed evaluation filled. + # See fill_crossing_replace_dict_me. + base_xgrow = {} + if crossing_routing is not None: + cfg_cache = {} + for idep, route in enumerate(crossing_routing): + if route is None or idep in crossing_bases: + continue + for (base_index, iflav) in route: + base_me = matrix_elements[base_index] + cross = (iflav - 1) // len( + base_me.get_external_flavors_with_iden()) + if not cross: + continue + key = (idep, base_index, cross) + if key not in cfg_cache: + cfg_cache[key] = self._crossgroup_configmap( + matrix_elements[idep], base_me, cross) + base_xgrow.setdefault(base_index, {})[cross] = ( + idep + 1, cfg_cache[key]) + + def _xgrow_kw(ime): + """Crossing kwargs for this subprocess, or nothing at all. + + Only a Track-A base that actually has a crossed subprocess routed to + it needs the multi-channel row map. Everything else goes through an + exporter whose write_matrix_element_v4 does not take the crossing + kwargs -- notably the loop-induced one, and a loop-induced matrix + element never crosses anyway (see the perturbative gate in + generate_matrix_elements) -- so handing it the kwarg is a TypeError. + """ + xg = base_xgrow.get(ime) + return {'xgrow_map': xg} if xg else {} + 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, + replace_dict = 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) + subproc_number=group_number, + **_xgrow_kw(ime)) calls,ncolor = replace_dict['return_value'] tfile = open(replace_dict['template_file']).read() file = misc.apply_template(tfile, replace_dict) @@ -7667,12 +10784,13 @@ def generate_subprocess_directory(self, subproc_group, else: filename = 'matrix%d.f' % (ime+1) calls, ncolor = \ - self.write_matrix_element_v4(writers.FortranWriter(filename), + self.write_matrix_element_v4(writers.FortranWriter(filename), matrix_element, fortran_model, proc_id=str(ime+1), config_map=subproc_group.get('diagram_maps')[ime], - subproc_number=group_number) + subproc_number=group_number, + **_xgrow_kw(ime)) if second_exporter: process_exporter_cpp = second_exporter.oneprocessclass(matrix_element,second_helas, prefix=ime) @@ -7698,7 +10816,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) @@ -7729,7 +10848,7 @@ def generate_subprocess_directory(self, subproc_group, filename = 'auto_dsig.f' self.write_super_auto_dsig_file(writers.FortranWriter(filename), - subproc_group) + subproc_group, group_number) filename = 'coloramps.inc' self.write_coloramps_file(writers.FortranWriter(filename), @@ -7767,6 +10886,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: @@ -7877,7 +11000,8 @@ def generate_subprocess_directory(self, subproc_group, #=========================================================================== # write_super_auto_dsig_file #=========================================================================== - def write_super_auto_dsig_file(self, writer, subproc_group): + def write_super_auto_dsig_file(self, writer, subproc_group, + group_number=None): """Write the auto_dsig.f file selecting between the subprocesses in subprocess group mode""" @@ -7972,11 +11096,137 @@ def write_super_auto_dsig_file(self, writer, subproc_group): file = open(pjoin(_file_path, \ 'iolibs/template_files/super_auto_dsig_group_v4.inc')).read() file = file % replace_dict + file += self.write_xgrow_routines(subproc_group, group_number) # Write the file writer.writelines(file) else: return replace_dict + + def write_xgrow_routines(self, subproc_group, group_number): + """Per-directory bodies of the XGROW helpers a cross-group (Track B) + base SMATRIX calls for its multi-channel row (see the me_confsub_j fill). + + The base's compiled matrix object is symlinked into every dependent P + directory, so it cannot carry the row itself: the row belongs to the + subprocess the call is FOR, and that subprocess's CONFSUB lives in ITS + directory. Each directory therefore links its own XGROW, resolved by + the linker exactly like genps.o (which is why GET_CHANNEL_CUT(P, I) in + the shared object already means the *dependent's* config I). + + * where the base is generated -- the identity: our own CONFSUB row. Only + cross 0 ever reaches it (a Track-B base group has no within-group + router, so its own auto_dsig calls it with a plain FLAV_IDX). + * in a dependent's directory -- the routed subprocess's own CONFSUB row, + each of its diagrams mapped to the base AMP2 slot the crossed + evaluation filled (_crossgroup_configmap, the same map its auto_dsig + uses for DSIG_XGCONFIG). + + Emitted here because auto_dsig.f is the one file written exactly once per + P directory, so a base serving several dependents in one directory still + gets a single definition. + """ + if group_number is None or not getattr(self, '_crossgroup', None): + return '' + mes = subproc_group.get('matrix_elements') + routines, seen = [], {} + # Bases generated in this directory: identity row. + base_ids = getattr(self, '_crossgroup_base_mes', set()) + for ime, me in enumerate(mes): + if id(me) in base_ids: + seen[ime + 1] = 'base' + routines.append( + '\n SUBROUTINE XGROW%(b)d(CROSS, XGJ)\n' + 'C Multi-channel row of SMATRIX%(b)d in the directory it\n' + 'C is generated in: its own. CROSS is always 0 here.\n' + ' IMPLICIT NONE\n' + " INCLUDE 'maxamps.inc'\n" + " INCLUDE 'maxconfigs.inc'\n" + ' INTEGER CROSS, XGJ(LMAXCONFIGS), I\n' + ' INTEGER CONFSUB(MAXSPROC,LMAXCONFIGS)\n' + " INCLUDE 'config_subproc_map.inc'\n" + ' DO I=1,LMAXCONFIGS\n' + ' XGJ(I) = CONFSUB(%(b)d, I)\n' + ' ENDDO\n' + ' RETURN\n' + ' END\n' % {'b': ime + 1}) + # Dependents routed out of this directory: their own row, remapped. + by_base = {} + for ime, me in enumerate(mes): + cg = self._crossgroup.get((group_number, ime)) + if cg is None: + continue + base_me = cg['base_me'] + nflav_base = len(base_me.get_external_flavors_with_iden()) + ngraphs_b = len(base_me.get('diagrams')) + nxc = (base_me.get_nexternal_ninitial()[0] + 1) ** 2 - 1 + for iflav in cg['flav_idx']: + cross = (iflav - 1) // nflav_base + if not 1 <= cross <= nxc: + continue + cmap = self._crossgroup_configmap(me, base_me, cross) + if sorted(cmap) != list(range(1, ngraphs_b + 1)): + continue # unusable map: leave the historical row + slot = by_base.setdefault( + cg['base_proc_id'], + {'nxc': nxc, 'ng': ngraphs_b, 'cols': [], 'cross': {}}) + col = (ime + 1, tuple(cmap)) + if col not in slot['cols']: + slot['cols'].append(col) + # Two subprocesses claiming the same crossing would be the same + # crossed process; keep the first and leave the rest alone. + slot['cross'].setdefault(cross, slot['cols'].index(col) + 2) + for b in sorted(by_base): + if b in seen: + # This directory both generates SMATRIX and routes to another + # directory's SMATRIX: one name, two bodies. That collision + # already exists for SMATRIX itself, so leave it alone. + logger.warning('Cross-group crossing: SMATRIX%d is both local ' + 'and routed in one directory; keeping the ' + 'historical multi-channel row.' % b) + continue + s = by_base[b] + # Column 1 is the fallback for a crossing this directory does not + # route (unreachable in practice): the first routed subprocess's own + # row, unmapped. + rows = [s['cols'][0][0]] + [c[0] for c in s['cols']] + cfgs = [list(range(0, s['ng'] + 1))] + cfgs += [[0] + list(c[1]) for c in s['cols']] + lines = [ + '\n SUBROUTINE XGROW%d(CROSS, XGJ)' % b, + 'C Multi-channel row of the symlinked SMATRIX%d for a call' % b, + 'C routed out of THIS directory: the routed subprocess own', + 'C CONFSUB row, each diagram mapped to the AMP2 slot the', + 'C crossed evaluation filled.', + ' IMPLICIT NONE', + " INCLUDE 'maxamps.inc'", + " INCLUDE 'maxconfigs.inc'", + ' INTEGER CROSS, XGJ(LMAXCONFIGS), I, IXR', + ' INTEGER CONFSUB(MAXSPROC,LMAXCONFIGS)', + " INCLUDE 'config_subproc_map.inc'", + ' INTEGER XGCOL(0:%d)' % s['nxc'], + self.format_integer_data_lines( + 'XGCOL', [s['cross'].get(c, 1) + for c in range(s['nxc'] + 1)]), + ' INTEGER XGROWP(%d)' % len(rows), + ' DATA XGROWP /%s/' % ','.join(str(x) for x in rows), + ' INTEGER XGCFG(0:%d,%d)' % (s['ng'], len(cfgs))] + for icol, col in enumerate(cfgs): + for st in range(0, len(col), 10): + chunk = col[st:st + 10] + lines.append(' DATA (XGCFG(I,%d),I=%d,%d) /%s/' + % (icol + 1, st, st + len(chunk) - 1, + ','.join(str(v) for v in chunk))) + lines += [' IXR = 1', + ' IF (CROSS.GE.0.AND.CROSS.LE.%d) IXR = XGCOL(CROSS)' + % s['nxc'], + ' DO I=1,LMAXCONFIGS', + ' XGJ(I) = XGCFG(CONFSUB(XGROWP(IXR), I), IXR)', + ' ENDDO', + ' RETURN', + ' END'] + routines.append('\n'.join(lines) + '\n') + return ''.join(routines) #=========================================================================== # write_mirrorprocs @@ -8271,11 +11521,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 +14847,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 2f5dee8e88..db308ea1d1 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 2910e6a66d..5a65ef1efb 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 85fb5b596d..a776539b69 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 4730c4fb51..23b623038c 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 9da836ec1b..ccc5c5773e 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 a5eb276740..431972850b 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 b64dfd4f2f..037bbeeb51 100644 --- a/madgraph/iolibs/template_files/cpp_process_class.inc +++ b/madgraph/iolibs/template_files/cpp_process_class.inc @@ -66,7 +66,23 @@ private: int igood[nflavors][ncomb]; int jhel[nflavors]; + // C-parity de-duplication of the helicity sum (uncrossed process only, see + // sigmaKin): flip[ihel] is the helicity row with every helicity negated (an + // involution, built once); csym_bad[flav] latches true once ANY row fails to + // pair up or ANY pair shows |M(ihel)| != |M(flip)| at a scan point, so the + // reuse is all-or-nothing per flavor and halves the loop uniformly; the good + // helicities of a flavor are then reduced to the lower-index representative + // of every surviving C-parity pair (igoodrep/nrep) carrying a doubled weight + // (repwgt), so the recycling sum computes one of the pair and counts it twice. + int flip[ncomb]; + bool flip_ready; + bool csym_bad[nflavors]; + int igoodrep[nflavors][ncomb]; + int nrep[nflavors]; + int repwgt[nflavors][ncomb]; + // 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 db3a44fee6..125140b12c 100644 --- a/madgraph/iolibs/template_files/cpp_process_function_definitions.inc +++ b/madgraph/iolibs/template_files/cpp_process_function_definitions.inc @@ -6,7 +6,8 @@ // Initialize process. CPPProcess::CPPProcess(string param_card_name) : - goodhel(), ntry(), sum_hel(), ngood(), igood(), jhel() + goodhel(), ntry(), sum_hel(), ngood(), igood(), jhel(), + flip(), flip_ready(), csym_bad(), igoodrep(), nrep(), repwgt() { // Instantiate the model class and set parameters that stay fixed during run SLHAReader slha(param_card_name, false); @@ -91,3 +92,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 dccadc17f6..a30adf9df4 100644 --- a/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc +++ b/madgraph/iolibs/template_files/cpp_process_sigmaKin_function.inc @@ -6,47 +6,140 @@ 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){ - // Calculate the matrix element for all helicities +// C-parity helicity de-duplication (mirror of the fortran SMATRIX): flip[ihel] +// is the helicity row with every helicity negated, an involution built once +// from the helicity table. dedup_ok gates the reuse to the UNCROSSED process: +// a crossing permutes/sign-flips the helicities, so a base-row flip is no +// longer the crossed C-parity partner and the crossed flavors keep the full sum. +if (!flip_ready){ + for(int i = 0; i < ncomb; i++){ + flip[i] = i; + for(int j = 0; j < ncomb; j++){ + bool same = true; + for(int k = 0; k < nexternal; k++){ + if (helicities[j][k] != -helicities[i][k]){ + same = false; + } + } + if (same){ + flip[i] = j; + break; + } + } + } + flip_ready = true; +} +const bool dedup_ok = (%(csym_dedup_ok)s); + +if (sum_hel[%(fidx)s] == 0 || ntry[%(fidx)s] < 10){ + // Scan phase: calculate the matrix element for all helicities (full sum). + double tstore[ncomb] = {}; 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; + tstore[ihel] = 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 + } + } + if (dedup_ok){ + // Drop the C-parity pairing of any row whose flipped partner gave a + // different |M|^2 (parity/C violation). One mismatch at any scan point + // permanently invalidates the pair (robust, like the zero-filter). + // All-or-nothing per flavor: a self-paired row (flip[ihel]==ihel) has no + // distinct partner, and one mismatching pair is enough to give up, so + // that when the reuse does run it halves the loop uniformly. + for(int ihel = 0; ihel < ncomb; ihel++){ + if (flip[ihel] == ihel){ + csym_bad[%(fidx)s] = true; + } else if (flip[ihel] > ihel){ + double a = tstore[ihel]; + double b = tstore[flip[ihel]]; + double diff = a - b; + if (diff < 0){ + diff = -diff; + } + double aa = a; + if (aa < 0){ + aa = -aa; + } + double bb = b; + if (bb < 0){ + bb = -bb; + } + if (diff > 1e-6 * (aa + bb)){ + csym_bad[%(fidx)s] = true; + } + } + } + // The reuse skips a good helicity only if its representative is itself + // a good helicity: a pair split across the good/dropped boundary would + // lose the skipped row entirely. Fold that into the flavor verdict. + for(int g = 1; g <= ngood[%(fidx)s]; g++){ + int ihel = igood[%(fidx)s][g]; + if (flip[ihel] == ihel || !goodhel[%(fidx)s][flip[ihel]]){ + csym_bad[%(fidx)s] = true; + } + } + // Reduce the good helicities to the lower-index representative of every + // surviving C-parity pair, carrying a doubled weight; the skipped + // higher-index partner has an identical |M|^2. + nrep[%(fidx)s] = 0; + for(int g = 1; g <= ngood[%(fidx)s]; g++){ + int ihel = igood[%(fidx)s][g]; + bool paired = !csym_bad[%(fidx)s]; + if (paired && ihel > flip[ihel]){ + continue; + } + nrep[%(fidx)s]++; + igoodrep[%(fidx)s][nrep[%(fidx)s]] = ihel; + if (paired){ + repwgt[%(fidx)s][nrep[%(fidx)s]] = 2; + } else { + repwgt[%(fidx)s][nrep[%(fidx)s]] = 1; } } } - jhel[flavor_id] = 0; - sum_hel[flavor_id]=min(sum_hel[flavor_id], ngood[flavor_id]); + jhel[%(fidx)s] = 0; + if (dedup_ok){ + sum_hel[%(fidx)s] = min(sum_hel[%(fidx)s], nrep[%(fidx)s]); + } else { + 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); + // Only use the "good" helicities (C-parity representatives when uncrossed). + int nsel = ngood[%(fidx)s]; + if (dedup_ok){ + nsel = nrep[%(fidx)s]; + } + for(int j=0; j < sum_hel[%(fidx)s]; j++){ + jhel[%(fidx)s]++; + if (jhel[%(fidx)s] >= nsel){ + jhel[%(fidx)s]=0; + } + double hwgt = double(nsel)/double(sum_hel[%(fidx)s]); + int ihel = igood[%(fidx)s][jhel[%(fidx)s]]; + double cwgt = 1.; + if (dedup_ok){ + ihel = igoodrep[%(fidx)s][jhel[%(fidx)s]]; + cwgt = double(repwgt[%(fidx)s][jhel[%(fidx)s]]); + } + calculate_wavefunctions(perm, helicities[ihel], flavor%(cross_cw_args)s); %(get_matrix_t_lines)s - matrix_element += t*hwgt; + matrix_element += t*hwgt*cwgt; } } -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 a2b70e503a..07c1b5fa5a 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/f2py_splitter.py b/madgraph/iolibs/template_files/f2py_splitter.py index b10eea20aa..cb33cc3935 100644 --- a/madgraph/iolibs/template_files/f2py_splitter.py +++ b/madgraph/iolibs/template_files/f2py_splitter.py @@ -36,7 +36,44 @@ return end - + + subroutine %(f2py_prefix)sf77_smatrixhel_idx(procindex, flav_idx, npdg, p, ALPHAS, SCALE2, nhel, ANS) + use model_object + use aloha_object + IMPLICIT NONE +C Same as f77_smatrixhel, but selecting the matrix element by its slot in +C get_pdg_order/get_prefix (PROCINDEX, 1-based) and taking the extended flavor +C index (FLAV_IDX = cross*NFLAV + flav) as given rather than resolving it from +C the PDG codes. This is the only way in to a FOLDED crossed subprocess: it has +C no PDG entry of its own, so the dispatch above cannot name it, and the FLAVOR +C array cannot express a crossing (see matrix_standalone_f2py_flav_idx.inc). +C The alphas/scale2 setup is deliberately the same as in f77_smatrixhel. +CF2PY double precision, intent(in), dimension(0:3,npdg) :: p +CF2PY integer, intent(in) :: procindex +CF2PY integer, intent(in) :: flav_idx +CF2PY integer, intent(in) :: npdg +CF2PY double precision, intent(out) :: ANS +CF2PY double precision, intent(in) :: ALPHAS +CF2PY double precision, intent(in) :: SCALE2 + integer procindex, flav_idx, npdg, nhel + double precision p(*) + double precision ANS, ALPHAS, PI, SCALE2 + include 'coupl.inc' + + if (scale2.eq.0)then + PI = 3.141592653589793D0 + G = 2* DSQRT(ALPHAS*PI) + CALL UPDATE_AS_PARAM() + else + CALL UPDATE_AS_PARAM2(scale2, ALPHAS) + endif + + ANS = 0d0 +%(smatrixhel_idx)s + + return + end + subroutine %(f2py_prefix)sf77_density(pdgs, npdg, procid, P, POS, N_CHANGING, ALLOW_HEL, N_COMB, ALPHAS, SCALE2, INTER) IMPLICIT NONE CF2PY double precision, intent(in) :: p diff --git a/madgraph/iolibs/template_files/f2py_wrapper_all.inc b/madgraph/iolibs/template_files/f2py_wrapper_all.inc index 527056ae86..67b9e662b1 100644 --- a/madgraph/iolibs/template_files/f2py_wrapper_all.inc +++ b/madgraph/iolibs/template_files/f2py_wrapper_all.inc @@ -19,7 +19,33 @@ CF2PY double precision, intent(in) :: SCALE2 DOUBLE PRECISION ANS, ALPHAS,SCALE2 call %(f2py_prefix)sf77_smatrixhel(pdgs, procid, npdg, p, alphas, scale2, nhel, ans) - + + RETURN + END + + SUBROUTINE %(f2py_prefix)sSMATRIXHEL_IDX(PROCINDEX, FLAV_IDX, NPDG, P, + $ ALPHAS, SCALE2, NHEL, ANS) + IMPLICIT NONE +C Crossing-aware twin of SMATRIXHEL. PROCINDEX is the 1-based +C get_pdg_order slot of the matrix element and FLAV_IDX the extended +C flavor index (cross*NFLAV+flav) of the process to evaluate. A folded +C crossed subprocess is reachable only this way: it has no PDG entry of +C its own, and the FLAVOR array cannot carry a crossing. + +CF2PY double precision, intent(in), dimension(0:3,npdg) :: p +CF2PY integer, intent(in) :: procindex +CF2PY integer, intent(in) :: flav_idx +CF2PY integer, intent(in) :: npdg +CF2PY double precision, intent(out) :: ANS +CF2PY double precision, intent(in) :: ALPHAS +CF2PY double precision, intent(in) :: SCALE2 + INTEGER PROCINDEX, FLAV_IDX, NPDG, NHEL + DOUBLE PRECISION P(*) + DOUBLE PRECISION ANS, ALPHAS, SCALE2 + + call %(f2py_prefix)sf77_smatrixhel_idx(procindex, flav_idx, npdg, p, + $ alphas, scale2, nhel, ans) + RETURN END 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 0000000000..61841c7f5c --- /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/madevent_makefile_source b/madgraph/iolibs/template_files/madevent_makefile_source index 7339361d56..466c664fde 100644 --- a/madgraph/iolibs/template_files/madevent_makefile_source +++ b/madgraph/iolibs/template_files/madevent_makefile_source @@ -71,6 +71,14 @@ $(BINDIR)gensudgrid: $(GENSUDGRID) $(LIBDIR)libpdf.$(libext) $(LIBDIR)libgammaUP # Dependencies +# The model form-factor ALOHA routines in DHELAS and PDF/pdfwrap_lhapdf.f both do +# "use model_object", whose F90 module (model_object.mod) is produced by the +# MODEL build. Under a parallel (-j) top-level build libmodel, libdhelas and +# libpdf are otherwise made concurrently, so order libdhelas and libpdf after +# libmodel. Order-only (|) so they are not relinked when libmodel merely rebuilds. +$(LIBDIR)libdhelas.$(libext): | $(LIBDIR)libmodel.$(libext) +$(LIBDIR)libpdf.$(libext): | $(LIBDIR)libmodel.$(libext) + dsample.o: DiscreteSampler.o dsample.f genps.inc StringCast.o vector.inc pawgraph.o: vector.inc DiscreteSampler.o: StringCast.o diff --git a/madgraph/iolibs/template_files/madmatrix/check_sa.cc b/madgraph/iolibs/template_files/madmatrix/check_sa.cc index 68e93edb50..ce2081173c 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 027f1aa44e..1a69fd6145 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 082c373aa2..9d1c947663 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%(csym_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 ) +%(csym_gh_flip)s%(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 */ @@ -581,7 +582,7 @@ namespace mg5amcCpu #endif calculate_jamps( ihel, allmomenta, allcouplings, hgFlavorVec, jamp_sv, false, allNumerators, allDenominators, jamp2_sv, ievt00 ); //maxtry? color_sum_cpu( allMEs, jamp_sv, ievt00 ); - for( int ieppV = 0; ieppV < neppV; ++ieppV ) +%(csym_gh_record)s for( int ieppV = 0; ieppV < neppV; ++ieppV ) { const int ievt = ievt00 + ieppV; //std::cout << "sigmaKin_getGoodHel allMEs[ievt]=" << allMEs[ievt] << std::endl; @@ -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 } } - } +%(csym_gh_check)s } } // end loop over flavor combinations (per-flavor good-helicity union) - } +%(goodhel_percross_build)s } #endif //-------------------------------------------------------------------------- @@ -627,7 +628,7 @@ namespace mg5amcCpu #endif cNGoodHel = nGoodHel; for( int ihel = 0; ihel < ncomb; ihel++ ) cGoodHel[ihel] = goodHel[ihel]; - return nGoodHel; +%(csym_pairbuild)s return nGoodHel; } //-------------------------------------------------------------------------- diff --git a/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc b/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc index 227301a6ea..5c28ec90fd 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc @@ -116,36 +116,36 @@ #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++ ) +%(csym_me_decl)s for( int ighel = 0; ighel < %(sigmakin_hel_bound)s; ighel++ ) { - const int ihel = cGoodHel[ighel]; - cxtype_sv jamp_sv[nParity * ncolor] = {}; // fixed nasty bug (omitting 'nParity' caused memory corruptions after calling calculate_jamps) +%(sigmakin_perlane_decl)s const int ihel = %(sigmakin_ihel_expr)s; +%(csym_skip)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 MEs_ighel2[ighel] = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 + neppV ) ); #endif - } +%(csym_record)s } // Event-by-event random choice of helicity #403 for( int ieppV = 0; ieppV < neppV; ++ieppV ) { 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 d19c93bb9b..571a323a3e 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_goodhel_helper.inc b/madgraph/iolibs/template_files/matrix_goodhel_helper.inc index e49f496875..b519f7c179 100644 --- a/madgraph/iolibs/template_files/matrix_goodhel_helper.inc +++ b/madgraph/iolibs/template_files/matrix_goodhel_helper.inc @@ -7,7 +7,26 @@ LOGICAL GOODHEL(NCOMB, MAXFLAVPERPROC) INTEGER NTRY(MAXFLAVPERPROC) common/BLOCK_GOODHEL/NTRY,GOODHEL +C Persist the C-parity de-duplication verdict next to the good +C helicities. Reading the file skips the scan (NTRY is forced past +C MAXTRIES below), so a run that never completed its scan must hand on +C "broken" rather than the optimistic default -- otherwise the next job +C would de-duplicate on a pairing nothing ever verified. + INTEGER NCSYMTOT + PARAMETER (NCSYMTOT=MAXFLAVPERPROC*MAXSPROC) + INTEGER CSYMBAD(NCSYMTOT), NCSCAN(NCSYMTOT) + common/BLOCK_CSYM/CSYMBAD,NCSCAN + INTEGER CSYMOUT(NCSYMTOT) + INTEGER ICS write(stream_id,*) GOODHEL + do ICS=1,NCSYMTOT + if (CSYMBAD(ICS).eq.0.and.NCSCAN(ICS).ge.20) then + CSYMOUT(ICS) = 0 + else + CSYMOUT(ICS) = 1 + endif + enddo + write(stream_id,*) CSYMOUT return end @@ -22,8 +41,22 @@ LOGICAL GOODHEL(NCOMB, MAXFLAVPERPROC) INTEGER NTRY(MAXFLAVPERPROC) common/BLOCK_GOODHEL/NTRY,GOODHEL + INTEGER NCSYMTOT + PARAMETER (NCSYMTOT=MAXFLAVPERPROC*MAXSPROC) + INTEGER CSYMBAD(NCSYMTOT), NCSCAN(NCSYMTOT) + common/BLOCK_CSYM/CSYMBAD,NCSCAN + INTEGER IOCS read(stream_id,*) GOODHEL NTRY(:) = MAXTRIES + 1 +C Inherit the verdict. The good-helicity filter now gates the full-sum +C loop, so the scan can no longer be redone in this job (the rows it +C would compare are not all evaluated any more): treat a missing or +C unfinished verdict as "do not de-duplicate". + read(stream_id,*,iostat=IOCS) CSYMBAD + if (IOCS.ne.0) then + CSYMBAD(:) = 1 + endif + NCSCAN(:) = 20 return end 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 0000000000..056af8b9c1 --- /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 b1523fc0e2..35188343c0 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 @@ -80,7 +81,29 @@ C row-level information to apply the identical-particle correction. DATA NB_FAIL /0/ double precision get_channel_cut external get_channel_cut - +C C-parity helicity de-duplication of the init full-sum loop (uncrossed +C base process only): FLIP(I) is the helicity row with every helicity +C negated (an involution built once). The reuse is ALL-OR-NOTHING per +C (flavor,subprocess): CSYMBAD latches 1 as soon as ANY row fails to pair +C up (a self-paired row, FLIP(I)=I, has no distinct partner) or ANY pair +C shows |M(I)|^2 != |M(FLIP(I))|^2 at a scan point. Only then is exactly +C half the loop skipped, each survivor counted twice. That uniformity is +C what keeps the multi-channel weight exact: halving every row scales +C AMP2(J) and XTOT by the same 1/2, so AMP2(config)/XTOT is unchanged, +C whereas a partial (per-row) de-duplication would rescale AMP2 +C non-uniformly across diagrams. NCSCAN counts only the passes that really +C ran the scan -- NTRY alone is not enough, since read_good_hel restores +C it above the threshold without ever scanning. CSYMBAD/NCSCAN sit in a +C COMMON block (zero-initialised, like NTRY) so the good-hel file can +C carry the verdict across jobs. + INTEGER FLIP(NCOMB), JHEL, KHEL + LOGICAL HELSAME, DEDUP + INTEGER CSYMBAD(MAXFLAVPERPROC,MAXSPROC) + INTEGER NCSCAN(MAXFLAVPERPROC,MAXSPROC) + COMMON/BLOCK_CSYM/CSYMBAD,NCSCAN + SAVE FLIP + DATA FLIP/NCOMB*0/ + c C This is just to temporarily store the reference grid for helicity of the DiscreteSampler so as to obtain its number of entries with ref_helicity_grid%n_tot_entries type(SampledDimension) ref_helicity_grid @@ -90,6 +113,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,9 +152,38 @@ 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 +C C-parity partner of each helicity row (all helicities negated), built once. + IF (FLIP(1).EQ.0) THEN + DO I=1,NCOMB + FLIP(I)=I + DO JHEL=1,NCOMB + HELSAME=.TRUE. + DO KHEL=1,NEXTERNAL + IF (NHEL(KHEL,JHEL).NE.-NHEL(KHEL,I)) HELSAME=.FALSE. + ENDDO + IF (HELSAME) THEN + FLIP(I)=JHEL + EXIT + ENDIF + ENDDO + ENDDO +C A self-paired row (FLIP(I)=I, every helicity 0) has no distinct partner, +C so the loop could not be halved uniformly: refuse the reuse outright. + DO I=1,NCOMB + IF (FLIP(I).EQ.I) THEN + DO KHEL=1,MAXSPROC + DO JHEL=1,MAXFLAVPERPROC + CSYMBAD(JHEL,KHEL)=1 + ENDDO + ENDDO + ENDIF + ENDDO + ENDIF + DEDUP = NCSCAN(%(me_flav_key)s,%(proc_id)s).GE.20 .AND. CSYMBAD(%(me_flav_key)s,%(proc_id)s).EQ.0 .AND. (%(me_csym_cross_ok)s) + IF (multi_channel) THEN DO I=1,NDIAGS AMP2(I)=0D0 @@ -149,17 +202,56 @@ 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 +C Fast phase: skip the higher-index C-parity partner (computed once at +C the lower index, counted twice below). + IF (DEDUP.AND.I.GT.FLIP(I)) CYCLE + 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) endif ANS=ANS+DABS(T) TS(I)=T +C The representative carries its skipped partner's identical |M|^2: +C copy TS(FLIP) (so the event-helicity CDF/DS grid pick both) and +C count it once more in ANS. + IF (DEDUP.AND.I.LT.FLIP(I)) THEN + ANS=ANS+DABS(T) + TS(FLIP(I))=T + IF (ISUM_HEL.NE.0.and.DS_get_dim_status('Helicity').eq.0.and.ALLOW_HELICITY_GRID_ENTRIES) + & call DS_add_entry('Helicity',FLIP(I),T) + ENDIF ENDIF ENDDO - IF(NTRY(IFLAV,%(proc_id)s).EQ.(MAXTRIES+1).and.DS_get_dim_status('Helicity').ne.-1) THEN +C Scan phase: drop the C-parity pairing of any row whose fully flipped +C partner gave a different |M|^2 (parity/C/polarization breaking). One +C mismatch at any scan point permanently invalidates the pair. + IF (%(me_csym_cross_ok)s.AND.NCSCAN(%(me_flav_key)s,%(proc_id)s).LT.20) THEN + NCSCAN(%(me_flav_key)s,%(proc_id)s)=NCSCAN(%(me_flav_key)s,%(proc_id)s)+1 + DO I=1,NCOMB + IF (FLIP(I).GT.I) THEN + IF (DABS(TS(I)-TS(FLIP(I))).GT.1D-6*(DABS(TS(I))+DABS(TS(FLIP(I))))) THEN + CSYMBAD(%(me_flav_key)s,%(proc_id)s)=1 + ENDIF + ENDIF + ENDDO + ENDIF +C Report the surviving C-parity pairs to the helicity-recycling optimizer +C (parsed by gen_ximprove): once the scan has settled (NTRY=20), each +C representative I_optim.f and reuses TS(rep) for it. +C NTRY (not NCSCAN, which saturates at 20) provides the one-shot trigger; +C in init_mode the full-sum branch runs every call, so the two coincide. + IF (init_mode.AND.%(me_csym_cross_ok)s.AND.CSYMBAD(%(me_flav_key)s,%(proc_id)s).EQ.0.AND.NCSCAN(%(me_flav_key)s,%(proc_id)s).GE.20.AND.NTRY(%(me_flav_key)s,%(proc_id)s).EQ.20) THEN + DO I=1,NCOMB + IF (I.LT.FLIP(I).AND.DABS(TS(I)).GT.ANS*LIMHEL/NCOMB) THEN + PRINT *, 'CSYM PAIR: %(proc_id)s ', I, FLIP(I) + ENDIF + ENDDO + ENDIF + 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 +268,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 +289,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 @@ -226,7 +318,7 @@ c Set right sign for ANS, based on sign of chosen helicity IF (MULTI_CHANNEL) THEN XTOT=0D0 DO I=1,LMAXCONFIGS - J = CONFSUB(%(proc_id)s, I) + J = %(me_confsub_j)s if (J.ne.0) then if(sde_strat.eq.1) then AMP2(J) = AMP2(J) * GET_CHANNEL_CUT(P, I) @@ -254,11 +346,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 +368,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 +408,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 45bcdb8211..e361aac523 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,9 +116,13 @@ C ---------- TS(:) = 0d0 - call MATRIX%(proc_id)s(P ,IFLAV, TS, AMP2, JAMP2, IVEC) - DO I=1,NCOMB - T=TS(I) + call MATRIX%(proc_id)s(%(hel_matrix_call_args)s) +C C-parity de-duplication: a dropped partner's HELAS calls were never +C generated, so its TS() is 0 here; copy the representative's identical |M|^2 +C back into it (the recycled MATRIX above computed only the representatives). +${csym_reuse} + 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 T=T*ABS(POL(JJ))*NB_SPIN_STATE_IN(JJ)/2d0 ! NB_SPIN_STATE(JJ)/2d0 is added for polarised beam @@ -144,7 +151,7 @@ c Set right sign for ANS, based on sign of chosen helicity IF (MULTI_CHANNEL) THEN XTOT=0D0 DO I=1,LMAXCONFIGS - J = CONFSUB(%(proc_id)s, I) + J = %(me_confsub_j)s if (J.ne.0)then if (sde_strat.eq.1)then AMP2(J) = AMP2(J) * GET_CHANNEL_CUT(P, I) @@ -167,11 +174,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 +196,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 +236,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_madevent_v4.inc b/madgraph/iolibs/template_files/matrix_madevent_v4.inc index 7b1d6bf1b9..c54d71dbbd 100644 --- a/madgraph/iolibs/template_files/matrix_madevent_v4.inc +++ b/madgraph/iolibs/template_files/matrix_madevent_v4.inc @@ -61,6 +61,29 @@ C INTEGER IDUM, NGOOD, J, JJ REAL XRAN1 EXTERNAL XRAN1 +C C-parity helicity de-duplication of the init full-sum loop (see below): +C FLIP(I) is the helicity row with every helicity negated (an involution +C built once). The de-duplication is ALL-OR-NOTHING per flavor: CSYMBAD(IFLAV) +C latches 1 as soon as ANY row fails to pair up (a self-paired row, FLIP(I)=I, +C has no distinct partner) or ANY pair shows |M(I)|^2 != |M(FLIP(I))|^2 at a +C scan point. Only when every row is in a genuine matched pair is the reuse +C enabled, and then exactly half the rows are evaluated and each counted twice. +C That uniformity is what makes the reuse safe for the multi-channel weight: +C AMP2(J) is accumulated inside MATRIX over this same loop, so a partial +C (per-row) de-duplication would rescale AMP2 non-uniformly across diagrams and +C bias AMP2(config)/XTOT even though the total |M|^2 stays exact. Halving every +C row scales AMP2(J) and XTOT by the same 1/2, leaving the ratio untouched. +C NCSCAN counts only the passes that actually ran the scan (NTRY alone is not +C enough: read_good_hel restores NTRY above the threshold without ever +C scanning, and the random-helicity branch bumps NTRY without a full sum). +C CSYMBAD/NCSCAN live in a COMMON block (zero-initialised, like NTRY above) so +C write_good_hel/read_good_hel can persist the verdict across jobs. + INTEGER FLIP(NCOMB), JHEL, KHEL + LOGICAL HELSAME, DEDUP + INTEGER CSYMBAD(MAXFLAVPERPROC), NCSCAN(MAXFLAVPERPROC) + COMMON/BLOCK_CSYM/CSYMBAD,NCSCAN + SAVE FLIP + DATA FLIP/NCOMB*0/ INTEGER FLAVOR(NEXTERNAL) INTEGER FLAVOR_FOR_SYM(NEXTERNAL) C Per-row FLAVOR lookup used by BROKEN_SYM. The IFLAV-indexed FLAVOR @@ -100,6 +123,34 @@ C BEGIN CODE C ---------- CALL GET_FLAVOR%(proc_id)s(IFLAV, FLAVOR) NTRY(IFLAV)=NTRY(IFLAV)+1 +C C-parity partner of each helicity row (all helicities negated): an +C involution, self-paired when every helicity is 0. Fixed per process, so +C built once (FLIP starts at 0). + IF (FLIP(1).EQ.0) THEN + DO I=1,NCOMB + FLIP(I)=I + DO JHEL=1,NCOMB + HELSAME=.TRUE. + DO KHEL=1,NEXTERNAL + IF (NHEL(KHEL,JHEL).NE.-NHEL(KHEL,I)) HELSAME=.FALSE. + ENDDO + IF (HELSAME) THEN + FLIP(I)=JHEL + EXIT + ENDIF + ENDDO + ENDDO +C A self-paired row (FLIP(I)=I, i.e. every helicity 0) has no distinct +C partner, so the loop could not be halved uniformly: refuse the +C de-duplication outright for every flavor in that case. + DO I=1,NCOMB + IF (FLIP(I).EQ.I) THEN + DO JHEL=1,MAXFLAVPERPROC + CSYMBAD(JHEL)=1 + ENDDO + ENDIF + ENDDO + ENDIF DO I=1,NEXTERNAL JC(I) = +1 ENDDO @@ -120,9 +171,20 @@ c WRITE(HEL_BUFF,'(20I5)') (0,I=1,NEXTERNAL) ENDDO ! If the helicity grid status is 0, this means that it is not yet initialized. +! C-parity de-duplication of this init full-sum loop is all-or-nothing per +! flavor and only kicks in once this flavor has actually completed the scan +! (NCSCAN>=20; the scan passes stay within MAXTRIES=25 so the reuse overlaps +! the grid build). A crossing is a separate subprocess directory in madevent, +! so every IFLAV row here is the uncrossed base process; polarized beams are +! self-excluded because the beam_polarization scaling makes the flipped +! partner's |M|^2 differ, so the scan latches CSYMBAD. + DEDUP = NCSCAN(IFLAV).GE.20 .AND. CSYMBAD(IFLAV).EQ.0 IF (ISUM_HEL.EQ.0.or.(DS_get_dim_status('Helicity').eq.0)) THEN DO I=1,NCOMB IF (GOODHEL(I,IFLAV) .OR. NTRY(IFLAV) .LE. MAXTRIES.OR.(ISUM_HEL.NE.0)) THEN +C Fast phase: every row is in a matched pair (all-or-nothing), so the +C higher-index partner is skipped and the lower index counted twice. + IF (DEDUP.AND.I.GT.FLIP(I)) CYCLE T=MATRIX%(proc_id)s(P,NHEL(1,I),IFLAV, IVEC) %(beam_polarization)s IF (ISUM_HEL.NE.0) then @@ -130,8 +192,29 @@ c WRITE(HEL_BUFF,'(20I5)') (0,I=1,NEXTERNAL) endif ANS=ANS+DABS(T) TS(I)=T +C The representative carries its skipped partner's identical +C contribution: copy |M|^2 to TS(FLIP) (so the per-helicity CDF and +C the DS grid pick both members), and count it once more in ANS. + IF (DEDUP.AND.I.LT.FLIP(I)) THEN + ANS=ANS+DABS(T) + TS(FLIP(I))=T + IF (ISUM_HEL.NE.0) call DS_add_entry('Helicity',FLIP(I),T) + ENDIF ENDIF ENDDO +C Scan phase: this pass evaluated every row, so it can test the pairing. +C A single mismatching pair (parity/C/polarization breaking) permanently +C disables the de-duplication for the whole flavor. + IF (NCSCAN(IFLAV).LT.20) THEN + NCSCAN(IFLAV)=NCSCAN(IFLAV)+1 + DO I=1,NCOMB + IF (FLIP(I).GT.I) THEN + IF (DABS(TS(I)-TS(FLIP(I))).GT.1D-6*(DABS(TS(I))+DABS(TS(FLIP(I))))) THEN + CSYMBAD(IFLAV)=1 + ENDIF + ENDIF + ENDDO + ENDIF IF(NTRY(IFLAV).EQ.(MAXTRIES+1)) THEN call reset_cumulative_variable() ! avoid biais of the initialization ENDIF 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 0000000000..2d7e757ac5 --- /dev/null +++ b/madgraph/iolibs/template_files/matrix_standalone_crossing_v4.inc @@ -0,0 +1,389 @@ + 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. +C FLAV_IDX comes back 0 for a code that names no crossing at all (see the +C two rejections below). PERM/SGN are left a valid permutation whatever +C happens, so a caller that gathers momenta with them never reads out of +C range; 0 is what GET_PDG_FOR_FLAVOR and GET_AMP already treat as "not a +C flavor", so no caller needs a new return convention. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) + INTEGER NCROSS + PARAMETER (NCROSS=(NEXTERNAL+1)*(NEXTERNAL+1)) +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 Out of range, or an overlapping-swap code: both transpositions {1,XI} and +C {2,XJ} active AND sharing a slot compose into a 3-cycle that the consumers +C read with opposite orientation, so it is pure redundancy. + IF (CROSS .LT. 0 .OR. CROSS .GT. NCROSS-1 .OR. + & (XI.NE.0 .AND. XI.NE.1 .AND. XJ.NE.0 .AND. XJ.NE.2 .AND. + & (XI.EQ.2 .OR. XJ.EQ.1 .OR. XI.EQ.XJ))) THEN + FLAV_IDX = 0 + RETURN + ENDIF + +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 + +C A crossing may only conjugate a leg that CHANGES SIDE. A transposition +C between two legs on the same side of the initial/final line conjugates +C both without moving either across, which is no crossing: for a 2 -> N +C process that is XI==2 / XJ==1, the beam swap, which must not conjugate +C anything (it would give e.g. u~ g > e+ ve d, not even charge conserving); +C for a 1 -> N one it is every XJ swap. + DO XK = 1, NEXTERNAL + IF (SGN(XK).EQ.-1 .AND. + & ((XK.LE.NINCOMING) .EQV. (PERM(XK).LE.NINCOMING))) THEN + FLAV_IDX = 0 + RETURN + ENDIF + ENDDO + + 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, and conjugation preserves both. +C So this half of the denominator is just the product of the per-particle +C spin*color (SPINCOL_PART, one entry per external leg) over the two legs +C the crossing puts in the initial state -- no per-crossing table needed. +C A crossing that cannot be applied returns 0, which SMATRIX / +C GET_PDG_FOR_FLAVOR / GET_ALL_INTER_CROSSED all map to a null result: this +C is the single gate that says whether a CROSS code is applicable at all. +C The flavor-dependent half is GET_IDENT_CROSS. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER NFLAV + PARAMETER (NFLAV=%(nflav)d) + INTEGER CROSS, XK, FACTOR, I, XIDX + INTEGER PERM(NEXTERNAL), SGN(NEXTERNAL) +C The DATA tables are emitted together (SPINCOL_PART and COUNTABLE here, +C plus the IDS_BASE/ANTIPID_BASE tables GET_IDENT_CROSS reads); each routine +C keeps its own copy rather than sharing a COMMON, which would need a BLOCK +C DATA unit to be DATA-initialised. + INTEGER SPINCOL_PART(0:NEXTERNAL-1) + INTEGER IDS_BASE(0:NEXTERNAL-1) + INTEGER ANTIPID_BASE(0:NEXTERNAL-1) + INTEGER COUNTABLE(0:NEXTERNAL-1) +%(iden_cross_lines)s + +C Decode through GET_CROSS_PERM rather than rebuilding the slot map here, +C so that which codes name a crossing at all is decided in exactly one +C place (it rejects out-of-range, overlapping-swap and same-side codes). + CALL %(proc_prefix)sGET_CROSS_PERM(CROSS*NFLAV+1, PERM, SGN, XIDX) + IF (XIDX .LT. 1) THEN + %(proc_prefix)sGET_SPINCOL_CROSS = 0 + RETURN + ENDIF +C Fail-safe for a decay chain: COUNTABLE is 0 for a leg that cannot be +C crossed (a leaf locked inside a decay block), and carrying one across the +C initial/final line would split its resonance. Generation already leaves +C such a crossing unrecorded, so nothing should ask for it -- this is the +C net under that, not the thing that makes it correct. + DO XK = 1, NEXTERNAL + IF (SGN(XK).EQ.-1 .AND. COUNTABLE(PERM(XK)-1).EQ.0) THEN + %(proc_prefix)sGET_SPINCOL_CROSS = 0 + RETURN + ENDIF + ENDDO +C Multiply the per-particle spin*color of the legs the crossing puts in the +C initial slots. + FACTOR = 1 + DO XK = 1, NINCOMING + FACTOR = FACTOR * SPINCOL_PART(PERM(XK)-1) + ENDDO + %(proc_prefix)sGET_SPINCOL_CROSS = FACTOR + + 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. Neither the per-slot representative +C PDG nor the FLAVOR source slot is tabulated per crossing: both follow from +C the crossing PERM/IC (as GET_SPINCOL_CROSS / GET_CROSS_PERM build it) +C applied to two NEXTERNAL-long base tables -- IDS_BASE, the base PDG of each +C leg, and ANTIPID_BASE, its charge conjugate for a leg that swapped side. + IMPLICIT NONE + INCLUDE 'nexternal.inc' + INTEGER CROSS + INTEGER FLAVOR(NEXTERNAL) +C SPINCOL_PART is unused here (GET_SPINCOL_CROSS owns it) but must be +C declared because the shared DATA block below initialises it. + INTEGER SPINCOL_PART(0:NEXTERNAL-1) + INTEGER IDS_BASE(0:NEXTERNAL-1) + INTEGER ANTIPID_BASE(0:NEXTERNAL-1) +C COUNTABLE(leg)=1 for a single external leg, 0 for a leaf locked inside a +C decay block. A crossing never moves a block leaf (GET_SPINCOL_CROSS +C rejects any that would), so decay products keep their base slots and must +C be skipped here: their contribution to the identical-final factor is +C resonance-level, carried whole by IDENT_RESONANCE. For a non-decay process +C every leg is countable and IDENT_RESONANCE is 1, so this is the plain leaf +C count it always was. + INTEGER COUNTABLE(0:NEXTERNAL-1) + INTEGER IDENT_RESONANCE + PARAMETER (IDENT_RESONANCE=%(ident_resonance)d) +%(iden_cross_lines)s + INTEGER K, L, N, FACT, XI, XJ, XT, I + INTEGER PERM(NEXTERNAL), ICS(NEXTERNAL), BPID(NEXTERNAL) + LOGICAL USED(NEXTERNAL) + +C Rebuild the slot->leg map PERM and its initial/final sign flips ICS from +C CROSS (identity plus the crossing's two transpositions), exactly as in +C GET_SPINCOL_CROSS, then read each slot's representative PDG straight off +C IDS_BASE (ANTIPID_BASE where the leg changed side). + XI = CROSS / (NEXTERNAL+1) + XJ = MOD(CROSS, NEXTERNAL+1) + DO K = 1, NEXTERNAL + PERM(K) = K + ICS(K) = 1 + ENDDO + IF (XI.NE.0 .AND. XI.NE.1) THEN + XT = PERM(1) + PERM(1) = PERM(XI) + PERM(XI) = XT + ICS(1) = -ICS(1) + ICS(XI) = -ICS(XI) + ENDIF + IF (XJ.NE.0 .AND. XJ.NE.2) THEN + XT = PERM(2) + PERM(2) = PERM(XJ) + PERM(XJ) = XT + ICS(2) = -ICS(2) + ICS(XJ) = -ICS(XJ) + ENDIF + DO K = 1, NEXTERNAL + IF (ICS(K).EQ.1) THEN + BPID(K) = IDS_BASE(PERM(K)-1) + ELSE + BPID(K) = ANTIPID_BASE(PERM(K)-1) + ENDIF + ENDDO + +C FLAVOR is not permuted by the crossing, so slot K reads FLAVOR(PERM(K)), +C the actual flavor of the original leg that moved into it. Start from +C IDENT_RESONANCE (the resonance-level part of the identical factor, which a +C crossing never changes) and multiply in the n! of the countable final legs +C only -- a decay-block leaf is skipped, its symmetry already inside +C IDENT_RESONANCE. + DO K = 1, NEXTERNAL + USED(K) = .FALSE. + ENDDO + FACT = IDENT_RESONANCE + DO K = NINCOMING+1, NEXTERNAL + IF (USED(K)) CYCLE + IF (COUNTABLE(PERM(K)-1).EQ.0) CYCLE + N = 1 + DO L = K+1, NEXTERNAL + IF (USED(L)) CYCLE + IF (COUNTABLE(PERM(L)-1).EQ.0) CYCLE + IF (BPID(K).EQ.BPID(L) .AND. + $ FLAVOR(PERM(K)).EQ.FLAVOR(PERM(L))) 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 9f9c015d44..0f37f611bc 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 0000000000..7755e57b1b --- /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 c2806387d6..1f176c9b98 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 ada2fd0eb1..68f45e3d7d 100644 --- a/madgraph/iolibs/template_files/matrix_standalone_v4.inc +++ b/madgraph/iolibs/template_files/matrix_standalone_v4.inc @@ -86,10 +86,32 @@ 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/ DATA GOODHEL/NGOODHEL_FLAV*.FALSE./ +C C-parity helicity de-duplication (see the SMATRIX loop): FLIP(IHEL) is the +C row with every helicity negated (built once, an involution); TSTORE caches +C the per-row |M|^2 within a scan point; DEDUP turns the reuse on in the fast +C phase only. The reuse is ALL-OR-NOTHING per flavor: CSYM(J) stays true only +C while EVERY row is in a genuine matched pair -- a self-paired row +C (FLIP(IHEL)=IHEL) or a single pair with |M(IHEL)|^2 != |M(FLIP)|^2 at any +C scan point clears it for good. Halving the whole loop (rather than an +C arbitrary subset of rows) is what keeps any per-row accumulation done +C inside MATRIX uniformly scaled, so the same rule is used by every backend. +C NTRY_CSYM counts only the uncrossed (cross 0) calls: CSYM is built and +C applied for the base process only, because a crossing permutes/sign-flips +C the helicities so FLIP (a base-row negation) is no longer the crossed +C C-parity partner. Crossed flavours therefore keep the full helicity sum. + INTEGER FLIP(NCOMB), JHEL, KHEL, NTRY_CSYM(NFLAV) + LOGICAL CSYM(NFLAV), HELSAME, DEDUP + REAL*8 TSTORE(NCOMB) + DATA FLIP/NCOMB*0/ + DATA CSYM/NNTRY_FLAV*.TRUE./ + DATA NTRY_CSYM/NNTRY_FLAV*0/ C C GLOBAL VARIABLES @@ -101,7 +123,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) @@ -119,6 +144,10 @@ c--------- if (HELRESET) then do i=1,NFLAV NTRY(i) = 0 + NTRY_CSYM(i) = 0 + enddo + do j=1,NFLAV + CSYM(j) = .true. enddo do i=1,NCOMB do j=1,NFLAV @@ -131,19 +160,45 @@ 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() +C C-parity partner of each helicity row (all helicities negated): an +C involution, self-paired when every helicity is 0. Fixed per process, so +C built once (FLIP starts at 0). + IF (FLIP(1).EQ.0) THEN + DO IHEL=1,NCOMB + FLIP(IHEL)=IHEL + DO JHEL=1,NCOMB + HELSAME=.TRUE. + DO KHEL=1,NEXTERNAL + IF (NHEL(KHEL,JHEL).NE.-NHEL(KHEL,IHEL)) HELSAME=.FALSE. + ENDDO + IF (HELSAME) THEN + FLIP(IHEL)=JHEL + EXIT + ENDIF + ENDDO + ENDDO + ENDIF + IF(USERHEL.EQ.-1) NTRY(FLAV_USE)=NTRY(FLAV_USE)+1 + IF(USERHEL.EQ.-1.AND.FLAV_IDX.LE.NFLAV) + & NTRY_CSYM(FLAV_USE)=NTRY_CSYM(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 @@ -157,24 +212,60 @@ C For this reason, we simply remove the filterin when there is only three ex ENDDO ENDDO ENDIF +C C-parity de-duplication is only safe for the plain unpolarized helicity +C sum of the uncrossed process (FLAV_IDX in [1,NFLAV]) and only once its own +C scan has settled (NTRY_CSYM>=20). + DEDUP = NTRY_CSYM(FLAV_USE).GE.20 .AND. CSYM(FLAV_USE) + & .AND. USERHEL.EQ.-1 + & .AND. POLARIZATIONS(0,0).EQ.-1 .AND. FLAV_IDX.LE.NFLAV 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 Fast phase: a row whose fully flipped C-parity partner has an +C identical |M|^2 (CSYM) is computed once, at the lower index, +C and counted twice -- skip the higher-index partner here. + IF (DEDUP.AND.IHEL.GT.FLIP(IHEL)) CYCLE +C MATRIX/GET_AMP get already crossed arrays and the reduced +C flavor index: the crossing was applied once, above. +%(smatrix_matrix_call)s +C Scan phase (uncrossed only): cache |M|^2 to test the +C C-parity partner below. + IF (FLAV_IDX.LE.NFLAV.AND.NTRY_CSYM(FLAV_USE).LT.20) + & TSTORE(IHEL)=T +C Fast phase: the representative carries its skipped partner's +C identical contribution. + IF (DEDUP.AND.IHEL.LT.FLIP(IHEL)) T=T+T 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) +C Scan phase: drop the C-parity pairing of any row whose fully flipped +C partner gave a different |M|^2 (parity/C violation). One mismatch at any +C scan point permanently invalidates the pair (robust, like the zero-filter). + IF (USERHEL.EQ.-1.AND.FLAV_IDX.LE.NFLAV + & .AND.NTRY_CSYM(FLAV_USE).LT.20 + & .AND.POLARIZATIONS(0,0).EQ.-1) THEN + DO IHEL=1,NCOMB + IF (FLIP(IHEL).EQ.IHEL) THEN +C Self-paired row: no distinct partner, so the loop cannot be +C halved uniformly -- refuse the reuse for this flavor. + CSYM(FLAV_USE)=.FALSE. + ELSE IF (FLIP(IHEL).GT.IHEL) THEN + IF (ABS(TSTORE(IHEL)-TSTORE(FLIP(IHEL))).GT. + & 1D-6*(ABS(TSTORE(IHEL))+ABS(TSTORE(FLIP(IHEL))))) THEN + CSYM(FLAV_USE)=.FALSE. + ENDIF + ENDIF + ENDDO + ENDIF +%(smatrix_iden_line)s IF(USERHEL.NE.-1) THEN ANS=ANS*HELAVGFACTOR ELSE @@ -196,6 +287,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 +360,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 +461,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 +471,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 +493,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 +616,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 +670,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 +686,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 +696,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 +725,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 +758,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 +804,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 +817,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 +916,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 +936,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 +949,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 +1119,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 +1209,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 455cec0773..75fceef53f 100755 --- a/madgraph/madevent/gen_ximprove.py +++ b/madgraph/madevent/gen_ximprove.py @@ -202,7 +202,8 @@ def get_helicity(self, to_submit=True, clean=True): zero_gc = list() all_zampperhel = set() all_bad_amps_perhel = set() - + all_csym_pairs = set() + for line in stdout.splitlines(): if "=" not in line and ":" not in line: continue @@ -212,6 +213,9 @@ def get_helicity(self, to_submit=True, clean=True): zero_gc.append(lsplit[0]) if 'Matrix Element/Good Helicity:' in line: all_hel.add(tuple(line.split()[3:5])) + if 'CSYM PAIR:' in line: + # (me_index, representative_hel, dropped_partner_hel) + all_csym_pairs.add(tuple(line.split()[2:5])) if 'Amplitude/ZEROAMP:' in line: all_zamp.add(tuple(line.split()[1:3])) if 'HEL/ZEROAMP:' in line: @@ -232,8 +236,18 @@ def get_helicity(self, to_submit=True, clean=True): all_good_hels = collections.defaultdict(list) for me_index, hel in all_hel: - all_good_hels[me_index].append(int(hel)) - + all_good_hels[me_index].append(int(hel)) + + # C-parity de-duplication: (representative -> dropped partner) pairs + # per matrix element, reported by matrix_orig.f. rep < flip, and + # both are good helicities with |M(rep)|^2 == |M(flip)|^2 at every + # scan point. The partner keeps its row (helicity table / |M|^2 sum) + # but its amplitudes are dropped from the recycled optim and its + # |M|^2 reused from the representative. + all_csym = collections.defaultdict(list) + for me_index, rep, flip in all_csym_pairs: + all_csym[me_index].append((int(rep), int(flip))) + #print(all_hel) if self.run_card['hel_zeroamp']: all_bad_amps = collections.defaultdict(list) @@ -271,8 +285,37 @@ def get_helicity(self, to_submit=True, clean=True): fsock.write(data) + # Crossing bases: bake the optim over the UNION good-hel of the + # crossing class so one compiled optim serves every crossing that + # enters it -- a cross-group dependent in another P directory (Track + # B) or a within-group matrix_router.f in this one (Track A). + # 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): - + + # Track B cross-group crossing: a dependent P directory reuses a + # base group's compiled matrix element, so its matrix_orig.f is + # a SYMLINK and crossgroup.mk symlinks the base's already-recycled + # matrix_optim.o over it. Running the (expensive) recycler here + # is redundant -- the resulting matrix_optim.f is never compiled + # (its .o comes from the base). But the P makefile discovers its + # matrix objects by the presence of matrix_optim.f, so a + # placeholder must still exist: copy the source (cheap) instead of + # recycling. The base directory, whose source is a real file, bakes + # the shared optim over the UNION good-hel of the whole class. + if os.path.islink(matrix_file): + files.cp(matrix_file, matrix_file.replace('orig', 'optim')) + continue + split_file = matrix_file.split('/') me_index = split_file[-1][len('matrix'):-len('_orig.f')] @@ -286,17 +329,50 @@ 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]) + # Crossing 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)] + + mtext = open(matrix_file).read() + nb_amp = int(re.findall(r'PARAMETER \(NGRAPHS=(\d+)\)', mtext)[0]) + if self.run_card['hel_zeroamp']: - bad_amps = [str(x) for x in sorted(all_bad_amps[me_index])] bad_amps_perhel = [x for x in sorted(all_bad_amps_perhel[me_index])] else: - bad_amps = [] + bad_amps = [] bad_amps_perhel = [] + + # C-parity de-duplication: for each surviving pair KEEP both rows + # in the helicity table (so the |M|^2 sum and the event-helicity + # CDF stay complete) but drop the partner's amplitudes -- add every + # (partner, graph) to bad_amps_perhel so its HELAS calls are never + # generated -- and reuse the representative's |M|^2 for it. The + # reuse indices are the OPTIM's re-indexed positions in good_hels + # (helicity indices are renumbered 1..len(good_hels) in the optim). + # Disabled for a crossing-class base (perms), which keeps every + # config unoptimised. + csym_reuse_pairs = [] + if not perms and all_csym[me_index]: + opt_index = {h: i + 1 for i, h in enumerate(sorted(good_set))} + bad_set = set(bad_amps_perhel) + for rep, flip in all_csym[me_index]: + if rep in good_set and flip in good_set: + for a in range(1, nb_amp + 1): + bad_set.add((flip, a)) + csym_reuse_pairs.append((opt_index[rep], opt_index[flip])) + bad_amps_perhel = sorted(bad_set) if __debug__: - mtext = open(matrix_file).read() - nb_amp = int(re.findall(r'PARAMETER \(NGRAPHS=(\d+)\)', mtext)[0]) logger.debug('(%s) nb_hel: %s zero amp: %s bad_amps_hel: %s/%s', split_file[-1], len(good_hels),len(bad_amps),len(bad_amps_perhel), len(good_hels)*nb_amp ) if len(good_hels) == 1: files.cp(matrix_file, matrix_file.replace('orig','optim')) @@ -305,6 +381,12 @@ def get_helicity(self, to_submit=True, clean=True): gauge = self.cmd.proc_characteristics['gauge'] recycler = hel_recycle.HelicityRecycler(good_hels, bad_amps, bad_amps_perhel, gauge=gauge) + # C-parity de-duplication: copy each dropped partner's |M|^2 from + # its representative (both are real fortran helicity indices). + if csym_reuse_pairs: + recycler.template_dict['csym_reuse'] = '\n'.join( + ' TS(%d) = TS(%d)' % (flip, rep) + for rep, flip in sorted(csym_reuse_pairs)) + '\n' # In case of bugs you can play around with these: recycler.hel_filt = self.run_card['hel_filtering'] recycler.amp_splt = self.run_card['hel_splitamp'] diff --git a/madgraph/madevent/hel_recycle.py b/madgraph/madevent/hel_recycle.py index af0a1e7daa..4729b346da 100755 --- a/madgraph/madevent/hel_recycle.py +++ b/madgraph/madevent/hel_recycle.py @@ -411,8 +411,15 @@ def __init__(self, good_elements, bad_amps=[], bad_amps_perhel=[], gauge='U'): self.template_dict['helas_calls'] = [] self.template_dict['jamp_lines'] = '\n' self.template_dict['amp2_lines'] = '\n' - self.template_dict['ncomb'] = '0' - self.template_dict['nwavefuncs'] = '0' + self.template_dict['ncomb'] = '0' + self.template_dict['nwavefuncs'] = '0' + # C-parity de-duplication: fortran that copies a dropped C-partner's + # |M|^2 back from its representative (TS(flip)=TS(rep)). Empty unless + # gen_ximprove supplies C-symmetric pairs: it keeps the partner's + # helicity row but adds all its amplitudes to bad_amps_perhel, so their + # HELAS calls are never generated and only the representatives are + # computed. The indices here are the optim's re-numbered helicities. + self.template_dict['csym_reuse'] = '\n' self.dag = DAG() diff --git a/madgraph/various/process_checks.py b/madgraph/various/process_checks.py index 423ec5d603..bce65fc5eb 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 b4d1bac787..243065d4a8 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,439 @@ 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': '', + # ---- C-parity good-helicity de-duplication (uncrossed only) ---- + # Two helicity rows that are exact mirrors (every helicity negated) + # give an identical |M|^2 under a parity/C-conserving amplitude, so + # one need not be recomputed. This is the NON-crossing path: cGoodHel + # stays the full good-helicity list (the per-helicity event-selection + # CDF and selected_hel_code stay exact), but calculate_jamps is called + # only for the lower-index representative of each surviving C-pair and + # its |M|^2 is reused for the partner -- halving the expensive kernel + # calls for a C-symmetric process. csym is detected in the (serial) + # getGoodHel scan (thread-safe), so sigmaKin only reads the tables. + # The crossing path keeps the full sum (see the crossing return): its + # per-lane SIMD loop already runs cNGoodMaxCross times regardless of a + # single crossing's list, so reusing a base-row |M|^2 would not save a + # kernel call there anyway. + 'csym_statics': + '#ifndef MGONGPUCPP_GPUIMPL\n' + ' static int cFlip[ncomb]; // C-parity partner: every helicity negated (an involution)\n' + ' static bool cCsymBad; // latched: ANY row unpaired or |M(ihel)| != |M(cFlip)| at a scan point\n' + ' static bool cCsymOk; // all-or-nothing: every good hel sits in a distinct C-symmetric pair\n' + '#endif', + 'csym_gh_flip': + ' fptype me_scan[ncomb][neppV]; // per-hel |M|^2 of this scan page, for the C-parity test\n' + ' cCsymBad = false;\n' + ' for( int _h = 0; _h < ncomb; _h++ ) {\n' + ' cFlip[_h] = _h;\n' + ' for( int _j = 0; _j < ncomb; _j++ ) {\n' + ' bool _same = true;\n' + ' for( int _k = 0; _k < npar; _k++ ) if( cHel[_j][_k] != -cHel[_h][_k] ) _same = false;\n' + ' if( _same ) { cFlip[_h] = _j; break; }\n' + ' }\n' + ' }\n', + 'csym_gh_record': + ' for( int _ie = 0; _ie < neppV; ++_ie ) me_scan[ihel][_ie] = allMEs[ievt00 + _ie];\n', + 'csym_gh_check': + ' for( int _h = 0; _h < ncomb; _h++ ) {\n' + ' if( cFlip[_h] > _h ) {\n' + ' for( int _ie = 0; _ie < neppV; ++_ie ) {\n' + ' const fptype _a = me_scan[_h][_ie];\n' + ' const fptype _b = me_scan[cFlip[_h]][_ie];\n' + ' fptype _d = _a - _b; if( _d < (fptype)0. ) _d = -_d;\n' + ' fptype _aa = _a < (fptype)0. ? -_a : _a;\n' + ' fptype _bb = _b < (fptype)0. ? -_b : _b;\n' + ' if( _d > (fptype)1e-6 * ( _aa + _bb ) ) cCsymBad = true;\n' + ' }\n' + ' }\n' + ' }\n', + 'csym_pairbuild': + '#ifndef MGONGPUCPP_GPUIMPL\n' + ' cCsymOk = !cCsymBad;\n' + ' for( int _h = 0; _h < ncomb; _h++ )\n' + ' if( isGoodHel[_h] && ( cFlip[_h] == _h || !isGoodHel[cFlip[_h]] ) ) cCsymOk = false;\n' + '#endif\n', + 'csym_me_decl': + ' fptype_sv meOfIhel[ncomb] = {}; // per-good-hel |M|^2 (page 1), for C-parity reuse\n' + '#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT\n' + ' fptype_sv meOfIhel2[ncomb] = {};\n' + '#endif\n', + 'csym_skip': + ' if( cCsymOk && ihel > cFlip[ihel] ) {\n' + ' // C-parity partner: reuse the representative\'s |M|^2 (identical), skip calculate_jamps.\n' + ' fptype_sv& _me1 = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 ) );\n' + ' _me1 = _me1 + meOfIhel[cFlip[ihel]];\n' + ' MEs_ighel[ighel] = _me1;\n' + '#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT\n' + ' fptype_sv& _me2 = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 + neppV ) );\n' + ' _me2 = _me2 + meOfIhel2[cFlip[ihel]];\n' + ' MEs_ighel2[ighel] = _me2;\n' + '#endif\n' + ' continue;\n' + ' }\n' + ' const fptype_sv _me1before = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 ) );\n' + '#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT\n' + ' const fptype_sv _me2before = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 + neppV ) );\n' + '#endif\n', + 'csym_record': + ' meOfIhel[ihel] = MEs_ighel[ighel] - _me1before;\n' + '#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT\n' + ' meOfIhel2[ihel] = MEs_ighel2[ighel] - _me2before;\n' + '#endif\n', + } + 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()) + # Per-leg base tables only: the crossing is decoded at runtime + # (cross_perm_ic, mirroring the fortran GET_CROSS_PERM) instead of + # tabulating anything per crossing. _build_flav_pdg_tables gives the base + # signed PDG per (flavor, leg) and its charge conjugate, from which + # flavorPDG rebuilds the crossed PDGs at runtime (see flavorpdg_body). + n_flavors, pdg_flat, antipdg_flat = Fort._build_flav_pdg_tables(self, me) + + def arr(vals): + return '{ ' + ', '.join(str(v) for v in vals) + ' }' + + crossing_decl = ( + " // ---- Crossing symmetry (extended id = cross*nmaxflavor + flav) ----\n" + " // A crossing is a fixed slot relabelling decoded from the crossing\n" + " // code at runtime (cross_perm_ic, mirroring the fortran\n" + " // GET_CROSS_PERM): perm[k] is the input slot landing in crossed slot\n" + " // k and ic[k] its NSF sign flip, left a valid permutation (identity\n" + " // for an inapplicable code) so a momentum gather never reads out of\n" + " // range. The two halves of the denominator are rebuilt from small\n" + " // per-leg tables, so no cross-indexed table is stored.\n" + " __host__ __device__ inline bool cross_perm_ic( int cross, int* perm, int* ic )\n" + " {\n" + " constexpr int ncross = ( npar + 1 ) * ( npar + 1 );\n" + " for ( int k = 0; k < npar; k++ ) { perm[k] = k; ic[k] = 1; }\n" + " if ( cross < 0 || cross >= ncross ) return false;\n" + " const int xi = cross / ( npar + 1 );\n" + " const int xj = cross %% ( npar + 1 );\n" + " // Overlapping-swap codes compose into a 3-cycle the consumers read\n" + " // with opposite orientation: pure redundancy, invalid.\n" + " if ( xi != 0 && xi != 1 && xj != 0 && xj != 2 &&\n" + " ( xi == 2 || xj == 1 || xi == xj ) ) return false;\n" + " if ( xi != 0 && xi != 1 )\n" + " { int t = perm[0]; perm[0] = perm[xi - 1]; perm[xi - 1] = t; ic[0] = -ic[0]; ic[xi - 1] = -ic[xi - 1]; }\n" + " if ( xj != 0 && xj != 2 )\n" + " { int t = perm[1]; perm[1] = perm[xj - 1]; perm[xj - 1] = t; ic[1] = -ic[1]; ic[xj - 1] = -ic[xj - 1]; }\n" + " return true;\n" + " }\n" + " // Initial-state spin*color average of the crossed process: product of\n" + " // the per-leg spin*color (spincol_part, conjugation invariant) over\n" + " // the legs the crossing puts in the initial state. 0 if inapplicable.\n" + " __device__ inline int spincol_cross( int cross )\n" + " {\n" + " static const int spincol_part[npar] = %(spincol_part)s;\n" + " int perm[npar], ic[npar];\n" + " if ( !cross_perm_ic( cross, perm, ic ) ) return 0;\n" + " int factor = 1;\n" + " for ( int k = 0; k < %(ninitial)d; k++ ) factor *= spincol_part[perm[k]];\n" + " return factor;\n" + " }\n" + " // Identical-final-state factor (product of n!) of the crossed\n" + " // process. Flavor dependent -> runtime: two crossed final legs are\n" + " // identical when they carry the same flavor group (same representative\n" + " // PDG -- ids_base, conjugated to antipid_base when the leg swapped\n" + " // side) and the same actual flavor. FLAVOR is not permuted, so slot k\n" + " // reads cFlavors[iflavor][perm[k]].\n" + " __device__ int ident_cross( int cross, int iflavor )\n" + " {\n" + " static const int ids_base[npar] = %(ids_base)s;\n" + " static const int antipid_base[npar] = %(antipid_base)s;\n" + " int perm[npar], ic[npar];\n" + " cross_perm_ic( cross, perm, ic );\n" + " int bpid[npar];\n" + " for ( int k = 0; k < npar; k++ )\n" + " bpid[k] = ( ic[k] == 1 ) ? ids_base[perm[k]] : antipid_base[perm[k]];\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 ( bpid[k] == bpid[l] &&\n" + " cFlavors[iflavor][perm[k]] == cFlavors[iflavor][perm[l]] )\n" + " {\n" + " used[l] = true;\n" + " n = n + 1;\n" + " fact = fact * n;\n" + " }\n" + " }\n" + " }\n" + " return fact;\n" + " }\n" + ) % {'spincol_part': arr(tables['spincol_part']), + 'ids_base': arr(tables['ids_base']), + 'antipid_base': arr(tables['antipid_base']), + 'ninitial': ninitial} + + # Per-leg helicity states used to re-encode a crossed helicity config + # into its canonical code. allow_reverse=True is NOT optional: it is the + # order the cHel/tHel table itself is built in (get_helicity_matrix + # above, allow_reverse=True) AND the order the fortran ENCODE_HEL STATES + # table uses (get_helicity_encoder_dict), which together define the + # canonical code. get_helicity_states reverses the list for an + # ANTIparticle leg, so with allow_reverse=False every such leg's digit + # lookup is off by one state and the code comes out wrong: for + # u u~ > t t~ legs 1 and 4 give (+1,-1) not (-1,+1), and all 16 rows + # mis-encode. Like the fortran encoder this deliberately ignores + # wf['polarization'] -- the code space is the FULL mixed-radix space, a + # polarized leg simply never reaches its filtered-out digits. + pdict = me.get('processes')[0].get('model').get('particle_dict') + hstates = [pdict[wf.get('pdg_code')].get_helicity_states(True) + 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), validated at runtime + # against the fortran backend -- see the generated comment. + 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" + " // The perm digit-permute with NO NSF sign flip is the right\n" + " // transform, and it is what mg7 needs: the LHE writer indexes the\n" + " // BASE helicity table POSITIONALLY (export_mg7 ships\n" + " // get_helicity_matrix() as `helicities`, lhe_output.cpp reads row\n" + " // `helicity_index` slot by slot), so the reported row must be the\n" + " // base row whose config EQUALS the crossed one -- not the row the\n" + " // lane evaluated. Validated at runtime against the fortran backend\n" + " // (SMATRIXHEL per canonical code at the same momenta and the same\n" + " // extended flavor id): for the recorded crossing of p p > w+ j and\n" + " // for u u~ > g g crossed to u g > u g, every reported code has a\n" + " // non-zero |M|^2 and the reported frequencies follow the fortran\n" + " // per-code |M|^2 weights.\n" + " //\n" + " // xhel_states MUST be the allow_reverse=True per-leg order: it is\n" + " // both the order cHel is built in and the order the fortran\n" + " // ENCODE_HEL STATES table uses. allow_reverse=False reverses every\n" + " // ANTIparticle leg, which silently shifts the code onto a row whose\n" + " // |M|^2 is zero and aborts helicity-by-helicity reweighting.\n" + " //\n" + " // Limitation (shared with the fortran ENCODE_HEL, whose D=1 fallback\n" + " // this mirrors): a crossing that lands a leg in a slot with a\n" + " // DIFFERENT number of helicity states -- e.g. a massive vector moved\n" + " // into a fermion slot -- has no representable base row, and the\n" + " // lookup falls back to digit 0. That can only happen for a crossing\n" + " // that is merely APPLICABLE and never recorded by the generation\n" + " // (a recorded one only ever swaps partons, all 2-state); consumers\n" + " // must intersect with the recorded crossing codes anyway.\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_nhstate[npar] = %(xnhstate)s;\n" + " static const int xhel_states[npar * maxhel] = %(xstates)s;\n" + " int xperm[npar], xic[npar];\n" + " cross_perm_ic( xcross, xperm, xic ); // NSF sign in xic is not used here\n" + " int code = 0;\n" + " for ( int k = 0; k < npar; k++ )\n" + " {\n" + " const int val = (int)cHel[base_ihel][xperm[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" + "#ifndef MGONGPUCPP_GPUIMPL\n" + " // Reported helicity of ONE lane. The host good-helicity loop runs\n" + " // over cNGoodMaxCross and every lane evaluates its OWN crossing's\n" + " // ighel-th good helicity (cGoodHelOfCross, see calculate_jamps), so\n" + " // the reported row must be read from that same per-crossing list.\n" + " // Reading the union cGoodHel[ighel] instead names a row the lane\n" + " // never evaluated: as soon as the crossings widen the union beyond a\n" + " // single crossing's list the two lists stop agreeing even for the\n" + " // identity crossing, and the event is written out with a helicity\n" + " // whose |M|^2 is zero (breaking helicity-by-helicity reweighting).\n" + " __device__ inline int selected_hel_code_lane( int ighel, unsigned int flavor_id )\n" + " {\n" + " const int lcross = (int)( flavor_id / nmaxflavor );\n" + " const int lngood = cNGoodPerCross[lcross];\n" + " // ighel < lngood always holds when the CDF selected this lane's\n" + " // row (the rows past lngood add nothing to the running sum); the\n" + " // clamp only keeps a degenerate lane inside the table.\n" + " const int lbase = cGoodHelOfCross[lcross][( ighel < lngood ) ? ighel\n" + " : ( lngood > 0 ? lngood - 1 : 0 )];\n" + " return selected_hel_code( lbase, flavor_id );\n" + " }\n" + "#endif\n" + ) % {'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" + " // Applied per lane straight onto MEs_sv: an invalid crossing must\n" + " // ASSIGN 0 (not multiply), because its unphysical momentum\n" + " // relabelling can make the lane's |M|^2 a NaN and nan*0 = nan.\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& me = reinterpret_cast( &MEs_sv )[ieppV];\n" + " if ( dcr == 0 )\n" + " me *= (fptype)broken_symmetry_factor( dfl ) / helcolDenominators[0];\n" + " else if ( spincol_cross( dcr ) == 0 )\n" + " me = (fptype)0.; // invalid crossing (out of range / overlapping swap) -> ME 0\n" + " else\n" + " me *= (fptype)1. / ( (fptype)spincol_cross( dcr ) * (fptype)ident_cross( dcr, dfl ) );\n" + " }" + ) + + # Crossed physical signed PDG per (extended id, leg), rebuilt at runtime + # like the fortran GET_PDG_FOR_FLAVOR: base signed PDG of the leg the + # crossing moves into slot ipar (base_pdg per (flavor, leg)), charge- + # conjugated when that leg swapped side -- no per-crossing PDG table. + flavorpdg_body = ( + " const int ncross = ( npar + 1 ) * ( npar + 1 );\n" + " if ( iflavor < 0 || iflavor >= ncross * nmaxflavor ) return 0;\n" + " static const int base_pdg[nmaxflavor * npar] = %(base_pdg)s;\n" + " static const int base_antipdg[nmaxflavor * npar] = %(base_antipdg)s;\n" + " const int cross = iflavor / nmaxflavor;\n" + " const int flav0 = iflavor %% nmaxflavor;\n" + " int perm[npar], ic[npar];\n" + " if ( !cross_perm_ic( cross, perm, ic ) ) return 0; // invalid crossing\n" + " const int src = perm[ipar];\n" + " return ( ic[ipar] == 1 ) ? base_pdg[flav0 * npar + src]\n" + " : base_antipdg[flav0 * npar + src];" + ) % {'base_pdg': arr(pdg_flat[:nflav * nexternal]), + 'base_antipdg': arr(antipdg_flat[:nflav * nexternal])} + + 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 row this lane actually evaluated + # (its crossing's ighel-th good helicity, NOT the union list), mapped + # to the crossed code for the event's crossing (the crossed mapping + # itself is unvalidated at runtime, see selected_hel_code). + 'selected_hel_code_1': + 'selected_hel_code_lane( ighel, iflavorVec[ievt] )', + 'selected_hel_code_2': + 'selected_hel_code_lane( 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', + # C-parity good-helicity de-duplication is disabled under crossing: + # the per-lane SIMD loop already runs cNGoodMaxCross times whatever a + # single crossing's good-hel list is, so reusing a base-row |M|^2 + # would save no kernel call, and a base-row FLIP is not the crossed + # C-parity partner anyway. Every csym hole is therefore empty here, + # leaving the validated crossing path byte-for-byte unchanged. + 'csym_statics': '', + 'csym_gh_flip': '', + 'csym_gh_record': '', + 'csym_gh_check': '', + 'csym_pairbuild': '', + 'csym_me_decl': '', + 'csym_skip': '', + 'csym_record': '', + } + #------------------------------------------------------------------------------------ import madgraph.core.helas_objects as helas_objects @@ -2501,13 +3050,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 +3197,169 @@ 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 '' + + 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).""" + return """#ifndef MGONGPUCPP_GPUIMPL + // === CROSSING SYMMETRY: per-event momentum permutation (NOT vectorized) === + // The crossing slot permutation and NSF signs are decoded per event from + // its crossing code (cross_perm_ic), not read from a per-crossing table. + 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 ); + int xperm[npar], xic[npar]; + cross_perm_ic( xcr, xperm, xic ); + for( int s = 0; s < npar; ++s ) + { + const int src = xperm[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)xic[s]; + } + } +#endif +""" + + @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 1820b10e96..9d8ee4bc23 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/models/template_files/fortran/makefile_madevent b/models/template_files/fortran/makefile_madevent index 1adb69234d..163b733bd9 100644 --- a/models/template_files/fortran/makefile_madevent +++ b/models/template_files/fortran/makefile_madevent @@ -49,6 +49,12 @@ clean: couplings.o: ../maxparticles.inc ../run.inc ../vector.inc couplings2.o: ../vector.inc +# flavor_couplings.o provides the model_object F90 module (MODULE MODEL_OBJECT) +# that every other object in $(MODEL) does "use model_object". Build it first so +# a parallel (-j) sub-make cannot compile a consumer before model_object.mod +# exists. Order-only (|) so consumers are not relinked when it merely rebuilds. +$(filter-out flavor_couplings.o,$(MODEL)): | flavor_couplings.o + ../run.inc: touch ../run.inc diff --git a/tests/acceptance_tests/test_cmd.py b/tests/acceptance_tests/test_cmd.py index 0f9a64b0b5..bd266bccac 100755 --- a/tests/acceptance_tests/test_cmd.py +++ b/tests/acceptance_tests/test_cmd.py @@ -233,6 +233,7 @@ def test_config(self): 'samurai': None, 'max_t_for_channel': 99, 'zerowidth_tchannel': True, + 'zerowidth_external': True, 'auto_convert_model': True, 'nlo_mixed_expansion': True, 'acknowledged_v3.1_syntax': True, @@ -605,7 +606,9 @@ def test_custom_propa(self): re.IGNORECASE) me_groups = me_re.search(log_output) self.assertTrue(me_groups) - self.assertAlmostEqual(float(me_groups.group('value')), 0.592626100) + # value shifted at the 7th digit by the NHEL helicity-summation reorder + # (MG7 --crossing branch); physics unchanged. + self.assertAlmostEqual(float(me_groups.group('value')), 0.5926263) def test_ufo_aloha_merged(self): """Test the import of models and the export of Helas Routine """ @@ -795,7 +798,13 @@ def test_standalone_wwjj(self): if os.path.isdir(self.out_dir): shutil.rmtree(self.out_dir) - self.do('generate p p > w+ w- j j QCD=0') + # --use_crossing=False pins the UNFOLDED subprocess layout: this test + # opens the q q~ > w+ w- q q~ directory itself, and the standalone output + # does support crossing, so by default that subprocess is folded into a + # base directory and no longer exists on its own. Nothing is being worked + # around here -- the folded matrix element is checked to give the same + # numbers by the crossing and consistency suites. + self.do('generate p p > w+ w- j j QCD=0 --use_crossing=False') self.do('output standalone %s ' % self.out_dir) sub_root = os.path.join(self.out_dir, 'SubProcesses') @@ -835,10 +844,11 @@ def test_standalone_merged_flavor_uq_zuq(self): mixes a fixed u leg with a merged-quark leg, and asserts that the standalone matrix elements for the two surviving flavor assignments match the reference values obtained by running each - flavor as its own explicit process: + flavor as its own explicit process (see the note by ``references`` + below: these were bumped ~0.1% by the ALOHA t-channel width drop): - u d > Z u d -> 1.4704291881825141E-006 - u u > Z u u -> 3.5590322244693227E-008 + u d > Z u d -> 1.4718113670817815E-006 + u u > Z u u -> 3.5626573789048226E-008 The same checks are repeated with ``--mask=False`` so the regression is guarded both with and without the per-flavor @@ -852,9 +862,23 @@ def test_standalone_merged_flavor_uq_zuq(self): unaffected. """ + # Reference matrix elements for u d > Z u d and u u > Z u u. + # + # Updated on the MG7 crossing branch (claude/fortran-cross-symmetry-3f13f3) + # after commit 4ec2ae7d5 "aloha: drop the T-channel (spacelike) + # propagator width at runtime". u q > Z u q proceeds through a spacelike + # (t-channel) electroweak propagator, and ALOHA now drops the width of a + # spacelike propagator: a spacelike momentum can never reach the pole, so + # the Breit-Wigner width term there is spurious. This shifts the matrix + # element by ~0.1%; it is independent of crossing and of the per-flavor + # mask (verified: identical for --use_crossing on/off and --mask on/off). + # + # Previous values (t-channel width kept), for reference: + # (2, 1, 23, 2, 1): 1.4704291881825141e-06 + # (2, 2, 23, 2, 2): 3.5590322244693227e-08 references = { - (2, 1, 23, 2, 1): 1.4704291881825141e-06, - (2, 2, 23, 2, 2): 3.5590322244693227e-08, + (2, 1, 23, 2, 1): 1.4718113670817815e-06, + (2, 2, 23, 2, 2): 3.5626573789048226e-08, } me_re = re.compile( @@ -917,6 +941,72 @@ def test_standalone_merged_flavor_uq_zuq(self): 'expected %s' % (label, pdg, results[pdg], expected))) + def test_standalone_crossing_folds_qqx_subprocess(self): + """The crossing (default) counterpart of the two tests below. + + test_standalone_flavor_mask and test_standalone_wwjj both pass + --use_crossing=False because they open one specific subprocess directory, + which the default (crossing on) standalone output folds away. That leaves + the folded layout of this very process untested here, so cover it: with + crossing on the q q~ > q q~ directory must be *gone*, the output must be + strictly smaller, and the base subprocess that absorbed it must carry the + crossing machinery plus a PDG entry for the folded initial state -- i.e. + the subprocess is folded, not dropped. + """ + def build(options, name): + out = pjoin(self.out_dir, name) + if os.path.isdir(out): + shutil.rmtree(out) + self.do('generate p p > j j QCD=0 %s' % options) + self.do('output standalone %s -f' % out) + sub = pjoin(out, 'SubProcesses') + return sorted(d for d in os.listdir(sub) if d.startswith('P')) + + if os.path.isdir(self.out_dir): + shutil.rmtree(self.out_dir) + os.makedirs(self.out_dir) + + crossed = build('', 'crossed') + plain = build('--use_crossing=False', 'plain') + + # Folding really happened: fewer directories, and the one the sibling + # tests inspect is not among them any more. + self.assertLess(len(crossed), len(plain), + 'crossing did not fold anything: %s vs %s' + % (crossed, plain)) + qqx_plain = [d for d in plain if 'QQx' in d and d.endswith('QQx')] + self.assertTrue(qqx_plain, 'uncrossed build lost q q~ > q q~: %s' % plain) + self.assertEqual([d for d in crossed if 'QQx' in d and d.endswith('QQx')], + [], 'q q~ > q q~ should be folded away: %s' % crossed) + + # ... and it is reachable from a surviving base rather than dropped: some + # base emits the crossing machinery and declares the q q~ initial state. + sub = pjoin(self.out_dir, 'crossed', 'SubProcesses') + with_machinery = [] + for d in crossed: + matrix = pjoin(sub, d, 'matrix.f') + if not os.path.exists(matrix): + continue + text = open(matrix).read() + if 'APPLY_CROSSING' in text: + with_machinery.append(d) + self.assertTrue(with_machinery, + 'no crossed base emits the crossing machinery: %s' + % crossed) + # GET_PDG_FOR_FLAVOR is what a caller uses to reach a folded crossing; + # check_sa demoes it, so the folded quark initial state must show up. + demoed = set() + for d in with_machinery: + check_sa = pjoin(sub, d, 'check_sa.f') + if not os.path.exists(check_sa): + continue + for m in re.finditer(r'PDG_FOR_FLAVOR\(\s*\d+\s*,\s*\d+\s*\)\s*=\s*' + r'(-?\d+)', open(check_sa).read()): + demoed.add(int(m.group(1))) + self.assertTrue(demoed & {1, 2, 3, 4, -1, -2, -3, -4}, + 'no quark initial state demoed by the folded bases: %s' + % sorted(demoed)) + def test_standalone_flavor_mask(self): """Acceptance test for the per-flavor masking optimization. @@ -939,7 +1029,12 @@ def test_standalone_flavor_mask(self): if os.path.isdir(self.out_dir): shutil.rmtree(self.out_dir) - self.do('generate p p > j j QCD=0') + # --use_crossing=False pins the UNFOLDED subprocess layout: this test + # inspects the q q~ > q q~ directory and its per-flavor mask, and the + # standalone output does support crossing, so by default that subprocess + # is folded into a base directory. The mask of the folded matrix element + # is covered by the crossing suite; this one is about the plain layout. + self.do('generate p p > j j QCD=0 --use_crossing=False') devnull = open(os.devnull, 'w') def find_qqx(sub_root): @@ -1347,7 +1442,9 @@ def test_standalone_cpp(self): me_groups = me_re.search(log_output) self.assertTrue(me_groups) - self.assertAlmostEqual(float(me_groups.group('value')), 6.4739191,5) + # g g > go go: shifted at the 5th digit by the NHEL helicity-summation + # reorder (MG7 --crossing branch); same value as the mssm short-xsec ref. + self.assertAlmostEqual(float(me_groups.group('value')), 6.4739329,5) # Cross-check standalone_mg7 (madmatrix) against standalone_cpp for this # massive BSM process. The Fortran/C++ ./check auto-bumps the CM energy @@ -1759,7 +1856,18 @@ def test_standalone_density(self): # We changed the value of the reference by a factor of 256, which is the inclusion of IDEN in get_inter in matrix. # original_sol = {(-1, -1, 1, 1): (0.02827952274928987, 0.0), (-1, -1, 1, -1): (-0.0041892876162345, -0.0041923830983622255), (-1, 1, 1, 1): (0.000469685615962711, 0.0006142055733429721), (-1, 1, 1, -1): (-0.01784029173125566, -0.00794999696313525), (-1, -1, -1, -1): (0.02532739017396033, 0.0), (-1, 1, -1, 1): (-0.00028182588524174187, 0.0024162264334765746), (-1, 1, -1, -1): (-0.00048593945847553023, -0.0006039982074415239), (1, 1, 1, 1): (0.025301510150454294, 0.0), (1, 1, 1, -1): (0.004212401136919661, 0.0042167644618831875), (1, 1, -1, -1): (0.028322721746299958, 0.0)} - original_sol = {(-1, -1, 1, 1): (0.00011046688573941356, 0.0), (-1, -1, 1, -1): (-1.6364404750916015e-05, -1.6376496477977443e-05), (-1, 1, 1, 1): (1.83470943735434e-06, 2.3992405208709848e-06), (-1, 1, 1, -1): (-6.968863957521743e-05, -3.105467563724707e-05), (-1, -1, -1, -1): (9.893511786703254e-05, 0.0), (-1, 1, -1, 1): (-1.1008823642255542e-06, 9.43838450576787e-06), (-1, 1, -1, -1): (-1.89820100967004e-06, -2.359367997818453e-06), (1, 1, 1, 1): (9.883402402521209e-05, 0.0), (1, 1, 1, -1): (1.6454691941092424e-05, 1.64717361792312e-05), (1, 1, -1, -1): (0.00011063563182148421, 0.0)} + # Updated on the MG7 crossing branch (claude/fortran-cross-symmetry-3f13f3). + # Two intended changes shifted these ~0.1%: (a) zerowidth_external + # (commit 35706c9ae, default on) drops the width of the internal top + # propagator because the top is an external final state of p p > j t t~; + # (b) the canonical helicity encoder (commit 2b22dd566) fixed a small + # C-parity asymmetry the old reference carried -- for QCD g g > g t t~ + # the t-tbar spin density matrix must obey rho(h,h') = rho(-h,-h'), which + # the new values satisfy to float precision (e.g. (1,1,1,1) == + # (-1,-1,-1,-1) and (-1,-1,1,1) == (1,1,-1,-1)) while the old ones did not. + # Previous values (width kept, slightly asymmetric): + # original_sol = {(-1, -1, 1, 1): (0.00011046688573941356, 0.0), (-1, -1, 1, -1): (-1.6364404750916015e-05, -1.6376496477977443e-05), (-1, 1, 1, 1): (1.83470943735434e-06, 2.3992405208709848e-06), (-1, 1, 1, -1): (-6.968863957521743e-05, -3.105467563724707e-05), (-1, -1, -1, -1): (9.893511786703254e-05, 0.0), (-1, 1, -1, 1): (-1.1008823642255542e-06, 9.43838450576787e-06), (-1, 1, -1, -1): (-1.89820100967004e-06, -2.359367997818453e-06), (1, 1, 1, 1): (9.883402402521209e-05, 0.0), (1, 1, 1, -1): (1.6454691941092424e-05, 1.64717361792312e-05), (1, 1, -1, -1): (0.00011063563182148421, 0.0)} + original_sol = {(-1, -1, 1, 1): (1.1055111552478938e-04, 0.0), (-1, -1, 1, -1): (-1.64093293295174e-05, -1.6423855436270287e-05), (-1, 1, 1, 1): (1.8665871580175305e-06, 2.379192499586592e-06), (-1, 1, 1, -1): (-6.968855681502337e-05, -3.105473609582678e-05), (-1, -1, -1, -1): (9.888413899750238e-05, 0.0), (-1, 1, -1, 1): (-1.1008919387924359e-06, 9.438278745771431e-06), (-1, 1, -1, -1): (-1.8665871580175398e-06, -2.3791924995866025e-06), (1, 1, 1, 1): (9.88841389975024e-05, 0.0), (1, 1, 1, -1): (1.640932932951739e-05, 1.6423855436270293e-05), (1, 1, -1, -1): (1.1055111552478935e-04, 0.0)} for key in original_sol: self.assertIn(key, sol) @@ -2694,80 +2802,120 @@ def test_density_mode_user_interface(self): self.assertAlmostEqual(rho_avg[i][j].imag, rho_avg_ref[i][j].imag, places=3) - def test_density_mode_user_interface(self): + @staticmethod + def read_average_density_matrix(path): + """read a Average_density_matrix_*.txt file and return the square matrix""" + + rho_avg = [] + with open(path, 'r') as f: + for line in f.readlines()[1:]: #the first line is a title + aux = line.strip("\t\n[]").split(",") + rho_avg.append([complex(elem.strip(" ()")) for elem in aux]) + return rho_avg + + def test_density_mode_multicore(self): ############################################################################ - # This test checks that the python interface of the density mode works properly ie. - # it creates a LHE file with a tag which contains the density matrix with the correct number of elements. - # We also check that the average density matrix is stable. - # To check if the value of the density matrix itself is correct see the other test_density_mode_* tests. + # When the reweighting is not run in process (force_run False, i.e. from + # ./bin/madevent), CommonRunCmd.do_reweight either starts a single job on the + # full event file or splits the file and starts one job per chunk of events. + # In the second case each job writes the average density matrix of its own + # chunk, so the mother interface has to recombine them into the canonical + # Average_density_matrix_.txt. This test checks that this file is + # created, that it agrees with the single core one and that the per chunk + # files are cleaned up. ############################################################################ - + + nevents = 3000 # more than nevt_job (2500) so that the file is really split + text = f"""generate g g > t t~ -output madevent {self.out_dir}_density0 +output madevent {self.out_dir}_density_mc launch -reweight=density -set run_card nevents 50000 -set helicity_direction [6] -set particle_in_density_matrix [6, -6] -set boost_choice [6, -6] +set run_card nevents {nevents} +set use_syst False """ - - #This bloc of code launches MadGraph with the commands written in mg5_cmd.txt - command_card = open('/tmp/mg5_cmd.txt','w') + command_card = open(pjoin(self.tmpdir, 'mg5_cmd.txt'), 'w') command_card.write(text) command_card.close() - - logfile = 'test_density_mode_ttbar.log' - subprocess.call([sys.executable,pjoin(MG5DIR,'bin','mg5_aMC'), - '/tmp/mg5_cmd.txt'], stdout=open(logfile, 'w'), stderr=subprocess.STDOUT) - - - - - lhe_path = pjoin(self.out_dir + '_density0/Events/run_01/unweighted_events.lhe.gz') - rho_mean_path = pjoin(self.out_dir + '_density0/Events/run_01/Average_density_matrix_unweighted_events.txt') - - self.assertTrue(os.path.isfile(lhe_path), f"File not found {lhe_path}") - self.assertTrue(os.path.isfile(rho_mean_path), f"File not found {rho_mean_path}") - - - for event in lhe_parser.EventFile(lhe_path): - density_check = event.density - break #we only want the first one - - for elem in density_check: - self.assertIsInstance(elem, complex) - - self.assertEqual(len(density_check), 10, f"The density matrix is not the correct length: {density_check}") - - rho_avg_ref = [[(0.3670142422790588+0j), (1.7429098337870793e-07-3.933851109770078e-05j), (-1.742909833606001e-07+3.9338510968347334e-05j), (0.11514189584464168-0j)], - [(1.7429098337870793e-07+3.933851109770078e-05j), (0.13298575772060628+0j), (0.06344292964491506-0j), (-1.7429098336059725e-07-3.933851096834704e-05j)], - [(-1.742909833606001e-07-3.9338510968347334e-05j), (0.06344292964491506+0j), (0.13298575772060628+0j), (1.7429098337870735e-07+3.9338511097700886e-05j)], - [(0.11514189584464168+0j), (-1.7429098336059725e-07+3.933851096834704e-05j), (1.7429098337870735e-07-3.9338511097700886e-05j), (0.36701424227905893+0j)]] - - #now let's read the average density matrix - with open(rho_mean_path, 'r') as f: - data = f.readlines()[1:] - rho_avg = [] - for i in range(len(data)): - aux = data[i].strip("\t\n[]").split(",") - try: - rho_avg.append([complex(aux[i].strip(" ()")) for i in range(len(aux))]) - except: #if the values are like "np.complex128(value)" - print("aux", aux) - aux2 = [aux[i].strip(" ()[]").strip("'").replace("np.complex128(","").strip(" ()") for i in range(len(aux))] - try: - rho_avg.append([complex(aux2[i]) for i in range(len(aux2))]) - except: - print("aux2", aux2) - raise ValueError - - - for i in range(len(rho_avg)): - for j in range(len(rho_avg[0])): - self.assertAlmostEqual(rho_avg[i][j].real, rho_avg_ref[i][j].real, places=3) #we ask 3 digits because we only use 50k events - self.assertAlmostEqual(rho_avg[i][j].imag, rho_avg_ref[i][j].imag, places=3) + logfile = pjoin(self.tmpdir, 'test_density_mode_multicore_generation.log') + subprocess.call([sys.executable, pjoin(MG5DIR, 'bin', 'mg5_aMC'), + pjoin(self.tmpdir, 'mg5_cmd.txt')], + stdout=open(logfile, 'w'), stderr=subprocess.STDOUT) + + me_dir = self.out_dir + '_density_mc' + run_dir = pjoin(me_dir, 'Events', 'run_01') + events = pjoin(run_dir, 'unweighted_events.lhe.gz') + self.assertTrue(os.path.isfile(events), f"File not found {events}") + + # the reweighting rewrites the event file in place: keep a pristine copy so + # that both paths reweight exactly the same events. + backup = pjoin(self.tmpdir, 'unweighted_events_orig.lhe.gz') + shutil.copyfile(events, backup) + + with open(pjoin(me_dir, 'Cards', 'reweight_card.dat'), 'w') as card: + card.write("""change helicity_direction [6] +change particle_in_density_matrix [6, -6] +change boost_choice [6, -6] +change matrix_normalisation True +""") + + def run_reweight(nb_core): + """run 'reweight run_01 --mode=density' the way ./bin/madevent does it + (out of process, force_run False) and return the density matrix files + present in the run directory afterwards""" + + #restore the original events and drop any previous density output + for path in (events, events[:-3]): + if os.path.exists(path): + os.remove(path) + shutil.copyfile(backup, events) + for name in os.listdir(run_dir): + if name.startswith('Average_density_matrix_'): + os.remove(pjoin(run_dir, name)) + + driver = f"""import sys +sys.path.insert(0, {MG5DIR!r}) +import madgraph.interface.madevent_interface as me_interface +cmd = me_interface.MadEventCmd(me_dir={me_dir!r}, force_run=True) +cmd.use_rawinput = False +cmd.haspiping = False +cmd.exec_cmd('set nb_core {nb_core}') +cmd.exec_cmd('set run_mode 2') +# force_run True would reweight in process: only with force_run False does +# do_reweight dispatch the work to single core/multicore child processes. +cmd.force_run = False +cmd.exec_cmd('reweight run_01 --mode=density -from_cards') +""" + driver_path = pjoin(self.tmpdir, 'rwgt_driver_%s.py' % nb_core) + with open(driver_path, 'w') as fsock: + fsock.write(driver) + logfile = pjoin(self.tmpdir, 'test_density_mode_multicore_%s.log' % nb_core) + subprocess.call([sys.executable, driver_path], + stdout=open(logfile, 'w'), stderr=subprocess.STDOUT) + + return sorted(name for name in os.listdir(run_dir) + if name.startswith('Average_density_matrix_')) + + #1) reference: one single job on the full event file + single = run_reweight(1) + self.assertEqual(single, ['Average_density_matrix_unweighted_events.txt']) + rho_single = self.read_average_density_matrix( + pjoin(run_dir, 'Average_density_matrix_unweighted_events.txt')) + self.assertEqual(len(rho_single), 4) + + #2) one job per chunk of events: same canonical file, no leftover + multi = run_reweight(2) + self.assertEqual(multi, ['Average_density_matrix_unweighted_events.txt'], + "the multicore density path did not produce the canonical " + "average density matrix (or left per chunk files behind)") + rho_multi = self.read_average_density_matrix( + pjoin(run_dir, 'Average_density_matrix_unweighted_events.txt')) + + self.assertEqual(len(rho_multi), len(rho_single)) + for i in range(len(rho_single)): + for j in range(len(rho_single[i])): + self.assertAlmostEqual(rho_multi[i][j].real, rho_single[i][j].real, places=10) + self.assertAlmostEqual(rho_multi[i][j].imag, rho_single[i][j].imag, places=10) def test_density_mode_ttbar(self): @@ -2821,9 +2969,7 @@ def test_density_mode_ttbar(self): density_check = event.density #reference density matrix - density_ref = [(0.4526973360805629+0j), (-2.1317321205040213e-05+0.0024340905341333923j), (2.13173212052136e-05-0.002434090538628891j), - (0.28550869973262555+0j), (0.04730266391943712+0j), (0.04700262219476668+0j), (2.1317321205213577e-05+0.0024340905386288922j), - (0.04730266391943711+0j), (-2.1317321205040145e-05-0.0024340905341333906j), (0.45269733608056295+0j)] + density_ref = [complex(0.45270438876343766, 0.0), complex(0.0, 0.0024345422714880808), complex(0.0, -0.002434542275983678), complex(0.28551318353826904, 0.0), complex(0.047295611236562354, 0.0), complex(0.047011148655689436, 0.0), complex(0.0, 0.0024345422759837264), complex(0.047295611236562354, 0.0), complex(0.0, -0.0024345422714880972), complex(0.45270438876343766, 0.0)] #1) here we check that the density matrix is computed properly for i in range(len(density_ref)): @@ -2833,17 +2979,17 @@ def test_density_mode_ttbar(self): rho_instance = dens.DensityMatrixObservables(density_check) #2) here we check that the concurrence is computed properly - concurrence_ref = 0.47641209333195317 + concurrence_ref = 0.47643514460330366 concurrence_check = rho_instance.Get_Concurrence() self.assertAlmostEqual(concurrence_ref, concurrence_check, places=7) #3) here we check that purity is computed properly - purity_ref = 0.5818411704583635 + purity_ref = 0.5818593450086657 purity_check = rho_instance.Get_Purity() self.assertAlmostEqual(purity_ref, purity_check, places=7) #4) here we check that magic is computed properly - magic_ref = 0.4706552252614239 + magic_ref = 0.4706253424888031 magic_check = rho_instance.Magic_Mixed() self.assertAlmostEqual(magic_ref, magic_check, places=7) @@ -3000,10 +3146,7 @@ def test_density_mode_decay1(self): density_check = event.density #reference density matrix - density_ref = [(0.00023359526522495882+0j), (2.9956750131603144e-05+1.3622977588717694e-05j), (-0.00037002831548626185-0.0001606402006384915j), - (0.0013988914248279838+0.0007925330810912253j), (0.0001701973522356173+0j), (-0.0003301297581403585+4.196117432997617e-05j), - (0.0003588942963018077+0.00015927990509450137j), (0.5380495499305434+0j), (0.03639176740610352+0.01649755017431808j), - (0.4615466574519961+0j)] + density_ref = [complex(0.00023372225290581268, 0.0), complex(2.992104346897159e-05, 1.3619536899390321e-05), complex(-0.0003642188579323614, -0.00015844039142370002), complex(0.00139802591124443, 0.0007920419915143803), complex(0.00017033924905323185, 0.0), complex(-0.00032992574370686504, 4.193590849513727e-05), complex(0.0003643209305432805, 0.0001612632121261497), complex(0.5380535522498318, 0.0), complex(0.036326666049751606, 0.016499687600510533), complex(0.46154238624820915, 0.0)] event_of_reference = """ @@ -3031,12 +3174,12 @@ def test_density_mode_decay1(self): self.assertAlmostEqual(concurrence_ref, concurrence_check, places=7) #3) here we check that purity is computed properly - purity_ref = 0.5057218059862959 + purity_ref = 0.5057128357315946 purity_check = rho_instance.Get_Purity() self.assertAlmostEqual(purity_ref, purity_check, places=7) #4) here we check that magic is computed properly - magic_ref = 0.018653388493735004 + magic_ref = 0.018629615817657534 magic_check = rho_instance.Magic_Mixed() self.assertAlmostEqual(magic_ref, magic_check, places=7) @@ -3094,12 +3237,7 @@ def test_density_mode_decay2(self): density_check = event.density #reference density matrix - density_ref = [(0.00021651020376335244+0j), (1.859345759680708e-05+3.319131194668537e-05j), (-8.654700949220674e-06+4.660850889005045e-06j), - (-5.964620125575636e-06-5.140944580422725e-06j), 0j, 0j, (0.00014764522936272262+0j), (1.2627222190362744e-05-3.412335641445621e-05j), - (8.443512531745642e-06-4.6314605809771255e-06j), 0j, 0j, (0.4140153688283749+0j), (0.0355514105428883+0.06346306191601571j), - (-0.033098118766739994+0.01782446293447554j), (-0.022810459480124095-0.019660482239005784j), (0.28234295658745207+0j), - (0.04829020692948074-0.13049773873783233j), (0.03229047222126599-0.017712065763110893j), (0.12282862506005009+0j), - (-0.015383774120452564-0.027546742046050884j), (0.18044889409099676+0j)] + density_ref = [complex(0.00021667298775917345, 0.0), complex(1.85743468695675e-05, 3.31618796958022e-05), complex(-8.543458188937342e-06, 4.609484294547139e-06), complex(-5.960810964036419e-06, -5.137669654422628e-06), complex(0.0, 0.0), complex(0.0, 0.0), complex(0.00014784864285082047, 0.0), complex(1.2619209329155549e-05, -3.410166864571879e-05), complex(8.5453056621656e-06, -4.676631517642126e-06), complex(0.0, 0.0), complex(0.0, 0.0), complex(0.41432659216924983, 0.0), complex(0.03551486851231066, 0.06340678451506068), complex(-0.03267269377361773, 0.01762802198822147), complex(-0.022795892127448817, -0.01964795795996489), complex(0.2827318650363218, 0.0), complex(0.048259563394429404, -0.130414798339352), complex(0.03267975905394175, -0.0178848126939771), complex(0.12249538940094126, 0.0), complex(-0.0153800506412504, -0.02753614388390608), complex(0.1800816317628771, 0.0)] #1) here we check that the density matrix is computed properly @@ -3110,14 +3248,14 @@ def test_density_mode_decay2(self): rho_instance = dens.DensityMatrixObservables(density_check) #2) here we check that the smaller eigenvalue of the partialy transposed density matrix is computed properly - flag_ref, eigval_ref = False, [1.30764975e-04, 2.33384118e-04, 1.00757194e-01, 1.28026668e-01, 2.55472741e-01, 5.15379248e-01] + flag_ref, eigval_ref = False, [0.0001309876717898993, 0.00023352764119576687, 0.10041292697359551, 0.12793991437047847, 0.2557969764840398, 0.5154856668589005] flag_check, eigval_check = rho_instance.PeresHorodecki_criterion(['boson', 'fermion']) self.assertEqual(flag_ref, flag_check) for i in range(len(eigval_ref)): self.assertAlmostEqual(eigval_ref[i], eigval_check[i], places=7) #3) here we check that purity is computed properly - purity_ref = 0.3574250017186387 + purity_ref = 0.357609015200801 purity_check = rho_instance.Get_Purity() self.assertAlmostEqual(purity_ref, purity_check, places=7) @@ -3190,10 +3328,7 @@ def test_density_mode_doublettbar(self): density_check = event.density #reference density matrix - density_ref = [(0.41585128247332614+0j), (-0.03826754879773473-0.08665010160467382j), (0.01819843853040962+0.0694772074195328j), - (-0.006036323974019095+0.028318452797874368j), (0.08409384779983874+0j), (-0.051323966834621225-0.010218484907272918j), - (-0.018157600093053276-0.06950829298296718j), (0.0841062677380868+0j), (0.0382601151338116+0.08669345314193963j), - (0.41594860198874833+0j)] + density_ref = [complex(0.41589996421540293, 0.0), complex(-0.03826383986149076, -0.08667179401359812), complex(0.018178019897460727, 0.06949276501681549), complex(-0.006036326766945857, 0.028318434038759072), complex(0.0841000357845971, 0.0), complex(-0.05132402629630901, -0.010218495717875446), complex(-0.018178019897460686, -0.06949276501681546), complex(0.08410003578459711, 0.0), complex(0.03826383986149076, 0.0866717940135981), complex(0.41589996421540276, 0.0)] lhe_path = pjoin(self.out_dir + '_density5/Events/run_01/unweighted_events.lhe.gz') for event in lhe_parser.EventFile(lhe_path): @@ -3207,17 +3342,17 @@ def test_density_mode_doublettbar(self): rho_instance = dens.DensityMatrixObservables(density_check) #2) here we check that the bounds of concurrence is computed properly - concurrence_ref = 0.028913810451469873 + concurrence_ref = 0.02891388250882494 concurrence_check = rho_instance.Get_Concurrence() self.assertAlmostEqual(concurrence_ref, concurrence_check, places=7) # #3) here we check that purity is computed properly - purity_ref = 0.42378825285881117 + purity_ref = 0.4237883055234033 purity_check = rho_instance.Get_Purity() self.assertAlmostEqual(purity_ref, purity_check, places=7) # #4) here we check that magic is computed properly - magic_ref = 0.480231580151087 + magic_ref = 0.48023161639925205 magic_check = rho_instance.Magic_Mixed() self.assertAlmostEqual(magic_ref, magic_check, places=7) @@ -3542,10 +3677,10 @@ def test_madevent_ufo_aloha_merged(self): def check_aloha_file(self): """check the content of aloha file FFV1P0_3.f and FFV2_3.f""" - ffv1p0 = """C This File is Automatically generated by ALOHA -C The process calculated in this file is: + ffv1p0 = """C This File is Automatically generated by ALOHA +C The process calculated in this file is: C Gamma(3,2,1) -C +C SUBROUTINE FFV1P0_3(F1, F2, COUP, M3, W3,V3) USE ALOHA_OBJECT IMPLICIT NONE @@ -3569,8 +3704,12 @@ def check_aloha_file(self): V3%W(:) = (0D0,0D0) RETURN ENDIF - DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI - $ * W3)) + IF (DBLE(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2).GT.0D0) THEN + DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 + $ -CI* W3)) + ELSE + DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3**2) + ENDIF V3%W(1)= DENOM*(-CI)*(F1 % W(1)*F2 % W(3)+F1 % W(2)*F2 % W(4)+F1 $ % W(3)*F2 % W(1)+F1 % W(4)*F2 % W(2)) V3%W(2)= DENOM*(-CI)*(-F1 % W(1)*F2 % W(4)-F1 % W(2)*F2 % W(3) @@ -3591,10 +3730,10 @@ def check_aloha_file(self): text = [l.strip() for l in text.strip().split('\n')] self.assertEqual(ffv1p0, text) - ffv2 = """C This File is Automatically generated by ALOHA -C The process calculated in this file is: + ffv2 = """C This File is Automatically generated by ALOHA +C The process calculated in this file is: C Gamma(3,2,-1)*ProjM(-1,1) -C +C SUBROUTINE FFV2_3(F1, F2, COUP, M3, W3,V3) USE ALOHA_OBJECT IMPLICIT NONE @@ -3625,8 +3764,12 @@ def check_aloha_file(self): TMP2 = (F1 % W(1)*(F2 % W(3)*(P3(0)+P3(3))+F2 % W(4)*(P3(1)+CI $ *(P3(2))))+F1 % W(2)*(F2 % W(3)*(P3(1)-CI*(P3(2)))+F2 % W(4) $ *(P3(0)-P3(3)))) - DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI - $ * W3)) + IF (DBLE(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2).GT.0D0) THEN + DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 + $ -CI* W3)) + ELSE + DENOM = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3**2) + ENDIF V3%W(1)= DENOM*(-CI)*(F1 % W(1)*F2 % W(3)+F1 % W(2)*F2 % W(4) $ -P3(0)*OM3*TMP2) V3%W(2)= DENOM*(-CI)*(-F1 % W(1)*F2 % W(4)-F1 % W(2)*F2 % W(3) @@ -3638,10 +3781,10 @@ def check_aloha_file(self): END -C This File is Automatically generated by ALOHA -C The process calculated in this file is: +C This File is Automatically generated by ALOHA +C The process calculated in this file is: C Gamma(3,2,-1)*ProjM(-1,1) -C +C SUBROUTINE FFV2_4_3(F1, F2, COUP1, COUP2, M3, W3,V3) USE ALOHA_OBJECT IMPLICIT NONE @@ -3667,7 +3810,7 @@ def check_aloha_file(self): V3 %W(I) = V3%W(I) + VTMP%W(I) ENDDO END - + """ text = open(os.path.join(self.out_dir,'Source', 'DHELAS', 'FFV2_3.f')).read() diff --git a/tests/acceptance_tests/test_cmd_reweight.py b/tests/acceptance_tests/test_cmd_reweight.py index e86c53d524..0cdb6d16b3 100755 --- a/tests/acceptance_tests/test_cmd_reweight.py +++ b/tests/acceptance_tests/test_cmd_reweight.py @@ -99,9 +99,24 @@ def get_MEcmd(self, event): files.cp(event, pjoin(self.run_dir,'Events','run_01', 'unweighted_events.lhe.gz')) mecmd = MECmd.MadEventCmdShell(me_dir=self.run_dir) - + return mecmd + def get_MEcmd_process(self, process, event): + """Same as get_MEcmd for an arbitrary process.""" + + mycmd = MGCmd.MasterCmd(mgme_dir=MG5DIR) + mycmd.use_rawinput = False + mycmd.haspiping = False + mycmd.run_cmd('import model sm; generate %s; output madevent %s' + % (process, self.run_dir)) + + os.mkdir(pjoin(self.run_dir, 'Events', 'run_01')) + files.cp(event, pjoin(self.run_dir, 'Events', 'run_01', + 'unweighted_events.lhe.gz')) + + return MECmd.MadEventCmdShell(me_dir=self.run_dir) + def get_aMCcmd(self, event): @@ -154,7 +169,58 @@ def test_oneloop_reweighting(self): self.assertIn('rwgt_1', rwgt_data) self.assertTrue(misc.equal(rwgt_data['rwgt_1'], solutions[i])) #misc.sprint(solutions) - + + def test_reweight_merged_antiparticle_labels(self): + """reweight a p p > w+ j sample whose events sit in the grouped + subprocess g q~ > w+ q~. + + Flavor grouping is on by default for reweight, so the generated legs + carry the merged codes (81 for the jet group) and that subprocess has + get_pdg_order [21,-81,24,-81] -- every grouped leg is an anti-particle, + so both merged codes are NEGATIVE. merged_particles is keyed by the + positive code only, so a membership test that forgets abs() leaves the + -81 labels in place; the fortran flavor mapping then resolves them to + "no flavour" and SMATRIXHEL returns an exact 0, which the reweight + reports as "Invalid matrix element". 3 of the 4 events in the fixture + are in that subprocess (both initial-state orderings, and a second + flavor pair), the 4th (g u > w+ d) is an uncrossed control. + """ + me_cmd = self.get_MEcmd_process( + 'p p > w+ j', pjoin(_pickle_path, 'wpj_merged_antiparticle.lhe.gz')) + + cmd_lines = """ + launch + set sminputs 1 132.0 + """ + ff = open(pjoin(self.run_dir, 'Cards', 'reweight_card.dat'), 'w') + ff.write(cmd_lines) + ff.close() + + if logger.level <= 10: + me_cmd.run_cmd('reweight run_01 --from_cards') + else: + with misc.stdchannel_redirected(sys.stdout, os.devnull): + me_cmd.run_cmd('reweight run_01 --from_cards') + + lhe = lhe_parser.EventFile(pjoin(self.run_dir, 'Events', 'run_01', + 'unweighted_events.lhe.gz')) + nb_grouped_antiparticle = 0 + nb_event = 0 + for event in lhe: + nb_event += 1 + rwgt_data = event.parse_reweight() + self.assertIn('rwgt_1', rwgt_data) + # the matrix element of the new hypothesis must be a real number: + # an exact 0 is the signature of an unresolved merged label + self.assertNotEqual(rwgt_data['rwgt_1'], 0.) + initial = [p.pid for p in event if p.status == -1] + if 21 in initial and any(-6 <= pid < 0 for pid in initial): + nb_grouped_antiparticle += 1 + + # guard the premise: the fixture must really exercise the subprocess + self.assertEqual(nb_event, 4) + self.assertEqual(nb_grouped_antiparticle, 3) + def test_mass_reweighting(self): """ testing that we can reweight the tt~ sample when increasing the top mass """ @@ -197,9 +263,19 @@ def test_mass_reweighting(self): # tot_mom rest frame before running the chi-finder, which fixes the # bug. The reweighted cross-section (235.28 pb) is unchanged; only # the per-event redistribution moves. - solutions = [216.3128, 237.66434, 344.95146, 293.51502, 229.39839, 295.96741, 336.38095, 434.56802, 182.61499, 404.7172, 488.22656, 154.80405, 264.45706, 373.69582, 229.70129, 474.87946, 322.87016, 394.84998, 84.186446, 118.32093] + # + # Reference refreshed again on branch claude/fortran-cross-symmetry: + # this tree-level p p > t t~ reweight now keeps crossing enabled + # (merge_crossing='record' with flavor grouping on), so the production + # |M|^2 is evaluated against the crossing-aware/folded SMATRIX instead + # of the pre-crossing separate-dir ME. That reorders the helicity sum, + # shifting each per-event weight by O(1e-3) absolute (~1e-5 relative) -- + # above misc.equal's ~1e-3 window, hence the update. The values are + # deterministic run-to-run and the reweighted cross-section is still + # 235.28 pb; keeping crossing is correct here because this is tree level. + solutions = [216.31277, 237.66447, 344.95053, 293.51516, 229.39706, 295.96583, 336.3812, 434.56831, 182.61181, 404.7172, 488.22684, 154.80089, 264.4548, 373.69601, 229.69985, 474.87946, 322.86878, 394.84998, 84.18549, 118.31887] for i,event in enumerate(lhe): - + rwgt_data = event.parse_reweight() #solutions.append(event.wgt) self.assertTrue(misc.equal(event.scale, event.get_ht_scale(0.5))) 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 0000000000..6f44b9003a --- /dev/null +++ b/tests/acceptance_tests/test_standalone_cross_symmetry.py @@ -0,0 +1,2869 @@ +################################################################################ +# +# 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 + + +def _massless_2to2(energy, cos_theta): + """A massless 2->2 point: (leg1_in, leg2_in, leg3_out, leg4_out).""" + halfe = 0.5 * 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)] + + +# The C-parity de-duplication halves the helicity sum by pairing every row with +# its fully flipped partner. Two all-massless 2->2 processes bracket the rule: +# u u~ > g g pure QCD, parity conserving -- every pair matches, so the +# reuse ENGAGES and its halve-and-double arithmetic must leave +# the answer alone. +# d u~ > e- ve~ pure charged current, maximally parity violating (V-A) -- only +# left-handed fermions couple, so the flipped partner of the one +# surviving row is identically zero and the all-or-nothing rule +# must REFUSE the reuse for the whole flavor. +PROC_CPARITY_PAIRED = 'u u~ > g g' +PROC_CPARITY_BROKEN = 'd u~ > e- ve~' + + +# 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. + """ + return _massless_2to2(self.energy, 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)) + + # ------------------------------------------------------------------ + # decay chains: the crossing acts at the production level, the whole + # decay block riding along on its production leg + # ------------------------------------------------------------------ + def _read_masses(self, pdir): + """Signed-PDG -> mass, read from the process' generated param_card.""" + card = pjoin(pdir, os.pardir, os.pardir, 'Cards', 'param_card.dat') + masses = {} + in_mass = False + with open(card) as fsock: + for line in fsock: + low = line.lower().strip() + if low.startswith('block mass'): + in_mass = True + continue + if in_mass and low.startswith('block'): + in_mass = False + if in_mass: + fields = line.split('#')[0].split() + if len(fields) == 2: + try: + masses[int(fields[0])] = float(fields[1]) + except ValueError: + pass + return masses + + def _massive_2ton(self, pdir, pdgs, seed=7): + """A phase-space point for a 2->(len(pdgs)-2) process with the leaf + masses of `pdgs` (signed PDGs, initial two first). + + A decay chain's matrix element is not on any resonance pole at a rambo + point, so the propagators are finite and the crossed / reference values + can be compared directly; only the external masses have to be right. + """ + import madgraph.various.rambo as rambo + import random + random.seed(seed) + masses = self._read_masses(pdir) + finals = pdgs[2:] + fmass = rambo.FortranList(len(finals)) + for i, pdg in enumerate(finals): + fmass[i + 1] = abs(masses.get(abs(pdg), 0.0)) + p_rambo, _ = rambo.RAMBO(len(finals), self.energy, fmass) + momenta = [(0.5 * self.energy, 0.0, 0.0, 0.5 * self.energy), + (0.5 * self.energy, 0.0, 0.0, -0.5 * self.energy)] + for i in range(1, len(finals) + 1): + momenta.append((p_rambo[(4, i)], p_rambo[(1, i)], + p_rambo[(2, i)], p_rambo[(3, i)])) + return momenta + + def _assert_decay_crossing(self, base_dir, base_line, ref_line, cross, pdgs): + """The base decay-chain SMATRIX at a crossing must reproduce a + fully-generated (--use_crossing=False) build of the crossed decay chain. + + `pdgs` is the crossed leaf signature (from compute_crossing_pdg_entries, + the order the momenta must be supplied in); it is both the reference + process order and the momentum order fed to both builds. The base carries + the crossing through the extended IFLAV, the reference evaluates it as its + own identity -- the two must agree to machine precision. + """ + ref_dir = self._generate(ref_line, 'Proc_dc_ref_%d' % cross, + options='--use_crossing=False') + nflav = self._read_nflav(base_dir) + momenta = self._massive_2ton(ref_dir, pdgs) + crossed = self._run(base_dir, momenta, _iflav(cross, 1, nflav=nflav)) + reference = self._run(ref_dir, momenta, IFLAV_IDENTITY) + self.assertNotEqual(reference, 0.0, + 'Sanity check failed: %s gives a null matrix element' + % ref_line) + scale = max(abs(crossed), abs(reference), 1e-99) + self.assertLessEqual( + abs(crossed - reference) / scale, self.tolerance, + '%s crossed (cross=%d) disagrees with %s: crossed=%r reference=%r' + % (base_line, cross, ref_line, crossed, reference)) + + def test_decay_chain_crossing_ttbar_jet(self): + """g u > t t~ u, t > b w+ must reproduce its production crossings. + + The crossing permutes the light partons (a jet moving between the initial + and the final state); the t decay block (b w+) rides along on the top and + is never split, and the t~/jet legs move as whole single legs. The base's + crossing-aware SMATRIX at the crossed flavor index must equal a fully + generated build of each crossed decay chain. + """ + base_line = 'g u > t t~ u, t > b w+' + base = self._generate(base_line, 'Proc_dc_base') + # (cross code, reference line, crossed leaf signature); the base leaves + # are [g,u,b,w+,t~,u], NEXTERNAL=6 so CROSS = I*7 + J. + cases = [ + (6 * 7 + 0, 'u~ u > t t~ g, t > b w+', (-2, 2, 5, 24, -6, 21)), + (0 * 7 + 6, 'g u~ > t t~ u~, t > b w+', (21, -2, 5, 24, -6, -2)), + ] + for cross, ref_line, pdgs in cases: + with self.subTest(cross=cross): + self._assert_decay_crossing(base, base_line, ref_line, cross, + pdgs) + + def test_decay_chain_crossing_identical_resonances(self): + """u u~ > z z g, z > e+ e- exercises the resonance-level denominator. + + Both z decay the same way, so the crossed identical-particle factor is + NOT a plain count over the crossed leaves (that would double-count the + two e+/two e-): it is resonance level (the two identical z count once). + The crossing must rebuild that factor -- IDENT_RESONANCE times the + countable single legs -- so the crossed value matches a full build. + """ + base_line = 'u u~ > z z g, z > e+ e-' + base = self._generate(base_line, 'Proc_dc_zz_base') + # base leaves [u,u~,e+,e-,e+,e-,g], NEXTERNAL=7 so CROSS = I*8 + J. + cases = [ + (0 * 8 + 7, 'u g > z z u, z > e+ e-', (2, 21, -11, 11, -11, 11, 2)), + ] + for cross, ref_line, pdgs in cases: + with self.subTest(cross=cross): + self._assert_decay_crossing(base, base_line, ref_line, cross, + pdgs) + + +class TestGoodHelCParityDedup(unittest.TestCase): + """The C-parity de-duplication of the helicity sum must be transparent. + + SMATRIX pairs every helicity row IHEL with FLIP(IHEL), the row with every + helicity negated. For the first 20 unpolarized calls it evaluates both and + compares |M|^2 (the scan phase); from then on -- and ONLY if every pair + matched -- it evaluates the lower-index row once, counts it twice and skips + its partner, halving the loop (the fast phase). + + Both halves of that contract are checked directly rather than through a + golden number: + + (a) the premise, per row: for a parity-conserving process the paired rows + really do have the same |M|^2 at the same momenta, and for a + parity-violating one they do not. Probed row by row through + SMATRIXHEL, whose helicity CODE comes from the process' own + ENCODE_HEL, so this also pins the pairing to the canonical encoding + rather than to a row index the test guessed. + + (b) the consequence: the plain unpolarized sum is the same before and + after the fast phase switches on -- both where the reuse engages (the + halve-and-double arithmetic) and where it must refuse itself. The + second is the regression: the verdict used to default to "de-duplicate" + and the validating scan could be skipped entirely (read_good_hel forces + NTRY past MAXTRIES), so a flavor whose pairs nothing had verified + silently summed half of its helicities. + + Verified by instrumenting SMATRIX to print DEDUP while writing these: over 30 + successive calls u u~ > g g ends with CSYM true and the fast phase ON from + call 20, while d u~ > e- ve~ ends with CSYM false and never enters it. The + two processes really do cover the engage and the refuse branch, so neither + stability check passes merely because nothing ever happened. + """ + + energy = 1000.0 + cos_theta = 0.3 + # > 20 unpolarized calls, so the last ones are in the fast phase. + nrepeat = 30 + # The fast phase accumulates 2*|M|^2 at the representative instead of adding + # the partner separately, so the sum is reassociated: equal to the last bit + # is not guaranteed, agreement to ~1e-12 is. + tolerance = 1e-12 + + debugging = getattr(unittest, 'debug', False) + + def setUp(self): + self.cmd = cmd_interface.MasterCmd() + self.cmd.no_notification() + self.tmpdir = tempfile.mkdtemp( + prefix='cparity_debug_' if self.debugging else 'cparity_') + + def tearDown(self): + if not self.debugging and os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + # ------------------------------------------------------------------ + def _generate(self, process, name): + """Standalone-output `process`, build the C-parity driver, return its + P* dir.""" + 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' % process) + self.cmd.exec_cmd('output standalone %s -f' % outdir) + + subproc_root = pjoin(outdir, 'SubProcesses') + pdirs = [pjoin(subproc_root, entry) + for entry in sorted(os.listdir(subproc_root)) + if entry.startswith('P') + and os.path.isdir(pjoin(subproc_root, entry))] + self.assertEqual(len(pdirs), 1, + 'Expected a single subprocess directory for %s, got %s' + % (process, pdirs)) + pdir = pdirs[0] + source = open(pjoin(pdir, 'matrix.f')).read() + # The probe drives flavor 1 directly, so the process must not have been + # merged into a multi-flavor matrix element behind our back. + nflav = re.search(r'PARAMETER\s*\(NFLAV=(\d+)\)', source) + self.assertTrue(nflav, 'Could not read NFLAV from %s' % pdir) + self.assertEqual(int(nflav.group(1)), 1, + '%s came out with NFLAV=%s; the probe assumes a single ' + 'flavor' % (process, nflav.group(1))) + ncomb = re.search(r'PARAMETER\s*\(\s*NCOMB=(\d+)\)', source) + self.assertTrue(ncomb, 'Could not read NCOMB from %s' % pdir) + self._write_driver(pdir, int(ncomb.group(1))) + retcode = self._call(['make', 'check'], pdir) + self.assertEqual(retcode, 0, 'Failed to compile the driver in %s' % pdir) + return pdir + + @staticmethod + def _call(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) + + def _write_driver(self, pdir, ncomb): + """Replace check_sa.f by a driver with the two probes this needs. + + MODE 1 walks the helicity table and reports (|M(h)|^2, |M(-h)|^2) for + every row, going through ENCODE_HEL so the codes are the process' own. + MODE 2 calls the plain unpolarized SMATRIX repeatedly at one point, so + the scan phase and the fast phase can be compared within a single run -- + the de-duplication state lives in SMATRIX and does not survive the + process. + """ + driver = ''' PROGRAM CPARITY_DRIVER + use model_object + IMPLICIT NONE + INCLUDE "coupl.inc" + INCLUDE "nexternal.inc" + INTEGER NCOMB + PARAMETER (NCOMB=%(ncomb)d) + REAL*8 P(0:3,NEXTERNAL), ANS, ANSFLIP + INTEGER I, J, MODE, NREP, IHEL, CODE, FCODE, IDEN_STAR + INTEGER NHEL_STAR(NEXTERNAL,NCOMB) + INTEGER THIS(NEXTERNAL), FLIPPED(NEXTERNAL) + call setpara('param_card.dat') + OPEN(UNIT=42,FILE='cparity_input.dat',STATUS='OLD') + READ(42,*) MODE + DO I=1,NEXTERNAL + READ(42,*) (P(J,I),J=0,3) + ENDDO + IF (MODE.EQ.1) THEN +C Per-row C-parity probe. SMATRIXHEL selects a single row by its +C canonical code and undoes the helicity average, the same on both +C rows of a pair, so the two values are directly comparable. + CALL GET_NHEL(IDEN_STAR,NHEL_STAR) + DO IHEL=1,NCOMB + DO J=1,NEXTERNAL + THIS(J) = NHEL_STAR(J,IHEL) + FLIPPED(J) = -NHEL_STAR(J,IHEL) + ENDDO + CALL ENCODE_HEL(THIS, CODE) + CALL ENCODE_HEL(FLIPPED, FCODE) + CALL SMATRIXHEL(P, CODE, 1, ANS) + CALL SMATRIXHEL(P, FCODE, 1, ANSFLIP) + WRITE(*,'(A,3(1X,I6),2(1X,ES25.17))') + & 'PAIR=', IHEL, CODE, FCODE, ANS, ANSFLIP + ENDDO + ELSE +C The plain unpolarized sum, repeatedly: NTRY_CSYM crosses its +C threshold part way through and the fast phase takes over. + READ(42,*) NREP + DO I=1,NREP + CALL SMATRIX(P,1,ANS) + WRITE(*,'(A,1X,I6,1X,ES25.17)') 'ANS=', I, ANS + ENDDO + ENDIF + CLOSE(42) + END +''' + with open(pjoin(pdir, 'check_sa.f'), 'w') as fsock: + fsock.write(driver % {'ncomb': ncomb}) + + def _probe(self, pdir, lines): + with open(pjoin(pdir, 'cparity_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 _momentum_lines(self): + return [' '.join('%.17e' % component for component in mom) + for mom in _massless_2to2(self.energy, self.cos_theta)] + + def _pairs(self, pdir): + """[(row, |M(h)|^2, |M(-h)|^2)] over the whole helicity table.""" + output = self._probe(pdir, ['1'] + self._momentum_lines()) + pairs = [(int(row), float(direct), float(flipped)) + for row, _code, _fcode, direct, flipped + in re.findall(r'PAIR=\s+(\d+)\s+(\d+)\s+(\d+)\s+' + r'(\S+)\s+(\S+)', output)] + self.assertTrue(pairs, 'no C-parity pair read from %s, got:\n%s' + % (pdir, output)) + return pairs + + def _repeated_sums(self, pdir): + """The unpolarized SMATRIX value of each of `nrepeat` successive calls.""" + output = self._probe(pdir, ['2'] + self._momentum_lines() + + ['%d' % self.nrepeat]) + values = [float(value) + for _call, value in re.findall(r'ANS=\s+(\d+)\s+(\S+)', output)] + self.assertEqual(len(values), self.nrepeat, + 'expected %d matrix elements from %s, got %d:\n%s' + % (self.nrepeat, pdir, len(values), output)) + return values + + def _assert_sum_is_stable(self, pdir, label): + """Every repeated call must give the first call's value. + + Call 1 is in the scan phase (full helicity sum, both members of every + pair evaluated); the last calls are past the threshold. If the reuse is + wrong -- a missing factor of two, or a de-duplication applied to a + flavor whose pairs do not match -- the value steps part way through. + """ + values = self._repeated_sums(pdir) + reference = values[0] + self.assertNotEqual(reference, 0.0, + '%s gives a null matrix element' % label) + for index, value in enumerate(values, start=1): + self.assertLessEqual( + abs(value - reference), self.tolerance * abs(reference), + '%s: call %d gives %r but call 1 gave %r -- the C-parity ' + 'de-duplication changed the unpolarized sum' + % (label, index, value, reference)) + + # ------------------------------------------------------------------ + def test_cparity_pairs_match_for_qcd(self): + """Parity-conserving: every row equals its fully flipped partner. + + This is the premise the fast phase rests on. Checked row by row, so a + pairing built on the wrong encoding fails here rather than silently + halving the sum somewhere else. + """ + pdir = self._generate(PROC_CPARITY_PAIRED, 'Proc_cparity_qcd') + pairs = self._pairs(pdir) + nonzero = 0 + for row, direct, flipped in pairs: + scale = max(abs(direct), abs(flipped)) + if scale == 0.0: + continue + nonzero += 1 + self.assertLessEqual( + abs(direct - flipped), 1e-10 * scale, + '%s row %d: |M(h)|^2=%r but |M(-h)|^2=%r; the C-parity pairing ' + 'the de-duplication relies on does not hold' + % (PROC_CPARITY_PAIRED, row, direct, flipped)) + self.assertGreater(nonzero, 1, + 'only %d non-zero helicity row(s) in %s: the pairing ' + 'is not being exercised' + % (nonzero, PROC_CPARITY_PAIRED)) + + def test_cparity_pairs_broken_for_charged_current(self): + """Maximally parity-violating: at least one pair must NOT match. + + Without this the "all-or-nothing refusal" half of the rule would never + be exercised -- if every process in the suite happened to be + parity-conserving, a de-duplication that never refuses would pass. + """ + pdir = self._generate(PROC_CPARITY_BROKEN, 'Proc_cparity_cc') + pairs = self._pairs(pdir) + mismatched = [(row, direct, flipped) + for row, direct, flipped in pairs + if abs(direct - flipped) + > 1e-10 * max(abs(direct), abs(flipped), 1e-99)] + self.assertTrue( + mismatched, + '%s: every helicity row matched its flipped partner, so this ' + 'process does not test the refusal path any more' % PROC_CPARITY_BROKEN) + + def test_dedup_leaves_the_paired_sum_unchanged(self): + """The reuse engages here, and must not move the answer.""" + pdir = self._generate(PROC_CPARITY_PAIRED, 'Proc_cparity_qcd_sum') + self._assert_sum_is_stable(pdir, PROC_CPARITY_PAIRED) + + def test_refused_dedup_leaves_the_broken_sum_unchanged(self): + """The regression: the reuse must refuse itself here. + + If it does not, the fast phase drops every row whose partner is zero and + doubles the wrong ones, and the sum moves at call 21. + """ + pdir = self._generate(PROC_CPARITY_BROKEN, 'Proc_cparity_cc_sum') + self._assert_sum_is_stable(pdir, PROC_CPARITY_BROKEN) + + +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 must not quietly + produce a matrix element missing those subprocesses -- it gets the recorded + crossings expanded back into explicit subprocesses instead, so the result is + the complete uncrossed output and no user flag is required. + """ + + # 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='', process=PROC_QG_QG, setup=()): + """Run generate+output for `fmt`; returns the output directory.""" + out = pjoin(self.tmpdir, name) + cmd = cmd_interface.MasterCmd() + cmd.no_notification() + cmd.exec_cmd('set automatic_html_opening False') + for line in setup: + cmd.exec_cmd(line) + cmd.exec_cmd('import model sm') + cmd.exec_cmd(('generate %s %s' % (process, options)).strip()) + cmd.exec_cmd('output %s %s -f' % (fmt, out)) + return out + + @staticmethod + def _subprocesses(out_dir): + path = pjoin(out_dir, 'SubProcesses') + return sorted(name for name in os.listdir(path) + if name.startswith('P')) + + def test_unsupported_output_accepts_crossing(self): + """Crossing on must NOT be refused by an output that cannot read it. + + The crossed subprocesses are recorded as metadata at generation, so an + output with no crossing machinery gets them expanded back into explicit + subprocesses instead of erroring out. Refusing here used to force the + user to pass --use_crossing=False even for a process that folds no + crossing at all (u g > u g folds none), which is why the gate moved from + the flag to the data. + """ + for fmt in self.UNSUPPORTED_FORMATS: + with self.subTest(format=fmt): + with_crossing = self._output(fmt, 'on_%s' % fmt) + without = self._output(fmt, 'off_%s' % fmt, + options='--use_crossing=False') + self.assertEqual(self._subprocesses(with_crossing), + self._subprocesses(without), + '%s output differs with crossing on' % fmt) + + def test_ungrouped_madevent_expands_folded_crossings(self): + """A folding process must lose nothing on an output without crossing. + + p p > j j QCD=0 really does fold crossings, so this is the case where a + silently-missing subprocess would change the cross-section: the + ungrouped madevent output (no crossing machinery) must come out with the + very same subprocesses as an explicitly uncrossed generation. + """ + ungrouped = ('set group_subprocesses False',) + on = self._output('madevent', 'me_on', process='p p > j j QCD=0', + setup=ungrouped) + off = self._output('madevent', 'me_off', process='p p > j j QCD=0', + options='--use_crossing=False', setup=ungrouped) + subs_on = self._subprocesses(on) + self.assertEqual(subs_on, self._subprocesses(off)) + # Guard the guard: a build that collapsed everything into one directory + # would satisfy the equality above only if both sides were broken. + self.assertGreater(len(subs_on), 1, + 'expected several crossed subprocesses, got %s' + % subs_on) + + 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_ic', 'spincol_part', + '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_ic', 'spincol_part', + 'ident_cross', 'xmom', 'ids_base'): + 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 TestMadeventDecayChainCrossing(unittest.TestCase): + """End-to-end: a decay-chain crossing routed through the base's SMATRIX in + madevent gives the same cross section as an independent build. + + ``p p > w+ j, w+ > j j`` crosses the light partons of the production while + the ``w+ > j j`` decay block rides along on the top-level W+; the crossed + subprocesses (``g q~ > w+ q~``, ...) reuse the base matrix element through + the crossing-aware SMATRIX (matrix2_router dispatches to SMATRIX1 with a + crossed FLAV_IDX and rebuilds the crossed, resonance-level denominator). A + ``--use_crossing=False`` build computes every subprocess independently + instead. With the same seed the routed and the independent integration must + agree -- a wrong crossed denominator, a split decay block, or a mis-routed + flavor would move the cross section. + + Runs two full (small) madevent generations, so it is slow. + """ + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='cross_mev_dc_') + + def tearDown(self): + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + def _xsec(self, options, name): + from madgraph import MG5DIR + outdir = pjoin(self.tmpdir, name) + card = pjoin(self.tmpdir, 'cmd_%s.txt' % name) + with open(card, 'w') as fsock: + fsock.write('generate p p > w+ j, w+ > j j %s\n' + 'output madevent %s -f\n' + 'launch\n' + 'set nevents 1000\n' + 'set iseed 424242\n' % (options, outdir)) + subprocess.call([sys.executable, pjoin(MG5DIR, 'bin', 'mg5_aMC'), card]) + results = pjoin(outdir, 'SubProcesses', 'results.dat') + self.assertTrue(os.path.isfile(results), + 'madevent produced no results (%s)' % results) + with open(results) as fsock: + # results.dat: cross-section, abs error, ... (in pb). + fields = fsock.readline().split() + return float(fields[0]), float(fields[1]) + + def test_decay_chain_crossing_xsec_matches(self): + crossed, err_c = self._xsec('', 'on') + independent, err_i = self._xsec('--use_crossing=False', 'off') + self.assertGreater(independent, 0.0, + 'independent build gives a null cross section') + scale = max(abs(crossed), abs(independent), 1e-99) + self.assertLessEqual( + abs(crossed - independent) / scale, 1e-2, + 'p p > w+ j, w+ > j j crossing-routed xsec %r +- %r disagrees with ' + 'the independent build %r +- %r' + % (crossed, err_c, independent, err_i)) + + +class TestMadeventInclusiveCrossingXsec(unittest.TestCase): + """End-to-end: routing crossed subprocesses through a shared base matrix + element must not move the INCLUSIVE cross section. + + The plain (no decay chain) counterpart of TestMadeventDecayChainCrossing, + and the configuration where the crossing router has the most to get wrong. + With flavor grouping ``p p > t t~ j j`` collapses to five subprocess groups, + and two of them -- gq_ttxgq and qq_ttxqq -- are served by a cross-GROUP + router: they carry a ``matrix_router.f`` (plus ``crossgroup_helunion.dat`` + and ``crossgroup.mk``) instead of their own matrix element, i.e. their + flavors are evaluated by ANOTHER group's matrix element under a crossing, + over the helicity union of the two groups. Nothing else in the suite + integrates that path -- Track B is exercised at the matrix-element level + only. + + The summed cross section is what catches it. A wrong crossed averaging + denominator, multi-channel row or good-helicity union leaves the per-flavor + matrix elements agreeing (those are compared in + TestStandaloneMadeventMatrixElementConsistency) while moving the integral, + which is exactly how the routed groups lost ~29% before the helicity union + was fed to the Track-A routers. + + Runs two full madevent integrations, but the flavor grouping keeps them + small: ~40s each. Reference numbers at the time of writing -- + 416.6 +- 2.4 pb routed vs 413.7 +- 2.6 pb independent, i.e. 0.8 sigma apart. + """ + + PROCESS = 'p p > t t~ j j' + NEVENTS = 1000 + SEED = 191919 + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='cross_mev_ttjj_') + + def tearDown(self): + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + def _generate_and_integrate(self, options, name): + """Generate + integrate the process; return (outdir, xsec, error) in pb.""" + from madgraph import MG5DIR + outdir = pjoin(self.tmpdir, name) + card = pjoin(self.tmpdir, 'cmd_%s.txt' % name) + with open(card, 'w') as fsock: + fsock.write('generate %s %s\n' + 'output madevent %s -f\n' + 'launch\n' + 'set nevents %d\n' + 'set iseed %d\n' + % (self.PROCESS, options, outdir, + self.NEVENTS, self.SEED)) + subprocess.call([sys.executable, pjoin(MG5DIR, 'bin', 'mg5_aMC'), card]) + results = pjoin(outdir, 'SubProcesses', 'results.dat') + self.assertTrue( + os.path.isfile(results), + 'madevent produced no results for %s (%s)' + % (options or 'the default (crossing on) build', results)) + with open(results) as fsock: + # results.dat: cross-section, abs error, ... (in pb). + fields = fsock.readline().split() + return outdir, float(fields[0]), float(fields[1]) + + @staticmethod + def _routed_groups(outdir): + """The subprocess groups served by a cross-group crossing router.""" + subproc = pjoin(outdir, 'SubProcesses') + routed = [] + for name in sorted(os.listdir(subproc)): + pdir = pjoin(subproc, name) + if not name.startswith('P') or not os.path.isdir(pdir): + continue + if any(re.match(r'matrix\d+_router\.f$', entry) + for entry in os.listdir(pdir)): + routed.append(name) + return routed + + def test_inclusive_crossing_xsec_matches(self): + crossed_dir, crossed, err_c = self._generate_and_integrate('', 'on') + independent_dir, independent, err_i = self._generate_and_integrate( + '--use_crossing=False', 'off') + + # Guard the premise: the default build must really evaluate some group + # through another group's matrix element, and the reference build must + # not -- otherwise this compares two identical builds and can never fail. + routed = self._routed_groups(crossed_dir) + self.assertTrue( + routed, 'no subprocess group is served by a crossing router, so the ' + 'comparison would be between two identical builds') + self.assertEqual( + self._routed_groups(independent_dir), [], + '--use_crossing=False still emitted a crossing router') + + self.assertGreater(independent, 0.0, + 'the independent build gives a null cross section') + # Same seed and the same channels, so the two runs must agree well + # inside their combined statistical error; the 1% floor absorbs the grid + # noise the different routing can introduce. + tolerance = max(1e-2 * independent, 3.0 * math.hypot(err_c, err_i)) + self.assertLessEqual( + abs(crossed - independent), tolerance, + '%s crossing-routed xsec %r +- %r disagrees with the independent ' + 'build %r +- %r (groups routed through a crossing: %s)' + % (self.PROCESS, crossed, err_c, independent, err_i, + ', '.join(routed))) + + +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 0c9dd01983..906320aeda 100644 --- a/tests/acceptance_tests/test_standalone_madevent_consistency.py +++ b/tests/acceptance_tests/test_standalone_madevent_consistency.py @@ -25,7 +25,6 @@ logger = logging.getLogger('madgraph.madevent') import madgraph.interface.master_interface as cmd_interface -import madgraph.various.misc as misc import madgraph.various.process_checks as process_checks @@ -66,44 +65,112 @@ def do(self, line): self.cmd.exec_cmd(line) def check_process(self, process, model='sm', tolerance=1e-6): + """Every backend must return the same matrix element per flavor. + + The reference is the plain (--use_crossing=False) fortran standalone. + Every other backend is compared to it flavor by flavor, matched by the + PDG tuple it prints (not by index -- the flavor ordering differs between + backends, and a crossing-folded backend may expose extra flavors): + + - fortran madevent, ungrouped, --use_crossing=False (the original + check; only the ungrouped ME exporter does not support crossing); + - fortran standalone WITH crossing (the crossing-aware SMATRIX must + reproduce the plain per-flavor matrix element); + - fortran madevent, grouped, WITH crossing + (ProcessExporterFortranMEGroup, which does support crossing); + - standalone_mg7 (the madmatrix / cudacpp CPU-SIMD backend). + """ self.do('set automatic_html_opening False') self.do('set group_subprocesses False') self.do('set apply_flavor_grouping True') self.do('set zerowidth_tchannel False') self.do('import model %s' % model) - self.do('generate %s' % process) - generated_process = self.cmd._curr_amps[0].get('process') - self.do('output standalone %s -f' % self.standalone_dir) - self.do('output madevent %s -f' % self.madevent_dir) - - standalone_dir = self._get_single_subprocess_dir( - pjoin(self.standalone_dir, 'SubProcesses')) - madevent_dir = self._get_single_subprocess_dir( - pjoin(self.madevent_dir, 'SubProcesses')) - standalone_rows, printed_phase_space = self._run_standalone(standalone_dir) + # -- Reference: plain fortran standalone (crossing machinery off) ------- + self.do('generate %s --use_crossing=False' % process) + generated_process = self.cmd._curr_amps[0].get('process') seeded_phase_space = self._get_seeded_phase_space(generated_process) + + ref_root = pjoin(self.tmpdir, 'standalone_plain') + self.do('output standalone %s -f' % ref_root) + ref_sub = self._get_single_subprocess_dir(pjoin(ref_root, 'SubProcesses')) + ref_rows, printed_phase_space = self._run_standalone(ref_sub) self._assert_phase_space_reasonable( - printed_phase_space, seeded_phase_space, standalone_dir) - madevent_by_iflav = self._run_hacked_madevent(madevent_dir, seeded_phase_space) - - self.assertTrue(len(standalone_rows) <= len(madevent_by_iflav), - 'Flavor-count mismatch for %s: standalone=%s madevent=%s' - % (process, len(standalone_rows), len(madevent_by_iflav))) - - for iflav, standalone_row in enumerate(standalone_rows, start=1): - self.assertIn(iflav, madevent_by_iflav, - 'Missing madevent flavor index %s for %s' % (iflav, process)) - standalone_me = standalone_row['value'] - madevent_me = madevent_by_iflav[iflav] - scale = max(abs(standalone_me), abs(madevent_me), 1e-99) - misc.sprint('flavor=%s: diff=%f%%'%( - standalone_row['pdg'], 100 * abs(standalone_me - madevent_me) / scale if scale != 0 else 0)) + printed_phase_space, seeded_phase_space, ref_sub) + reference = self._rows_by_pdg(ref_rows, ref_sub) + + # -- (1) fortran madevent, ungrouped, crossing off (the original check) - + # madevent enumerates flavors in the same order as the standalone check + # (its GET_FLAVOR returns group indices, not PDGs, so it is matched to + # the reference by that shared IFLAV order rather than by PDG). + me_root = pjoin(self.tmpdir, 'madevent_plain') + self.do('output madevent %s -f' % me_root) + me_sub = self._get_single_subprocess_dir(pjoin(me_root, 'SubProcesses')) + me_by_iflav = self._run_hacked_madevent(me_root, me_sub, seeded_phase_space) + self._compare_by_iflav( + process, 'madevent (ungrouped, crossing off)', + ref_rows, me_by_iflav, tolerance) + + # -- (2) fortran standalone WITH crossing ------------------------------- + self.do('generate %s' % process) + sacross_root = pjoin(self.tmpdir, 'standalone_crossing') + self.do('output standalone %s -f' % sacross_root) + sacross_sub = self._get_single_subprocess_dir( + pjoin(sacross_root, 'SubProcesses')) + sacross_rows, _ = self._run_standalone(sacross_sub) + self._compare_to_reference( + process, 'standalone (crossing on)', + reference, self._rows_by_pdg(sacross_rows, sacross_sub), tolerance) + + # -- (3) fortran madevent, grouped, WITH crossing (MEGroup) ------------- + self.do('set group_subprocesses True') + self.do('generate %s' % process) + meg_root = pjoin(self.tmpdir, 'madevent_group_crossing') + self.do('output madevent %s -f' % meg_root) + self.do('set group_subprocesses False') + meg_sub = self._get_single_subprocess_dir(pjoin(meg_root, 'SubProcesses')) + meg_by_iflav = self._run_hacked_madevent( + meg_root, meg_sub, seeded_phase_space, + smatrix_name='SMATRIX1', make_target='madevent_forhel') + self._compare_by_iflav( + process, 'madevent (grouped, crossing on)', + ref_rows, meg_by_iflav, tolerance) + + # -- (4) standalone_mg7 (madmatrix / cudacpp CPU-SIMD) ------------------ + # Skipped (not failed) if no C++ compiler or the madmatrix build + # toolchain is unavailable. Matched by flavor order like madevent: the + # extended flavor id is cross*nflav+flav, so the base flavors are ids + # 0..nflav-1, in the same order as the standalone check. + mg7_by_iflav = self._run_standalone_mg7(process, seeded_phase_space, ref_rows) + if mg7_by_iflav is not None: + self._compare_by_iflav( + process, 'standalone_mg7', ref_rows, mg7_by_iflav, tolerance) + + def _rows_by_pdg(self, rows, subproc_dir): + """{PDG tuple -> matrix element} from _extract_standalone_flavors rows.""" + by_pdg = {} + for row in rows: + by_pdg[tuple(row['pdg'])] = row['value'] + self.assertEqual(len(by_pdg), len(rows), + 'Duplicate PDG flavor rows in %s' % subproc_dir) + return by_pdg + + def _compare_to_reference(self, process, label, reference, other, tolerance): + """Assert `other` reproduces every reference flavor (matched by PDG).""" + self.assertTrue(other, 'No matrix elements produced by %s for %s' + % (label, process)) + for pdg, ref_me in reference.items(): + self.assertIn(pdg, other, + 'Flavor %s missing from %s for %s' % (pdg, label, process)) + other_me = other[pdg] + scale = max(abs(ref_me), abs(other_me), 1e-99) + rel = abs(ref_me - other_me) / scale + logger.debug('%s flavor=%s: diff=%f%%', label, pdg, 100 * rel) self.assertLessEqual( - abs(standalone_me - madevent_me) / scale, - tolerance, - 'Incompatible matrix elements for %s flavor=%s iflav=%s: standalone=%s madevent=%s' - % (process, standalone_row['pdg'], iflav, standalone_me, madevent_me)) + rel, tolerance, + 'Incompatible matrix elements for %s flavor=%s (%s): ' + 'reference=%s %s=%s' + % (process, pdg, label, ref_me, label, other_me)) def _get_single_subprocess_dir(self, root_dir): subproc_dirs = [pjoin(root_dir, name) for name in sorted(os.listdir(root_dir)) @@ -157,22 +224,129 @@ def _assert_phase_space_reasonable(self, printed, seeded, subproc_dir): 'printed=%s seeded=%s' % (subproc_dir, ipart, icomp, printed_val, seeded_val)) - def _run_hacked_madevent(self, subproc_dir, phase_space): - source_dir = pjoin(self.madevent_dir, 'Source') + def _compare_by_iflav(self, process, label, ref_rows, by_iflav, tolerance): + """Assert a madevent backend reproduces the reference, matched by IFLAV. + + The standalone check loops flavors in the same order that the madevent + driver loops IFLAV, so reference row i (1-based) is madevent IFLAV i. + A grouped/crossing madevent may expose extra flavors past the reference + count; only the reference flavors are required to agree. + """ + self.assertTrue(by_iflav, 'No matrix elements produced by %s for %s' + % (label, process)) + for iflav, row in enumerate(ref_rows, start=1): + self.assertIn(iflav, by_iflav, + 'Missing IFLAV=%s (flavor %s) from %s for %s' + % (iflav, row['pdg'], label, process)) + ref_me = row['value'] + other_me = by_iflav[iflav] + scale = max(abs(ref_me), abs(other_me), 1e-99) + rel = abs(ref_me - other_me) / scale + logger.debug('%s flavor=%s: diff=%f%%', label, row['pdg'], 100 * rel) + self.assertLessEqual( + rel, tolerance, + 'Incompatible matrix elements for %s flavor=%s iflav=%s (%s): ' + 'reference=%s %s=%s' + % (process, row['pdg'], iflav, label, ref_me, label, other_me)) + + def _run_hacked_madevent(self, madevent_root, subproc_dir, phase_space, + smatrix_name='SMATRIX', make_target='madevent'): + # The grouped exporter names its per-subprocess routine SMATRIX1 and + # hides it behind helicity recycling (SMATRIX1 lives only in + # matrix1_orig.f -> the 'madevent_forhel' target). The test processes + # all group into a single subprocess (MAXSPROC=1), required by the + # single-SMATRIX driver below. + maxamps = pjoin(subproc_dir, 'maxamps.inc') + if os.path.isfile(maxamps): + match = re.search(r'MAXSPROC\s*=\s*(\d+)', open(maxamps).read()) + if match: + self.assertEqual(int(match.group(1)), 1, + 'Driver assumes MAXSPROC=1 in %s' % subproc_dir) + source_dir = pjoin(madevent_root, 'Source') retcode = self._call_with_optional_redirection(['make'], source_dir) self.assertEqual(retcode, 0, 'Failed to compile MadEvent source in %s' % source_dir) - self._write_hacked_driver(pjoin(subproc_dir, 'driver.f'), phase_space) + self._write_hacked_driver(pjoin(subproc_dir, 'driver.f'), phase_space, + smatrix_name) - retcode = self._call_with_optional_redirection(['make', 'madevent'], subproc_dir) - self.assertEqual(retcode, 0, 'Failed to compile hacked madevent in %s' % subproc_dir) + retcode = self._call_with_optional_redirection(['make', make_target], subproc_dir) + self.assertEqual(retcode, 0, + 'Failed to compile hacked madevent (%s) in %s' + % (make_target, subproc_dir)) - output = subprocess.Popen(['./madevent'], + output = subprocess.Popen(['./' + make_target], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=subproc_dir).communicate()[0].decode() return self._extract_madevent_by_iflav(output, subproc_dir) + def _run_standalone_mg7(self, process, phase_space, ref_rows): + """{IFLAV -> matrix element} for standalone_mg7 at the seeded momenta. + + Returns None (skip) if there is no C++ compiler or the madmatrix build + toolchain cannot build check_sa.exe. check_sa.exe reads the external + momenta from an LHE file (-e), so the same seeded point is used as for + the fortran backends; the base flavors are the extended ids 0..nflav-1. + """ + if not shutil.which(os.environ.get('CXX', 'g++')): + return None + outdir = pjoin(self.tmpdir, 'standalone_mg7') + self.do('generate %s' % process) + try: + self.do('output standalone_mg7 %s -f' % outdir) + except Exception: + return None + pdir = self._get_single_subprocess_dir(pjoin(outdir, 'SubProcesses')) + + nevt = 8 + lhe = pjoin(pdir, 'seeded.lhe') + self._write_lhe_events(lhe, phase_space, nevt) + + rc = self._call_with_optional_redirection( + ['make', '-j2', 'check_sa.exe'], pdir) + if rc != 0: + return None + + by_iflav = {} + for iflav in range(1, len(ref_rows) + 1): + flavor_id = iflav - 1 # extended id, cross=0 -> id = flavor (0-based) + output = subprocess.Popen( + ['./check_sa.exe', 'perf', '-v', '-f', str(flavor_id), + '-e', lhe, '1', str(nevt), '1'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cwd=pdir).communicate()[0].decode() + values = re.findall(r'Matrix element =\s*([-\d.eE+]+)', output) + self.assertTrue(values, + 'No matrix element from standalone_mg7 flavor id %s ' + 'for %s:\n%s' % (flavor_id, process, output)) + by_iflav[iflav] = float(values[0]) + return by_iflav + + def _write_lhe_events(self, path, phase_space, nevents): + """Write `nevents` identical minimal LHE events at `phase_space`. + + check_sa.exe only reads (E, px, py, pz) from each particle line; the + pdg/status/colour columns are placeholders. The momenta are replicated + across the SIMD page so every lane evaluates the seeded point. + """ + def as_float(value): + if isinstance(value, str): + return float(value.replace('d', 'e').replace('D', 'E')) + return float(value) + + npar = len(phase_space) + lines = [] + for _ in range(nevents): + lines.append('') + lines.append('%d 0 0.0 0.0 0.0 0.0' % npar) + for momentum in phase_space: + e, px, py, pz = (as_float(v) for v in momentum) + lines.append('1 1 0 0 0 0 %.17E %.17E %.17E %.17E 0.0' + % (px, py, pz, e)) + lines.append('') + with open(path, 'w') as fsock: + fsock.write('\n'.join(lines) + '\n') + def _call_with_optional_redirection(self, command, cwd): if logger.isEnabledFor(logging.INFO): return subprocess.call(command, cwd=cwd) @@ -181,6 +355,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() @@ -217,7 +399,7 @@ def _extract_madevent_by_iflav(self, output, subproc_dir): self.assertTrue(by_iflav, 'No madevent flavor matrix elements found in %s' % subproc_dir) return by_iflav - def _write_hacked_driver(self, driver_path, phase_space): + def _write_hacked_driver(self, driver_path, phase_space, smatrix_name='SMATRIX'): lines = [ ' PROGRAM DRIVER', ' use model_object', @@ -277,12 +459,13 @@ def _write_hacked_driver(self, driver_path, phase_space): (component, iparticle, formatted_value)) lines.extend([ + # The per-flavor PDG is read from leshouche.inc in python (madevent's + # GET_FLAVOR returns group indices, and its signature differs between + # the plain and grouped exporters), so the driver only emits IFLAV. ' DO IFLAV=1,MAXFLAVPERPROC', - ' CALL GET_FLAVOR(IFLAV,FLAVOR)', - ' CALL SMATRIX(P, IFLAV, 0.5D0, 0.5D0, 1, IVEC, ANS,', + ' CALL %s(P, IFLAV, 0.5D0, 0.5D0, 1, IVEC, ANS,' % smatrix_name, ' $ SELECTED_HEL, SELECTED_COL)', " WRITE(*,*) 'IFLAV = ', IFLAV", - " WRITE(*,*) 'PDG', (FLAVOR(J),J=1,NEXTERNAL)", " WRITE(*,*) 'Matrix element = ', ANS, ' GeV^',-(2*NEXTERNAL-8)", ' ENDDO', ' END', diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_1.c b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_1.c index 31eac5225c..cf3e1b34ee 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_1.c +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_1.c @@ -14,7 +14,11 @@ P2[1] = -F2.p[1]; P2[2] = -F2.p[2]; P2[3] = -F2.p[3]; F2.flv_index = F1.flv_index; - denom = COUP/((P2[0]*P2[0])-(P2[1]*P2[1])-(P2[2]*P2[2])-(P2[3]*P2[3]) - M2 * (M2 -cI* W2)); + if ((P2[0]*P2[0])-(P2[1]*P2[1])-(P2[2]*P2[2])-(P2[3]*P2[3]) > 0.){ + denom = COUP/((P2[0]*P2[0])-(P2[1]*P2[1])-(P2[2]*P2[2])-(P2[3]*P2[3]) - M2 * (M2 -cI* W2)); + } else { + denom = COUP/((P2[0]*P2[0])-(P2[1]*P2[1])-(P2[2]*P2[2])-(P2[3]*P2[3]) - M2*M2); + } F2.W[0]= denom*(-cI)*(F1.W[0]*(P2[0]*(-V3.W[0]+V3.W[3])+(P2[1]*(V3.W[1]-cI*(V3.W[2]))+(P2[2]*(+cI*(V3.W[1])+V3.W[2])+P2[3]*(-V3.W[0]+V3.W[3]))))+(F1.W[1]*(P2[0]*(V3.W[1]+cI*(V3.W[2]))+(P2[1]*(-1.)*(V3.W[0]+V3.W[3])+(P2[2]*(-1.)*(+cI*(V3.W[0]+V3.W[3]))+P2[3]*(V3.W[1]+cI*(V3.W[2])))))+M2*(F1.W[2]*(V3.W[0]+V3.W[3])+F1.W[3]*(V3.W[1]+cI*(V3.W[2]))))); F2.W[1]= denom*cI*(F1.W[0]*(P2[0]*(-V3.W[1]+cI*(V3.W[2]))+(P2[1]*(V3.W[0]-V3.W[3])+(P2[2]*(-cI*(V3.W[0])+cI*(V3.W[3]))+P2[3]*(V3.W[1]-cI*(V3.W[2])))))+(F1.W[1]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-1.)*(V3.W[1]+cI*(V3.W[2]))+(P2[2]*(+cI*(V3.W[1])-V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+M2*(F1.W[2]*(-V3.W[1]+cI*(V3.W[2]))+F1.W[3]*(-V3.W[0]+V3.W[3])))); F2.W[2]= denom*cI*(F1.W[2]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-V3.W[1]+cI*(V3.W[2]))+(P2[2]*(-1.)*(+cI*(V3.W[1])+V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+(F1.W[3]*(P2[0]*(V3.W[1]+cI*(V3.W[2]))+(P2[1]*(-V3.W[0]+V3.W[3])+(P2[2]*(-cI*(V3.W[0])+cI*(V3.W[3]))-P2[3]*(V3.W[1]+cI*(V3.W[2])))))+M2*(F1.W[0]*(-V3.W[0]+V3.W[3])+F1.W[1]*(V3.W[1]+cI*(V3.W[2]))))); diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_2.c b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_2.c index a417173bf6..30d5727b98 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_2.c +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Cppwriter_C/ffv1c1_2.c @@ -14,7 +14,11 @@ P1[1] = -F1.p[1]; P1[2] = -F1.p[2]; P1[3] = -F1.p[3]; F1.flv_index = F2.flv_index; - denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1 * (M1 -cI* W1)); + if ((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) > 0.){ + denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1 * (M1 -cI* W1)); + } else { + denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1*M1); + } F1.W[0]= denom*(-cI)*(F2.W[0]*(P1[0]*(V3.W[0]+V3.W[3])+(P1[1]*(-1.)*(V3.W[1]+cI*(V3.W[2]))+(P1[2]*(+cI*(V3.W[1])-V3.W[2])-P1[3]*(V3.W[0]+V3.W[3]))))+(F2.W[1]*(P1[0]*(V3.W[1]-cI*(V3.W[2]))+(P1[1]*(-V3.W[0]+V3.W[3])+(P1[2]*(+cI*(V3.W[0])-cI*(V3.W[3]))+P1[3]*(-V3.W[1]+cI*(V3.W[2])))))+M1*(F2.W[2]*(V3.W[0]-V3.W[3])+F2.W[3]*(-V3.W[1]+cI*(V3.W[2]))))); F1.W[1]= denom*cI*(F2.W[0]*(P1[0]*(-1.)*(V3.W[1]+cI*(V3.W[2]))+(P1[1]*(V3.W[0]+V3.W[3])+(P1[2]*(+cI*(V3.W[0]+V3.W[3]))-P1[3]*(V3.W[1]+cI*(V3.W[2])))))+(F2.W[1]*(P1[0]*(-V3.W[0]+V3.W[3])+(P1[1]*(V3.W[1]-cI*(V3.W[2]))+(P1[2]*(+cI*(V3.W[1])+V3.W[2])+P1[3]*(-V3.W[0]+V3.W[3]))))+M1*(F2.W[2]*(V3.W[1]+cI*(V3.W[2]))-F2.W[3]*(V3.W[0]+V3.W[3])))); F1.W[2]= denom*cI*(F2.W[2]*(P1[0]*(-V3.W[0]+V3.W[3])+(P1[1]*(V3.W[1]+cI*(V3.W[2]))+(P1[2]*(-cI*(V3.W[1])+V3.W[2])+P1[3]*(-V3.W[0]+V3.W[3]))))+(F2.W[3]*(P1[0]*(V3.W[1]-cI*(V3.W[2]))+(P1[1]*(-1.)*(V3.W[0]+V3.W[3])+(P1[2]*(+cI*(V3.W[0]+V3.W[3]))+P1[3]*(V3.W[1]-cI*(V3.W[2])))))+M1*(F2.W[0]*(-1.)*(V3.W[0]+V3.W[3])+F2.W[1]*(-V3.W[1]+cI*(V3.W[2]))))); diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_F77writer_feynman/ffv1_3.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_F77writer_feynman/ffv1_3.f index 0b282e87df..d7e4864717 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_F77writer_feynman/ffv1_3.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_F77writer_feynman/ffv1_3.f @@ -21,7 +21,11 @@ subroutine FFV1_3(F1, F2, COUP, M3, W3,V3) V3%W(:) = (0d0,0d0) return endif - denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + if (dble(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2).gt.0d0) then + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + else + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3**2) + endif V3%W(1)= denom*(-CI)*(F2 % W(3)*F1 % W(1)+F2 % W(4)*F1 % W(2)+F2 % W(1)*F1 % W(3)+F2 % W(2)*F1 % W(4)) V3%W(2)= denom*(-CI)*(-F2 % W(4)*F1 % W(1)-F2 % W(3)*F1 % W(2)+F2 % W(2)*F1 % W(3)+F2 % W(1)*F1 % W(4)) V3%W(3)= denom*(-CI)*(-CI*(F2 % W(4)*F1 % W(1)+F2 % W(1)*F1 % W(4))+CI*(F2 % W(3)*F1 % W(2)+F2 % W(2)*F1 % W(3))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_1.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_1.f index 1c5515cf7a..1c1acb5eec 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_1.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_1.f @@ -14,7 +14,11 @@ subroutine RFSC1_1(R1, S3, COUP, M2, W2,F2) complex*16 denom F2%P(:) = +R1%P(:)+S3%P(:) P2(:) = -F2 % P (:) - denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + if (dble(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2).gt.0d0) then + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + else + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2**2) + endif F2%W(1)= denom*CI * S3 % W(1)*(P2(0)*(-1d0)*(-R1 % W(1)+R1 % W(6)+R1 % W(13)+CI*(R1 % W(10)))+(P2(1)*(R1 % W(2)+R1 % W(14)-R1 % W(5)+CI*(R1 % W(9)))+(P2(2)*(+CI*(R1 % W(2)+R1 % W(14))-CI*(R1 % W(5))-R1 % W(9))-P2(3)*(-R1 % W(1)+R1 % W(6)+R1 % W(13)+CI*(R1 % W(10)))))) F2%W(2)= denom*CI * S3 % W(1)*(P2(0)*(R1 % W(2)+R1 % W(14)-R1 % W(5)+CI*(R1 % W(9)))+(P2(1)*(-1d0)*(-R1 % W(1)+R1 % W(6)+R1 % W(13)+CI*(R1 % W(10)))+(P2(2)*(-CI*(R1 % W(1))+CI*(R1 % W(6)+R1 % W(13))-R1 % W(10))-P2(3)*(R1 % W(2)+R1 % W(14)-R1 % W(5)+CI*(R1 % W(9)))))) F2%W(3)= denom*CI * M2*S3 % W(1)*(-R1 % W(1)+R1 % W(6)+R1 % W(13)+CI*(R1 % W(10))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_2.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_2.f index d308376acc..07c38452ca 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_2.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_Fortranwriter_spin3half/rfsc1_2.f @@ -17,7 +17,11 @@ subroutine RFSC1_2(F2, S3, COUP, M1, W1,R1) if (M1.ne.0d0) OM1=1d0/M1**2 R1%P(:) = +F2%P(:)+S3%P(:) P1(:) = -R1 % P (:) - denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + if (dble(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2).gt.0d0) then + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + else + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1**2) + endif R1%W(1)= denom*1d0/3d0 * CI * M1*S3 % W(1)*(OM1*(P1(0)*(F2 % W(3)*(M1*M1*OM1*(-P1(0)+P1(3))+(+2d0*(P1(0))-P1(3)))+F2 % W(4)*(M1*M1*OM1*(P1(1)-CI*(P1(2)))+(-P1(1)+CI*(P1(2)))))-F2 % W(3)*(P1(3)*P1(3)+P1(1)*P1(1)+P1(2)*P1(2)))-F2 % W(3)) R1%W(2)= denom*1d0/3d0 * CI * M1*S3 % W(1)*(OM1*(P1(0)*(F2 % W(3)*(M1*M1*OM1*(P1(1)+CI*(P1(2)))+(-P1(1)-CI*(P1(2))))+F2 % W(4)*(M1*-M1*OM1*(P1(0)+P1(3))+(+2d0*(P1(0))+P1(3))))-F2 % W(4)*(P1(1)*P1(1)+P1(2)*P1(2)+P1(3)*P1(3)))-F2 % W(4)) R1%W(3)= denom*CI * S3 % W(1)*(F2 % W(3)*(OM1*(P1(0)*(M1*M1*(OM1*(-1d0/3d0)*(-P1(0)*P1(0)+P1(3)*P1(3)+P1(1)*P1(1)+P1(2)*P1(2))+ -5d0/3d0)+(-P1(0)*P1(0)+P1(3)*P1(3)+P1(1)*P1(1)+P1(2)*P1(2)))+1d0/3d0*(P1(3)*M1*M1))+(+7d0/3d0*(P1(0))-1d0/3d0*(P1(3))))+F2 % W(4)*(M1*1d0/3d0 * M1*OM1*(P1(1)-CI*(P1(2)))+(-1d0/3d0*(P1(1))+1d0/3d0 * CI*(P1(2))))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_aloha_MP_mode/ffvm_3.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_aloha_MP_mode/ffvm_3.f index c9d2c1b4d6..09e4353c22 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_aloha_MP_mode/ffvm_3.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_aloha_MP_mode/ffvm_3.f @@ -26,7 +26,11 @@ subroutine FFVM_3(F1, F2, COUP, M3, W3,V3) return endif TMP0 = (F1 % W(3)*(F2 % W(1)*(P3(0)+P3(3))+F2 % W(2)*(P3(1)-CI*(P3(2))))+F1 % W(4)*(F2 % W(1)*(P3(1)+CI*(P3(2)))+F2 % W(2)*(P3(0)-P3(3)))) - denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + if (dble(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2).gt.0d0) then + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + else + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3**2) + endif V3%W(1)= denom*(-CI)*(F2 % W(1)*F1 % W(3)+F2 % W(2)*F1 % W(4)-P3(0)*OM3*TMP0) V3%W(2)= denom*(-CI)*(-F2 % W(2)*F1 % W(3)-F2 % W(1)*F1 % W(4)-P3(1)*OM3*TMP0) V3%W(3)= denom*(-CI)*(+CI*(F2 % W(2)*F1 % W(3))-CI*(F2 % W(1)*F1 % W(4))-P3(2)*OM3*TMP0) @@ -62,7 +66,11 @@ subroutine MP_FFVM_3(F1, F2, COUP, M3, W3,V3) return endif TMP0 = (F1 % W(3)*(F2 % W(1)*(P3(0)+P3(3))+F2 % W(2)*(P3(1)-CI*(P3(2))))+F1 % W(4)*(F2 % W(1)*(P3(1)+CI*(P3(2)))+F2 % W(2)*(P3(0)-P3(3)))) - denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + if (dble(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2).gt.0d0) then + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3 * (M3 -CI* W3)) + else + denom = COUP/(P3(0)**2-P3(1)**2-P3(2)**2-P3(3)**2 - M3**2) + endif V3%W(1)= denom*(-CI)*(F2 % W(1)*F1 % W(3)+F2 % W(2)*F1 % W(4)-P3(0)*OM3*TMP0) V3%W(2)= denom*(-CI)*(-F2 % W(2)*F1 % W(3)-F2 % W(1)*F1 % W(4)-P3(1)*OM3*TMP0) V3%W(3)= denom*(-CI)*(+CI*(F2 % W(2)*F1 % W(3))-CI*(F2 % W(1)*F1 % W(4))-P3(2)*OM3*TMP0) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_1.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_1.f index b4a5e5d977..c97240c9ef 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_1.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_1.f @@ -16,7 +16,11 @@ subroutine FFV1C1_1(F1, V3, COUP, M2, W2,F2) F2%P(:) = +F1%P(:)+V3%P(:) P2(:) = -F2 % P (:) F2 % FLV_INDEX = F1 % FLV_INDEX - denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + if (dble(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2).gt.0d0) then + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + else + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2**2) + endif F2%W(1)= denom*(-CI)*(F1 % W(1)*(P2(0)*(-V3 % W(1)+V3 % W(4))+(P2(1)*(V3 % W(2)-CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))+V3 % W(3))+P2(3)*(-V3 % W(1)+V3 % W(4)))))+(F1 % W(2)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-1d0)*(V3 % W(1)+V3 % W(4))+(P2(2)*(-1d0)*(+CI*(V3 % W(1)+V3 % W(4)))+P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(3)*(V3 % W(1)+V3 % W(4))+F1 % W(4)*(V3 % W(2)+CI*(V3 % W(3)))))) F2%W(2)= denom*CI*(F1 % W(1)*(P2(0)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(V3 % W(1)-V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))+P2(3)*(V3 % W(2)-CI*(V3 % W(3))))))+(F1 % W(2)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-1d0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))-V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+M2*(F1 % W(3)*(-V3 % W(2)+CI*(V3 % W(3)))+F1 % W(4)*(-V3 % W(1)+V3 % W(4))))) F2%W(3)= denom*CI*(F1 % W(3)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(-1d0)*(+CI*(V3 % W(2))+V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+(F1 % W(4)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-V3 % W(1)+V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))-P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(1)*(-V3 % W(1)+V3 % W(4))+F1 % W(2)*(V3 % W(2)+CI*(V3 % W(3)))))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_2.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_2.f index 712a0706a4..ae4e0486c5 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_2.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_C/ffv1c1_2.f @@ -16,7 +16,11 @@ subroutine FFV1C1_2(F2, V3, COUP, M1, W1,F1) F1%P(:) = +F2%P(:)+V3%P(:) P1(:) = -F1 % P (:) F1 % FLV_INDEX = F2 % FLV_INDEX - denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + if (dble(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2).gt.0d0) then + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + else + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1**2) + endif F1%W(1)= denom*(-CI)*(F2 % W(1)*(P1(0)*(V3 % W(1)+V3 % W(4))+(P1(1)*(-1d0)*(V3 % W(2)+CI*(V3 % W(3)))+(P1(2)*(+CI*(V3 % W(2))-V3 % W(3))-P1(3)*(V3 % W(1)+V3 % W(4)))))+(F2 % W(2)*(P1(0)*(V3 % W(2)-CI*(V3 % W(3)))+(P1(1)*(-V3 % W(1)+V3 % W(4))+(P1(2)*(+CI*(V3 % W(1))-CI*(V3 % W(4)))+P1(3)*(-V3 % W(2)+CI*(V3 % W(3))))))+M1*(F2 % W(3)*(V3 % W(1)-V3 % W(4))+F2 % W(4)*(-V3 % W(2)+CI*(V3 % W(3)))))) F1%W(2)= denom*CI*(F2 % W(1)*(P1(0)*(-1d0)*(V3 % W(2)+CI*(V3 % W(3)))+(P1(1)*(V3 % W(1)+V3 % W(4))+(P1(2)*(+CI*(V3 % W(1)+V3 % W(4)))-P1(3)*(V3 % W(2)+CI*(V3 % W(3))))))+(F2 % W(2)*(P1(0)*(-V3 % W(1)+V3 % W(4))+(P1(1)*(V3 % W(2)-CI*(V3 % W(3)))+(P1(2)*(+CI*(V3 % W(2))+V3 % W(3))+P1(3)*(-V3 % W(1)+V3 % W(4)))))+M1*(F2 % W(3)*(V3 % W(2)+CI*(V3 % W(3)))-F2 % W(4)*(V3 % W(1)+V3 % W(4))))) F1%W(3)= denom*CI*(F2 % W(3)*(P1(0)*(-V3 % W(1)+V3 % W(4))+(P1(1)*(V3 % W(2)+CI*(V3 % W(3)))+(P1(2)*(-CI*(V3 % W(2))+V3 % W(3))+P1(3)*(-V3 % W(1)+V3 % W(4)))))+(F2 % W(4)*(P1(0)*(V3 % W(2)-CI*(V3 % W(3)))+(P1(1)*(-1d0)*(V3 % W(1)+V3 % W(4))+(P1(2)*(+CI*(V3 % W(1)+V3 % W(4)))+P1(3)*(V3 % W(2)-CI*(V3 % W(3))))))+M1*(F2 % W(1)*(-1d0)*(V3 % W(1)+V3 % W(4))+F2 % W(2)*(-V3 % W(2)+CI*(V3 % W(3)))))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c1_1.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c1_1.f index e14ac8b9eb..d021025f29 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c1_1.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c1_1.f @@ -22,7 +22,11 @@ subroutine FFV1C1_1(F1, V3, COUP, M2, W2,F2) F2 % FLV_INDEX = F1 % FLV_INDEX TMP0 = (P3(0)*P3(0)-P3(1)*P3(1)-P3(2)*P3(2)-P3(3)*P3(3)) FCT0 = exp(TMP0) - denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + if (dble(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2).gt.0d0) then + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + else + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2**2) + endif F2%W(1)= denom*(-CI )* FCT0*(F1 % W(1)*(P2(0)*(-V3 % W(1)+V3 % W(4))+(P2(1)*(V3 % W(2)-CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))+V3 % W(3))+P2(3)*(-V3 % W(1)+V3 % W(4)))))+(F1 % W(2)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-1d0)*(V3 % W(1)+V3 % W(4))+(P2(2)*(-1d0)*(+CI*(V3 % W(1)+V3 % W(4)))+P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(3)*(V3 % W(1)+V3 % W(4))+F1 % W(4)*(V3 % W(2)+CI*(V3 % W(3)))))) F2%W(2)= denom*CI * FCT0*(F1 % W(1)*(P2(0)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(V3 % W(1)-V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))+P2(3)*(V3 % W(2)-CI*(V3 % W(3))))))+(F1 % W(2)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-1d0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))-V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+M2*(F1 % W(3)*(-V3 % W(2)+CI*(V3 % W(3)))+F1 % W(4)*(-V3 % W(1)+V3 % W(4))))) F2%W(3)= denom*CI * FCT0*(F1 % W(3)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(-1d0)*(+CI*(V3 % W(2))+V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+(F1 % W(4)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-V3 % W(1)+V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))-P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(1)*(-V3 % W(1)+V3 % W(4))+F1 % W(2)*(V3 % W(2)+CI*(V3 % W(3)))))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c2_1.f b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c2_1.f index dffa908e3f..c5137c4acb 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c2_1.f +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_fortranwriter_CFF/ffv1c2_1.f @@ -24,7 +24,11 @@ subroutine FFV1C1_1(F1, V3, COUP, M2, W2,F2) F2 % FLV_INDEX = F1 % FLV_INDEX TMP0 = (P3(0)*P3(0)-P3(1)*P3(1)-P3(2)*P3(2)-P3(3)*P3(3)) FCT1 = mymdl_VEC(TMP0) - denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + if (dble(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2).gt.0d0) then + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2 * (M2 -CI* W2)) + else + denom = COUP/(P2(0)**2-P2(1)**2-P2(2)**2-P2(3)**2 - M2**2) + endif F2%W(1)= denom*(-CI )* FCT1*(F1 % W(1)*(P2(0)*(-V3 % W(1)+V3 % W(4))+(P2(1)*(V3 % W(2)-CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))+V3 % W(3))+P2(3)*(-V3 % W(1)+V3 % W(4)))))+(F1 % W(2)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-1d0)*(V3 % W(1)+V3 % W(4))+(P2(2)*(-1d0)*(+CI*(V3 % W(1)+V3 % W(4)))+P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(3)*(V3 % W(1)+V3 % W(4))+F1 % W(4)*(V3 % W(2)+CI*(V3 % W(3)))))) F2%W(2)= denom*CI * FCT1*(F1 % W(1)*(P2(0)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(V3 % W(1)-V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))+P2(3)*(V3 % W(2)-CI*(V3 % W(3))))))+(F1 % W(2)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-1d0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(+CI*(V3 % W(2))-V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+M2*(F1 % W(3)*(-V3 % W(2)+CI*(V3 % W(3)))+F1 % W(4)*(-V3 % W(1)+V3 % W(4))))) F2%W(3)= denom*CI * FCT1*(F1 % W(3)*(P2(0)*(V3 % W(1)+V3 % W(4))+(P2(1)*(-V3 % W(2)+CI*(V3 % W(3)))+(P2(2)*(-1d0)*(+CI*(V3 % W(2))+V3 % W(3))-P2(3)*(V3 % W(1)+V3 % W(4)))))+(F1 % W(4)*(P2(0)*(V3 % W(2)+CI*(V3 % W(3)))+(P2(1)*(-V3 % W(1)+V3 % W(4))+(P2(2)*(-CI*(V3 % W(1))+CI*(V3 % W(4)))-P2(3)*(V3 % W(2)+CI*(V3 % W(3))))))+M2*(F1 % W(1)*(-V3 % W(1)+V3 % W(4))+F1 % W(2)*(V3 % W(2)+CI*(V3 % W(3)))))) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_1.py b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_1.py index 2d7bca5c77..44a31cea48 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_1.py +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_1.py @@ -7,7 +7,10 @@ def RFSC1_1(R1,S3,COUP,M2,W2): F2.momenta[2] = +R1.momenta[2]+S3.momenta[2] F2.momenta[3] = +R1.momenta[3]+S3.momenta[3] P2 = [-F2.momenta[j] for j in range(4)] - denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + if (P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2).real > 0: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + else: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2**2) F2.W[0]= denom*1j * S3.W[0]*(P2[0]*(-1)*(-R1.W[0]+R1.W[5]+R1.W[12]+1j*(R1.W[9]))+(P2[1]*(R1.W[1]+R1.W[13]-R1.W[4]+1j*(R1.W[8]))+(P2[2]*(+1j*(R1.W[1]+R1.W[13])-1j*(R1.W[4])-R1.W[8])-P2[3]*(-R1.W[0]+R1.W[5]+R1.W[12]+1j*(R1.W[9]))))) F2.W[1]= denom*1j * S3.W[0]*(P2[0]*(R1.W[1]+R1.W[13]-R1.W[4]+1j*(R1.W[8]))+(P2[1]*(-1)*(-R1.W[0]+R1.W[5]+R1.W[12]+1j*(R1.W[9]))+(P2[2]*(-1j*(R1.W[0])+1j*(R1.W[5]+R1.W[12])-R1.W[9])-P2[3]*(R1.W[1]+R1.W[13]-R1.W[4]+1j*(R1.W[8]))))) F2.W[2]= denom*1j * M2*S3.W[0]*(-R1.W[0]+R1.W[5]+R1.W[12]+1j*(R1.W[9])) diff --git a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_2.py b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_2.py index 0958548914..798a37eaac 100644 --- a/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_2.py +++ b/tests/input_files/IOTestsComparison/TestAlohaWriter/short_pythonwriter_spin3half/rfsc1_2.py @@ -13,7 +13,10 @@ def RFSC1_2(F2,S3,COUP,M1,W1): flv_index2 = F2.flavor if flv_index1 != -1 and flv_index2 != -1 and flv_index1 != flv_index2: return R1 - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) R1.W[0]= denom*1j/3 * M1*S3.W[0]*(OM1*(P1[0]*(F2.W[2]*(M1*M1*OM1*(-P1[0]+P1[3])+(+2*(P1[0])-P1[3]))+F2.W[3]*(M1*M1*OM1*(P1[1]-1j*(P1[2]))+(-P1[1]+1j*(P1[2]))))-F2.W[2]*(P1[3]*P1[3]+P1[1]*P1[1]+P1[2]*P1[2]))-F2.W[2]) R1.W[4]= denom*-1j/3 * M1*S3.W[0]*(OM1*(P1[1]*(F2.W[2]*(M1*M1*OM1*(P1[0]-P1[3])+(-P1[0]+P1[3]))+F2.W[3]*(M1*M1*OM1*(-P1[1]+1j*(P1[2]))+(+2*(P1[1])-1j*(P1[2]))))+F2.W[3]*(P1[2]*P1[2]+P1[3]*P1[3]-P1[0]*P1[0]))+F2.W[3]) R1.W[8]= denom*-1/3 * M1*S3.W[0]*(OM1*(P1[2]*(F2.W[2]*(M1*M1*OM1*(+1j*(P1[0])-1j*(P1[3]))+(-1j*(P1[0])+1j*(P1[3])))+F2.W[3]*(M1*-M1*OM1*(+1j*(P1[1])+P1[2])+(+1j*(P1[1])+2*(P1[2]))))+F2.W[3]*(P1[1]*P1[1]+P1[3]*P1[3]-P1[0]*P1[0]))+F2.W[3]) diff --git a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/cpp.cc b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/cpp.cc index c7f4a28490..8c3d5dd414 100644 --- a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/cpp.cc +++ b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/cpp.cc @@ -19,7 +19,11 @@ P1[1] = -V1.p[1]; P1[2] = -V1.p[2]; P1[3] = -V1.p[3]; TMP0 = (V2.W[0]*P1[0]-V2.W[1]*P1[1]-V2.W[2]*P1[2]-V2.W[3]*P1[3]); - denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1 * (M1 -cI* W1)); + if ((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) > 0.){ + denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1 * (M1 -cI* W1)); + } else { + denom = COUP/((P1[0]*P1[0])-(P1[1]*P1[1])-(P1[2]*P1[2])-(P1[3]*P1[3]) - M1*M1); + } V1.W[0]= denom*S3.W[0]*(-cI*(V2.W[0])+cI*(P1[0]*OM1*TMP0)); V1.W[1]= denom*S3.W[0]*(-cI*(V2.W[1])+cI*(P1[1]*OM1*TMP0)); V1.W[2]= denom*S3.W[0]*(-cI*(V2.W[2])+cI*(P1[2]*OM1*TMP0)); diff --git a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/fortran.f b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/fortran.f index 6172a93895..16572a4516 100644 --- a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/fortran.f +++ b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/fortran.f @@ -20,7 +20,11 @@ subroutine VVS1_1(V2, S3, COUP, M1, W1,V1) V1%P(:) = +V2%P(:)+S3%P(:) P1(:) = -V1 % P (:) TMP0 = (V2 % W(1)*P1(0)-V2 % W(2)*P1(1)-V2 % W(3)*P1(2)-V2 % W(4)*P1(3)) - denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + if (dble(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2).gt.0d0) then + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI* W1)) + else + denom = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1**2) + endif V1%W(1)= denom*S3 % W(1)*(-CI*(V2 % W(1))+CI*(P1(0)*OM1*TMP0)) V1%W(2)= denom*S3 % W(1)*(-CI*(V2 % W(2))+CI*(P1(1)*OM1*TMP0)) V1%W(3)= denom*S3 % W(1)*(-CI*(V2 % W(3))+CI*(P1(2)*OM1*TMP0)) diff --git a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/vvs1.py b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/vvs1.py index 17179c77da..42df29c1a2 100644 --- a/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/vvs1.py +++ b/tests/input_files/IOTestsComparison/test_aloha_creation/short_aloha_multiple_lorentz_and_symmetry/vvs1.py @@ -10,7 +10,10 @@ def VVS1_1(V2,S3,COUP,M1,W1): V1.momenta[3] = +V2.momenta[3]+S3.momenta[3] P1 = [-V1.momenta[j] for j in range(4)] TMP0 = (V2.W[0]*P1[0]-V2.W[1]*P1[1]-V2.W[2]*P1[2]-V2.W[3]*P1[3]) - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) V1.W[0]= denom*S3.W[0]*(-1j*(V2.W[0])+1j*(P1[0]*OM1*TMP0)) V1.W[1]= denom*S3.W[0]*(-1j*(V2.W[1])+1j*(P1[1]*OM1*TMP0)) V1.W[2]= denom*S3.W[0]*(-1j*(V2.W[2])+1j*(P1[2]*OM1*TMP0)) diff --git a/tests/input_files/wpj_merged_antiparticle.lhe.gz b/tests/input_files/wpj_merged_antiparticle.lhe.gz new file mode 100644 index 0000000000..b3da4f29a0 Binary files /dev/null and b/tests/input_files/wpj_merged_antiparticle.lhe.gz differ diff --git a/tests/parallel_tests/decay_comparator.py b/tests/parallel_tests/decay_comparator.py index e2ed05418d..472df7f1c5 100755 --- a/tests/parallel_tests/decay_comparator.py +++ b/tests/parallel_tests/decay_comparator.py @@ -349,7 +349,7 @@ def check_3body(self, part, multi1='all', multi2='all', multi3='all', log=None, os.system('rm -rf %s >/dev/null' % dir_name) os.system('rm -rf %s_dec >/dev/null' % dir_name) self.cmd.run_cmd('set automatic_html_opening False --no-save') - self.cmd.exec_cmd('generate %s > %s %s %s $ all $$ %s --optimize' % + self.cmd.exec_cmd('generate %s > %s %s %s $ all $$ %s --optimize' % (part, multi1, multi2, multi3, ' '.join(to_avoid))) print('generate %s > %s %s %s $ all $$ %s --optimize' % \ (part, multi1, multi2, multi3, ' '.join(to_avoid))) diff --git a/tests/parallel_tests/input_files/mg5_short_paralleltest_heft.pkl b/tests/parallel_tests/input_files/mg5_short_paralleltest_heft.pkl index eace2c4e7f..b1f2bff711 100644 Binary files a/tests/parallel_tests/input_files/mg5_short_paralleltest_heft.pkl and b/tests/parallel_tests/input_files/mg5_short_paralleltest_heft.pkl differ diff --git a/tests/parallel_tests/input_files/mg5_short_paralleltest_mssm.pkl b/tests/parallel_tests/input_files/mg5_short_paralleltest_mssm.pkl index e8ff01ded7..8403789b12 100644 Binary files a/tests/parallel_tests/input_files/mg5_short_paralleltest_mssm.pkl and b/tests/parallel_tests/input_files/mg5_short_paralleltest_mssm.pkl differ diff --git a/tests/parallel_tests/input_files/mg5_short_paralleltest_sm.pkl b/tests/parallel_tests/input_files/mg5_short_paralleltest_sm.pkl index 20c2c6d45f..f02a230d12 100644 Binary files a/tests/parallel_tests/input_files/mg5_short_paralleltest_sm.pkl and b/tests/parallel_tests/input_files/mg5_short_paralleltest_sm.pkl differ diff --git a/tests/parallel_tests/input_files/mg5_short_paralleltest_sqso.pkl b/tests/parallel_tests/input_files/mg5_short_paralleltest_sqso.pkl index 1c260c8656..a62797260b 100644 Binary files a/tests/parallel_tests/input_files/mg5_short_paralleltest_sqso.pkl and b/tests/parallel_tests/input_files/mg5_short_paralleltest_sqso.pkl differ diff --git a/tests/parallel_tests/me_comparator.py b/tests/parallel_tests/me_comparator.py index 83411edf5c..22de93acbc 100755 --- a/tests/parallel_tests/me_comparator.py +++ b/tests/parallel_tests/me_comparator.py @@ -482,6 +482,49 @@ def __init__(self, cms, gauge): self.type = '%s_%s' %(self.cms, self.gauge) self.name = 'MG5_%s_%s' %(self.cms, self.gauge) + # Above this collision energy every electroweak resonance (M_W, M_Z, M_H, + # M_top ~ 80-173 GeV) is far off shell, so a fixed-width propagator can have + # its width dropped without hitting a pole. At/near a resonance (the 90 GeV + # runs sit on the Z pole) the width is physical and must be kept. + RESONANCE_SAFE_ENERGY = 300.0 + + def fix_energy_in_check(self, dir_name, energy): + """Set the collision energy (parent behaviour) and, for the fixed-width + runs *well above the resonances*, zero every width in the param_card. + + Rationale: this test compares the complex-mass scheme against the + fixed-width scheme in several gauges. A finite width i*M*Gamma is what + breaks gauge/scheme invariance at O(Gamma) -- and the default treatment + keeps it for timelike (s-channel) but drops it for spacelike (t-channel) + propagators (aloha.t_channel_width / zerowidth_tchannel), an imbalance + the FD gauge is sensitive enough to fail on (e+ e- > e+ ve d u~ at + 500 GeV). Off resonance the width is a pure O(Gamma) nuisance, so zero + every width in the fixed-width runs: the three fixed-width gauges then + agree exactly. On the Z pole (90 GeV) the width regulates a real + resonance, so it is kept there -- zeroing it would blow the propagator + up (e.g. b b~ > b b~ g). The complex-mass (cms='True') runs always keep + their widths: the width lives inside the complex mass and defines the + scheme. + """ + if self.cms == 'False' and energy >= self.RESONANCE_SAFE_ENERGY: + self._zero_widths_in_param_card(dir_name) + return super(MG5_UFO_gauge_Runner, self).fix_energy_in_check( + dir_name, energy) + + @staticmethod + def _zero_widths_in_param_card(dir_name): + """Rewrite every DECAY width to 0 in /Cards/param_card.dat.""" + card = os.path.join(dir_name, 'Cards', 'param_card.dat') + if not os.path.exists(card): + return + with open(card) as fsock: + text = fsock.read() + # DECAY [ # comment] -> DECAY 0.000000e+00 [ # ...] + text = re.sub(r'(?im)^(DECAY\s+\d+\s+)[+-]?\d*\.?\d+(?:[eEdD][+-]?\d+)?', + r'\g<1>0.000000e+00', text) + with open(card, 'w') as fsock: + fsock.write(text) + def format_mg5_proc_card(self, proc_list, model, orders): """Create a proc_card.dat string following v5 conventions.""" @@ -489,6 +532,16 @@ def format_mg5_proc_card(self, proc_list, model, orders): v5_string += "set automatic_html_opening False\n" v5_string += 'set complex_mass_scheme %s \n' % self.cms v5_string += 'set gauge %s \n' % self.gauge + # Keep the width in spacelike (t-channel) propagators. This matters for + # the on-resonance (90 GeV) runs, where widths are NOT zeroed below: + # the complex-mass scheme carries i*M*Gamma in every propagator (it + # lives in the complex mass M^2 -> M^2 - i*M*Gamma), t-channel included, + # whereas the default fixed-width treatment DROPS it for spacelike + # momenta -- an s/t imbalance that violates gauge invariance at + # O(Gamma). Above the resonances the widths are zeroed outright (see + # fix_energy_in_check), so this is a no-op there. Ignored for the CMS + # runs (the width already lives in the complex mass). + v5_string += 'set zerowidth_tchannel False \n' v5_string += "import model %s \n" % os.path.join(self.model_dir, model) couplings = MERunner.get_coupling_definitions(orders) diff --git a/tests/parallel_tests/test_aloha.py b/tests/parallel_tests/test_aloha.py index 7a6c9f1a7e..df263a8c5d 100755 --- a/tests/parallel_tests/test_aloha.py +++ b/tests/parallel_tests/test_aloha.py @@ -1084,7 +1084,7 @@ def test_short_expand_veto(self): analytical2= analytical2.expand() for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): try: - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) except: pass @@ -1214,7 +1214,7 @@ def test_short_part_spin32propagator(self): M1 = math.sqrt(P1_0 **2 - P1_1 **2 -P1_2 **2 -P1_3 **2) for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): try: - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) except: pass for ind in zero.listindices(): @@ -1337,7 +1337,7 @@ def test_short_spin2propagator4(self): OM1 = 1.0/48#(P1_0 **2 - P1_1 **2 -P1_2 **2 -P1_3 **2) for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): try: - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) except: pass @@ -2854,7 +2854,7 @@ def test_short_aloha_ZPZZ(self): P2_0,P2_1,P2_2,P2_3 = 101, 111, 121, 134 P3_0,P3_1,P3_2,P3_3 = 1001, 1106, 1240, 1320 for name, cexpr in abstract_ZP.contracted.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) for ind in expr.listindices(): self.assertEqual(eval(str(expr.get_rep(ind))), 178727040j) @@ -2920,10 +2920,10 @@ def test_short_use_of_library_spin2(self): M3 = 500 #for name, cexpr in one_exp.contracted.items(): - # exec('%s = %s' % (name, cexpr)) + # globals()[name] = eval(str(cexpr)) for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): try: - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) except: pass for ind in one_exp.listindices(): @@ -3017,7 +3017,7 @@ def test_short_aloha_FFT2(self): M3 = 500 for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) for ind in zero.listindices(): self.assertAlmostEqual(eval(str(zero.get_rep(ind))),0) @@ -3082,7 +3082,7 @@ def test_short_aloha_FFV(self): P3_0,P3_1,P3_2,P3_3 = 10, 11, 12, 13 for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) for ind in abstract.expr.listindices(): self.assertAlmostEqual(eval(str(abstract.expr.get_rep(ind))) - @@ -3116,7 +3116,7 @@ def test_short_aloha_FFVP1N(self): #evaluate all contraction for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) # evaluate FFV_0 val_V = eval(str(V.expr.get_rep((0,)))) @@ -3174,7 +3174,7 @@ def test_short_aloha_FFV_MG4(self): s4 = -j*((OM3*(P3_3*((F2_1*((F1_3*(-P3_0-P3_3))+(F1_4*(-P3_1-1*j*P3_2))))+(F2_2*((F1_3*(-P3_1+1*j*P3_2))+(F1_4*(-P3_0+P3_3)))))))+(-(F1_3*F2_1)+(F1_4*F2_2))) for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) self.assertEqual(s1, eval(str(abstract_M.expr.get_rep([0])))) self.assertEqual(s2, eval(str(abstract_M.expr.get_rep([1])))) @@ -3193,7 +3193,7 @@ def test_short_aloha_FFV_MG4(self): zero = abstract_6.expr - abstract_M.expr - \ 2* abstract_P.expr for name, cexpr in aloha_lib.KERNEL.reduced_expr2.items(): - exec('%s = %s' % (name, cexpr)) + globals()[name] = eval(str(cexpr)) for ind in zero.listindices(): self.assertEqual(eval(str(zero.get_rep(ind))),0) @@ -4020,7 +4020,10 @@ def SSS1_1(S2,S3,COUP,M1,W1): S1.momenta[2] = +S2.momenta[2]+S3.momenta[2] S1.momenta[3] = +S2.momenta[3]+S3.momenta[3] P1 = [-S1.momenta[j] for j in range(4)] - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) S1.W[0]= denom*1j * S3.W[0]*S2.W[0] return S1 @@ -4140,7 +4143,10 @@ def FFV1C1_1(F1,V3,COUP,M2,W2): F2.momenta[3] = +F1.momenta[3]+V3.momenta[3] P2 = [-F2.momenta[j] for j in range(4)] F2.flavor = F1.flavor - denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + if (P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2).real > 0: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + else: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2**2) F2.W[0]= denom*(-1j)*(F1.W[0]*(P2[0]*(-V3.W[0]+V3.W[3])+(P2[1]*(V3.W[1]-1j*(V3.W[2]))+(P2[2]*(+1j*(V3.W[1])+V3.W[2])+P2[3]*(-V3.W[0]+V3.W[3]))))+(F1.W[1]*(P2[0]*(V3.W[1]+1j*(V3.W[2]))+(P2[1]*(-1)*(V3.W[0]+V3.W[3])+(P2[2]*(-1)*(+1j*(V3.W[0]+V3.W[3]))+P2[3]*(V3.W[1]+1j*(V3.W[2])))))+M2*(F1.W[2]*(V3.W[0]+V3.W[3])+F1.W[3]*(V3.W[1]+1j*(V3.W[2]))))) F2.W[1]= denom*1j*(F1.W[0]*(P2[0]*(-V3.W[1]+1j*(V3.W[2]))+(P2[1]*(V3.W[0]-V3.W[3])+(P2[2]*(-1j*(V3.W[0])+1j*(V3.W[3]))+P2[3]*(V3.W[1]-1j*(V3.W[2])))))+(F1.W[1]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-1)*(V3.W[1]+1j*(V3.W[2]))+(P2[2]*(+1j*(V3.W[1])-V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+M2*(F1.W[2]*(-V3.W[1]+1j*(V3.W[2]))+F1.W[3]*(-V3.W[0]+V3.W[3])))) F2.W[2]= denom*1j*(F1.W[2]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-V3.W[1]+1j*(V3.W[2]))+(P2[2]*(-1)*(+1j*(V3.W[1])+V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+(F1.W[3]*(P2[0]*(V3.W[1]+1j*(V3.W[2]))+(P2[1]*(-V3.W[0]+V3.W[3])+(P2[2]*(-1j*(V3.W[0])+1j*(V3.W[3]))-P2[3]*(V3.W[1]+1j*(V3.W[2])))))+M2*(F1.W[0]*(-V3.W[0]+V3.W[3])+F1.W[1]*(V3.W[1]+1j*(V3.W[2]))))) @@ -4172,7 +4178,10 @@ def FFV1C1_2(F2,V3,COUP,M1,W1): F1.momenta[3] = +F2.momenta[3]+V3.momenta[3] P1 = [-F1.momenta[j] for j in range(4)] F1.flavor = F2.flavor - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) F1.W[0]= denom*(-1j)*(F2.W[0]*(P1[0]*(V3.W[0]+V3.W[3])+(P1[1]*(-1)*(V3.W[1]+1j*(V3.W[2]))+(P1[2]*(+1j*(V3.W[1])-V3.W[2])-P1[3]*(V3.W[0]+V3.W[3]))))+(F2.W[1]*(P1[0]*(V3.W[1]-1j*(V3.W[2]))+(P1[1]*(-V3.W[0]+V3.W[3])+(P1[2]*(+1j*(V3.W[0])-1j*(V3.W[3]))+P1[3]*(-V3.W[1]+1j*(V3.W[2])))))+M1*(F2.W[2]*(V3.W[0]-V3.W[3])+F2.W[3]*(-V3.W[1]+1j*(V3.W[2]))))) F1.W[1]= denom*1j*(F2.W[0]*(P1[0]*(-1)*(V3.W[1]+1j*(V3.W[2]))+(P1[1]*(V3.W[0]+V3.W[3])+(P1[2]*(+1j*(V3.W[0]+V3.W[3]))-P1[3]*(V3.W[1]+1j*(V3.W[2])))))+(F2.W[1]*(P1[0]*(-V3.W[0]+V3.W[3])+(P1[1]*(V3.W[1]-1j*(V3.W[2]))+(P1[2]*(+1j*(V3.W[1])+V3.W[2])+P1[3]*(-V3.W[0]+V3.W[3]))))+M1*(F2.W[2]*(V3.W[1]+1j*(V3.W[2]))-F2.W[3]*(V3.W[0]+V3.W[3])))) F1.W[2]= denom*1j*(F2.W[2]*(P1[0]*(-V3.W[0]+V3.W[3])+(P1[1]*(V3.W[1]+1j*(V3.W[2]))+(P1[2]*(-1j*(V3.W[1])+V3.W[2])+P1[3]*(-V3.W[0]+V3.W[3]))))+(F2.W[3]*(P1[0]*(V3.W[1]-1j*(V3.W[2]))+(P1[1]*(-1)*(V3.W[0]+V3.W[3])+(P1[2]*(+1j*(V3.W[0]+V3.W[3]))+P1[3]*(V3.W[1]-1j*(V3.W[2])))))+M1*(F2.W[0]*(-1)*(V3.W[0]+V3.W[3])+F2.W[1]*(-V3.W[1]+1j*(V3.W[2]))))) @@ -4200,7 +4209,10 @@ def FFV1C1_1(F1,V3,COUP,M2,W2): F2.momenta[3] = +F1.momenta[3]+V3.momenta[3] P2 = [-F2.momenta[j] for j in range(4)] F2.flavor = F1.flavor - denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + if (P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2).real > 0: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + else: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2**2) F2.W[0]= denom*(-1j)*(F1.W[0]*(P2[0]*(-V3.W[0]+V3.W[3])+(P2[1]*(V3.W[1]-1j*(V3.W[2]))+(P2[2]*(+1j*(V3.W[1])+V3.W[2])+P2[3]*(-V3.W[0]+V3.W[3]))))+(F1.W[1]*(P2[0]*(V3.W[1]+1j*(V3.W[2]))+(P2[1]*(-1)*(V3.W[0]+V3.W[3])+(P2[2]*(-1)*(+1j*(V3.W[0]+V3.W[3]))+P2[3]*(V3.W[1]+1j*(V3.W[2])))))+M2*(F1.W[2]*(V3.W[0]+V3.W[3])+F1.W[3]*(V3.W[1]+1j*(V3.W[2]))))) F2.W[1]= denom*1j*(F1.W[0]*(P2[0]*(-V3.W[1]+1j*(V3.W[2]))+(P2[1]*(V3.W[0]-V3.W[3])+(P2[2]*(-1j*(V3.W[0])+1j*(V3.W[3]))+P2[3]*(V3.W[1]-1j*(V3.W[2])))))+(F1.W[1]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-1)*(V3.W[1]+1j*(V3.W[2]))+(P2[2]*(+1j*(V3.W[1])-V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+M2*(F1.W[2]*(-V3.W[1]+1j*(V3.W[2]))+F1.W[3]*(-V3.W[0]+V3.W[3])))) F2.W[2]= denom*1j*(F1.W[2]*(P2[0]*(V3.W[0]+V3.W[3])+(P2[1]*(-V3.W[1]+1j*(V3.W[2]))+(P2[2]*(-1)*(+1j*(V3.W[1])+V3.W[2])-P2[3]*(V3.W[0]+V3.W[3]))))+(F1.W[3]*(P2[0]*(V3.W[1]+1j*(V3.W[2]))+(P2[1]*(-V3.W[0]+V3.W[3])+(P2[2]*(-1j*(V3.W[0])+1j*(V3.W[3]))-P2[3]*(V3.W[1]+1j*(V3.W[2])))))+M2*(F1.W[0]*(-V3.W[0]+V3.W[3])+F1.W[1]*(V3.W[1]+1j*(V3.W[2]))))) @@ -4250,7 +4262,10 @@ def FFFF1_1(F2,F3,F4,COUP,M1,W1): P1 = [-F1.momenta[j] for j in range(4)] F1.flavor = F2.flavor TMP0 = (F4.W[0]*F3.W[0]+F4.W[1]*F3.W[1]+F4.W[2]*F3.W[2]+F4.W[3]*F3.W[3]) - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) F1.W[0]= denom*-1j * TMP0*(F2.W[2]*(P1[0]+P1[3])+(F2.W[3]*(P1[1]+1j*(P1[2]))-F2.W[0]*M1)) F1.W[1]= denom*1j * TMP0*(F2.W[2]*(-P1[1]+1j*(P1[2]))+(F2.W[3]*(-P1[0]+P1[3])+F2.W[1]*M1)) F1.W[2]= denom*1j * TMP0*(F2.W[0]*(-P1[0]+P1[3])+(F2.W[1]*(P1[1]+1j*(P1[2]))+F2.W[2]*M1)) @@ -4284,7 +4299,10 @@ def FFFF1C1_1(F1,F3,F4,COUP,M2,W2): P2 = [-F2.momenta[j] for j in range(4)] F2.flavor = F1.flavor TMP0 = (F4.W[0]*F3.W[0]+F4.W[1]*F3.W[1]+F4.W[2]*F3.W[2]+F4.W[3]*F3.W[3]) - denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + if (P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2).real > 0: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2 * (M2 -1j* W2)) + else: + denom = COUP/(P2[0]**2-P2[1]**2-P2[2]**2-P2[3]**2 - M2**2) F2.W[0]= denom*-1j * TMP0*(F1.W[2]*(P2[0]+P2[3])+(F1.W[3]*(P2[1]+1j*(P2[2]))-F1.W[0]*M2)) F2.W[1]= denom*1j * TMP0*(F1.W[2]*(-P2[1]+1j*(P2[2]))+(F1.W[3]*(-P2[0]+P2[3])+F1.W[1]*M2)) F2.W[2]= denom*1j * TMP0*(F1.W[0]*(-P2[0]+P2[3])+(F1.W[1]*(P2[1]+1j*(P2[2]))+F1.W[2]*M2)) @@ -4318,7 +4336,10 @@ def FFFF1C2_1(F2,F4,F3,COUP,M1,W1): P1 = [-F1.momenta[j] for j in range(4)] F1.flavor = F2.flavor TMP0 = (F4.W[0]*F3.W[0]+F4.W[1]*F3.W[1]+F4.W[2]*F3.W[2]+F4.W[3]*F3.W[3]) - denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + if (P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2).real > 0: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1 * (M1 -1j* W1)) + else: + denom = COUP/(P1[0]**2-P1[1]**2-P1[2]**2-P1[3]**2 - M1**2) F1.W[0]= denom*-1j * TMP0*(F2.W[2]*(P1[0]+P1[3])+(F2.W[3]*(P1[1]+1j*(P1[2]))-F2.W[0]*M1)) F1.W[1]= denom*1j * TMP0*(F2.W[2]*(-P1[1]+1j*(P1[2]))+(F2.W[3]*(-P1[0]+P1[3])+F2.W[1]*M1)) F1.W[2]= denom*1j * TMP0*(F2.W[0]*(-P1[0]+P1[3])+(F2.W[1]*(P1[1]+1j*(P1[2]))+F2.W[2]*M1)) diff --git a/tests/unit_tests/core/test_diagram_generation.py b/tests/unit_tests/core/test_diagram_generation.py index 729c50ce3b..8913d3b11a 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/interface/test_reweight_density.py b/tests/unit_tests/interface/test_reweight_density.py new file mode 100644 index 0000000000..fdfc8bc8b5 --- /dev/null +++ b/tests/unit_tests/interface/test_reweight_density.py @@ -0,0 +1,223 @@ +############################################################################## +# +# Copyright (c) 2010 The MadGraph Development team and Contributors +# +# This file is a part of the MadGraph 5 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 MadGraph license which should accompany this +# distribution. +# +# For more information, please visit: http://madgraph.phys.ucl.ac.be +# +################################################################################ +""" Test of the average density matrix helpers of the density mode. + +Those helpers are shared by DensityInterface (which accumulates the average +while it reweights the events) and by CommonRunCmd.do_reweight (which has to +re-build the average from the recombined event file after a multicore run). +""" + +from __future__ import absolute_import +import os +import shutil +import tempfile +import unittest + +import madgraph.interface.reweight_interface as rwgt_interface + +pjoin = os.path.join + + +class TestAverageDensityMatrix(unittest.TestCase): + """check the average density matrix helpers""" + + # a 2x2 density matrix is stored as its upper triangle: (00, 01, 11) + events = [(1.0, [0.6+0j, 0.1+0.2j, 0.4+0j]), + (2.0, [0.5+0j, 0.0-0.1j, 0.5+0j]), + (1.0, [0.7+0j, 0.2+0.0j, 0.3+0j])] + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='rwgt_density') + self.lhe_path = pjoin(self.tmpdir, 'unweighted_events.lhe') + self.write_lhe(self.lhe_path, self.events) + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + @staticmethod + def write_lhe(path, events): + """write a minimal event file where each event carries a tag, + exactly as DensityInterface does""" + + text = ['', '', ''] + for wgt, density in events: + text.append('') + text.append(' 2 1 %+13.7e 1.0000000e+02 7.5000000e-03 1.2000000e-01' % wgt) + text.append(' 21 -1 0 0 501 502 0. 0. 0. 0. 0. 0. 1.') + text.append(' 6 1 1 2 501 0 0. 0. 0. 0. 0. 0. 1.') + text.append(' %s' % \ + ''.join('%s ' % complex(value) for value in density)) + text.append('') + text.append('') + with open(path, 'w') as fsock: + fsock.write('\n'.join(text) + '\n') + + def test_average_normalised(self): + """with matrix_normalisation the average is weighted by the event weight""" + + rho_avg = rwgt_interface.average_density_matrix_from_lhe(self.lhe_path, True) + + total_wgt = sum(wgt for wgt, _ in self.events) + solution = [sum(wgt * density[i] for wgt, density in self.events) / total_wgt + for i in range(len(self.events[0][1]))] + self.assertEqual(len(rho_avg), len(solution)) + for value, expected in zip(rho_avg, solution): + self.assertAlmostEqual(value.real, expected.real, places=12) + self.assertAlmostEqual(value.imag, expected.imag, places=12) + + def test_average_not_normalised(self): + """without matrix_normalisation the average is a plain event average""" + + rho_avg = rwgt_interface.average_density_matrix_from_lhe(self.lhe_path, False) + + nb_event = len(self.events) + solution = [sum(density[i] for _, density in self.events) / nb_event + for i in range(len(self.events[0][1]))] + for value, expected in zip(rho_avg, solution): + self.assertAlmostEqual(value.real, expected.real, places=12) + self.assertAlmostEqual(value.imag, expected.imag, places=12) + + def test_average_no_density(self): + """an event file without density matrix returns nothing""" + + path = pjoin(self.tmpdir, 'no_density.lhe') + with open(path, 'w') as fsock: + fsock.write(""" + + + + 2 1 +1.0000000e+00 1.0000000e+02 7.5000000e-03 1.2000000e-01 + 21 -1 0 0 501 502 0. 0. 0. 0. 0. 0. 1. + 6 1 1 2 501 0 0. 0. 0. 0. 0. 0. 1. + + +""") + self.assertEqual(rwgt_interface.average_density_matrix_from_lhe(path), None) + + def test_label_and_path(self): + """the canonical name does not depend on the file being gzipped or not""" + + self.assertEqual(rwgt_interface.average_density_matrix_label(self.lhe_path), + 'unweighted_events') + self.assertEqual(rwgt_interface.average_density_matrix_label(self.lhe_path + '.gz'), + 'unweighted_events') + self.assertEqual(rwgt_interface.average_density_matrix_path(self.lhe_path + '.gz'), + pjoin(self.tmpdir, 'Average_density_matrix_unweighted_events.txt')) + + def test_write_average(self): + """the values are written as plain complex, not as numpy repr""" + + rho_avg = rwgt_interface.average_density_matrix_from_lhe(self.lhe_path, True) + path = rwgt_interface.write_average_density_matrix(rho_avg, self.lhe_path) + + self.assertEqual(path, pjoin(self.tmpdir, + 'Average_density_matrix_unweighted_events.txt')) + text = open(path).read() + self.assertTrue(text.startswith( + 'Average density matrix of LHE file unweighted_events:\n')) + self.assertNotIn('np.complex', text) + + # the consumer parser reads the file line by line as a list of complex + rho_square = [] + for line in text.split('\n')[1:]: + if not line.strip(): + continue + rho_square.append([complex(value.strip(' ()')) + for value in line.strip('\t[]').split(',')]) + self.assertEqual(len(rho_square), 2) + for row in rho_square: + self.assertEqual(len(row), 2) + # hermitian, and the trace is the sum of the (normalised) diagonal + self.assertAlmostEqual(rho_square[0][1].real, rho_square[1][0].real, places=12) + self.assertAlmostEqual(rho_square[0][1].imag, -rho_square[1][0].imag, places=12) + self.assertAlmostEqual(rho_square[0][0].real, rho_avg[0].real, places=12) + self.assertAlmostEqual(rho_square[1][1].real, rho_avg[2].real, places=12) + + def test_combine_density_matrix(self): + """the multicore recombination writes the canonical file and removes the + per chunk ones""" + + # simulate what the multicore jobs leave behind: the recombined event file + # plus one average density matrix per chunk of events + chunks = [self.lhe_path + '.gz_%s.lhe' % i for i in range(3)] + for chunk in chunks: + with open(rwgt_interface.average_density_matrix_path(chunk), 'w') as fsock: + fsock.write('average of a single chunk of events\n') + + canonical = rwgt_interface.combine_density_matrix(self.lhe_path, chunks) + + self.assertEqual(canonical, pjoin(self.tmpdir, + 'Average_density_matrix_unweighted_events.txt')) + self.assertEqual(sorted(name for name in os.listdir(self.tmpdir) + if name.startswith('Average_density_matrix_')), + ['Average_density_matrix_unweighted_events.txt']) + + # and the content is the one of a single core run over the same events + rho_avg = rwgt_interface.average_density_matrix_from_lhe(self.lhe_path, True) + reference = pjoin(self.tmpdir, 'reference') + os.mkdir(reference) + rwgt_interface.write_average_density_matrix(rho_avg, self.lhe_path, + output_dir=reference) + self.assertEqual(open(canonical).read(), + open(pjoin(reference, + 'Average_density_matrix_unweighted_events.txt')).read()) + + def test_matrix_normalisation_from_card(self): + """the option is read from the reweight card as DensityInterface does""" + + card = pjoin(self.tmpdir, 'reweight_card.dat') + def write_card(*lines): + with open(card, 'w') as fsock: + fsock.write('\n'.join(lines) + '\n') + + # default value of DensityInterface when the option is absent + write_card('# change matrix_normalisation False', + 'change particle_in_density_matrix [6, -6]') + self.assertEqual(rwgt_interface.get_matrix_normalisation(card), True) + + write_card('change matrix_normalisation True') + self.assertEqual(rwgt_interface.get_matrix_normalisation(card), True) + + write_card('change matrix_normalisation False') + self.assertEqual(rwgt_interface.get_matrix_normalisation(card), False) + + # anything else is refused, as in do_change_matrix_normalisation + write_card('change matrix_normalisation garbage') + self.assertEqual(rwgt_interface.get_matrix_normalisation(card), False) + + # last occurence wins, as when the card is executed line by line + write_card('change matrix_normalisation True', + 'change matrix_normalisation False') + self.assertEqual(rwgt_interface.get_matrix_normalisation(card), False) + + self.assertEqual(rwgt_interface.get_matrix_normalisation( + pjoin(self.tmpdir, 'no_such_card.dat')), True) + + def test_combine_density_matrix_uses_the_card(self): + """matrix_normalisation False switches to the plain event average""" + + card = pjoin(self.tmpdir, 'reweight_card.dat') + with open(card, 'w') as fsock: + fsock.write('change matrix_normalisation False\n') + + canonical = rwgt_interface.combine_density_matrix(self.lhe_path, + reweight_card=card) + rho_avg = rwgt_interface.average_density_matrix_from_lhe(self.lhe_path, False) + rho_square = [[complex(value.strip(' ()')) + for value in line.strip('\t[]').split(',')] + for line in open(canonical).read().split('\n')[1:] if line.strip()] + + self.assertAlmostEqual(rho_square[0][0].real, rho_avg[0].real, places=12) + self.assertAlmostEqual(rho_square[1][1].real, rho_avg[2].real, places=12) diff --git a/tests/unit_tests/iolibs/test_export_cpp.py b/tests/unit_tests/iolibs/test_export_cpp.py index d621b4dd59..6658239fed 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') diff --git a/tests/unit_tests/iolibs/test_export_v4.py b/tests/unit_tests/iolibs/test_export_v4.py index f2d500e560..fa9d319234 100644 --- a/tests/unit_tests/iolibs/test_export_v4.py +++ b/tests/unit_tests/iolibs/test_export_v4.py @@ -3639,7 +3639,11 @@ def test_generate_helas_diagrams_uux_uuxuux(self): """) - # Test leshouche.inc output + # Test leshouche.inc output. + # The madevent exporter drops the ICOLUP colour-flow table from + # leshouche.inc when the matrix element carries a canonical colour code + # (drop_icolup): addmothers.f now rebuilds the Les Houches colour tags + # from colorflow.inc instead. leshouche.inc keeps only IDUP and MOTHUP. writer = writers.FortranWriter(self.give_pos('leshouche')) exporter.write_leshouche_file(writer, matrix_element) writer.close() @@ -3648,18 +3652,27 @@ def test_generate_helas_diagrams_uux_uuxuux(self): """ DATA (IDUP(I,1,1),I=1,6)/2,-2,2,-2,2,-2/ DATA (MOTHUP(1,I),I=1, 6)/ 0, 0, 1, 1, 1, 1/ DATA (MOTHUP(2,I),I=1, 6)/ 0, 0, 2, 2, 2, 2/ - DATA (ICOLUP(1,I,1,1),I=1, 6)/501, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,1,1),I=1, 6)/ 0,501, 0,502, 0,503/ - DATA (ICOLUP(1,I,2,1),I=1, 6)/501, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,2,1),I=1, 6)/ 0,501, 0,503, 0,502/ - DATA (ICOLUP(1,I,3,1),I=1, 6)/502, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,3,1),I=1, 6)/ 0,501, 0,501, 0,503/ - DATA (ICOLUP(1,I,4,1),I=1, 6)/503, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,4,1),I=1, 6)/ 0,501, 0,501, 0,502/ - DATA (ICOLUP(1,I,5,1),I=1, 6)/502, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,5,1),I=1, 6)/ 0,501, 0,503, 0,501/ - DATA (ICOLUP(1,I,6,1),I=1, 6)/503, 0,502, 0,503, 0/ - DATA (ICOLUP(2,I,6,1),I=1, 6)/ 0,501, 0,502, 0,501/ +""") + + # Test colorflow.inc output: the six colour flows that used to be the + # ICOLUP rows above are now encoded by the canonical colour code, which + # addmothers.f decodes back into the very same tags. The old rows, for + # reference (colour anti-colour per external leg, one flow per line): + # 501 0 502 0 503 0 / 0 501 0 502 0 503 + # 501 0 502 0 503 0 / 0 501 0 503 0 502 + # 502 0 502 0 503 0 / 0 501 0 501 0 503 + # 503 0 502 0 503 0 / 0 501 0 501 0 502 + # 502 0 502 0 503 0 / 0 501 0 503 0 501 + # 503 0 502 0 503 0 / 0 501 0 502 0 501 + writer = writers.FortranWriter(self.give_pos('colorflow')) + exporter.write_colorflow_file(writer, matrix_element) + writer.close() + + self.assertFileContains('colorflow', + """ DATA NCOLSLOT(1)/3/ + DATA (ICOLCSL(I,1),I=1,3)/2,3,5/ + DATA (ICOLASL(I,1),I=1,3)/1,4,6/ + DATA (ICOLCODE(I,1),I=1,6)/21,15,19,7,11,5/ """) # Test pdf output (for auto_dsig.f) @@ -10086,8 +10099,12 @@ def test_header(self): F1%P(:) = +F2%P(:)+V3%P(:) P1(:) = -F1 % P (:) F1 % FLV_INDEX = F2 % FLV_INDEX - DENOM = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 -CI - $ * W1))""" + IF (DBLE(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2).GT.0D0) THEN + DENOM = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1 * (M1 + $ -CI* W1)) + ELSE + DENOM = COUP/(P1(0)**2-P1(1)**2-P1(2)**2-P1(3)**2 - M1**2) + ENDIF""" abstract_M = create_aloha.AbstractRoutineBuilder(FFV1).compute_routine(1) abstract_M.add_symmetry(2) @@ -10095,8 +10112,12 @@ def test_header(self): self.assertTrue(os.path.exists('/tmp/FFV1_1.f')) textfile = open('/tmp/FFV1_1.f','r').read() - split_sol = solution.split('\n') - self.assertEqual(split_sol, textfile.split('\n')[:len(split_sol)]) + # rstrip each line: the ALOHA line wrapper can leave a trailing space + # (e.g. after "(M1 " when the width term spills to a continuation), and + # that cosmetic whitespace is not what this test is checking. + split_sol = [l.rstrip() for l in solution.split('\n')] + split_cur = [l.rstrip() for l in textfile.split('\n')[:len(split_sol)]] + self.assertEqual(split_sol, split_cur) class UFO_model_to_mg4_Test(unittest.TestCase): diff --git a/tests/unit_tests/various/test_reweight_interface.py b/tests/unit_tests/various/test_reweight_interface.py new file mode 100644 index 0000000000..0035b15684 --- /dev/null +++ b/tests/unit_tests/various/test_reweight_interface.py @@ -0,0 +1,115 @@ +################################################################################ +# +# 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 +# +################################################################################ +"""Unit tests for the reweight interface helpers.""" +from __future__ import absolute_import + +import unittest + +import madgraph.interface.reweight_interface as rwgt_interface + + +class FakeEvent(object): + """Only what _pdg_for_me_call touches: the concrete per-leg PDGs.""" + + def __init__(self, pdgs): + self.pdgs = list(pdgs) + + def get_pdg(self, momenta): + return list(self.pdgs) + + +class FakeModel(dict): + """Stand-in for a model carrying a merged_particles map.""" + + def __init__(self, merged): + dict.__init__(self) + self['merged_particles'] = merged + + +class TestPdgForMeCall(unittest.TestCase): + """The merged-particle labels carried by the generated process must be + converted back to the concrete PDGs of the event before they are handed to + the fortran, for BOTH signs of the merged code. + + merged_particles is keyed by the positive code only, so the membership test + has to be done on abs(). A subprocess whose grouped legs are all + anti-particles -- g q~ > w+ q~, get_pdg_order [21,-81,24,-81] -- used to + keep its -81 labels, which the fortran flavor mapping resolves to "no + flavour": SMATRIXHEL returned an exact 0 (raising "Invalid matrix element") + and GET_DENSITY returned an all-zero density matrix. + """ + + # {merged code: members}, as apply_flavor_grouping builds it: POSITIVE keys + MERGED = {81: [1, 2, 3, 4], 82: [11, 13]} + + def setUp(self): + self.obj = rwgt_interface.ReweightInterface.__new__( + rwgt_interface.ReweightInterface) + self.obj.merged_particles = None + # __del__ calls do_quit; keep it a no-op on this bare instance + self.obj.exitted = True + self.model = FakeModel(self.MERGED) + + def call(self, orig_order, event_pdgs, model=None): + event = FakeEvent(event_pdgs) + model = self.model if model is None else model + return self.obj._pdg_for_me_call(event, orig_order, None, model) + + def test_negative_merged_labels_are_resolved(self): + """g q~ > w+ q~ -- the regression: every grouped leg is an antiparticle + so both merged codes are NEGATIVE.""" + # process legs [21,-81,24,-81], event is g d~ > w+ u~ + out = self.call(((21, -81), (24, -81)), [21, -1, 24, -2]) + self.assertEqual(out, [21, -1, 24, -2]) + + def test_positive_merged_labels_are_resolved(self): + """g q > w+ q -- positive merged codes, the case that always worked.""" + out = self.call(((21, 81), (24, 81)), [21, 2, 24, 1]) + self.assertEqual(out, [21, 2, 24, 1]) + + def test_mixed_sign_merged_labels_are_resolved(self): + """q q~ > w+ g -- one leg of each sign.""" + out = self.call(((81, -81), (24, 21)), [2, -1, 24, 21]) + self.assertEqual(out, [2, -1, 24, 21]) + + def test_negative_lepton_merged_label_is_resolved(self): + """The same for the charged-lepton group (82), to pin that the fix is + not specific to the jet code.""" + out = self.call(((-82, 82), (24, -24)), [-11, 13, 24, -24]) + self.assertEqual(out, [-11, 13, 24, -24]) + + def test_no_merged_leg_keeps_the_process_order(self): + """A process with no grouped leg must keep orig_order untouched, so the + legs stay in the order the matrix element expects.""" + out = self.call(((21, 21), (24, -24)), [21, 21, -24, 24]) + self.assertEqual(out, [21, 21, 24, -24]) + + def test_no_flavor_grouping_keeps_the_process_order(self): + """Without flavor grouping merged_particles is empty and the event PDGs + must not be substituted (the pre-flavor-grouping behavior).""" + out = self.call(((21, -1), (24, -2)), [21, -1, 24, -2], + model=FakeModel({})) + self.assertEqual(out, [21, -1, 24, -2]) + + def test_falls_back_to_self_merged_particles(self): + """When there is no model to consult (the load_from_pickle path) the + map saved on the instance is used -- with the same sign handling.""" + self.obj.merged_particles = self.MERGED + out = self.call(((21, -81), (24, -81)), [21, -1, 24, -2], model=None) + self.assertEqual(out, [21, -1, 24, -2]) + + +if __name__ == '__main__': + unittest.main()