Crossing symmetry - #49
oliviermattelaer wants to merge 191 commits into
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>
…the partner The C-parity de-duplication in the madmatrix / cudacpp CPU-SIMD backend kept the FULL good-helicity list and merely skipped calculate_jamps for the higher -index member of each mirror pair, reusing its |M|^2. cGoodHel is now REDUCED in sigmaKin_setGoodHel to the lower-index representative of each pair, each representative is counted twice, and the event-by-event helicity choice returns the representative or its cFlip partner at equal rate. That halves the sigmaKin trip count, and it halves nGoodHel -- which is what sizes the ghelAllJamps / ghelAllMEs super-buffers on the GPU side, so it is the prerequisite for ever extending this to the device. The 50/50 needs no extra random number: conditional on the CDF landing in bin [lo,hi) the selection variate is exactly uniform there, so its position within the bin is an independent U(0,1). Drawing a fresh one would desynchronise the stream shared with the Fortran integrator. Two safety fixes fall out of getting this to work at all: * The pair-equality test was RELATIVE only. For two rows whose |M|^2 is numerically zero that compares roundoff noise against itself and latches "not C-symmetric" at random -- so on u u~ > g g, whose MHV-vanishing gluon configurations sit at |M|^2 ~ 1e-30 out of ~10 and are still admitted by the `!= 0` good-helicity filter, the de-duplication NEVER ENGAGED. It now also requires the difference to be significant against the largest |M|^2 of the same (flavor, page). The same relative-only test exists in the fortran and standalone_cpp backends and may be equally inert there. * cCsymScanned: the verdict now defaults OFF unless the validating scan actually ran. Previously cCsymBad was zero-initialised, so any path reaching setGoodHel without getGoodHel would have read "no mismatch seen" as "C -symmetric" and enabled the dedup unvalidated. Still gated to the uncrossed base process (a crossing permutes AND sign-flips helicities, so a base-row mirror is not the crossed C-partner) and to CPU builds; the crossing dict leaves every csym hole empty, so that path is byte-for-byte unchanged. Validated on u u~ > g g, g g > t t~, g g > g g with FPTYPE=d (not m, which hides ULP differences in this backend), 2M events per process, dedup-on vs a -DMGONGPU_NOCSYM build on identical inputs: |M|^2 max relative difference 6.2e-16 .. 1.2e-15, summed |M|^2 identical to all 17 digits; selected-helicity chi2/ndf 0.167 (3 dof), 0.945 (11), 1.478 (5); intra-pair split chi2/ndf 0.993, 0.636, 1.071. Negative control: with the 50/50 deliberately broken, |M|^2 stays bit -identical while the helicity chi2/ndf goes to 4.5e5 -- a cross-section check cannot see this class of bug. End-to-end `output mg7` cross-section (multichannel, real integrator), 3 independent 40k-event runs each: 60999 +- 43 pb vs 61029 +- 43 pb, 0.49 sigma. Chiral p p > w+ j, d u~ > e- ve~ and u u~ > e+ e- self-exclude and are byte -identical; u u~ > e+ e- is the one that exercises the |M|^2-mismatch arm (mirror rows both good and distinct, differing by a factor 4). Mixed precision (FPTYPE=m) and scalar (BACKEND=cppnone) paths both exercised. Timing (arm64, cppsse4, FPTYPE=d, MinTimeInMatrixElems, interleaved runs): process vs branch tip vs no dedup vs skip-and-reuse u u~ > g g 2.16x 1.86x 0.99x g g > t t~ 2.22x 2.06x 1.02x g g > t t~ g -- 1.97x 1.00x So the halving itself is a wash against the old skip-and-reuse on CPU, as expected -- the kernel was already being skipped. The 2.2x against the branch tip is the de-duplication finally engaging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… relative
The scan that decides whether two mirror helicity rows have the same |M|^2 used
a RELATIVE tolerance only:
IF (DABS(TS(I)-TS(FLIP(I))).GT.1D-6*(DABS(TS(I))+DABS(TS(FLIP(I)))))
For a pair whose |M|^2 is *numerically zero* that compares roundoff noise
against itself and latches at random, and since the verdict is all-or-nothing a
single noise pair vetoes every real pair. Also require the difference to be
significant against TSMAX, the largest |M|^2 of the same scan point.
Whether a vanishing helicity configuration lands on exact 0 or on ~1e-30 turns
out to depend on the process AND on the backend, so this was measured rather
than assumed, by instrumenting the generated code to print its own verdict:
backend process before after
standalone fortran u u~ > g g DEDUP=T (0.0) unchanged
standalone fortran g g > t t~ DEDUP=F DEDUP=T
madevent ungrouped g g > t t~ DEDUP=F DEDUP=T
madevent grouped g g > t t~ DEDUP=F DEDUP=T, 6 CSYM PAIRs
Only pairs 1/16 and 4/13 were ever rejected, at at most 6.3e-30 against a scan
maximum of 3.2e+02 -- 32 orders of magnitude down.
Validation is exact rather than statistical: the fortran de-duplication writes
TS(FLIP)=T, so the event-selection CDF still runs over the FULL helicity list
and the random-number stream is untouched. A same-seed dedup-on/dedup-off pair
is therefore event-by-event identical, and is:
ungrouped 7.211495 pb both, 20000/20000 identical helicity combinations
grouped 7.206236 pb both, 20000/20000 identical helicity combinations
(20k events, lpp1=lpp2=0 partonic at 500+500 GeV, iseed=33, helicities read
from the LHE spin column; chi2 = 0 exactly on the helicity histogram.)
Grouped side-effect: with pairs now reported to gen_ximprove, matrix1_optim.f
gains 6 `TS(flip) = TS(rep)` reuse assignments and drops from 52 to 39 HELAS
calls for g g > t t~.
NOT ported to standalone_cpp: its verdict already passes (its noise values come
out bit-identical between mirror rows), so the floor would change nothing there.
That backend has two unrelated pre-existing problems instead -- `sum_hel` can
never leave 0, so the branch consuming igoodrep/repwgt is unreachable, and
`igood[flav][ngood]` overflows its row by one when every helicity is good --
both of which need their own fix and validation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The crossed path kept the full helicity sum. That was not a physics limitation:
a crossing acts on a helicity row as a slot permutation plus a per-leg sign
flip, and global negation commutes with both, so mirror(crossed row) ==
crossed(mirror row) and each crossing's good-hel set is closed under the mirror.
Measured directly by driving SMATRIXHEL per row on u u~ > g g at extended flavor
ids 1, 3, 4, 5, 6 and 21 (sums 6.75 / 34.6 / 129.6, so the crossings really do
change the physics): worst row-vs-mirror relative difference 0, on every row.
What makes it harder than the uncrossed path is that lanes of ONE SIMD page may
carry DIFFERENT crossings. The verdict, the reduced list, the weight and the
50/50 are therefore all per crossing and applied per lane:
* the scan latches cCsymBadCross[cross] (iflav encodes cross*nmaxflavor+flav),
with the same absolute floor as the uncrossed path;
* goodhel_percross_build reduces each cGoodHelOfCross[c] to the lower-index
representative of every pair, halving cNGoodPerCross[c] and cNGoodMaxCross;
* sigmaKin builds a 0/1 per-lane vector once per page and doubles each lane's
contribution through it;
* selected_hel_code_lane_csym returns the lane's representative or its mirror
at equal rate, the coin recycled from the selection variate so no extra
random number is drawn.
ALL-OR-NOTHING ACROSS CROSSINGS, for an implementation reason that is worth
recording. Reducing only some crossings leaves cNGoodPerCross non-uniform, and
the lanes of a shorter crossing then reach the ighel >= cNGoodPerCross padding
row (_hr = -1 in calculate_jamps). That row yields NaN rather than 0: its zeroed
wavefunctions give a 0/0 propagator, and for a VALID crossing the per-event
denominator multiplies instead of assigning 0, so the NaN reaches the output.
This is PRE-EXISTING -- reproduced with -DMGONGPU_NOCSYM (no de-duplication code
active at all) by shortening one crossing's list by hand -- and latent today
only because every crossing happens to have the same good-hel count (checked:
p p > w+ j is uniform at 6). Keeping the verdict uniform preserves that
invariant exactly rather than arming the trap. Fixing the padding row is a
separate change.
Validated against a -DMGONGPU_NOCSYM build on identical inputs, FPTYPE=d,
2M events, with the driver cycling flavor ids so ONE page carries several
crossings:
crossings fed |M|^2 max rel diff summed |M|^2 helicity chi2/ndf
0 (identity) 7.4e-16 identical 0.167 (3)
3 4.9e-16 identical 1.023 (3)
0,3,4,5 mixed 7.4e-16 identical 0.540 (5)
Negative control on the mixed configuration: removing the mirror pick leaves
|M|^2 bit-identical and the summed |M|^2 unchanged while the helicity chi2/ndf
goes to 2.7e5 -- a cross-section check cannot see this class of bug.
Chiral p p > w+ j self-excludes and is byte-identical under crossing.
FPTYPE=m and BACKEND=cppnone both agree. No NaN in any run.
Timing (arm64, cppsse4, FPTYPE=d, MinTimeInMatrixElems, interleaved, 6 reps):
cNGoodMaxCross 8 -> 4 and 1.98x, close to ideal because here the halving really
does halve the trip count -- unlike the uncrossed path, where the previous
skip-and-reuse had already removed the kernel call.
The uncrossed path is untouched: --use_crossing=False generates byte-identical
code to before this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed() clause Conflict was add/add at EOF of .github/workflows/acceptancetest.yml: this branch appended the crossing-symmetry CI jobs, zen-kilby appended acceptancetest_standalone_mg7_openmp. Independent jobs, both kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A loop-induced ([noborn=]) process is exported by the *tree-level* output
machinery: master_interface borrows the MadLoop interface only to validate
the model, then switches back to 'MadGraph' and calls create_loop_induced.
Only the madevent formats have a loop-induced exporter to route it to, so
every other format handed the LoopHelasMatrixElement to a tree-level
exporter and died deep inside it:
output standalone -> IndexError in write_check_sa
output matrix -> "wavefunction_rank has not been computed"
output mg7 -> KeyError on the first loop leg, which the mg7
exporter's edge-name map does not contain
All pre-existing, none of them a regression. Refuse those formats up front
instead, and point at [sqrvirt=], which stays in the MadLoop interface and
reaches the MadLoop exporters: g g > h h [sqrvirt=QCD] + output standalone
gives a working standalone directory whose ./check returns exactly the same
3.2829343688358318E-005 as the [noborn=] madevent PV dir.
The check lives in MadGraphCmd.do_output, ahead of the rmtree that cleans an
existing output directory, so a guaranteed refusal never deletes one first;
ExportV4Factory and ExportCPPFactory carry the same check as a backstop for
direct callers. [virt=]/[sqrvirt=] are untouched -- they go through
loop_interface.do_output (output_type='madloop') and reach none of the three
sites -- and 'output aloha' returns before the check.
Add tests/unit_tests/loop/test_loop_induced_output.py, which nothing covered
before: the refusals (asserting the message still names sqrvirt), and the
routes that must keep working -- madevent on [noborn=], standalone on
[sqrvirt=] and on [virt=].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e6d89 # Conflicts: # madgraph/interface/madgraph_interface.py
…rix (uncrossed + crossed) Halves the good-helicity list to one representative per mirror pair, counts each twice, and returns representative or partner at equal rate (coin recycled from the selection variate, so the shared RNG stream is untouched). Also fixes the relative-only pair tolerance that made the de-duplication inert, and defaults the verdict OFF unless the validating scan ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> # Conflicts: # madgraph/iolibs/template_files/madmatrix/process_sigmaKin_function.inc
test_standalone_mg7_openmp failed as soon as the generated process took the crossing branch: that branch's sigmaKin loop is bounded by cNGoodMaxCross, a file-scope static, and 'omp parallel for default( none )' requires every such variable in shared(). Pre-existing -- it reproduces at 92088eb, before the C-parity merge -- and invisible until now because the test arrived on a branch based on dcdd3df, which predates the per-lane crossing work, so the process it generates there never reached that loop. Renamed the hole csym_omp_shared -> extra_omp_shared: cNGoodMaxCross is needed whether or not the C-parity de-duplication is compiled in, and leaving it behind a csym-named key is how the next variable gets missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both "keep both": * helas_objects: the quartic-merge caches and the flavor-mask cache are independent per-matrix-element runtime caches. * the madmatrix sigmaKin loop: the C-parity work renamed its holes (csym_me_decl -> csym_page_decl, csym_record -> csym_weight) when it moved from skipping a partner to halving the good-helicity list, so the BLAS variant of the same loop keeps its own wrapper around the NEW names. color_sum_blas_loop.inc only carries the crossing holes, which are unchanged, so it needed nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…passing for one Two defects, one hiding the other. 1. The standalone makefile handed $(LINKLIBS) -- which carries $(BLASLIBS), '-framework Accelerate' on macOS -- to f2py. On python>=3.12 f2py must use the meson backend, which does not parse '-framework': it prints "Unknown option '-framework'", builds no module, and STILL EXITS 0. So matrix2py.so could not be built at all on macOS, and the failure was silent -- it even masked a second, unrelated meson failure underneath by turning f2py's exit 1 into exit 0. BLASLIBS now goes through LDFLAGS, which meson honours (verified: a bogus -l in LDFLAGS does fail the link, so the flag really does reach the linker), and only plain -L/-l stay in the f2py argument list as LINKLIBS_NOBLAS. The $(FC) links of check_sa and check_sa_born_splitOrders still use the full $(LINKLIBS) and are unchanged; no standalone template references a BLAS symbol today, so this is currently dead weight there, but it stops being a trap the moment one does. 2. _crossing_build_f2py took 'make exited 0 and a matrix2py*.so exists' as success. The makefile touches the bare .so unconditionally to give make a timestamp (f2py names the real module matrix2py.cpython-<ver>-<plat>.so), so after a failed f2py that file is present and EMPTY. check_crossing then built nothing, enumerated nothing, and returned [] -- surfacing as 'check crossing returned no comparison' rather than the build_failed its caller skips on, which is what the test docstring promises. Success is now that the module IMPORTS, probed out of process so a bad dlopen cannot hurt the caller. CI never saw either: linux picks -lblas, which f2py accepts. test_check_crossing_command / test_check_crossing_s_channel_graceful pass in CI (acceptancetest_crossing_cpp, 'Ran 15 tests ... OK'; test_manager exits 1 on a skip, so they really ran). Locally they now skip with the accurate reason instead of failing, and with meson present 'make matrix2py.so' produces an importable matrix2py.cpython-314-darwin.so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
madspace does not support crossing yet, so the shipped default must be the
un-crossed output everywhere. --use_crossing=True opts in; --use_crossing=False
is now redundant but still honoured. Flipped in the four places that decided it:
MadGraphCmd._use_crossing / ._output_use_crossing (class attrs and the per-process
reset), the do_add fallback when the flag is absent, and the exporter's
self.opt['use_crossing'] fallback. One .get('use_crossing', True) in
get_crossing_pdg_lines still defaulted on and is now False, so no path can
re-enable it implicitly. Help text and the output completion updated.
Verified end to end: 'generate p p > j j QCD=0; output standalone' gives the
3-directory unfolded layout by default (as on main) and 1 folded directory with
--use_crossing=True.
TESTS NOW STATE THEIR CHOICE. The crossing suite had 45 explicit
--use_crossing=False against only 2 explicit True -- everything else inherited
the default, so flipping it would have silently converted ~60 crossing-ON tests
into a second copy of the crossing-OFF ones: still green, testing nothing. Every
generate in test_standalone_cross_symmetry.py is now pinned, via a _pin_crossing
helper where the caller supplies an options string (it leaves an explicit choice
alone) and literally otherwise. Same for the two crossing-ON arms of
test_standalone_madevent_consistency.py, the standalone_mg7 backend arm, and
test_standalone_crossing_folds_qqx_subprocess, whose 'crossed' arm was the bare
default and is now --use_crossing=True. Nothing in the test suite reads the
product default in either direction any more.
Checked green with the flip: the 13 static crossing tests, the three
layout-sensitive test_cmd tests, and every IOTest job that was green in CI
(iotest_examples, modification_to_cuts, cpp_write, loop_sqso, ewsudakov,
gnuplot). Full unit sweep 1042 tests: the only failures are the 6 reserved
codegen goldens that were already red (fks, export, unit_proc), plus one local
'No module named scipy'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default flip left the accumulator behind:
self._use_crossing = self._use_crossing and use_crossing
That was right while the default was True (any line asking for False wins), but
with the class attribute now False it can never be lifted -- False and True is
False -- so --use_crossing=True was parsed, accepted, and silently ignored. No
routers, no CROSSUSE, crossing unreachable. CI caught it in the three tests whose
non-vacuity guards exist for exactly this ('no subprocess group is served by a
crossing router, so the comparison would be between two identical builds').
Resolved per line instead: an explicit True switches it on, an explicit False
switches it off for good (a multi-line definition must not end up half crossed,
which is what the old protected), and a line with no flag inherits what
the definition already chose. The sticky-off marker is reset with the rest of
the per-definition state.
Measured on 'define q1 = g u u~; generate q1 q1 > q1 q1':
no flag 0 routers
--use_crossing=True 3 routers
--use_crossing=False 0 routers
True + 'add process g g > u u~' 3 routers (inherits)
True + 'add process ... --use_crossing=False' 0 routers (sticky off)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7f50f03 ('let each output pick the diagram order') and the DDM basis work added color_basis and merge_quartic_vertices as set-options but did not extend the expected dict in test_config, which enumerates every option exhaustively. The merge of that branch into the crossing tip (921b203) therefore turned acceptancetest_5 red. Not a crossing failure and not caused by the default flip; it just landed in the same window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FLIP(I) -- the row whose helicity configuration is I's with every helicity
negated -- was rebuilt at run time by an O(NCOMB^2 * NEXTERNAL) scan over the
materialized NHEL table, in four templates. The generator already holds
everything that search rediscovers: a configuration is a mixed-radix code over
the per-leg states, so the partner is a digit-wise complement of the code.
_helstate_data now computes it alongside HELALLOW and emits DATA FLIP.
Removed from matrix_standalone_v4.inc, matrix_madevent_v4.inc,
matrix_madevent_group_v4.inc and hel_warmup_v4.inc: the search itself, the
HELSAME/JHEL/KHEL scratch, the DATA FLIP/NCOMB*0/ sentinel and the
IF (FLIP(1).EQ.0) lazy-init guard -- one less piece of order-dependent mutable
state in a routine already juggling FLAV_USE/CROSSUSE. The grouped template
keeps a latch (CSYM_DONE) for the self-paired scan that follows, which writes
CSYMBAD and is not idempotent-cheap.
Semantics are unchanged, including the two ways to land on FLIP(I)=I, which
every consumer reads as "no distinct partner, keep the de-duplication off":
a row that negates to itself, and -- the case the search could only discover by
failing -- a row whose negation is not in the allowed set, which is what
polarization produces (a leg keeps its full radix but only some states are
selected, leaving HELALLOW a non-contiguous subset).
Verified rather than assumed. The emitted table is identical to what the
removed search computes, checked directly on g g > t t~, u u~ > g g, g g > g g,
e+ e- > z z (NCOMB=36, so the 0-helicity state is exercised), p p > w+ j, and
p p > z{0} j -- the polarized case, confirmed non-contiguous HELALLOW. Same
check re-run against the generated fortran of all three backends (standalone,
madevent single, madevent grouped): emitted == search on every matrix*.f, and
an involution on the encoder-based file whose NHEL is only materialized at run
time. The standalone build links and gives Matrix element = 0.59262631132419219
for g g > t t~, matching the 0.5926263 pinned at test_cmd.py:616.
TestGoodHelCParityDedup + the crossing static suites 17/17. Full unit sweep
1042 tests: failure set byte-identical to before this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_v4.inc emitted DATA HELALLOW twice: a private copy in SMATRIX,
which matches a user-requested helicity code (USERHEL.EQ.HELALLOW(IHEL)), and
another in FILL_NHEL, which decodes the list into the NHEL table. Both are live;
neither was dead code. The list is NCOMB long and grows with the multiplicity,
and a reader had no way to tell the two tables were the same table.
FILL_NHEL now owns the single DATA and publishes it through a COMMON that
SMATRIX reads. A DATA statement cannot initialise a COMMON outside a BLOCK DATA
-- and a BLOCK DATA can be dropped by the linker when it is not referenced --
so the list stays a local DATA and is copied into the COMMON on the first call.
Every reader is ordered behind that call: SMATRIX calls FILL_NHEL at the top and
only reads the table ~30 lines later, with no RETURN or GOTO in between (checked),
and it is the only other consumer in the file. No generator change: the emitted
DATA is untouched, only who declares it.
An accessor function was the other option and is worse here: Fortran does not
guarantee short-circuit evaluation of .OR., so USERHEL.EQ.-1 .OR.
USERHEL.EQ.GET_HELALLOW(IHEL) risks a call per helicity per event in the loop.
Scope, checked rather than assumed: only this template duplicated it. The
madevent and madweight templates never mention HELALLOW; matrix_standalone_hel
has one; and the split-orders variants (matrix_standalone_splitOrders_v4.inc,
the matchbox one, split_orders_helping_functions.inc) carry neither the encoder
nor the C-parity machinery at all -- a generated split-orders matrix.f has zero
DATA HELALLOW, zero DATA FLIP and zero HELSAME.
Verified: g g > t t~ still gives 0.59262631132419219 (the value pinned at
test_cmd.py:616), and p p > z{0} j -- polarized, so HELALLOW really is
non-identity (3,4,9,10,15,16,21,22) and the changed USERHEL path is exercised --
is bit-identical against a build from the parent commit on both matrix elements.
Generated file goes from 2 DATA HELALLOW to 1. Crossing/C-parity suites 17/17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_splitOrders_v4.inc had drifted from
matrix_standalone_v4.inc: 16 routines against 23, and no reader could tell
which of the differences were meant. They are decided one by one here, and
the dividing line is the crossing -- this template has no crossing machinery
(fill_crossing_replace_dict fills holes it does not have, use_crossing_ic
excludes split orders), so what exists only to decode an extended FLAV_IDX
stays out and what is meaningful without one comes in.
CARRIED
DECODE_HEL / FILL_NHEL / the HELALLOW list. The helicity table was emitted
TWICE (SMATRIX_SPLITORDERS and GET_NHEL) and initialised a COMMON from a
DATA statement, which Fortran only allows inside a BLOCK DATA -- it worked
because gfortran accepts it. Now decoded once into PROCESS_NHEL, with the
FILL_NHEL call added to GET_DENSITY as well: it reads that table without
going through SMATRIX first, and would otherwise see zeros.
The C-parity de-duplication (FLIP/CSYM). Adapted, not copied: the pair test
and the doubling run per squared-order component rather than on their sum,
since two components could in principle cancel into an equal total. The
separate NTRY_CSYM of the default template is collapsed into NTRY -- it
exists there only so a crossed call does not advance the scan. Kept
verbatim: the verdict is all-or-nothing per flavor (a self-paired row or
one mismatched pair turns it off for good), and the pair tolerance has an
absolute floor as well as a relative one, without which two numerically
zero rows compare roundoff to itself and veto every real pair.
GET_NHEL_IDX, in its non-crossing form. A caller that reads GET_NHEL's
static IDEN to undo the averaging gets the wrong denominator for every
non-representative flavor of a merged matrix element, and merged flavors
are exactly what this template already handles (BROKEN_SYM).
LEFT OUT, each for a reason
GET_DENSITY_IDX, GET_ALL_INTER_IDX, GET_ALL_INTER_CROSSED,
GET_INTER_RESCALE. The whole quartet exists because a FLAVOR array cannot
express a crossing; with no crossing the _IDX entries duplicate their
FLAVOR-array twins and _CROSSED/RESCALE are pass-throughs. Same for
GET_PDG_FOR_FLAVOR and the f2py flav_idx wrappers, which decode nothing
else.
ENCODE_HEL. Nothing in the tree calls it -- the crossing routines that
would inline their own encode in CROSS_GHIDX. A split-orders matrix.f is
written once per MadLoop and FKS born as well as per standalone
subprocess, so a dead routine there is paid for many times over.
The canonical-code helicity label (HELCODE). The default template matches
USERHEL against the code; here it stays the ROW NUMBER, because that is
what the callers pass: MadLoop hands SMATRIXHEL_SPLITORDERS its USERHEL,
which the EW-Sudakov driver fills from `do chosen_hel=1,SDK_GET_NCOMB()`
(Template/NLO/SubProcesses/check_sudakov.f). The two labels coincide for
every unpolarized process; relabelling would silently move the Born of a
polarized NLO run.
HELSAME, named as missing: it exists nowhere in the tree, in any template.
matrix_standalone_matchbox_splitOrders_v4.inc, the further-reduced variant:
out of scope, so that this lands ahead of a queued golden regeneration.
matrix_template_provides answers by searching the template text, so a comment
naming an omitted routine would make it claim the routine exists. The
templates therefore describe what they leave out instead of naming it, and
test_splitorders_template_carries_the_non_crossing_standalone_api pins both
lists plus the two predicates that read them (the crossing demo and the
folded color sum stay off).
Also: check_sa_splitOrders.f could not compile at all against a model with
flavored couplings -- coupl.inc declares TYPE(FLV_COUPLING) and the driver
had no `use model_object`, which check_sa.f has carried since those landed.
Nothing noticed because `make` builds check_sa.f only.
Verified by compiling and running the generated code, not by `make` exiting.
* u u~ > u u~ QED^2==2, g g > t t~ QCD^2==4, e+ e- > w+{0} w- QED^2==4
(polarized, so HELALLOW is really non-identity: 4,5,6,13,14,15,...):
check_sa and check_sa_born_splitOrders byte-identical to a build from
602235d, the polarized one included.
* p p > j j QCD^2==4: all 8 subprocesses, every one of their 65 flavor
columns plus the out-of-range indices, evaluated 40 times so the fast
phase is reached, and each cross-checked against the same total rebuilt
one helicity at a time through SMATRIXHEL_SPLITORDERS (which never
de-duplicates). Byte-identical, column for column.
* The de-duplication engages and halves the loop: 1.94x on g g > g g,
1.88x on the merged QQx_QQx (28 flavors), 1.92x on gQ_gQ, 1.92x on
g g > t t~ -- the last shifting the total by 1.2e-16, the reassociation
of summing half the rows doubled, and agreeing with the per-helicity
rebuild to the same 1.2e-16. It correctly refuses u u~ > u u~ QED^2==2,
whose Z axial couplings break the pairing.
* MadLoop born_matrix.f is written from this template: u u~ > u u~
[virt=QCD] builds and its driver returns 2.8276928588371737, bit-identical
to the base build. A matchbox output builds and returns the same.
IOTests, reported rather than regenerated (the user regenerates next):
* testIO_sqso_uux_uuxuuxx: red before, red after (its golden already
predates a template reordering).
* test_export_fks: 11 tests, the same 2 red before and after
(testIO_test_ppw_fksall, testIO_test_wprod_fksew).
* test_loop_exporters: same 1 red before and after.
* test_export_v4 unit tests: same 3 red before and after, +1 new passing.
* testIO_MatchBoxOutput: green before, RED after -- its
check_sa_born_splitOrders.f golden picks up the `use model_object` line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`p p > j j QCD^2==4 --use_crossing=True` (standalone) folded 8 subprocesses onto 3 and wrote no decoder into any of them: 50 of the 65 flavor columns became unreachable, and an extended FLAV_IDX -- the only way to ask for one -- returned 0 without a word, because SMATRIX_SPLITORDERS' range guard treats an index above NFLAV as an unresolvable flavor. Same generation without the squared order folds to the same 3 directories and DOES write the machinery, so the loss was invisible unless you counted subprocesses across the two. The cause is that the decision was made per FORMAT while the machinery is written per TEMPLATE. `standalone` is in _crossing_folding_formats, so the interface kept the folded crossings; but write_matrix_element_v4 picks matrix_standalone_splitOrders_v4.inc for any process carrying split_orders, and that template has no crossing holes -- fill_crossing_replace_dict fills holes it does not have. _crossing_needs_expansion now answers per amplitude: a folding format still gets the crossings expanded back when the amplitude carries a squared order. Expanding is the same output the user gets from --use_crossing=False, so nothing is approximated and nothing is lost -- verified byte for byte: every generated source of `p p > j j QCD^2==4 --use_crossing=True` (8 dirs, 65 flavor columns) is now identical to the --use_crossing=False build, matrix.f, check_sa.f, check_sa_born_splitOrders.f, f2py_matrix_wrapper.f and flavor_dispatch.py alike. The call had to move out of the grouped path's `if not _output_folds_crossings()` for the predicate to be asked at all: `output standalone` takes the grouped branch (checked, not assumed), where the format gate short-circuited it. The rest of that block -- MG_SPLIT_CROSSING peeling, the decay-chain regeneration -- stays behind the gate and is untouched. This is the safety net, not the feature: folding a squared-order process is still worth having and needs the crossing holes plus a split-orders snippet set in that template. Until then the flag degrades to the complete explicit output instead of a wrong one. Unaffected, checked: `p p > j j --use_crossing=True` without a squared order still folds to 3 directories with the machinery written; madevent is unchanged (it is not a folding format, so its crossings were already expanded, with and without squared orders). Tests: the crossing/C-parity acceptance suite is 67/68, and the one failure (test_inclusive_crossing_xsec_matches, "no subprocess group is served by a crossing router") reproduces identically at 602235d, so it is not from this change. test_export_v4 unit tests unchanged (same 3 pre-existing reds). No IOTest golden is affected: every suite pins crossing off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
do_add parsed --use_crossing and set self._use_crossing, and THEN called
check_add. With no model imported yet, check_generate imports the Standard
Model for the user, and do_import calls clean_process(), which resets exactly
those attributes. The LOCAL merge_crossing was already computed from the flag,
so generation still folded the crossings onto their base; the exporter reads
the ATTRIBUTE, found it False, and wrote no decoder for them.
That silently drops partonic channels. `p p > j j --use_crossing=True` came out
as 3 subprocess directories reaching 15 of its 65 flavor columns; the other 50
had no entry point at all, and the extended FLAV_IDX that names them returns 0
in silence, because SMATRIX reads an index above NFLAV as an unresolvable
flavor. The same generation with `import model sm` on the line before was
correct, which is why this survived: every test in the crossing suite imports
its model explicitly.
Resolving the flag AFTER check_add is the fix. The pop has to stay where it
is -- the flag must be out of `args` before check_add validates them -- so only
the resolution and the merge_crossing it feeds moved down, past check_add and
past the `add model` early return that has no use for either.
It also turns the madevent crossing router back on, which was dark for the same
reason and not (as I first assumed) because madevent cannot fold: routing is
not folding, it needs opt['use_crossing'] and nothing more. `p p > t t~ j j
--use_crossing=True` now writes matrix<i>_router.f in P1_gq_ttxgq and
P1_qq_ttxqq, the two the acceptance suite documents, and it does the same WITH
a squared order (`p p > j j QCD^2==4` routes P1_gq_gq and P1_qq_qq). So madevent
does support crossing with squared orders; it was only ever switched off.
Verified:
* `p p > j j --use_crossing=True` with no model line now produces exactly
what the same script with `import model sm` produces, and what
602235d produces with that line -- matrix.f byte-identical per
subprocess, plus check_sa.f, f2py_matrix_wrapper.f, flavor_dispatch.py.
* Crossing/C-parity acceptance suite 68/68. It was 67/68 before this, the
failure being test_inclusive_crossing_xsec_matches, "no subprocess group
is served by a crossing router" -- which reproduced at 602235d and is
what this repairs.
* Squared-order standalone still expands rather than folds (8 dirs, 65
flavor columns), as the previous commit intends; crossing off is
untouched. test_export_v4 (same 3 pre-existing reds) and test_cmd 15/15.
Correction to the previous commit's message: its closing "p p > j j
--use_crossing=True without a squared order still folds to 3 directories with
the machinery written" was measured on this bug, so the machinery was in fact
NOT written in that run. The statement holds now, and held then for a script
with an explicit model import; the conclusion that a squared-order process must
not fold is unaffected -- rechecked here with the import in place, where
602235d folds to 3 directories and still writes no decoder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_splitOrders_v4.inc now carries the crossing machinery, so a
squared-order standalone output folds its crossed subprocesses onto their base
like any other. `p p > j j QCD^2==4 --use_crossing=True` goes from 8 subprocess
directories to 3, and all 65 flavor columns stay reachable -- through the
extended FLAV_IDX rather than through a directory of their own.
The previous commit made that case EXPAND instead, because folding into a
template with no decoder dropped the crossed subprocesses outright. That was
the safety net; this is the feature, and _crossing_needs_expansion goes back to
answering on the format alone.
NO TEMPLATE SPLIT WAS NEEDED, though the option was open. Every crossing
addition sits behind a hole of its own, and with crossing off every hole
reproduces what was there before -- so the FKS and MadLoop borns, written from
this same template and always uncrossed (a perturbative process breaks crossing
symmetry, so use_crossing is False for them by construction), are untouched.
Duplicating a thousand-line template to express that would have been worse than
one fill function.
fill_crossing_replace_dict_so is that function, and it is separate from
fill_crossing_replace_dict rather than shared with it: this SMATRIX has ANS and
T as VECTORS over the squared split orders and MATRIX as a subroutine, so the
default template's snippets do not fit -- `T=MATRIX(...)`, `ANS=ANS/IDEN*...`
and `IF (T.NE.0D0)` all have to become loops or read BUFF. Sharing them would
mean a template with a hole it cannot fill.
Three things had to come with it:
- use_crossing_ic. The external NSF flag has to be read through IC(i) for a
crossed leg to flip, and this template was excluded outright. It threads a
meaningful IC (SMATRIX passes JC, MATRIX passes it on to GET_AMP), so the
exclusion was only ever "crossing is not supported here"; it is keyed on
the template now rather than on split_orders.
- NTRY_CSYM is back, in the ON fill only. The C-parity scan must not be
advanced or consulted by a crossed call -- a crossing permutes and
sign-flips the helicities, so a base-row negation is no longer the partner.
With crossing off there are no crossed calls and NTRY remains that counter,
which is why the OFF fill keeps it.
- matrix_template_has_pdg_decoder. The crossing demonstration in check_sa.f
was gated on the DEFAULT template's hole name, so it stayed dark for a
folded split-orders output that could perfectly well run it. It asks the
real question now -- "does this matrix.f define GET_PDG_FOR_FLAVOR" -- and
both holes answer it.
Crossing OFF is unchanged. The only difference in the emitted file is
FLAV_IDX -> FLAV_USE, which is the same value there (the entry guard still
rejects an index above NFLAV when there is no crossing to name), plus the
comments. Checked numerically, not just read: u u~ > u u~ QED^2==2 and
g g > t t~ QCD^2==4 give the same check_sa value to the last digit, all 65
flavor columns of p p > j j QCD^2==4 are byte-identical through the flavor
sweep, and the MadLoop born of u u~ > u u~ [virt=QCD] still returns
2.8276928588371737.
Crossing ON, validated two ways:
- test_split_orders_merged_flavor_crossing_every_flavor, the split-orders
twin of test_merged_flavor_crossing_every_flavor: every flavor of the
merged `_quark _anti_quark > g _quark _anti_quark QED^2==0` crossed onto
`_quark g > _quark _quark _anti_quark QED^2==0`, matched through
GET_FLAVOR_INDEX rather than by index, to 1e-11. Sweeping the whole table
is what catches a crossed denominator built from the representative flavor
instead of the actual one. It also asserts the crossed value is not exactly
zero, which is what a matrix element with no decoder returns.
- p p > j j QCD^2==4 end to end: the folded build's crossing demonstration
against the unfolded build, matched by signed-PDG signature. Every one of
the 8 shared signatures agrees to at most 1.6e-16 (one ulp), and the folded
build reaches 22 further signatures that returned 0 before.
Tests: crossing/C-parity acceptance suite 69/69 (68 + the new one).
test_export_fks same 2 pre-existing reds, test_loop_exporters same 1,
test_export_v4 same 3, test_cmd 15/15. IOTest goldens move again for every
split-orders matrix element (the FLAV_USE rename), on top of the
check_sa_born_splitOrders golden the first commit moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Leg polarization ({0}/{T}/...) selects helicity STATES on a named leg, and a
crossing moves legs between the initial and the final state. The per-row
polarization gate (IS_BORN_HEL_SELECTED) would therefore have to re-read the
selection through the crossing to mean anything, and nothing did that: a
polarized process was free to fold crossings and then consult the selection with
the base's leg numbering.
Beam polarization is already refused outright against crossing
(common_run_interface.py:6886, EVA at 6895); this is the same decision for the
per-leg case, taken at GENERATION so that no crossing is ever recorded rather
than emitted and then mis-consulted at run time.
Verified on the predicate itself:
p p > z j leg pol [[],[],[],[]] breaks_crossing=False
p p > z{0} j leg pol [[],[],[0],[]] breaks_crossing=True
p p > w+{T} j leg pol [[],[],[1,-1],[]] breaks_crossing=True
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…end shares The standalone matrix element applied SIGMA -- permute the helicity slots AND sign-flip -- while the recycled optim and the madmatrix lanes can only realise TAU, sign-flip in place, because they bake the helicity values and take no permutation at run time. Two maps for one concept is what produced the -28.5% cross-section bug (crossgroup_helunion.dat carrying sigma where tau was read), and it meant no single good-helicity relation described every backend. APPLY_CROSSING_TABLE now leaves the NHEL slots alone and lets the sign ride on IC, which HELAS folds into its spinors as nhel*nsf. IC stays aligned with the MOMENTA -- it is also the NSF direction flag -- so slot XK gets P_IN(PERM(XK)) with NHEL_IN(XK), and the effective helicity is NHEL_IN(XK)*SGN(XK). CROSS_GHIDX gates in the same space (TGT(K), not TGT(PERM(K))): the gate has to live where the evaluation lives. No union was needed. SMATRIX resolves the crossing per call, so it gates exactly on tau(h); the union G U tau(G) exists only for the optim, which bakes one row list shared by every crossing and so cannot gate per crossing. The relation test is restated, not weakened. It asserted the crossed good-hel set equals the identity's mapped through sigma -- true by construction of the old code. It now asserts equality through tau, which still holds because tau is a clean bijection (each leg's states are closed under negation). Equality, not containment. Prerequisite, committed separately: a polarized leg now breaks crossing symmetry. Leg polarization selects helicity STATES on a named leg and a crossing moves legs across the initial/final boundary, so IS_BORN_HEL_SELECTED would have to re-read the selection through the crossing; beam polarization was already refused outright against crossing, and this is the same decision per leg. VALIDATION NOTE WORTH KEEPING: none of this can be checked on a Mac without meson. With the stock toolchain the whole suite reports "Ran 23 tests ... OK" while silently skipping every test that evaluates a crossed matrix element (the exit code is 1, the visible verdict is OK). These runs used a throwaway venv with numpy+meson+ninja+six, which is what made the two sigma-pinned failures visible at all. Measured with that venv: TestStandaloneCrossSymmetry 23/23, TestGoodHelCParityDedup + TestCheckCrossingCommand 6/6 including the standalone_mg7 backend -- 33 tests, every |M|^2 comparison among them. Before restating the probe, tau failed exactly the two sigma-pinned relation tests and passed the other 21, which is how the map was confirmed correct rather than the tests merely made green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main landed two renames the branch predates -- standalone (Fortran) is now
standalone_fortran, standalone_mg7 (madmatrix) is now standalone, bin/mg5_aMC is
bin/madgraph, the madmatrix backend tokens are scalar/simd_128/simd_256/
avx512y/simd_512, and the product is MadGraph7 -- and removed three features
the branch still carried. The merge translates the branch to the new
vocabulary and honours the removals rather than resurrecting them:
- BLAS colour sum (madevent + fortran standalone): gone again, including
the hel-recycling path, which now always takes the unfolded GET_MATRIX
route. madmatrix keeps its BLAS, as on main.
- colour reflection folding (9f8b1b3): get_jamp_folding and the
JFOLD/COLREP gather are gone; the tests asserting on them are updated.
get_color_data_lines keeps plain=True for the FKS templates, which
cannot read the INIT_CF-compressed matrix either.
- standalone_cpp: TestStandaloneCppCrossSymmetry, the cpp check-crossing
backend and its driver are dropped.
Conflict resolutions worth knowing:
- beam polarisation (main) x C-parity dedup and crossing (branch): the
dedup is gated off when a beam is polarised (the partner row has the
opposite beam helicity), and a crossed FLAV_IDX with a polarised beam
stops with an error rather than weighting the wrong leg.
- matrix_madevent_v4.inc: fixes an orphan ENDDO/ENDIF left by f54bc1c
(the FLIP DATA commit), which broke every non-grouped madevent output.
- MadSpin decay.py / interface_madspin.py taken from main: the branch's
changes there were crossing-only, dead with crossing off by default.
Onium (main) against branch-only machinery:
- get_matrix_template returns the onium template itself, so
matrix_template_provides answers for the file actually written.
- _helstate_data collapses each constituent pair to its 2J+1 states, the
rows get_helicity_matrix enumerates (it crashed on list.index).
- _module_color_flows gives no flows for a bound state, so madevent writes
the onium-aware ICOLUP table instead of the colour-code tables.
- ALOHA's spacelike-width test takes the value of a dual (p-wave)
momentum: DBLE of a dual stays dual and did not compile.
- test_quarkonium_standalone: the u a > Upsilon u chib2 reference moves
1.8195973e-20 -> 1.8195052e-20, the t-channel Z width now dropped where
the momentum is spacelike; zerowidth_tchannel False reproduces main's
value to every digit.
Also fixed while here: a polarized leg (af590a2) or a bound state breaks
crossing symmetry for the exporter, but generation still RECORDED the
crossings onto the base, so p p > z{0} j --use_crossing=True came out as the
g q directory alone, the q q~ and g q~ subprocesses reachable from nowhere.
Generation now keeps such a process unfolded (also for a polarized decay
leg); test_crossing_breaking_process_keeps_every_subprocess pins it.
Known drift, left for regeneration: the IOTest goldens of
testIO_sqso_uux_uuxuuxx, testIO_export_matrix_element_v4_{standalone,
madevent_group,madevent_nogroup}, testIO_test_{ppw_fksall,wprod_fksew} and
testIO_UnitProcOutputIOTests (the last three already red before the merge).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main replaced its per-test jobs by bundles (257 -> 77 jobs). The crossing
branch had added its own jobs next to the ones main folded away, and the
three workflow conflicts were exactly those blocks. Main's bundles are taken
as they are; the branch's tests go back in the same style instead of as
separate jobs:
- acceptancetest.yml: acceptancetest_crossing_bundle -- crossing static,
fortran standalone + C-parity dedup, madmatrix + check crossing, and
the madmatrix OpenMP build (four jobs before).
- acceptancetest_madevent.yml: acceptancetest_madevent_crossing_bundle --
crossed LHE labels, router colour selection, crossing cross sections,
folded layout + reweight labels, density multicore (five jobs before).
- unittest.yml: TestAverageDensityMatrix / TestPdgForMeCall become a step
of unittest_new_coverage, which now restores the pip cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every madevent launch in it failed within seconds on 'lhapdf-config: No such file': the default LO PDF is an LHAPDF set (NNPDF4.0) since main moved to it, and the crossing jobs this bundle replaces predate that and only restored the pip cache. The one step that launches nothing passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MadSpin rebuilds its production processes from the banner's proc card, flags included, so an event sample generated with --use_crossing=True handed that flag to MadSpin's own generation. For the density modes that is output standalone_fortran --density=1, which folds: p p > w+ j came out as the single g q > w+ q directory carrying the crossing machinery, where the default gives all six subprocesses. MadSpin finds a production matrix element per event flavor, through the processes the matrix element lists and the FLAVOR-array entry points, and reaches no folded crossing that way (main's MadSpin has no crossing support at all). without_crossing() pins --use_crossing=False on every generate / add process MadSpin runs -- production, full and decay, density and legacy modes alike -- dropping any flag already on the line; [...] lines are left alone, as reweight does in its density mode. Pinned where the command is built, so the logged line is the one that runs. Checked end to end on p p > w+ j, decay w+ > e+ ve (default spinmode, i.e. the density path): with --use_crossing=True in the proc card MadSpin now writes the same six directories as the default, none with crossing code, and decays all 200 events. MadSpin unit tests: 616 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…idth too The expected FFV2_3.f came out of the main merge half converted: the branch's spacelike-width IF for FFV2_3, but main's single-line denominator for FFV2_4_3, the assembled routine main added. ALOHA writes both through the same t-channel switch, so both carry the IF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two tests the main merge left inconsistent: - test_density_mode_multicore (kept from the branch) calls read_average_density_matrix, a static helper that went with the density section main rewrote. Restored verbatim. - test_standalone_cpp: the standalone_fortran cross-check main added reads 6.4739191, the value with the width kept in the gluino exchanged in the t/u channels. The branch drops it -- the gluino is an external field (zerowidth_external) and its momentum is spacelike (zerowidth_tchannel), either rule alone is enough -- and gives 6.4739329, the value the C++ assertion above already had. Measured: with both rules set to False the merged tree gives 6.4739191277160320, main's number to every digit; the per-helicity amplitudes differ only in the t/u-channel diagrams. The C++ assertion's comment blamed a helicity-summation reorder, which cannot move a double at the 5th digit; it now says what does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implementation:
helmap.
G_base U tau(G_base)(crossgroup_helunion.dat); wavefunction recycling is retained.tauis that flipping applied in place (the crossing's NSF sign flips), and it is the ONLY part of the transform the recycled optim can realise: it bakes its helicity configs into the HELAS calls and receives just(PUSE, IC)-- IC carries the signs, nothing carries the slot permutation.sigmathatmatrix<b>_orig.fapplies viaAPPLY_CROSSING_TABLE) is the wrong one here. BakingG_base U sigma(G_base)cost -28.5% onq q~ > q q~; fixed in db46ee2, pinned run-free byTestCrossingRecycledHelicityUnion.TS(K).NE.0D0for a crossing base, so each member weights channels and colour over exactly its own good set. A shared optim necessarily holds rows that are dead for whichever member is calling.- [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:
acceptancetest_flavor::test_flavor_grouping_consistency_mlmgreen on all four settings (the crossing+recycling one was 3706990 +- 10989, 105.8 sigma). Colour flow held:TestMadeventCrossingBaseColorFlowandTestMadeventRouterColorSelectionpass,g g > u u~crossing-on and--use_crossing=Falseboth 3.023e6 +- 1406 pb.Known limitations / follow-ups
merge option