Crossing symmetry - #49
Conversation
… driver Crossing symmetry lets one generated matrix element evaluate physically related processes by moving legs between the initial and final state, encoded in an extended flavor index (id = cross*nmaxflavor + flavor). - Fortran, C++ and mg7 (madmatrix) exporters emit the crossing tables, the per-event momentum/NSF permutation, the crossed denominator (initial spin*color x identical-final-state factor) and the sigma-permuted good-helicity remap / union. - `check crossing <proc> [--exporter=standalone|standalone_cpp|standalone_mg7] [--simd=...]` matches each reference subprocess against the crossing of the collapsed output that reproduces it; help text and auto-completion updated, and the "not checked" message now distinguishes its three causes. - Fix a stack buffer overflow in the mg7 UMAMI SIMD flavor-sorting path: it grouped events into SIMD vectors by the raw extended flavor id into arrays sized nmaxflavor, overflowing them for any crossing (id >= nmaxflavor) and crashing check_sa.exe (SIGABRT/SIGSEGV). Group by the reduced flavor instead and write flavor_indices per event, padding unused lanes with a valid crossing-0 id so the per-event momentum gather never indexes the crossing tables out of range. Validated: `check p p > j j j --exporter=standalone_mg7` now reports 1161/1161 flavor/crossing subprocesses passing with zero "not checked" (g d > d d d~ and g d > d u u~ previously crashed); the standalone cross-symmetry acceptance tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pleteness) --use_crossing=False mapped to merge_crossing=True, which takes the "do not generate diagrams" branch in generate_diagrams_multiprocess and drops the crossed subprocesses from the amplitude list entirely -- an incomplete partonic sum (verified: p p > j j gave 3 of 8 subprocesses, missing g q > g q and q q~ > g g). In 3.x merge_crossing defaulted to False (crossed processes kept as cross_amplitudes); only the old --no_crossing debug flag dropped them. Decouple the two: keep crossed subprocesses unconditionally (merge_crossing=False). --use_crossing now only decides, at the exporter stage (via self._use_crossing), 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). --use_crossing=True generation is byte-identical (already mapped to False); only --use_crossing=False changes, and it is now a complete output again -- the fallback the madevent gate already points users to. Verified: p p > j j --use_crossing=False -> output madevent now yields all 8 subprocesses (5 P dirs); test_standalone_cross_symmetry 36/36 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te-equivalent Scaffold the crossing-aware evaluation path in the madevent group matrix template without changing behaviour yet. Add holes to the SMATRIX/MATRIX of matrix_madevent_group_v4.inc: - smatrix_me_cross_decl / smatrix_me_cross_decode (decode IFLAV -> CROSS, FLAV_USE and APPLY_CROSSING to build crossed P/NHEL/IC), - me_flav_key token (IFLAV off / FLAV_USE on) at every GOODHEL/NTRY/GET_FLAVOR, - me_matrix_args for both MATRIX call sites, - smatrix_me_iden_line (crossed denominator branch), - me_matrix_ic_param / me_matrix_ic_decl (runtime IC into MATRIX), - crossing_routines_me + smatrix_me_goodhel_or. fill_crossing_replace_dict_me() fills them; its OFF branch reproduces the historical madevent code and the ON branch is added in slice 2. The madevent exporter calls it with use_crossing=False for now, so the output is functionally identical (a p p > j j regeneration differs only by two comment lines and three blank lines). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es+links) Implement the crossing-on fills of fill_crossing_replace_dict_me: decode the extended FLAV_IDX into (CROSS, FLAV_USE), short-circuit an unusable crossing (spin*color = 0) to a zero ME, and build the crossed P/NHEL/IC once via APPLY_CROSSING_TABLE before the helicity loop; MATRIX now takes a runtime IC and its helas calls read <base_nsf>*IC(i) (use_crossing_ic). The denominator uses GET_SPINCOL_CROSS*GET_IDENT_CROSS for a genuine crossing and the historical IDEN*BROKEN_SYM for CROSS=0. The crossing routines are emitted with a per-proc qualifier (CR<pid>_GET_CROSS_PERM, ...) so the matrix<i>.f of one group do not clash at link time. The good-helicity filter is bypassed for a crossing-enabled ME (a crossing permutes/flips helicities; a shared-flavor remap can optimise this later). write_matrix_element_v4 computes me_use_crossing (opt use_crossing, group template only, and the process definition must not pin an s-channel) and drives use_crossing_ic from it. The madevent crossing gate is unchanged, so this stays dormant until the per-crossing auto_dsig/metadata land: a normal --use_crossing=False output is byte-identical, and the ON matrix element (matrix1_orig) compiles and links (madevent_forhel) for p p > j j. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Helicity recycling (hel_recycle.py) rewrites matrix_orig.f into a single sweep with fixed, baked-in helicity values. That cannot coexist with the runtime helicity permutation a crossing applies: one baked helicity set cannot serve every crossing of a merged matrix element, and the _hel template MATRIX has no runtime IC to thread. So the group exporter now turns helicity recycling off when the output is written with crossing on, and compiles the crossing-aware matrix<i>.f directly. Verified: with crossing on the group emits matrix<i>.f (no _orig/template pair) and the default `make madevent` target links for p p > j j; a normal --use_crossing=False output is unchanged (recycling still on, matrix<i>_orig.f + template_matrix<i>.f). Gated on opt use_crossing, so it is inert until the madevent crossing gate is opened. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Helicity recycling IS compatible with crossing, via the same permutation that lets goodhel be shared. hel_recycle.py rewrites only the helicity argument of an external call (NHEL(k) -> a baked +-1) and copies the momentum slot and the NSF argument unchanged, so an IC-aware matrix_orig.f yields IC-aware baked calls (e.g. IXXXXX(P(0,1),ZERO,+1,+1*IC(1),...)). The recycled MATRIX loops over 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 matrix element -- no NHEL table nor an explicit helicity remap is needed in the recycled code. So: revert the "disable helicity recycling for a crossing output" stopgap, and give matrix_madevent_group_v4_hel.inc the same crossing holes as the non-hel template (decode + PUSE/IC build, crossed denominator, IC into MATRIX, per-proc CR<pid>_ routines), minus NHELUSE. fill_crossing_replace_dict_me fills the _hel variants (smatrix_hel_cross_decl/decode, hel_matrix_call_args, hel_matrix_ic_param). Verified (crossing gate bypassed in a throwaway harness): with recycling ON the group emits matrix1_orig.f + template_matrix1.f (both crossing-aware, no leftover holes); madevent_forhel links; and running HelicityRecycler on them produces a matrix1_optim.f whose baked calls keep *IC(k) and which compiles+links into the full madevent. OFF (--use_crossing=False) output unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ProcessExporterFortran.partition_crossing_classes(matrix_elements): group a subprocess group's matrix elements into crossing-equivalence classes. A member is tied to a base when the base's crossing enumeration (compute_crossing_pdg_entries) reproduces the member's crossed physical-PDG signature -- the same key check_crossing matches on -- returning per member a (base_index, cross) pair. This is the basis for sharing one matrix<i>.f across a base and its crossings (the base SMATRIX, driven by an extended FLAV_IDX, evaluates the whole class), which the exporter wiring will use next. Pure function, not wired into the exporter yet. Unit test TestCrossingPartition on p p > j j: within P_gq_gq, g q~ > g q~ is found as a crossing of g q > g q (2 MEs -> 1 base); within P_qq_qq, q~ q~ > q~ q~ crosses q q > q q (3 -> 2), while q q~ > q q~ stays its own base. Cross-*group* crossings (g g > q q~ vs q q~ > g g, separate P dirs) are not merged -- that needs crossing-aware grouping, a later step. test_standalone_cross_symmetry: 37/37 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous ME-level partition was wrong: it matched a module by its first flavor's signature only, so it silently missed genuine crossings (e.g. it claimed u u~ > u u~ is not a crossing of u u > u u, when it is exactly cross=4). More fundamentally, the crossing relates flavor combinations, not whole modules: a flavor-merged module bundles flavors that cross to different bases (the Q Q~ > Q Q~ module carries both u u~ > u u~, a crossing of Q Q > Q Q, and d d~ > u u~, which is not), so an ME-level base/cross map cannot be right. Rework the helper to route per flavor: a module may drop its own matrix<i>.f only when EVERY one of its flavors is a genuine crossing of some base module's flavor; otherwise it stays a base. Returns (bases, routing), routing[i] giving one (base_index, iflav) per flavor -- the base SMATRIX and the 1-based extended FLAV_IDX (cross*nflav+flav) to reach that flavor. This is what per-flavor auto_dsig routing (piece 2) needs. Verified on p p > j j: gq_gq g q~ > g q~ routes to g q > g q (cross 4), 2 MEs -> 1 base; qq_qq q~ q~ > q~ q~ routes to q q > q q (cross 19) and is eliminated, while Q Q~ > Q Q~ stays a base because of its d d~ > u u~ flavor. TestCrossingPartition updated; suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the n_flav/NFLAV consistency (needed before the per-flavor auto_dsig routing) turned up a real M0 bug. fill_crossing_replace_dict_me sized the extended-FLAV_IDX NFLAV from _build_flav_table_flat() (compute_flavor_masks, the STANDALONE convention), but the madevent GET_FLAVOR table is sized by get_external_flavors_with_iden() (== replace_dict 'max_flavor', what FLAVOR(NEXTERNAL,max_flavor)/MAXFLAVPERPROC use). For a merged group ME the two differ -- e.g. Q Q~ > g g has iden 1 but masks 4 -- so NFLAV=4 while the FLAVOR table has a single row, and FLAV_USE=mod(IFLAV-1,4)+1 would index it out of bounds at run time. Use get_external_flavors_with_iden() for NFLAV. This also makes the madevent decode agree with compute_crossing_pdg_entries (which already uses iden), so partition_crossing_classes' routed FLAV_IDX decodes correctly in the base SMATRIX -- the property the per-flavor routing (piece 2) relies on. Verified on p p > j j (crossing on): every matrix<i>_orig.f now has PARAMETER(NFLAV) equal to its FLAVOR(NEXTERNAL,rows) count (0 mismatches over 8 matrices, incl. the NFLAV=2 module), and forhel still links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e ME Wire the crossing merge into the madevent group output. generate_subprocess_ directory now partitions the group's matrix elements (partition_crossing_ classes) and, for a subprocess whose every flavor is a crossing of a base subprocess's flavor, writes a light router matrix<i>.f instead of the full one: it keeps GET_FLAVOR<i> (for the PDF) and a router SMATRIX<i> that dispatches per flavor to the base SMATRIX with the crossed FLAV_IDX, dropping the heavy MATRIX<i>/helas. The base SMATRIX (M0) crosses the momenta and rebuilds the crossed denominator, so the routed ANS is the subprocess's matrix element. get_nhel<i> lives in auto_dsig<i>.f so it is unaffected; the auto_dsig itself is unchanged (it still calls SMATRIX<i>/GET_FLAVOR<i>). - New template matrix_madevent_group_router_v4.inc; write_matrix_router_file. - ProcessExporterFortranMEGroup.supports_crossing = True (the _check_crossing_ support gate now lets a grouped madevent output through with --use_crossing). - Helicity recycling is turned off for a merged group for now (the router writes a static matrix<i>.f while recycling builds matrix<i>_optim.f at run time and the makefile globs one or the other; mixing them needs build-system work). - gen_infohtml.get_diagram_nb: tolerate a matrix<i>.f with no diagram comment (a router shares the base's diagrams). Validated: p p > j j, low-stat integration. use_crossing=True (merged/router) gives 5.449e8 +- 2.8e6 pb vs use_crossing=False (complete reference) 5.448e8 +- 2.8e6 pb -- agree within MC error. P1_qq_qq runs 3 subprocesses on 2 heavy MEs, P1_gq_gq 2 on 1; both link and integrate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… disable) Let a merged crossing group keep helicity recycling on its heavy base matrix elements while the light routers stay static. A router now writes matrix<i>_router.f (not matrix<i>.f): the makefile globs matrix*_router.o into both the forhel and the optimized binaries, gen_ximprove recycles only matrix*orig.f so it leaves routers untouched, and a base is recycled exactly as before (matrix<b>_orig.f + template -> matrix<b>_optim.f). Removes the stopgap that disabled recycling for a whole merged group. gen_infohtml.get_diagram_nb also looks for matrix<i>_router.f and returns 0 when a subprocess has no matrix file of its own (a router shares the base's diagrams). Validated p p > j j, hel_recycling=True: bases produce matrix<b>_optim.f, the router matrix3_router.o compiles and links, the full forhel->recycle->madevent run completes and gives 5.468e8 +- 3.0e6 pb vs the use_crossing=False reference 5.448e8 +- 2.8e6 pb (agree in MC error). Also validated (earlier commits' merge path): p p > t t~ j j merged == reference 344.8 pb exactly (massive tops), and p p > j j j merged 4.912e7 vs reference 4.923e7 pb (agree in MC error). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TestCrossingUnsupportedOutput asserted that a madevent output refuses a process generated with crossing. That was true before Track A; the grouped madevent exporter now supports crossing (supports_crossing=True, the crossing router shares a base matrix element), so move 'madevent' out of UNSUPPORTED_FORMATS (matchbox stays) and assert instead that both standalone and madevent accept it. Verified: TestCrossingUnsupportedOutput 3/3 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Checking the LHE colour flow (thanks to the reviewer's prompt) turned up a real bug. A router returns the base's selected colour-flow index ICOL, but events are written through the router's own leshouche ICOLUP. For p p > j j the two flow orders happen to coincide, but in general the crossed colour reps decompose the shared colour basis in a DIFFERENT order (verified: for p p > j j j and p p > t t~ j j the router flow set equals the crossed base set but is permuted), so ICOLUP(ICOL) is a valid but WRONG flow -- colour conservation still holds, so it is invisible in the event, but the parton shower would get the wrong colour connection. Add a per-flavor COLMAP in the router: match each crossed base flow (leg j <- base flow leg perm^-1(j), colour<->anticolour where that leg swapped initial/final) to the local flow of the same topology, and remap ICOL after the base call. Emitted only when non-identity. Helicity needs no such map: the router's own get_nhel already enumerates the crossed helicities in the base's order (rh[hb] = bh[hb] with the crossed legs sign-flipped, verified for every router in p p > j j / j j j). Verified: p p > j j j routers now carry COLMAP /4,3,2,1/ and /3,4,1,2/, compile, and the full run gives 4.929e7 +- 3.3e5 pb vs reference 4.923e7 +- 4.2e5 (xsec unchanged, as ICOL only labels the flow) with colour conserved in all events. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crossing reuses one matrix element across physically distinct (crossed) initial states, so a per-beam property is ill-defined. Rather than return silently-wrong numbers, tag the output and raise a clear error at run time. Generation: in generate_subprocess_directory, append 'crossing' to proc_characteristic['limitations'] -- but only when crossing is materially applied (a router is emitted, or a base evaluates a cross>0 flavor). A process where crossing does nothing (e.g. two unrelated single-process groups) stays untagged and is never blocked. Run time: in check_card_consistency (LO branch, next to the dressed_ee check), when 'crossing' is tagged, raise InvalidCmd if polbeam1/polbeam2 != 0 (beam polarisation) or pdlabel/pdlabel1/pdlabel2 == 'eva' (EVA luminosity). The message points to 'generate <process> --use_crossing=False'. The guard sits inside the existing RunCardLO gate, so NLO cards (no polbeam) are untouched. Verified: p p > j j tags ['crossing'] (round-trips as a list); the guard blocks polbeam=-100 and pdlabel=eva, and does NOT block when limitations=[] with the same settings; --use_crossing=False gives limitations=[], 0 routers, 8 full matrix.f (falls back to the 3.x per-subprocess matrix elements). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hoton) For lepton/photon beams (define EP=e+ a; EM=e- a; generate EP EM > ...) each initial state lands in its own single-process P directory and the crossings relate DIFFERENT directories, so the within-group router (Track A) never fires and every group compiles its own matrix element even when several are crossings of one base. This adds cross-group reuse: a dependent group symlinks the base group's crossing-aware SMATRIX and routes to it, keeping its own phase space and PDFs. - compute_crossgroup_routing(): flatten every group's matrix elements, run the (group-agnostic) crossing partition, and return, per dependent, the base directory/proc_id and the crossed FLAV_IDX per flavor. It bows out entirely if ANY group has within-group routing -- that is the hadronic p p case, which Track A owns and where cross-group merging is a separate deferred optimisation. Wired into export_processes before the group loop. - generate_subprocess_directory: a dependent writes NO matrix element of its own; it symlinks the base group's matrix<b>.f (+ template with hel recycling) and its auto_dsig routes to the base SMATRIX. - auto_dsig_v4.inc: backward-compatible holes (default == original) for the flavor lookup and the SMATRIX call in BOTH the scalar and vectorised (SMATRIX_MULTI) paths, plus per-program-unit declaration holes. A dependent inlines its own beams (DSIG_XGFLAV, since it cannot own a GET_FLAVOR without clashing with the symlinked base's) and calls SMATRIX<base>(P, DSIG_XGROUTE (IFLAV), ...). Verified p p > j j auto_dsig is BYTE-IDENTICAL. Validated on EP EM > EP EM: check crossing 5/5 at machine precision; the dependent compiles and links; and a full generate_events (direct e+e- collider) gives 43.23 +- 0.17 pb (crossing) vs 43.40 +- 0.16 pb (independent) with every subprocess -- all three cross-group dependents included -- agreeing. Deferred: sharing the compiled .o across directories (recompilation is still per-dir), COLMAP/hel-map for the LHE event records, CONFIGMAP for multi-channel at higher multiplicity, and tagging the 'crossing' limitation for the cross- group case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…P dirs Cross-group dependents symlinked the base group's matrix-element SOURCE but each directory still recompiled it. Reuse the compiled object instead, and build every P directory in one parallel call. - write_crossgroup_mk(): each dependent P dir gets a crossgroup.mk (pulled in by the shared makefile via `-include crossgroup.mk`, a no-op where absent) whose specific rule symlinks matrix<b>_orig.o from the base directory rather than recompiling the symlinked source; the base object is built first (the specific rule overrides the %.o:%.f pattern, and a recursive rule is the standalone ordering fallback). Only matrix<b>_orig.o -- the full matrix element, identical across the crossing class -- is shared; matrix<b>_optim.o is NOT, because gen_ximprove bakes each subprocess's own good-helicity set into it (it is subprocess-specific). Without recycling the single matrix<b>.o is shared. - write_crossgroup_parallel_makefile(): SubProcesses/makefile_madevent builds every P directory with `make -f makefile_madevent -jN` (or `... forhel`), delegating each to its own makefile and adding `<dep>/madevent: <base>/madevent` ordering so make compiles the base before its dependents while running the rest in parallel. Verified: parallel forhel build (-j4) links all binaries with the dependents' matrix1_orig.o symlinked to the base (compiled once); a full generate_events still gives 43.23 pb; p p is unaffected (no crossgroup files, auto_dsig byte-identical, the -include is a no-op); acceptance suite 37/37 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…roup A cross-group dependent evaluates its matrix element through the base group's SMATRIX, which selects a helicity/colour in the BASE's enumeration; but the event is written through this subprocess's OWN get_helicities / ICOLUP, so the index must be translated. Without it the LHE events carried the base's helicity labels against the dependent's table -- e.g. e+ e- > a a events showed unphysical same-helicity e+e- pairs (the cross section, summed over helicities, was already right, so this was invisible until the events were inspected). - _crossgroup_helmap(dep_me, base_me, cross): 1-based map from a base helicity index to the dependent helicity index carrying the physically-crossed configuration. The base APPLY_CROSSING permutes NHEL by PERM and flips it by the IC sign, so dep config[k] = base_row[PERM[k]]*SGN[k]; the result is a clean permutation of the dependent's helicity table. - _dsig_crossgroup_fills emits per-flavor DSIG_XGHEL (and DSIG_XGCOL for colour, which is the identity and thus skipped for colourless lepton/photon) and applies selected_hel = DSIG_XGHEL(selected_hel, IFLAV) after the base SMATRIX call in both the scalar and vectorised (SMATRIX_MULTI) paths. Emitted only when non-identity; the new dsig_smatrix_vec_post hole attaches to the call's closing paren so the default is byte-identical. Verified: e+ e- > a a events now have physical opposite-helicity e+e- (matching the independent generation, zero same-helicity events); xsec 43.32 pb; p p auto_dsig byte-identical; acceptance suite 37/37 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The base group's compiled orig object was already shared, but each dependent
still compiled its own _optim: helicity recycling bakes each subprocess's good-
helicity INDEX set, and under crossing those sets are permutations of each other,
so the base's baked set does not cover a dependent's. The fix mirrors what the
within-group case gets for free (there the base's forhel survey already sees all
the crossed flavors): bake the base optim over the UNION good-hel of the crossing
class. Because G_dep = pi^-1(G_base), the union is derivable from the base's set
plus the crossing permutations -- no dependent surveys needed.
- _crossgroup_base_helperm(base_me, cross): the base->base helicity permutation
pi[hb] = the base index whose NHEL row equals the crossed row of hb (factored
_crossed_helicity_configs, shared with _crossgroup_helmap). The dependent for
that crossing is good at h iff pi[h] is good for the base.
- export persists these per base directory in crossgroup_helunion.dat.
- gen_ximprove reads it and, before hel_recycle, expands the base's good-hel to
the union.
- crossgroup.mk re-adds the matrix<b>_optim.o symlink. The `-include crossgroup.mk`
moves to the END of the SubProcesses makefile so its specific rules override the
$(MATRIX) static-pattern rule for the optim objects (orig already worked from the
top since orig is not in $(MATRIX)).
Verified on EP EM > EP EM: base optim now bakes the union {3,5,8,9,12,14} (6, was
4); all three dependents' matrix1_orig.o AND matrix1_optim.o are symlinks to the
base (compiled once); xsec 43.32 pb; e+ e- > a a event helicities still physical;
p p matrix+auto_dsig byte-identical with no crossgroup files; acceptance suite
37/37 OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The within-group case already tags the 'crossing' limitation so check_card_consistency blocks beam polarisation / EVA (a per-beam property is ill-defined once one matrix element is shared across crossed initial states). Cross-group (lepton/photon) reuse shares a matrix element across even more distinct initial states (e+ vs e- vs gamma), so tag it the same way when compute_crossgroup_routing returns any routing. Verified: EP EM > EP EM now reports limitations = ['crossing'] and the guard blocks polbeam/EVA; the non-crossing EP EM > mu+ mu- (two unrelated groups) stays []; p p > j j still tags via the within-group path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A cross-group dependent samples its own config's poles in genps, but the base SMATRIX enhances AMP2(channel) in the BASE's diagram numbering, so the channel must name the base diagram of the same topology -- otherwise the importance sampling is mis-paired. This never changes the result (summing the channels gives the full integral for any bijective pairing), only the convergence, which matters at higher multiplicity. - _diagram_leg_subsets(me): per diagram, the set of its internal propagators' canonical external-leg subsets (from get_s_and_t_channels; propagators are the negative leg numbers, the trailing single-external-leg t-channel one dropped; a subset and its complement are one propagator) -- a crossing-covariant topology signature. - _crossgroup_configmap(dep, base, cross): map each dependent diagram's subsets through the crossing leg permutation and match to the base diagram with that signature; identity if not a clean permutation. - _dsig_crossgroup_fills emits DSIG_XGCONFIG and wraps the channel as DSIG_XGCONFIG(channel, IFLAV) in both the scalar call and the new vec channel hole, only when non-identity. Verified on EP EM > EP EM: a e > a e is the identity (absent), but a e- > a e- and e+ e- > a a get /2,1/ (identity was mis-pairing the two diagrams there); xsec 43.32 pb; p p matrix+auto_dsig byte-identical; acceptance suite 37/37 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-group reuse is the same engine as the lepton/photon case; it was gated off for p p only to protect the validated Track A. Relax the gate from all-or-nothing (bail out if ANY group has within-group routing) to a per-group exclusion: a group is a cross-group candidate only when every one of its members is a within-group base (no router). Multi-ME p p groups (the `j`-multiparticle ones with within-group routers) are skipped and stay Track A; the single-crossing-class groups now also share across P directories -- e.g. q q~ > g g reuses g g > q q~. Validated p p > j j: detection routes P1_qq_gg -> P1_gg_qq; the dependent carries all four maps, including the first non-trivial COLOURED COLMAP (/2,1/), CONFIGMAP (/1,3,2/), the hel-map and the union optim; its matrix1_orig.o and matrix1_optim.o symlink the base (compiled once). Cross section 6.974e8 pb, bit-identical to the --use_crossing=False reference; colour conserved in 1000/1000 events (both); Track A multi-ME groups byte-identical to before the degate; acceptance suite 37/37 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Outside the complex-mass scheme a spacelike (t-channel, P^2<0) propagator has no pole to regulate, so keeping the width i*M*Gamma in the denominator is spurious (it breaks gauge cancellations). The ALOHA propagator routine now tests the sign of P^2 at runtime and drops the width for spacelike momenta, keeping it for timelike (s-channel) ones. This replaces the old code-generation mechanism (helas_call_writers rewrote the call to pass ZERO as the width for is_t_channel() wavefunctions). That was topology-based and matched only madevent's fk_ widths, so standalone / standalone_cpp (which pass MDL_ widths) never got the treatment; doing it inside ALOHA applies it consistently to every tree-level backend. The existing zerowidth_tchannel option (default True) now drives the new aloha.t_channel_width flag instead of the call-rewrite; True keeps the proper treatment, False restores the width everywhere. It is a generation-time (output) option, so setting it at madevent run time now raises a clear error. Fortran/C++/Python (and GPU via C++) writers updated; ignored under the complex-mass scheme. IOTest references for the changed propagator routines still need regenerating. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…icity recycling Two correctness fixes for cross-group (Track B) crossing, where a dependent subprocess routes to a base group's symlinked, crossing-aware matrix element. 1. Colour selection. The base SMATRIX picked the event colour flow with select_color, which masks the base-order JAMP2 with the DEPENDENT binary's ICOLAMP + ICONFIG (mismatched in flow order AND config space); the picked flow could be incompatible with the sampled config and addmothers then failed to reduce its ICOLUP (a p p > t t~ j j refine crash). The base now publishes its per-flow JAMP2 (COMMON/TO_XG_JAMP2) and the dependent permutes it into its own flow order and runs its own SELECT_COLOR (the XG_SELCOL helper) -- a native colour selection. Emitted only for MEs that are cross-group bases, so every other madevent ME stays byte-identical. 2. Helicity recycling. The recycled optim bakes the helicity configs into its wavefunction calls and takes no runtime NHEL, but a crossed dependent is evaluated with a permuted NHELUSE from APPLY_CROSSING. The full helicity sum is invariant under that permutation, but the optim's base good-hel SUBSET is not the dependent's, so its partial sum was over the wrong configs (~2% low for e.g. photon-lepton Compton crossings). A cross-group base now keeps ALL helicity configs in the shared optim (exact for every crossing), still recycling wavefunctions. Validated: p p > t t~ j j completes, total within MC error, colour conserved; lepton/photon EP EM > EP EM crossing now matches the non-crossing reference (was ~1.8% low); p p > j j and p p > j j j unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ra NSF sign) The LHE event helicity (SPINUP, unwgt.f jpart(7,i)=nhel(i)) is the raw NHEL table value, never NHEL*IC. APPLY_CROSSING permutes NHEL but flips only the IC/NSF flags, so the correct crossed label for leg k is base_row[PERM[k]] with NO extra sign: the base MATRIX gives leg k physical helicity NHEL(k)*IC(k)=base_row[PERM[k]]*SGN[k]*IC_IN[PERM[k]]=base_row[PERM[k]]*IC_dep[k], matching the dependent's native label iff NHEL_dep[k]=base_row[PERM[k]]. Both event-helicity maps were multiplying in SGN, double-counting the crossing flip and flipping every fermion/vector leg that swaps initial<->final. Invisible to xsec/colour/flavour (all helicity-summed) and to non-chiral p p > j j (where the per-leg density is (++)==(--)), but wrong for chiral finals: on p p > w+ w- j j the incoming/outgoing antiquark helicities came out fully flipped (~34 sigma). Two maps carried the bug, both fixed by using the UNSIGNED crossed config: - Track B cross-group dependent: _crossgroup_helmap (DSIG_XGHEL). Added a `signed` flag to _crossed_helicity_configs; the helmap now passes signed=False. _crossgroup_base_helperm keeps signed=True -- its good-hel-set remap IS the GHREMAP sigma (table-space, validated by _GOODHEL_PROBE), which needs the sign. - Track A within-group router: write_matrix_router_file returned the base's selected IHEL straight through (COLMAP was applied to ICOL, nothing to IHEL). It now applies the same unsigned _crossgroup_helmap to IHEL, mirroring COLMAP. Validated: p p > w+ w- j j crossing vs --use_crossing=False (10k evt) -> every quark/antiquark/W/gluon helicity bin consistent (max |pull| 1.2 sigma, was 34); the discriminating chiral standalone density matrix (u d~ > w+ g crossed to u g > w+ d, the crossed d 100%-polarised) matches native per-helicity to machine precision via both the compiled Fortran GET_DENSITY_IDX and the f2py PY_GET_DENSITY_IDX wrapper (2 new acceptance tests). xsec unchanged (labels do not affect |M|^2); goodhel/GHREMAP tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Do we need any new functionality for that on the madspace side or will it just affect the generation of the subprocesses.json file and we will have a few more flavors within each subprocess? |
|
The things that was needed for madevent were multiple map to correctly connect the information
So yes only subprocesses.json need to be modified. I forwarded your question to claude: Short answer: It's not just "a few more flavors within each subprocess" — a crossing changes the initial state, so a crossed process can't live inside an existing subprocess's flavor list. But the extra work on the madspace side is small and localized: for the cross-section it's essentially just "pass the extended flavor id to the shared ME"; the phase-space machinery is untouched. The only genuinely new piece is for event generation (the per-crossing helicity/colour/channel index maps), and that's a design choice about where those tables live. Why it isn't just more flavors in a subprocess. In subprocesses.json each subprocess has a fixed incoming/outgoing, and its flavors are what the exporter calls all_flavors_same_initial — different PDG assignments that share the same initial-state slots, hence the same kinematics/phase space. A crossing by definition moves a leg between the initial and final state, so a crossed process has a different incoming/outgoing (different PDF legs, generally different masses). It violates the same-initial invariant, so it must be its own subprocess entry, not an extra entry in the base's flavors list. What actually changes in subprocesses.json. You get additional subprocess entries (the crossed ones), each with its own incoming/outgoing/channels/helicities and its own phase space, but pointing at the same me_path (shared compiled ME) plus one new field: the crossing / extended flavor id (cross*nflav + flav). This mirrors the Fortran madevent design exactly — each crossed process keeps its own phase-space setup and only the matrix-element object is shared. What madspace needs — split by use case: Cross-section / integration: essentially nothing new. Each crossed subprocess is an ordinary subprocess with its own incoming/outgoing, so phase space, PDFs, cuts, and channel sampling all work as today. The one requirement is that the ME call forwards the extended flavor id so the ME applies its internal crossing. The madmatrix/cudacpp backend already supports this (supports_crossing=True, extended flavor id) and returns the correct crossed |M|² internally — so no new PS math and no correctness work for the xsec. Event generation (spin/colour in the event record): here there is something. The ME returns the selected helicity, colour flow, and multichannel channel in the base process's enumeration, but the event for the crossed process needs them in the crossed process's own convention. That's three small per-crossing maps — the direct analogues of the Fortran DSIG_XGHEL (helicity), COLMAP (colour), and CONFIGMAP (channel↔diagram). Decision to make: either emit them into subprocesses.json per crossed subprocess and have madspace apply them after the ME call, or bake them into the C++ ME. (Note CONFIGMAP is only an importance-sampling/variance matter, not correctness.) Confirmed: the madmatrix/cudacpp ME currently returns the selected helicity, colour, and channel/iconfig in the base process's numbering — only the |M|² is crossed. So the crossed-process index maps (helicity/colour/channel, = Fortran DSIG_XGHEL/COLMAP/CONFIGMAP) are not yet handled by the ME. For cross-sections that's irrelevant; for event generation someone has to apply them — cleanest inside the C++ ME (the colour one especially, since it needs the base ME to publish its per-flow jamp2 and re-select in the dependent's colour basis), otherwise emitted into subprocesses.json and applied by madspace. |
…decoder
Represent a helicity configuration as a single mixed-radix "canonical code"
over the per-leg helicity states (STATES/NHSTATE), with the last external leg
as the least-significant digit -- matching get_helicity_matrix()'s
itertools.product order, so for a non-polarized process the code of row i is
exactly i (nothing is relabelled). A polarization restriction ({0}/{L}/...)
keeps the full per-leg multiplicity as the radix (helicity 0 / longitudinal
stays a first-class state) and leaves the allowed-code list HELALLOW as the
selected, non-contiguous subset.
matrix_standalone_v4.inc: drop the explicit NHEL config DATA table; add
DECODE_HEL / ENCODE_HEL / FILL_NHEL. PROCESS_NHEL is now materialized at
runtime by decoding HELALLOW (FILL_NHEL, called from SMATRIX / GET_NHEL /
GET_DENSITY_IDX), keeping the density-matrix and f2py interfaces intact. The
external helicity label (USERHEL) is the canonical code.
f2py: the get_nhel_entry accessor (all_matrix.f) now fills the table via
GET_NHEL instead of copying the PROCESS_NHEL common raw -- otherwise an early
caller (reweighting builds its per-config helicity map at init, before any
matrix-element evaluation) would read the not-yet-materialized table as zeros.
GET_NHEL is defined by every standalone matrix.f, so this also links for
split-order processes (which have no FILL_NHEL).
Validated: e+ e- > mu+ mu- and e+ e- > w+{0} w- |M|^2 bit-identical to the old
table; 39/39 standalone cross-symmetry tests (fortran + cpp + mg7; density +
crossing + polarization + f2py); reweight per-event weights bit-identical
(p p > t t~ mass reweight).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…LIDATED] For a crossed event the reported per-event helicity (allselhel) was the base row index; it should be the CROSSED code. Add a selected_hel_code helper to the madmatrix (cudacpp) backend that mirrors the fortran APPLY_CROSSING_TABLE + ENCODE_HEL: permute the base NHEL config by the crossing slot permutation (no sign flip -- the NSF sign lives in IC) and re-encode it in the canonical mixed-radix code over the base per-leg helicity states. cross 0 is the identity (base row+1), so non-crossing output is byte-identical, and the |M|^2 path is untouched. *** CAUTION: COMPILE-CHECKED ONLY, NOT VALIDATED AT RUNTIME. *** The |M|^2 path evaluates each helicity row with the helicity read by DESTINATION slot (cHel[ihel][s]) and the leg permutation absorbed by the good-helicity union sum, so whether the SELECTED row needs this perm digit-permute, an NSF sign flip, both, or nothing must be confirmed by a cudacpp event-level run that checks the reported crossed-event helicity against the fortran backend. The generated code says the same. Until that check exists, do NOT rely on allselhel for crossed events -- the |M|^2 is correct and unaffected. Verified here: standalone_mg7 crossing .cc compiles (scalar SIMD), and `check crossing p p > t t~ j --exporter=standalone_mg7` still passes 4/4 (the |M|^2 crossing is intact); off-path (use_crossing=False) output is byte-identical (plain fill keeps cGoodHel[ighel]+1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n on the C++ hot path The cudacpp crossing good-hel filter used a UNION over all crossings (goodhel_scan_count = ncross*nflav): sigmaKin looped once over that union with a scalar helicity, and rows that vanish for a given event's crossing contributed 0. For a chiral process the union is far larger than any one crossing needs (p p > w+ j: base good = 6, union = 24 -> 4x waste). Make the helicity PER-LANE (user's design): loop once over the per-crossing good-hel count; each lane uses its crossing's ighel-th good helicity. The helicity only ever reaches the EXTERNAL wavefunctions (ixxxxx/oxxxxx/ vxxxxx) -- everything after (get_amp) stays fully SIMD, no masking. So each event computes exactly its own good helicities. C++ path (scalar-SIMD AND mixed precision); GPU stays on the union (not testable here). Gated on use_crossing: a non-crossing build is byte- identical (plain fills keep cNGoodHel / cGoodHel[ighel] / scalar ihel). - getGoodHel records a per-cross good-hel table (cGoodHelOfCross, cNGoodPerCross, cNGoodMaxCross) during its existing scan. - sigmaKin C++ loop: for ighel<cNGoodMaxCross, pass ighel to calculate_jamps. - calculate_jamps gains a host param int _ighel; the crossing external block loops each leg's helicity states (2 fermion/massless-vec, 3 massive-vec; scalars carry none), reuses the per-lane NSF (icsign) blend, and mask- blends by the per-lane helicity. The per-lane row is derived PER PAGE from ievt0 (the iParity page start), so mixed precision's second neppV page is handled correctly (a page-0-only precompute zeroed it #firstpass). Validated on cppsse4 (neppV=2, sse2neon on arm64) with a mixed-crossing driver (different crossings in one SIMD vector): per-event |M|^2 is BYTE-IDENTICAL to the union build in BOTH pure-double (FPTYPE=d) and mixed precision (FPTYPE=m, nParity=2). check crossing --exporter=standalone_mg7 --simd=sse4 passes 3/3 (p p > w+ j) and 4/4 (p p > t t~ j). Non-crossing output byte-identical. See [[mg7-perlane-helicity]]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@Qubitol, I add to take some decision concerning the nice handling of helicity/crossing to be optimised with SIMD, here is the documentation of such strategy. Would be nice if you can comment on this. Per‑lane helicity: the SIMD strategy for
|
| helicity rows computed / event | |
|---|---|
| any single crossing (base good set) | 6 |
| union over all 25 crossings | 24 (all of them) |
→ the union computes 4× more helicities per event than any single crossing
needs. (For pure‑QCD processes the base set is already near‑full, so the union
is barely larger and the win is small — the payoff is on EW/chiral matrix
elements.)
4. The strategy: per‑lane helicity
The key observation: helicity only ever enters the external wavefunctions
(ixxxxx / oxxxxx / vxxxxx). Everything after them — internal wavefunctions,
FFV* vertices, colour decomposition (get_amp) — is helicity‑agnostic and
already fully SIMD.
So we do not need a shared helicity across the vector. Instead:
- loop once over the per‑crossing good‑hel count (not the union);
- at iteration
ighel, each lane uses its own crossing'sighel‑th good
helicity — i.e. the helicity becomes a per‑lane quantity; - the external wavefunction for a leg is built for each of the leg's helicity
states and mask‑blended lane‑wise (does this lane want statev?); get_ampthen runs full‑width on the blended wavefunctions — no lane masking
downstream, no union waste.
This is the SIMD‑native realisation of the same shared‑filter idea the Fortran
and standalone_cpp backends express with a per‑event gate (CROSS_GHIDX /
ghfilt): each event ends up evaluating exactly its own good helicities.
External block, per leg (schematic)
{ static const int _st[NS] = { <helicity states of this leg> }; // e.g. {-1,1} or {-1,0,1}
for (int _vi = 0; _vi < NS; _vi++) {
const int _v = _st[_vi];
ROUTINE(xmom, MASS, _v, +nsf, …, aloha_x[0], s); // build wf at helicity _v, both NSF
ROUTINE(xmom, MASS, _v, -nsf, …, aloha_x[1], s);
const fptype_sv _sp = (icsign[s] + 1)*0.5, _sm = (1 - icsign[s])*0.5; // per-lane NSF blend
fptype_sv _hm{};
for (int _ie = 0; _ie < neppV; _ie++) { // per-lane helicity mask
int _hr = (_ighel < 0) ? ihel
: cGoodHelOfCross[ iflavorVec[ievt0 + _ie] / nmaxflavor ][_ighel]; // (padded to -1)
((fptype*)&_hm)[_ie] = (_hr >= 0 && cHel[_hr][s] == _v) ? 1. : 0.;
}
// accumulate the lane-selected wavefunction (get_amp is unchanged after this)
w_sv[s] += _hm * (_sp*w_x[0] + _sm*w_x[1]);
pvec_sv[s] += _hm * (_sp*pvec_x[0] + _sm*pvec_x[1]);
} }Scalars (spin 1) carry no helicity, so their block stays the plain NSF blend.
The per‑leg state list comes from _hel_state_values(spin, mass) (fermion →
{-1,1}, massless vector → {-1,1}, massive vector → {-1,0,1}, spin‑2 too).
Data structures
getGoodHel builds a per‑cross table during its existing scan:
static int cGoodHelOfCross[ncross][ncomb]; // good rows per crossing
static int cNGoodPerCross[ncross]; // count per crossing
static int cNGoodMaxCross; // loop bound (= base count)sigmaKin's summation loop then runs ighel < cNGoodMaxCross.
5. The mixed‑precision subtlety (please look here, reviewers)
calculate_jamps processes nParity pages in a loop, and the external block runs
once per page:
for (int iParity = 0; iParity < nParity; ++iParity) { // nParity = 2 in mixed precision
const int ievt0 = ievt00 + iParity * neppV; // this page's first event
… external block …
}So the per‑lane helicity must be derived per page from ievt0. A first
implementation precomputed a _ihlane[neppV] array in sigmaKin for page 0 only;
in mixed precision (nParity = 2) that silently zeroed the second page —
every second neppV‑page of events came out 0 under divergent crossings.
The fix (and the current design): pass the good‑hel index _ighel to
calculate_jamps, and let the external block resolve _hr from
iflavorVec[ievt0 + _ie] per page. _ighel < 0 falls back to the scalar
ihel (used by getGoodHel's scan and by the non‑crossing path).
6. Scope and gating
- C++ path only — scalar‑SIMD and mixed precision. GPU
(MGONGPUCPP_GPUIMPL) stays on the union; it isn't ported and needs a CUDA
box to validate. (The CUDA async helicity loop is deliberately left on
cNGoodHel; note it is textually identical to the C++ loop and is
distinguished by the following line,fptype* hAllJampsvscxtype_sv jamp_sv.) - Fully gated on
use_crossing. A non‑crossing build is byte‑identical:
the OFF fills keepcNGoodHel/cGoodHel[ighel]/ the scalarihel, and the
_ighelparameter is not even added tocalculate_jamps. - Codegen lives in
madmatrix/model_handling.py
(get_madmatrix_crossing_dictholes +_crossing_external_block) and two
templates (process_function_definitions.inc,process_sigmaKin_function.inc).
7. Validation
check_sa -f <flavorID> forces one crossing for the whole run, so all lanes
share a crossing — it cannot exercise the very thing this change is about.
A small mixed‑crossing driver was used: patch the host flvVec to cycle
crossings per event (MG_FLVMIX="c0,c1,…"), so neighbouring lanes in a SIMD
vector carry different crossings.
Results on cppsse4 (neppV = 2, via sse2neon on arm64):
- per‑event |M|² is byte‑identical to the union build, in both
FPTYPE=d(pure double) andFPTYPE=m(mixed precision,nParity=2), with
divergent crossings inside a SIMD vector; check crossing --exporter=standalone_mg7 --simd=sse4: 3/3 (p p > w+ j)
and 4/4 (p p > t t~ j);--use_crossing=Falseemits zero per‑lane machinery (historical output).
Work per event drops from 24 → 6 helicity iterations on w+ j (~4× on the
dominant get_amp); even the external‑wavefunction call count nets down
(≈ 192 → 108) despite the per‑state blend, because the iteration count falls
faster than the per‑iteration cost rises.
8. Open items
- GPU per‑lane port (needs CUDA to validate).
- The reported per‑event helicity
allselhelfor crossed events (separate
commit1eb6ec73c) is compile‑checked but runtime‑unvalidated — it does not
affect |M|², only the LHE helicity label; it needs an event‑level cudacpp run
against the Fortran backend to settle the permute‑vs‑NSF convention.
Alongside --simd, 'check crossing --exporter=standalone_mg7' now accepts --precision to pick the madmatrix float type: m mixed (default, = the madmatrix default), d double, f float. It is passed to the build as 'FPTYPE=<x>'. Ignored by the other crossing backends. - MG7_PRECISION_CHOICES + validation in _Mg7CrossingBackend (process_checks). - do_check: parse/validate --precision, completion, help text, default 'm'. Verified: --precision=d --simd=sse4 builds double and passes p p > w+ j 3/3 (values shift from the mixed-precision run and agree with the crossed subprocess to ~1e-16); an invalid value is rejected with a clear message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rectory Like the fortran 'standalone' exporter, 'standalone_mg7' now consumes the crossed_processes metadata directly instead of reconstructing the crossed subprocesses: do_output skips the Stage-C reconstruction for standalone_mg7 too, so the crossings collapse into the base directory and are reached through the base's crossing-aware sigmaKin (extended flavor id). p p > w+ j: 3 dirs (gQ, gQx, QQx) -> 1 (gQ); p p > t t~ j: 4 -> 2 (gg, gQ). The base dir keeps the full crossing machinery (spincol_cross / flavorPDGs_ cross / per-lane good-hel), builds, and check crossing --exporter= standalone_mg7 still passes 3/3 (w+ j). Only the dir count changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ir RAMBO point With the crossings folded into the base directory, check_sa.exe now also demonstrates each folded crossed subprocess (like the fortran check_sa.f), evaluated at the RAMBO point generated for ITS OWN mass permutation. - Exporter writes crossing_demo.dat in the P* dir: the extended flavor ids of the folded crossings (one per asked direction, mirror pairs collapsed), matched label-aware against compute_crossing_pdg_entries so a merged _quark leg matches any same-sign flavor -- the id is cross*nflav+flav0, i.e. the mg7 flavor id, so flavorPDG(id,k) gives the crossed PDG. Absent when the ME folds no crossings (check_sa then just shows the base flavors). - check_sa.cc (shared template): after the base flavors, read crossing_demo.dat and for each id build the crossed masses (base mass of the leg with the same |PDG|), RAMBO those masses, compute |M|^2 via UMAMI at the extended id, and print the per-leg PDG + momenta + |M|^2. RANMAR is made re-seedable (reset_rng) so every crossing is shown at the first draw for its mass permutation -- the point a standalone run of that subprocess would use. Verified (cppnone/none, FPTYPE=d and =m): p p > w+ j gQ shows flavorID 4 (g d~ > w+ u~) and 20 (d~ u > w+ g); p p > t t~ j gQ shows 5 (g d~ > t t~ d~) and 30 (d~ d > t t~ g); all matrix elements match check crossing / the independent standalone values. Non-crossing dirs (gg) get no demo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op the CROSS-indexed tables Both halves of the crossed averaging/symmetry denominator in matrix_standalone_crossing_v4.inc are now recomputed at runtime from small per-particle tables (one entry per external leg) instead of tables indexed by the crossing code: - GET_SPINCOL_CROSS: SPINCOL_CROSS_TABLE(0:NCROSS-1) -> SPINCOL_PART(0:NEXTERNAL-1), the per-leg spin*color (conjugation invariant). It decodes CROSS into its two transpositions, rejects the invalid (overlapping-swap / out-of-range) codes with the same condition get_crossing_permutation uses, builds the slot->leg map, and multiplies SPINCOL_PART over the legs the crossing puts in the initial state. - GET_IDENT_CROSS: BASEPID_CROSS_TABLE + SRC_CROSS_TABLE (both 0:NCROSS*NEXTERNAL-1) -> IDS_BASE(0:NEXTERNAL-1) (base leg PDG) + ANTIPID_BASE(0:NEXTERNAL-1) (its charge conjugate). It rebuilds the crossing PERM and its initial/final sign flips ICS the same way, reads each slot's representative PDG off IDS_BASE (ANTIPID_BASE where the leg swapped side), and looks up FLAVOR(PERM(k)); the n! identical-particle count is unchanged. The now-unused NCROSS parameter is dropped there. For a 2->3 process this is 36 + 2*180 = 396 CROSS-indexed integers down to 3*5 = 15 per-leg integers, and no CROSS-dimensioned array survives. compute_crossing_tables now also returns spincol_part/ids_base/antipid_base, with a generation-time assertion that both reconstructions reproduce the old spincol/basepid/ source for every applicable crossing (generation fails if they ever drift). get_iden_cross_lines emits the three per-leg DATA tables. The C++/cudacpp exporters still read the unchanged basepid/source/spincol, so this change is Fortran-only and the shared template also covers the madevent-group crossing routine. Validated: test_standalone_cross_symmetry TestStandaloneCrossSymmetry 20/20 (incl. the IDEN 36->192 identical-quark crossing and every density-matrix test); full suite otherwise green except one pre-existing, unrelated cudacpp failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ultiplying by 0 The per-event crossing-aware denominator zeroed an inapplicable crossing (spincol_cross==0: out of range or overlapping swap) by setting its lane factor to 0 and multiplying it into MEs_sv. But an invalid crossing's unphysical momentum relabelling can make that lane's |M|^2 a NaN, and nan*0 = nan, so the matrix element came out NaN rather than 0. Apply the denominator per lane directly onto MEs_sv and ASSIGN 0 for an invalid crossing (rather than multiply), which is NaN-safe. Valid lanes still multiply, so their result is unchanged. Fixes test_standalone_cross_symmetry TestStandaloneMg7CrossSymmetry test_invalid_overlapping_swap_returns_zero (previously "no matrix element parsed" because the driver printed "nan"); the per-event mixed-crossing test stays byte-identical. Class now 5/5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntime, drop the CROSS-indexed tables The C++ backends stored the crossing as several tables indexed by the crossing code (spincol_cross[ncross], basepid_cross/src_cross[ncross*nexternal], cross_perm/cross_ic[ncross*nexternal]; standalone_mg7 also had flavorPDGs_cross[ncross*nflav*npar] and an xhel_perm[ncross*npar]). The fortran standalone stores none of these -- GET_CROSS_PERM decodes the slot permutation and NSF signs from the crossing code at runtime, and the denominator / crossed PDGs are rebuilt from small per-leg tables. Bring both C++ backends onto the same scheme. New runtime decode cross_perm_ic(cross, perm, ic) mirrors GET_CROSS_PERM/ SWAP_LEGS (identity plus the crossing's two transpositions; returns false and leaves perm/ic the identity for an inapplicable code, so a momentum gather never reads out of range). From it: - spincol_cross(cross): product of the per-leg spin*color (spincol_part, one entry per leg) over the initial-state slots; 0 for an invalid crossing. - ident_cross(cross, flavor): identical-final-state n! factor from ids_base / antipid_base (per-leg base PDG and its charge conjugate) plus the actual flavor at slot perm[k]. - standalone_mg7 flavorPDG: crossed signed PDG rebuilt from the base per-(flavor, leg) PDG tables and the runtime perm/ic, like GET_PDG_FOR_FLAVOR -- no per-crossing PDG table. - standalone_mg7 per-event momentum permutation and selected_hel_code decode the crossing per event instead of reading cross_perm/cross_ic / xhel_perm. cross_perm_ic is __host__ __device__ (host flavorPDG and the device paths both call it). No cross-indexed crossing table survives in either generated source; for a 2->3 process standalone_mg7 drops from O(ncross*nflav*npar) integers to a handful of per-leg tables. use_crossing=False output is unchanged. Also fixes a latent overflow: _build_flav_pdg_tables can return more flavor rows than nmaxflavor, so base_pdg is filled from the first nflav rows only. Validated: test_standalone_cross_symmetry 43/43 (standalone_cpp 4/4, standalone_mg7 5/5 incl. per-event mixed-crossing SIMD and the overlapping-swap zero); p p > w+ j standalone_mg7 builds and its crossing demo prints the correct crossed PDGs (g d~ > w+ u~, d~ u > w+ g) and matrix elements. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… propagator
A particle that appears as an external (initial/final) state is an on-shell
asymptotic state, so an internal propagator of the same field must not carry the
i*M*Gamma resonance term in its denominator -- e.g. the s/u-channel top in
t a > t a. This mirrors the existing T-channel width drop, but is keyed on the
particle being external rather than on the propagator momentum being spacelike.
New settable option `zerowidth_external` (default True, like zerowidth_tchannel):
a code-generation option applied per matrix element at output time via
HelasMatrixElement.set_onshell_particles_width_to_zero(), which flags the
relevant internal-propagator wavefunctions with a runtime `onshell_zero_width`
annotation. HelasWavefunction.get('width') then returns ZERO for them, so every
UFO backend (standalone, madevent, standalone_cpp, standalone_mg7) reads a single
source for the propagator's W argument; in the complex-mass scheme the same ZERO
makes that propagator use the real mass. External legs take no width argument and
are untouched; `set zerowidth_external False` restores the widths.
Validated on t a > t a across all four backends (MDL_WT->ZERO, mdl_WT->ZERO,
cIPD[1]->0., MDL_WT->FK_ZERO; option off restores them); ME compiles/runs and
shifts as expected off the top resonance. test_standalone_cross_symmetry 43/43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When two helicity configurations are exact mirrors -- every helicity negated -- a parity/C-conserving amplitude gives them an identical |M|^2, so one of the two need not be recomputed. Extend the good-helicity machinery of SMATRIX to detect and exploit this alongside the existing zero-filter: - FLIP(IHEL): the row with every helicity negated, built once at runtime from the PROCESS_NHEL table (an involution; self-paired when all helicities are 0). - CSYM(IHEL,FLAV): stays true only while |M(IHEL)|^2 == |M(FLIP(IHEL))|^2 at EVERY good-helicity scan point. One mismatch anywhere permanently drops the pair, so a parity-violating (chiral) process simply never de-duplicates. - Fast phase: the lower-index representative of a surviving pair is evaluated once and its |M|^2 counted twice; the higher-index partner is skipped. Gated to the plain unpolarized sum and, crucially, to the UNCROSSED process only (FLAV_IDX in [1,NFLAV], tracked by a base-only counter NTRY_CSYM): a crossing permutes/sign-flips the helicities, so a base-row FLIP is not the crossed C-parity partner. Crossed flavours therefore keep the full helicity sum. Pure fixed template text -- no export_v4 change; the density matrix (a separate routine) is untouched and still computes every helicity. Validated: u u~ > g g deduped sum == full sum to all digits (and a x3-weight probe confirms the path fires, halving the C-symmetric rows); d u~ > w- g (chiral) never de-duplicates; p p > w+ j crossed flavours match the full sum; test_standalone_cross_symmetry 43/43 (incl. the density-matrix invariants). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port of the fortran standalone C-parity good-helicity de-duplication (b8c086f) to the standalone_cpp backend. Two helicity configurations that are exact mirrors -- every helicity negated -- give an identical |M|^2 under a parity/C-conserving amplitude, so one of the two need not be recomputed. - flip[ihel]: the helicity row with every helicity negated, an involution built once from the helicities[] table. - csym_bad[flav][ihel]: latches true once |M(ihel)| != |M(flip)| at any scan point (parity/C violation). One mismatch anywhere permanently drops the pair, so a chiral process simply never de-duplicates. - The good helicities of a flavor are reduced to the lower-index representative of every surviving C-parity pair (igoodrep/nrep) carrying a doubled weight (repwgt); the recycling sum then evaluates one of the pair and counts it twice, and the higher-index partner is skipped. Gated to the plain unpolarized sum and, crucially, to the UNCROSSED process (csym_dedup_ok = "cross == 0", or unconditionally "true" when the crossing machinery is off): a crossing permutes/sign-flips the helicities, so a base-row flip is no longer the crossed C-parity partner. Crossed flavors keep the full helicity sum. amp2/jamp2 are unaffected: a C-symmetric skip halves every entry uniformly (per-diagram symmetry holds whenever csym does), and every consumer normalises (amp2(iconfig)/XTOT, color CDF), so the ratios are invariant. The scan phase (sum_hel==0 or ntry<10) still full-sums; the reuse is confined to the helicity-recycling fast phase (single new hole csym_dedup_ok in get_crossing_replace_dict + new members in cpp_process_class.inc). Validated (u u~ > g g): ngood=8 -> nrep=4, deduped sum == full sum to all digits (136.37530903958569); a x3-weight probe gives 1.5x, confirming the weight path fires. d u~ > w- g (chiral) never de-duplicates (nrep=ngood=6). The crossed value (flavor_id 3) keeps the full sum and differs from the identity. test_standalone_cross_symmetry 43/43. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port of the fortran standalone C-parity good-helicity de-duplication (b8c086f) to the ungrouped madevent SMATRIX (matrix_madevent_v4.inc). 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. - FLIP(I): the helicity row with every helicity negated, an involution built once from the PROCESS NHEL table. - CSYM(I,IFLAV): stays true while |M(I)| == |M(FLIP(I))| at every scan point (NTRY<20). One mismatch anywhere permanently drops the pair, so a chiral process (or a polarized beam, whose per-helicity beam_polarization scaling breaks the mirror) never de-duplicates -- no explicit polarization gate is needed, the scan self-excludes it. - Fast phase (NTRY>=20, still within MAXTRIES=25 so the reuse overlaps the grid build): the lower-index representative of a surviving pair is evaluated once and counted twice in ANS; its |M|^2 is copied to TS(FLIP) and, when the DS helicity grid is being built, DS_add_entry is called for the partner too, so the per-helicity event-selection CDF and the grid stay exact. The skipped MATRIX call is the only saving -- confined to the init full-sum loop; the per-event random-helicity branch is untouched. Gated to the uncrossed base process: madevent keeps each crossing in its own subprocess directory, so every IFLAV row here is the base process and a base-row FLIP is always the genuine C-partner. amp2/jamp2 are unaffected: the skip halves every entry uniformly (per-diagram/per-colour symmetry holds whenever CSYM does), and their only consumers are ratios (set_amp2_line's AMP2(iconfig)/XTOT, the colour CDF), so they are invariant. Validated with an in-code self-check (recompute the full sum at the same momenta whenever DEDUP fires): u u~ > g g de-duplicates 4 non-zero C-pairs and the deduped ANS equals the full sum at every one of ~28000 event calls (0 mismatches); d u~ > w- g (chiral) de-duplicates 0 non-zero pairs; both generate correct cross-sections (7.433e4 pb, 1650 pb) and 100 events. test_standalone_cross_symmetry 43/43. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port of the fortran standalone C-parity good-helicity de-duplication (b8c086f) to the madmatrix / cudacpp CPU-SIMD backend. Two helicity rows that are exact mirrors -- every helicity negated -- give an identical |M|^2 under a parity/C-conserving amplitude, so calculate_jamps need not be run for both. Applied to the NON-crossing helicity loop only (the plain get_madmatrix_ crossing_dict path); every csym hole is empty in the crossing path, leaving the validated per-lane/per-crossing SIMD machinery byte-for-byte unchanged. This is the right place: the crossing 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 there, and a base-row FLIP is not the crossed C-parity partner anyway. Mechanism (all C++ only; GPU and the crossing path keep the full sum): - cFlip[ihel]: helicity row with every helicity negated (an involution, built once from cHel). cCsymBad[ihel]: latched true once |M(ihel)| != |M(cFlip)| at some getGoodHel scan point. cCsymPair[ihel]: a good, distinct, C-symmetric pair member, finalised in sigmaKin_setGoodHel. - Detection lives in the serial sigmaKin_getGoodHel scan (per-page |M|^2 stored and compared against the flipped partner across the scan events/flavors), so sigmaKin only READS the tables -> thread-safe under the OpenMP page loop. - sigmaKin keeps the FULL cGoodHel list -- so the per-helicity event-selection CDF (MEs_ighel) and selected_hel_code stay exact -- but calls calculate_jamps only for the lower-index representative of each surviving pair, storing its |M|^2 (meOfIhel) and reusing it for the higher-index partner. Handles both the single- and mixed-precision (two-page) C++ layouts. numerators/denominators/jamp2 are unaffected: the skip halves every entry uniformly (per-diagram/per-colour symmetry holds whenever csym does) and their consumers normalise (ratios), so they are invariant. Validated (SSE4 mixed-precision check_sa.exe): u u~ > g g deduped |M|^2 equals the crossing-on full sum byte-for-byte over 8 events; an x2-reuse probe gives exactly 1.5x, confirming the reuse path fires; d u~ > w- g (chiral) is byte-identical to the full sum AND unchanged by the x2 probe (never fires); the crossing-on build contains no csym token (path unchanged). test_standalone_cross_symmetry 43/43 (incl. TestStandaloneMg7CrossSymmetry 5/5 and test_use_crossing_false_byte_identical, which checks deduped == full sum). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m loop Extend the C-parity good-helicity de-duplication (b8c086f) to the GROUPED madevent SMATRIX (matrix_madevent_group_v4.inc -> matrix<i>_orig.f / matrix<i>.f), the default madevent path. Mirrors the single-process port (matrix_madevent_v4.inc) but threaded through the grouped (helicity, flavor, subprocess)-indexed GOODHEL/CSYM and the crossing machinery. - FLIP(I): helicity row with every helicity negated, an involution built once. - CSYM(I,me_flav_key,proc_id): stays true while |M(I)| == |M(FLIP(I))| at every scan point; a chiral process or a polarized beam self-excludes (its per- helicity beam_polarization scaling breaks the mirror), so no explicit gate. - Fast phase (NTRY>=20): the lower-index representative of a surviving pair is evaluated once and counted twice in ANS; its |M|^2 is copied to TS(FLIP) and (while the DS helicity grid is built) DS_add_entry is called for the partner, so the per-helicity event-selection CDF and grid stay exact. Gated to the uncrossed base process via the new me_csym_cross_ok hole (".TRUE." without crossing, "CROSSUSE.EQ.0" with it): a crossing permutes/sign- flips the helicities so a base-row FLIP is not the crossed C-partner; crossed dependents keep the full sum. amp2/jamp2 are unaffected (uniform halving -> ratio invariant). Only the init full-sum loop changes; the per-event random- helicity branch is untouched. Validated (u u~ > g g, hel_recycling off so matrix<i>_orig.f drives events): 4 non-zero C-pairs de-duplicated, deduped ANS == full sum at every one of ~77000 event calls (0 mismatches); LHE helicity distribution stays symmetric (no representative bias); cross-section consistent. test_standalone_cross_ symmetry 43/43 (incl. the grouped TestMadevent* tests, whose good-hel discovery now runs matrix<i>_orig.f with the dedup). NB: this covers matrix<i>_orig.f (good-hel discovery + hel_recycling-off runs). The recycled matrix<i>_optim.f event-generation loop is a separate template (matrix_madevent_group_v4_hel.inc + hel_recycle.py), addressed next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tim.f
Extend the C-parity de-duplication to the helicity-recycling event-generation
hot path (matrix<i>_optim.f), so a C-symmetric process computes only one
representative of each mirror pair. This is the default grouped+recycling
madevent path, generated at run time by gen_ximprove/hel_recycle from
matrix_madevent_group_v4_hel.inc.
Mechanism (four coordinated pieces):
- Discovery: matrix<i>_orig.f, once its C-parity scan has settled (NTRY=20 in
the forhel init_mode run), PRINTs "CSYM PAIR: <me> <rep> <flip>" for each
surviving pair (representative rep<flip, both good, |M(rep)|^2==|M(flip)|^2 at
every scan point). Gated to the uncrossed base process (me_csym_cross_ok).
- gen_ximprove parses those lines. For each pair it KEEPS both helicity rows
(so the |M|^2 sum and the event-helicity CDF stay complete) but adds every
(partner, graph) to bad_amps_perhel, so hel_recycle never generates the
partner's HELAS calls -- only the representatives' amplitudes are computed.
- The new ${csym_reuse} template hole (filled by gen_ximprove with the optim's
re-indexed helicity positions) copies each partner's |M|^2 back from its
representative (TS(flip)=TS(rep)) right after the recycled MATRIX returns.
- Disabled for a crossing-class base (perms): it keeps every config, and the
reuse would be redundant since the sum is already crossing-invariant.
amp2/jamp2 are unaffected: the partner's are 0 (dropped) rather than reused,
i.e. every entry is uniformly halved -> the multichannel/color ratios that
consume them are invariant.
Validated (u u~ > g g, default grouped + hel_recycling): matrix1_optim.f drops
2 of 4 good helicities' amplitudes and reuses TS(4)=TS(1), TS(3)=TS(2); the
cross-section is BYTE-IDENTICAL to a dedup-off recycling reference at the same
seed (7.399e4 +- 546.6 pb, 1000 events); all four helicities are populated in
the LHE (the dropped partners via the reuse) with a symmetric distribution.
d u~ > w- g (chiral) reports no pair, so its optim is unchanged. p p > w+ j
(chiral, crossing class) is likewise untouched. test_standalone_cross_symmetry
43/43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit 4ec2ae7 moved the T-channel (spacelike) propagator width drop into the ALOHA propagator routine itself: the denominator is now emitted as a runtime P^2>0 conditional (keep the i*M*Gamma width when timelike, drop it when spacelike) instead of the fixed single-line COUP/(P^2 - M(M-CI*W)). The reference files for the off-shell (propagator) routines predated that change and still carried the old single-line denominator, so 8 ALOHA IOTests reported their propagator routines as differing. Regenerate those 15 golden files (Fortran, C++ and Python writers) to the conditional form. The kept form deliberately repeats the P^2 sub-expression rather than caching it in a temporary: the compiler common-subexpression- eliminates it (verified in -O2 assembly), so a temporary would be cosmetic. Amplitude routines and the complex-mass-scheme references are unchanged (the P^2>0 branch is suppressed under CMS). All 12 ALOHA IOTests now pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…PEP 667)
The short ALOHA vertex tests verify an expression numerically by
eval(str(expr.get_rep(...))). That string references ALOHA's common-subexpression
temporaries (TMP0/TMP1/..., from KERNEL.reduced_expr2, or a routine's contracted
items), which the tests first defined with
exec('%s = %s' % (name, cexpr))
inside the test method. Under Python 3.13+ (PEP 667) a name created by exec() in
an optimized/function scope is written to a transient locals snapshot and is no
longer visible to a subsequent eval() in that function, so the tests errored with
NameError: name 'TMPk' is not defined (the ordinary F*/P* locals stay visible, so
only the exec-injected temporaries were lost).
Replace the idiom with
globals()[name] = eval(str(cexpr))
which evaluates each temporary (using the still-visible real locals, and earlier
temporaries already in globals) and stores it as a module global that the
existing eval() calls read. Equivalent on older Python, correct on 3.13/3.14.
Fixes test_short_aloha_{FFV,FFT2,FFVP1N,FFV_MG4,ZPZZ} and the siblings sharing the
idiom (test_short_{expand_veto,part_spin32propagator,spin2propagator4,
use_of_library_spin2}). Test-only change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_build_flav_pdg_tables builds the crossing GET_PDG_FOR_FLAVOR table and is called
unconditionally from write_matrix_element_v4. It took the per-leg ids from
process.get('legs'), which for a decay chain are the CORE process legs
(p p > w+ w- -> 4 legs), while compute_flavor_masks() is indexed by the FULL
external legs after the decays (p p > w+ w-, w+ > j j, w- > j j -> 6 leaves). The
flavor tuple then had more entries than leg_ids, so leg_ids[leg] ran off the end
and every msF/msP (density / madspin full-flavor) decay-chain output crashed with
IndexError.
Source the legs from get_legs_with_decays() instead, which expands the decays so
leg_ids lines up with the flavor tuple, and is identical to get('legs') when there
are no decays (so non-decay crossing output is unchanged).
Fixes tests/unit_tests/madspin test_ww_jjjj_full_flavor_data_has_16_entries and
unblocks the decay-chain msF/density acceptance outputs that were erroring on this
crash. test_standalone_cross_symmetry stays 43/43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
set_onshell_particles_width_to_zero built its "external particle" set from
process.get('legs'), which for a decay chain are the CORE legs (d d~ > z z ->
[d,d~,z,z]). It therefore treated the decaying z as an external state and zeroed
the width on the resonant z propagator that z > e+ e- needs, turning its
Breit-Wigner into an on-shell 1/(p^2-M^2) singularity: the full ME of
d d~ > z z, z > e+ e- blew up to ~1e18 and the density-matrix convolution ratio
collapsed to ~0 (test_standalone_density_dd / _uu). This mis-fires for any decay
chain where a core-final resonance decays (Z/W/H/top production+decay).
Fix, mirroring the _build_flav_pdg_tables decay-chain fix:
- take the external set from get_legs_with_decays() (the asymptotic leaves; a
no-op without decays), so a decaying core particle is represented by its decay
products rather than itself;
- never drop the width of a decay-chain resonance (onshell is True), covering the
case where the same field is both a resonance and an asymptotic leg (one top
decays while the other stays final).
Verified: the z propagator keeps MDL_WZ while t a > t a still drops the top width
(intended case unchanged); test_standalone_density_{dd,uu} pass;
test_standalone_cross_symmetry stays 43/43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…DEN assert
compute_crossing_tables built the crossing tables from the core production
legs while a decay-chain matrix element's NEXTERNAL counts the decay leaves,
so on e+ e- > z z, z > mu+ mu-, z > e+ e- the two core z looked like an
identical pair (denominator 8) though they decay differently (true
denominator 4), and the generation-time IDEN sanity assert crashed codegen.
Crossing stays enabled for decay chains -- it acts within the production, and
a decay chain records no cross>0 (so nothing ever crosses the production/decay
boundary, which is the intended behavior). The fix:
- build the tables over get_legs_with_decays() so they match the real
NEXTERNAL (a no-op for non-decay processes, crossing suite still 43/43);
- exempt a decay chain from the exact identity-denominator assert: its
identical-particle factor is a resonance-level property (two z decaying
the same way count once, differently not at all) that no leg-PDG count
reproduces, and it never reaches GET_IDENT_CROSS since cross=0 is
normalised by the static IDEN. Keep the weaker invariant that the initial
spin*color divides IDEN.
test_decay_chain_symmetry_factor passes; test_standalone_cross_symmetry 43/43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or the new codegen
Two unit expectations drifted from committed codegen changes:
- test_header: ALOHA now wraps a propagator denominator in the t-channel
width conditional (IF(DBLE(P^2)>0) THEN keep-width ELSE drop-width ENDIF).
Update the expected FFV1_2 body to that form, and rstrip each compared
line so the cosmetic trailing space the ALOHA line wrapper leaves after
"(M1 " does not silently re-break the test (a bare trailing space in the
source would be stripped by editors/pre-commit).
- test_generate_helas_diagrams_uux_uuxuux: the madevent exporter drops the
ICOLUP colour-flow table from leshouche.inc when the matrix element has a
canonical colour code (drop_icolup) -- addmothers.f rebuilds the Les
Houches tags from colorflow.inc. Refactor to check the same feature in the
new layout: leshouche.inc now asserts IDUP + MOTHUP only, and a new
colorflow.inc assertion checks the canonical code
(NCOLSLOT/ICOLCSL/ICOLASL/ICOLCODE) that carries those six flows. The old
ICOLUP rows are kept inline as a reference comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 9 test_standalone_madevent_consistency_* tests all errored: they output
madevent with the default use_crossing on, but the ungrouped madevent exporter
does not support crossing, so _check_crossing_support raised. Generate the
reference with --use_crossing=False (the plain fortran standalone) and extend
the check into a cross-backend matrix element comparison at one shared seeded
phase-space point, per flavor:
- fortran madevent, ungrouped, crossing off (the original check);
- fortran standalone WITH crossing (the crossing-aware SMATRIX must
reproduce the plain per-flavor matrix element);
- fortran madevent, grouped, WITH crossing (ProcessExporterFortranMEGroup);
- standalone_mg7 (madmatrix / cudacpp CPU-SIMD), skipped if the C++ build
toolchain is unavailable.
Backend specifics handled: the grouped exporter hides SMATRIX1 behind helicity
recycling (matrix1_orig.f), so its driver builds the madevent_forhel target;
madevent's GET_FLAVOR returns per-leg group indices, not PDGs, so the madevent
backends are matched to the reference by the shared IFLAV order while the
standalone/mg7 backends (which expose real PDGs via GET_PDG_FOR_FLAVOR) match
by PDG; standalone_mg7's check_sa.exe reads the seeded momenta from an LHE file
(-e) so it evaluates the same point.
All 9 pass with all four backends (mg7 within 1e-7, float precision). Per-flavor
diffs moved from misc.sprint to logger.debug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A decay-chain matrix element now reuses the crossings of its core production process. The crossing acts at the production level; a decaying resonance's whole decay block rides along on its production leg and is never split, and a crossing that would pull a decay-block leaf into the initial state is rejected (spin*color 0). - compute_crossing_tables: _leaf_block_sizes (per-leg leaf counts) drives a block-size table; a crossing that carries a decay-block leaf across the initial/final line gets spincol=0. Emits a per-leaf COUNTABLE mask and a scalar IDENT_RESONANCE = identical_particle_factor // (n! over countable final legs). All no-ops for non-decay. Called class-qualified so the C++/mg7 exporters can reuse it unbound. - GET_IDENT_CROSS (matrix_standalone_crossing_v4.inc): FACT starts at IDENT_RESONANCE and counts only COUNTABLE final legs, giving the resonance-level crossed denominator; reduces to the historical plain leaf count for non-decay. - diagram_generation: thread merge_crossing into the decay-chain production so the base amplitude records crossed_processes and the combined decay-chain ME inherits them (gated off when a decay pins an s-channel). Standalone p p > t t~ j, t > b w+ collapses 7 dirs -> 2. - _crossed_signatures: expand recorded production crossings to leaves so the check_sa demo is enabled and restricted to the real crossed subprocesses. - madevent/summation backends keep the pre-dedup output: the decay chains are regenerated fully (merge_crossing=False) at output time, byte-identical to a MG_MERGE_CROSSING=off build. Validated (test_standalone_cross_symmetry, two new tests): the base decay-chain crossing SMATRIX at a crossed FLAV_IDX reproduces a --use_crossing=False build of the crossed decay chain exactly, for a plain jet crossing and for the resonance-denominator case (u u~ > z z g, z > e+ e-, IDENT_RESONANCE=2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
p p > w+ j, w+ > j j: the crossed decay-chain subprocesses route through the base decay-chain crossing SMATRIX (matrix2_router -> SMATRIX1 with a crossed FLAV_IDX). Assert the crossing-routed cross section equals a --use_crossing=False build (independent matrix elements). Verified identical (1.39e4 pb, same error bar), confirming madevent supports the production-crossing of a decay chain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Track B cross-group crossing dependent (e.g. q q~ > t t~ g g, reusing g g > t t~ q q~) has its matrix<i>_orig.f symlinked and crossgroup.mk symlinks the base's compiled matrix<i>_orig.o AND the base's recycled matrix<i>_optim.o over it. gen_ximprove still ran the (expensive) helicity recycler on the dependent's symlinked source, producing a matrix<i>_optim.f that is never compiled -- pure redundant work (one wasted recycler run per Track B dependent). Skip it: for a symlinked matrix source, copy orig->optim (cheap) instead of recycling. A placeholder matrix<i>_optim.f must remain because the P makefile discovers its matrix objects by the presence of that file (a bare skip leaves SMATRIX1 undefined at link); the placeholder is never compiled since crossgroup.mk overrides its .o with the base's. Only symlinked (Track B dependent) sources are affected; base directories, whose sources are real files, still bake the shared optim over the union good-hel of the whole crossing class. Verified on p p > t t~ j j: cross section 397.8 pb unchanged, build OK, the dependent's optim.f == orig.f (recycler skipped) while the base is recycled; recycler runs drop from 6 to 5. Applies to non-decay and decay chains equally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ference
Seven flavor tests broke on the crossing branch from two independent, intended
feature changes; none is a regression in the crossing/mask machinery.
Crossing default-on (6 tests) -- add --use_crossing=False, which isolates each
test's actual subject from the orthogonal crossing codegen (crossing reduces to
the base flavor before the mask/grouping/xsec logic runs, and its correctness is
covered by the crossing and standalone-madevent-consistency suites):
- test_standalone_flavor_mask, test_standalone_wwjj: crossing folds the
inspected subprocess into another directory, so the test can no longer find
it (p p > j j gives one folded P0_QQ_QQ dir, not the three separate ones);
- test_madevent_flavor_zud_nogroup: ungrouped madevent has no crossing
support, so `output madevent` raised;
- test_flavor_grouping_consistency{,_width,_mlm}: the ungrouped settings hit
the same guard.
ALOHA t-channel width (1 test) -- test_standalone_merged_flavor_uq_zuq: u q > Z u q
runs through a spacelike (t-channel) electroweak propagator, and commit 4ec2ae7
drops the width of a spacelike propagator (its momentum can never reach the pole,
so the Breit-Wigner term there is spurious). This shifts the matrix element ~0.1%,
independent of crossing and of the per-flavor mask. The reference values are
updated to the new numbers; the previous values are kept in a comment.
Verified: merged_flavor_uq_zuq, standalone_flavor_mask, standalone_wwjj all pass.
The madevent tests are unblocked (guard cleared); their event generation is
environment-dependent here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
One subtelty to keep in mind (maybe we should change the crossing encoding?): Why
|
| process | NGRAPHS |
crossing of … |
|---|---|---|
u u > t t~ u u |
14 | — |
u u~ > t t~ u u~ |
14 | u u > t t~ u u ✅ |
d u > t t~ d u |
7 | — |
d d~ > t t~ u u~ |
7 | d u > t t~ d u ✅ |
Reachability of the crossed signatures (from compute_crossing_pdg_entries):
u u~ > t t~ u u~ reachable from u u > t t~ u u : True (both leg orders)
d d~ > t t~ u u~ reachable from d u > t t~ d u :
(1,-1,6,-6, -2, 2) = d d~ > t t~ ū u : True
(1,-1,6,-6, 2, -2) = d d~ > t t~ u ū : False <-- the order the merged ME uses
Why the merged Q Qx ME still cannot route
Q Qx > t t~ Q Qx bundles (via flavour grouping) three flavour classes. Two of
them do cross to Q Q:
d d~ > t t~ d d~(same flavour) → crossing ofd d > t t~ d d✅d u~ > t t~ d u~(mixed) → crossing ofd u > t t~ d u✅
The third is the flavour-changing annihilation d d~ > t t~ u u~ (initial d d̄,
final u ū, via d d̄ → g → t t̄ and g → u ū). It is a crossing of
d u > t t~ d u, but only with the two final same-flavour-group legs in the
order (ū, u):
crossing of d u > t t~ d u delivers : … , ū(5) , u(6) -> sig (1,-1,6,-6,-2, 2)
merged Q Qx > t t~ Q Qx lists : … , u(5) , ū(6) -> sig (1,-1,6,-6, 2,-2) (Q at 5, Qx at 6)
Going from (ū, u) to (u, ū) is a swap of the two final legs. The crossing
code encodes a crossing as
CROSS = I*(NEXTERNAL+1) + J # swap initial-1 with final-I, initial-2 with final-J
i.e. only crossings of the two initial legs with final ones — there is no
final↔final permutation in the encoding. Since u and ū are different
particles, feeding the base's crossing SMATRIX the dependent's momenta in the
wrong order would evaluate the wrong |M|², so partition_crossing_classes
correctly refuses the match. Routing a merged member requires every flavour
to cross to a single base; this one flavour cannot, so the whole Q Qx ME stays
a base and gets its own matrix<i>_orig.f (and its own helicity-recycling run).
Consequences / how it could be enabled
- The current output is correct —
Q Qxgenuinely needs a matrix element it
can evaluate; nothing is double counted or lost. - This is not the cross-group Track-B case:
g g > t t~ q q~↔q q~ > t t~ g g
are reused (symlinked base matrix element), because there the crossing is a
plain initial↔final swap the encoding can express. - To share
Q QwithQ Qxone would have to generalise the crossing to also
permute final legs (or have flavour grouping assign, for that flavour, the leg
order the initial-leg crossing can actually reach). Either is a real extension
of the crossing machinery, not a bug fix.
One-line summary
q q~ > t t~ q q~ is a genuine crossing of q q > t t~ q q, but the merged
Q Qx matrix element also carries the flavour-changing d d~ > t t~ u u~, whose
crossing needs a final-leg reorder the I*(N+1)+J encoding cannot express —
so the merged ME cannot be routed as a whole and keeps its own matrix element.
Implementation:
helmap.
- [x] C symmetry of helicity are now checked compatible with helicity recycling
- [x] Block beam polarisation + EVA under crossing (clear crash rather than silent wrong results).
- [x] handled only for the production side in case of decay chain syntax.
Validation:
Known limitations / follow-ups
merge option