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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
… 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
|
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 <[email protected]>
|
I'm looking to a new way to encode helicity to avoid to have to do a mapping. If it works we can try similar (but more complex?) idea for the color as well. |
Adopt the canonical helicity code (phase 1) in madevent and use it to replace the precomputed base->dependent helicity map with a runtime encode. get_nhel (auto_dsig): the per-event helicity label is the mixed-radix code, so GET_NHEL<i> decodes it (per-leg NHSTATE/STATES) instead of indexing an NHEL config table. Byte-neutral: the stored id is the full-table row, which for a non-polarized process equals the canonical code, and the per-leg STATES order matches get_helicity_lines by construction. Drops the auto_dsig NHEL table. Crossing: a dependent's event is written through its own get_nhel, which decodes the DEPENDENT code, while the shared base SMATRIX selects a BASE code -- so relabel by permuting the code's mixed-radix digits with the crossing permutation (GET_CROSS_PERM). This is exact because dep_states[k] == base_states[PERM[k]] with no reversal (the incoming->antiparticle wf flip and the crossing's conjugate + initial/final swap cancel), making the relabel a pure digit permutation over the base NHSTATE -- no STATES, no sign. Applied in both the within-group router (Track A, was HELMAP) and the cross-group auto_dsig (Track B, was DSIG_XGHEL); both precomputed maps are removed, as are the now dead _crossgroup_helmap / _crossgroup_remap_decl. Validated byte-identical vs the old maps: u u~ > d d~ (get_nhel decode == table, all 16 codes; identical events/xsec), p p > j j (both tracks; xsec 6.967e8), p p > w+ j (chiral W; xsec 2.163e4); 39/39 standalone cross-symmetry tests pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
End-to-end guard for the crossed-helicity label in the LHE (phase-4 GET_NHEL decoder + phase-5 runtime crossing encode). p p > w+ j puts the W+ -- a massive vector with three helicity states -- in a leg the crossing moves (u g > w+ d, ...), so a bug in the base->crossed helicity relabel scrambles its helicity. The test runs a small madevent generation and asserts the W+ polarisation is physical: all three helicity states populated, the two transverse states chirally asymmetric, and the longitudinal (0) fraction inside a physical window -- a scrambled relabel reads a quark leg's +-1 into the W+ slot and breaks this. Thresholds are loose (survive PDF/param updates) but catch the failure modes. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
Done with having coding of NHEL representing the value of the helicity (rather than an arbitrary look-up table). Documentation on the encoding to interface that with madspace:Writing event helicities with the canonical helicity encoding (madspace)
Per external leg k (1..nexternal): NHSTATE[k] = number of physical helicity states (2 for a fermion / massless vector, 3 for a massive vector, 4/5 for spin-3/2, spin-2). code = 1 + Σ_k digit[k] · Π_{j>k} NHSTATE[j]
Fortran madevent (auto_dsig.f): GET_NHEL(code, ipart) → code>0: the helicity of leg ipart; code=0: NHSTATE[ipart].
Summary:tl;dr for madspace: the selected helicity is now a self-describing 1-based mixed-radix code; write SPINUP[ipart] = get_nhel(code, ipart). If you go through the per-subprocess SMATRIX/get_nhel, crossing is transparent. If you call the base ME directly (C++ today), read the base code at GET_CROSS_PERM's permuted legs — and never apply SGN to the label. |
|
Yes, I think using a smart encoding makes a lot of sense. If the encoding/decoding speed is an issue (I'd be surprised though), we can also consider a less compact but easier to decode representation with a fixed width of 3bits per particle for up to 8 helicity states. |
|
Ok we agree, let me try to do the same for the color of the events then. |
First step of the colour analogue of the canonical helicity encoding. A colour flow is labelled by its connectivity once the INITIAL-state legs swap their colour/anticolour roles -- the LHE convention runs initial-state colour lines "through", so without that flip a label sits in the same slot on two legs and the flow is not a colour<->anticolour bijection. Ordering the colour and the anticolour slots by leg (a gluon holds one slot of each kind, a sextet two), digit i is the anticolour slot that colour slot i connects to and code = sum_i digit_i * N^i. Adds _color_flow_canon / _color_flow_code / _color_flow_codes, and refactors _router_colmap to share the canonical form rather than keep its own copy (the generated output is byte-identical; only run artefacts differ). The code is injective over a process's colour basis -- verified up to g g > g g g (24 flows over 5 colour slots) by the new test -- so it identifies a flow without a per-process flow table. It is also crossing-covariant: relabelling the legs with the crossing permutation carries a base flow's code onto the crossed process's own flow code (checked against _router_colmap for p p > j j and p p > j j j, all flows, zero mismatches), which is what will let a crossed subprocess drop the base->dependent colour map. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the inverse of _color_flow_code. Decoding needs only the code and the process's SLOT STRUCTURE (_color_flow_slots: which legs carry a colour resp. an anticolour index) -- and that structure is FLOW-INDEPENDENT, being fixed by the colour representations after the initial-state flip rather than by which flow is picked. It is therefore the colour analogue of the per-leg helicity-state counts, and it is all a consumer needs to rebuild a flow from its code. The new test asserts, for every flow of every process checked (up to g g > g g g, 24 flows over 5 colour slots), that decode(code(flow)) reproduces the flow's canonical connectivity exactly and that the slot structure does not vary between flows. That round trip is the guarantee required before colour tags are rebuilt from the code instead of read from the generated ICOLUP table. Still keeps ICOLUP as the source of the tag LABELS, so the generated output and the events are unchanged; only the machinery is added. Known gap: a leg carrying two slots of the same kind (a sextet) needs encode and decode to agree on the tie-break between its slots -- untested, flagged in the docstring. Co-Authored-By: Claude Opus 4.8 <[email protected]>
End-to-end guard on the colour written to the event, the colour counterpart of the p p > w+ j helicity-asymmetry test. Structure: every event's tags must form a clean colour<->anticolour bijection once the initial-state legs swap roles (the canonical form the colour-flow code is built on) -- each colour label matched by exactly one anticolour label. That is what breaks first if the flow written to the event is ever rebuilt wrongly, which is exactly the risk when the tags start being decoded from the canonical colour-flow code instead of read from the ICOLUP table. Physics: g g > t t~ dominates (measured 87%), and its TWO colour flows (three connections each) are both populated and balanced -- measured 50.5/49.5, the symmetry of the two ways to connect the gluons to the top line. A colour selection stuck on one flow, or producing a wrong topology, fails here. Thresholds are loose (gg > 0.6, each flow within 0.25-0.75) so PDF/param drift does not false-fail. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ric) Replaces the p p > t t~ colour test, which was too weak to be useful: the two g g > t t~ colour flows are related by a symmetry and split 50/50 (measured 50.5/49.5), so the test passed just as happily with the two flow labels SWAPPED -- precisely the bug it was meant to catch. u u~ > u u~ instead splits ~98/2 (measured 2932/68 of 3000 events), so the test can pin down WHICH flow is which and a swap inverts the ratio. The dominant topology is identified topologically -- whether each colour connection stays inside the initial/final groups (signature II+FF, ~0.977) or crosses between them (IF+FI, ~0.023) -- rather than by raw leg indices, so the check does not depend on leg ordering. The structural half is kept and still applies per event: the tags must form a perfect colour<->anticolour matching in the canonical (initial-flipped) form, which is what breaks first once colour tags are rebuilt from the canonical colour-flow code instead of read from ICOLUP. Also measured on the way (not asserted, for the record): u d > u d exposes a single LHE flow -- the event-level basis is leading-colour, so the 1/N piece is not a separately selectable flow -- and g g > g g splits 2 dominant (~0.317 each) vs 4 suppressed (~0.09 each). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…canonical code
Track A step (ii) of the colour encoding: the base returns its selected colour
flow in ITS flow order, and the router must express it in this subprocess's.
That went through COLMAP_<proc>_<flav>, one explicit map per base-flavor pair,
precomputed by _router_colmap. It now goes through the canonical colour-flow
code instead:
decode the base's code with the base's slot structure
-> relabel both legs of every connection with the crossing permutation
-> re-encode in this subprocess's slot order
-> look the result up in this subprocess's own code table
_color_code_tables(me) supplies the three arrays that needs -- codes, colour-slot
legs, anticolour-slot legs -- and they are per-ME, so every crossing sharing a
base reuses them, where COLMAP needed one array per pair. It is the same trick
already used for the helicity, with one difference noted in _color_flow_code: the
helicity relabel is a pure digit REORDER, while colour needs a permutation
CONJUGATION (digit positions and digit values both relabel).
The direction of that relabel is not ambiguous even though it looks like it
should be: get_crossing_permutation builds two DISJOINT transpositions and marks
the overlapping 3-cycle codes invalid, so a crossing is always an involution and
perm == inv. A probe over p p > j j, j j j, w+ j j, t t~ j and t t~ j j confirmed
it on all 16 crossing pairs, with the relabelled connections landing on exactly
the dependent's own slot structure every time.
_router_colmap is kept and is still the fallback for an ME with no usable code
(no colour basis, or a flow that is not a clean colour<->anticolour bijection);
none of the processes exercised here needed it.
Not converted, deliberately: Track B's DSIG_XGCOL. It is not a label relabel like
COLMAP -- it permutes the base's published per-flow JAMP2 into the dependent's
flow order so the dependent can run its OWN select_color (the native reselect).
That reorder is intrinsic, it survives the next step too, and it is already
derived from the same canonical form, so moving it to runtime would rebuild the
identical array.
Validated: the emitted fortran, transcribed and simulated, reproduces the old
COLMAP /4,3,2,1/ for 1_gQx_ttxQx <- 1_gQ_ttxQ (cross=5); all four code-path
routers compile; and event records are BYTE-IDENTICAL against the previous
generation for p p > t t~ j (500 ev, seed 321) and p p > j j j (500 ev, seed
4242), same cross-section. 43/43 crossing acceptance tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
… ICOLUP table Step (i) of the colour encoding. The event's colour tags came from the ICOLUP table baked into leshouche.inc; addmothers now rebuilds them from the canonical colour-flow code, and leshouche.inc stops writing the table. What did NOT change is the runtime label: ICOL stays the flow INDEX, because it also indexes JAMP2 and ICOLAMP in the selection. Only the source of the tags moves. That is what keeps code 0 harmless -- a process with a single colour connection encodes to 0 (u u~ > z, u d~ > w+, and p p > e+ e- in a real run), which would have collided with ICOL=0 meaning "no colour" had the code become the label. New per-P-directory colorflow.inc carries NCOLSLOT / ICOLCSL / ICOLASL / ICOLCODE -- the per-flow codes plus the slot structure, which is flow independent, so it is a handful of integers where ICOLUP was 2*nexternal*nflow. addmothers decodes it: connection k joins colour slot ICOLCSL(k) to anticolour slot ICOLASL(digit+1), each connection gets its own tag, and the two roles are swapped back on the initial-state legs, undoing the reversal color_flow_decomposition applies to follow the les houches convention. NCOLSLOT=0 marks an ME with no usable code -- no colour basis, a SEXTET (stored as a negative tag in the opposite slot, a sign a decoder cannot restore, now refused explicitly), or an epsilon structure. leshouche.inc still writes ICOLUP for those and addmothers still reads it, so the fallback is intact. The colour helpers moved from ProcessExporterFortranMEGroup up to ProcessExporterFortranME: non-group madevent ships addmothers.f too and needs them. MadWeight is untouched -- it inherits ProcessExporterFortran, ships no addmothers.f, and keeps drop_icolup=False, so its leshouche.inc is unchanged. The python readers of leshouche.inc (madweight Cards.read_leshouches_file, madevent_interface.get_subP_ids) parse only IDUP. This RENUMBERS the tags: color_flow_decomposition numbers them by the colour string's traversal order, not by connection order, so the rebuilt tags are a relabelling. Validated by colour CONNECTIVITY equivalence (tags relabelled by order of first appearance) with every other event field required to match exactly: p p > t t~ j 500 ev and p p > j j j 500 ev (grouped) and u u~ > u u~ 800 ev (non-grouped) all give 0 colour mismatches and 0 other-field mismatches at unchanged cross-sections. Only 13 of the 500 t t~ j events kept their raw tags, so the check is not passing by coincidence. p p > e+ e- exercises the code-0 path end to end: 400/400 events put the colour on the initial quark and the anticolour on the antiquark. 43/43 crossing acceptance tests pass. IOTests were run read-only (-R): the 26 differing references are identical with and without this change, i.e. all pre-existing on this branch, and none of them is a leshouche/colour file. Co-Authored-By: Claude Opus 4.8 <[email protected]>
C4, data layer. get_subprocess_info now also writes color_codes (the canonical colour-flow code of each flow) and color_slots (the flow-independent colour / anticolour slot legs needed to decode a code), using the SAME encoding as the fortran madevent output -- get_color_code_tables delegates to export_v4's _color_flow_canon / _color_flow_code / _color_flow_slots, so a code means the same thing on both backends. color_codes/color_slots are null when the flows have no usable code (a sextet's negative tag, or an epsilon structure); a consumer falls back to color_flows. The existing color_flows (ICOLUP-style per-flow tags) is kept: besides being the fallback, the LHE writer still derives INTERNAL propagator/decay-line colours from it, so it is not redundant even where a code exists. Validated against the fortran colorflow.inc: u u~ > g g gives codes [7,11] and slots color=[2,3,4]/acolor=[1,3,4] (identical to the madevent P1_gg_qq tables); u u~ > e+ e- gives [0] (the single-connection code, matching p p > e+ e- in madevent). This is only the encoder side -- the runtime that decodes a code to LHE tags is a separate step (the mg7 colour selection is currently a stub, see api.cpp color_out=0). Co-Authored-By: Claude Opus 4.8 <[email protected]>
C4, ME side. coloramps.h now carries colorflowcode[ncolor] -- the canonical
colour-flow code of each flow -- next to the icolamp colour masks, plus a
colorflowcode_valid flag. This is the "encoding" the ME returns: given the flow
index it already selects (select_col_and_diag, 0-based icolC), the code is
colorflowcode[icolC], a self-describing integer the LHE writer can decode
without an ICOLUP-style tag table.
edit_coloramps computes the codes with get_color_code_tables -- the same encoder
that fills subprocesses.json -- so the C++ table, the json "color_codes", and
the fortran colorflow.inc all agree. colorflowcode_valid is false for flows with
no usable code (a sextet's two-slot leg, an epsilon structure); the code array
is then filler and a consumer falls back to the per-flow tag table. Note the
valid flag is separate from the code value, so code 0 (a single colour
connection, e.g. u u~ > e+ e-) is unambiguous -- unlike the fortran icol=0
"no colour" sentinel, which is why the fortran side keeps the index as the label.
The flow-independent slot structure a decoder also needs is not baked here; it is
static per subprocess and already travels in subprocesses.json as "color_slots".
Validated at generation: u u~ > g g bakes colorflowcode = {7,11} (identical to
the json color_codes and the fortran ICOLCODE), u u~ > e+ e- bakes {0} with
valid=true; the generated CPPProcess.cc (which includes coloramps.h) passes
g++ -std=c++17 -fsyntax-only. The runtime that returns and decodes the code
(api.cpp selection wiring + the madspace decoder) is specified separately -- the
mg7 event-level colour selection is still a stub (api.cpp color_out=0), the same
gap the helicity encode has.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
Done for the color encoding: The matrix element emit a self-describing colour code (the colour analogue of the helicity encoding). The encoder is done on the MG7 side; the runtime that returns and decodes it is the madspace/cudacpp half described below. What the ME now provides
The code of the selected flow is colorflowcode[icolC], where icolC is the 0-based flow index the ME already picks in select_col_and_diag (allselcol = icolC + 1). Same integer on all three backends — the fortran colorflow.inc, the JSON color_codes, and the C++ colorflowcode[] agree byte-for-byte (u u~ > g g → {7, 11}). The encodingOrder colour slots and anticolour slots by leg on the initial-state-flipped flow (every colour connects to an anticolour). Then code = Σᵢ digitᵢ·Nⁱ (N = #anticolour slots), where digitᵢ is the anticolour-slot index that colour slot i connects to. Two runtime pieces to implement (specified, not yet built):
Three things to know
color_flows — can be removed (but need validation/developement for sextet/epsilon_ijk, so fallback to it in those case)Redundant for every valid-code subprocess (decode gives the externals; internal lines derive from them), but required as the sextet/epsilon fallback, so it stays in the JSON. Validation boundaryEncoder validated (JSON + coloramps.h cross-checked against the fortran output; CPPProcess.cc compiles). The api.cpp/madspace pieces above need a cudacpp build + an mg7 event run to validate end-to-end — the same path the helicity encode is deferred behind. |
…symmetry-3f13f3 # Conflicts: # madgraph/iolibs/export_mg7.py
… GHREMAP table) The good-helicity filter (GOODHEL) is shared across the crossings of a flavor, but a crossing permutes and sign-flips the helicities, so a crossed row and its identity counterpart are different rows. The standalone backend bridged that with a baked GHREMAP(NCROSS*NCOMB) table (crossed row -> identity row); madevent did not bridge it at all -- it simply disabled the filter for crossed MEs (the '.OR. .TRUE.' gate) and computed every helicity. Both now use one shared runtime routine, CROSS_GHIDX: the crossed->identity map is a fixed permutation, so it is cheaper to recompute it from the config than to store it -- permute and sign-flip the row's config with the crossing's PERM/SGN (GET_CROSS_PERM), then re-encode it in the canonical mixed-radix order (the same STATES/NHSTATE the encoder/decoder use). All that survives as DATA is a small per-crossing GHFILT flag (NCROSS ints, was NCROSS*NCOMB) marking which crossings are filterable; a non-filterable one (initial-initial swap, inapplicable, or a non-bijection) returns GHIDX=0 -> compute every helicity, never train. For CROSS=0 it returns IHEL, so the uncrossed path is unchanged. This removes the GHREMAP table from the standalone and, more usefully, gives madevent good-helicity filtering for crossed MEs for the first time (it had been computing all NCOMB helicities). madevent precomputes GHIDXA(I) once per SMATRIX call and gates/trains the shared GOODHEL(FLAV_USE) column through it; the index is clamped with MAX(GHIDXA(I),1) because the gate reads GOODHEL before the GHIDXA(I).EQ.0 guard and fortran does not short-circuit .OR. (madevent builds with -fbounds-check). Encoding validated in python against compute_ghremap (0 mismatches, t t~ / t t~ j / t t~ j j). 43/43 crossing acceptance tests pass on a clean run. Unified madevent p p > t t~ j gives 576.6 +- 3.1 pb vs the unfiltered 578.8 (0.4%, within MC error), confirming the filter skips only zero-contribution helicities. Speed context (standalone microbenchmark, this branch): the runtime encode is ~10x a table load in isolation but invisible once the ME runs (<=4% for the lightest ME, ~0% for t t~ j j); the table's only real cost was generated-code size, now removed. Co-Authored-By: Claude Opus 4.8 <[email protected]>
… them (merge_crossing='record') Stage A of generating crossing-related subprocesses only once. At generation, MultiProcess.generate_matrix_elements already detects crossings (the crossing- invariant sorted_legs signature) and has two modes: merge_crossing=False reuses the base diagrams but still appends a separate amplitude -> separate ME -> separate directory (the redundant P1_gQx_ttxQx / P1_QQx_ttxg dirs); merge_crossing=True skips the crossed process entirely but DROPS it, silently losing that partonic contribution. This adds a third mode, merge_crossing='record': on a crossing hit, do not generate a separate amplitude, but record the crossed process (with the base and crossed leg permutations) on the base amplitude's new 'crossed_processes' slot. The partonic contribution is not lost -- the base's crossing-aware SMATRIX can evaluate it via the crossed FLAV_IDX -- and the exporter/driver will reach it from this metadata (Stages B/C). No diagrams are built for the crossed process. The slot is added to Amplitude and carried onto HelasMatrixElement (populated in its constructor from the base amplitude). Nothing is wired into the interface yet, so the default output is unchanged (merge_crossing=False) -- verified: standalone p p > t t~ j still writes 4 directories. The two Amplitude unit tests that pin the exact property set are updated; diagram_generation (50) and helas_objects (65) unit suites pass. Verified at generation: with merge_crossing='record', p p > t t~ j yields 2 base MEs (g g > t t~ g, g Q > t t~ Q); the g Q base carries the 4 crossed subprocesses (g q~ > t t~ q~, q q~ > t t~ g and their beam-swaps) with permutations -- the complete partonic set (the base handles g q + q g via its mirror flag). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ory (merge_crossing='record') Stage B. With merge_crossing='record' (env-gated for staged rollout) the crossed subprocesses are no longer generated, so the standalone exporter -- which writes one directory per matrix element -- stops emitting the redundant P1_gQx_ttxQx / P1_QQx_ttxg directories: p p > t t~ j drops from 4 SubProcess dirs to 2 (gg, gQ). The crossed partonic contributions are not lost: the base gQ matrix.f is BYTE-IDENTICAL to the full-generation one (verified), so its already-validated crossing-aware SMATRIX evaluates the g q~ / q q~ configs at the crossed FLAV_IDX exactly as before. check_sa.f now exercises them: the crossing-demo loop in _get_check_sa_crossing_example (previously a dormant IF(.FALSE.) example) is enabled precisely when the ME carries crossed_processes, so the folded subprocesses -- which no longer have a directory of their own -- are the one place this driver evaluates them, and it does. Default output is unchanged: without the env var no crossing is recorded, crossed_processes is empty, the demo gate stays .false., and standalone still writes 4 directories. TEMP: madevent still generates the crossed MEs separately (Stage C re-points its Track A/B summation at the metadata); the env gate becomes the default for crossing-enabled output once both backends read the metadata. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…tadata (merge_crossing='record') Stage C. merge_crossing='record' does not generate the crossed subprocesses, so the standalone output folds them into the base directory. The grouped backends (madevent, mg7/cudacpp) need each crossing back as an integration unit -- it is a distinct partonic channel with its own PDF and phase space -- so this expands the recorded crossed_processes metadata into crossed amplitudes just before grouping, reusing the base's diagrams via MultiProcess.cross_amplitude (no diagram regeneration). The existing grouping + crossing routing (Track A/B) then handles them exactly as an unmerged (merge_crossing=False) generation. Record mode records a crossing and its beam-swap as two separate entries (neither is in the amplitude list when the other is met, so the generator's mirror check never fires), so the reconstruction folds the beam-swap back into has_mirror_process, matching what generate_matrix_elements does -- otherwise an extra mirror directory (e.g. P1_qg_ttxq) would appear. Result: record-mode grouped output is BYTE-IDENTICAL to a default build. Verified matrix1_orig.f / auto_dsig1.f / leshouche.inc identical across every P dir for p p > t t~ j, p p > w+ j and p p > j j; p p > t t~ j xsec 576.6 pb in both modes; mg7 record mode generates cleanly. The block is gated on any ME carrying crossed_processes, so a default (merge_crossing=False) generation is untouched. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…crossings by default) Flips the staged env gate to the real default: a crossing-enabled generation now uses merge_crossing='record', so the crossed subprocesses are recorded on the base instead of generated on their own. Consequences: * fortran `standalone` output writes one directory per base matrix element -- the crossings are folded into the base's crossing-aware SMATRIX (p p > t t~ j: 4 SubProcess dirs -> 2). This is the visible goal. * every summation / event-generation backend (madevent, me7, mg7/cudacpp) reconstructs the crossings from the metadata at output (do_output), so its output is BYTE-IDENTICAL to before -- madevent p p > t t~ j still 3 dirs, base matrix.f identical. The do_output reconstruction is skipped only for self._export_format=='standalone' (which folds); reconstructing is the safe default for anything else. --use_crossing=False keeps the complete unmerged generation, and MG_MERGE_CROSSING=off is a debug escape hatch to the same. NOTE: this changes the fortran standalone crossing OUTPUT STRUCTURE, so the standalone-crossing IOTest reference files must be regenerated. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ow the default) TestCrossingPartition exercises partition_crossing_classes, which routes each subprocess flavor of the FULL (unmerged) matrix-element list to a base -- exactly what the madevent output reconstructs from the recorded crossings before grouping. With merge_crossing='record' now the default, a bare `generate` folds the crossings away at generation, so there was nothing left for the routing to eliminate. Force MG_MERGE_CROSSING=off around the generation so the test sees the unmerged modules. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…dalone RAMBO point The crossed-process demonstration in check_sa.f now shows exactly the crossings that are real subprocesses of the generation (folded in via merge_crossing='record'), each at the very momenta a standalone (non-crossed) run of that subprocess would evaluate -- so the crossed value is copy/paste-comparable with the subprocess's own check. - _crossed_signatures: match each folded crossed process LABEL-AWARE against the runtime-reachable PDG set, so a merged _quark leg matches any same-sign flavor and a flavor-changing (W) vertex pairs correctly (fixes the earlier same-flavor rep signature that no W crossing could reach). Returns representative signed-PDG signatures + a 'complete' flag; incomplete falls back to the full applicable-crossing loop. - _get_check_sa_crossing_example: emit those signatures into XCSIG and show a crossing only when GET_PDG_FOR_FLAVOR matches one; scan FLIP1/ FLIP2 over all legs (1..NEXTERNAL) so single-leg crossings are reached (the old NINCOMING+1.. range only hit double crossings). Print the base phase-space point P row k = P(:,k) with the crossed PDG XPDG(k): every shown crossing keeps the massive particles final and only relabels massless partons, so its per-slot mass pattern equals P and a standalone run draws the identical RAMBO point. - drop the now-unused APPLY_CROSSING/XPUSE/XNHIN/XICIN plumbing. Verified: p p > t t~ j and p p > w+ j demos reproduce the momenta and matrix element of standalone g d~ > t t~ d~ / d~ d > t t~ g / g d~ > w+ u~ runs row-for-row; crossing acceptance suite green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Implementation:
helmap.
- [x] Block beam polarisation + EVA under crossing (clear crash rather than silent wrong results).
Validation:
Known limitations / follow-ups
merge option