Crossing symmetry - #49
Conversation
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>
|
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
|
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…plitude split_amps groups the unfolded amplitudes of one HELAS line by the wavefunctions in every column but the busiest, and it found each group by walking the whole amplitude list again for every combination: `all(w in amp.args for w in wfcts)`, 12.5 million evaluations over g g > g g g g g and the largest single cost left in the recycling step once the DAG and the line parsing were fixed. Index instead: one bit per amplitude, per wavefunction name, so a group is the AND of the masks of its wfcts. Walking the set bits lowest first keeps sub_amps in new_amps order, which is the order the list comprehension produced, and the initial mask is all-amplitudes-set so an empty wfcts still selects everything as `all(...)` over nothing did. A HELAS call never uses the same wavefunction twice, so a name identifies the column it came from and "is w anywhere in this amplitude" is the same question as "is w in its own column" -- the two group identically, which was also measured: 0 of 90 360 groups differ on g g > 5g, 0 of 228 on g g > g g g. Interleaved A/B, best of three, matrix1_optim.f byte-identical throughout: g g > g g g g g 4.07 s -> 3.37 s, g g > t t~ g g g g 39.14 s -> 33.75 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit a86fb4e5d7056903d315cb31db9419d7f51cd291)
…ines
The recycled standalone MATRIX carried its own colour sum: a DO K = 1, NCOMB
loop over the rewriter's jamp_lines followed by a plain triangular CF sum. That
copy had no BLAS batch, no reflection folding, ran the C-parity partners whose
AMP row is all zeros, and above all read AMP(K,i) at stride NCOMB -- a fresh
cache line per amplitude, where the standard GET_JAMP reads AMP(i) contiguously.
The amplitude sharing was real (1.73x at g g > 5g) and the colour stage ate all
of it and more: 1.48x SLOWER end to end at 5g, 2.07x at 6g.
It now gathers one helicity's amplitudes out of the helicity-major AMP into a
contiguous buffer and calls the SAME GET_JAMP and colour sum the standard output
uses, so it inherits whatever the colour side gains instead of drifting from it:
- GET_MATRIX_BATCHV, a per-column variant of GET_MATRIX_BATCH (same two
DSYMMs, reduced per column instead of into one scalar). The recycled driver
needs a |M|^2 per helicity row -- SMATRIXHEL and the polarization filter
select on it -- so the scalar batch was unusable. Emitted only into the
recycled copy of the shared routines; the two share head and body, and the
text GET_MATRIX_BATCH emits is unchanged.
- the rows whose helas calls were never generated are skipped (HRDEAD/HRROW,
fed by a new ${csym_dead} slot next to ${csym_reuse}), which is exactly 2x
wherever every row is C-parity paired.
- the colour flows go through the standard path's COLREPB/NCOLORFOLD, so a
folding applies here the moment one applies there.
- the gather takes 8 rows at a time: 8 complex*16 is one 128 byte cache line,
so a line is fetched per amplitude instead of per (amplitude, row). Measured
at 5g, NHRBLK 1/2/4/8/16/32/64 -> colour 3.2/2.8/2.4/2.3/2.3/2.7/2.5 ms.
AMP loses its NGRAPHS+ntmp width -- the colour flow temporaries live in the
gather buffer now -- which halves it (519 MB rather than 1.15 GB at 6g).
Steady state per phase space point, standalone, shipped flags, back to back:
g g > 4g 0.60 ms -> 0.29 ms 2.07x FASTER than not recycling
g g > 5g 14.5 ms -> 10.0 ms 1.45x
g g > 6g 0.582 s -> 0.329 s 1.77x (was 2.07x SLOWER)
with the colour stage itself 13.3 -> 2.3 ms at 5g and 1.20 -> 0.111 s at 6g.
|M|^2 moves by 1-2 ulp and cannot not: the recycled driver has to divide by
DENOM per helicity row where the scalar batch divides once at the end.
Two things this had to get right. Dropping %(color_data_lines)s from the
recycled MATRIX gives NaN rather than zero -- that DATA is the only thing that
fills DENOM -- so the CF/DENOM common stays even though the sum moved out. And
hel_recycle.split_helas_block cut the helas block at `JAMP(` or
`DO K = 1, NCOMB`; with those gone from the standalone driver it swallowed the
gather loop into the last chunk subroutine, so the driver now marks the end of
the block explicitly and the rewriter matches that as well.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude/strange-tesla-b2c15d forked at aab7dc3 and gave the recycled MATRIX its colour stage back to the shared GET_JAMP + GET_MATRIX_BATCHV, which is what turns helicity recycling from a loss into a win. This branch had gone on from the same commit with the things g g > 6g needed to build and run at all. Both halves are wanted and they compose. One conflict, in matrix_standalone_hel_v4.inc, and it is the same line twice: they narrow AMP to (NCOMB,NGRAPHS) because the colour flow temporaries moved into their gather buffer, and this branch had made AMP allocatable. Resolved as theirs plus the allocation: the narrowing halves AMP (1.15 GB -> 519 MB at g g > 6g) but 519 MB is still a fixed-size local linked into __DATA, and it is __DATA plus 825 MB of __TEXT running into the arm64 dyld shared region that stops the 6g binary from starting at all. Allocating removes the cliff at any multiplicity and measures free. Their INIT_CF call and jamp_decl/jamp_tmp_decl holes correctly go away with the private colour sum -- the shared routines make that call themselves. The other two files auto-merged and both sides survive: their END OF RECYCLED HELAS BLOCK marker (needed precisely because moving JAMP( and DO K = 1, NCOMB out of the driver made split_helas_block swallow the gather loop) sits next to this branch's per-core chunk writer, and their GET_MATRIX_BATCHV next to _hel_recycling_chunk_files. Validated on g g > 5g after merging: 128/128 good helicities, 64 C-parity pairs reused, 18 chunk files, |M|^2 6.6739867626784550E-007 against the standard standalone's ...84560E-007 -- the 1 ulp their commit says cannot not move, since the recycled driver divides by DENOM per helicity row. Steady state per phase space point, back to back on one machine: baseline 0.0134 s recycled, this merge 0.0093 s 1.44x FASTER against 0.0218 s, i.e. 1.48x SLOWER, on this branch before the merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…event colour stage The recycled AMP is helicity major -- one CombineAmp fills one amplitude for a whole set of rows, so a column of AMP is what the rewritten HELAS block produces -- and reading a row back walks NGRAPHS entries NCOMB apart, a cache line per amplitude where the unrecycled matrix element reads AMP contiguously. The colour stage now copies the row into a contiguous buffer first, NHRBLK=8 rows at a time so the copy itself reads whole lines, and the rewritten colour flow and AMP2 lines read that. This is the last of the four ingredients of 92475e8 that madevent lacked. It only pays at the top end, so it is gated on the size of AMP. Per full ME evaluation (SMATRIX1: one phase space point, every recycled helicity row, colour summed), HEAD -> gathered: g g > g g g 45 graphs, 14 kB AMP 6.85 us (+5% if forced on) g g > g g g g 510 graphs, 408 kB AMP 113 us (+10% if forced on) g g > t t~ g g g 1890 graphs, 3.9 MB AMP 1.017 ms -> 0.898 ms g g > g g g g g 7245 graphs, 13 MB AMP 5.08 ms -> 5.08 ms g g > 6g 126630 graphs, 482 MB AMP 0.539 s -> 0.276 s Below the gate the rows sharing a cache line are visited within NHRBLK iterations of each other anyway, so the hardware already gets that reuse and the copy is a second pass over AMP for nothing. Above it the working set no longer fits and the gather is worth 2x. Ordinary processes keep the loop the template has always carried, byte for byte. The decision lives in hel_recycle, not the exporter: it depends on the RECYCLED NCOMB, which only exists at survey time. The two loop holes sit inside a literal DO/ENDDO pair so the fortran writer still sees the helicity loop and indents its body -- making the whole loop line a hole silently de-indents and rewraps the entire colour stage. AMPK is deliberately not SAVEd: SMATRIX1_MULTI carries an !$OMP PARALLEL over the matrix element and the link line already passes -fopenmp, so the day FFLAGS does too, locals become thread private and an explicit SAVE would be the one thing left shared. Cross sections unchanged at g g > g g g (3.642e+07 +- 2.868e+05 pb), g g > g g g g, g g > t t~ g g g and g g > 6g, the last agreeing digit for digit between the gathered and ungathered builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mg7 exporter can run its color sum on the (n-2)! Del Duca-Dixon-Maltoni
structures while a color flow is still picked among the (n-1)! trace ones, but
set_channels_colors_map built active_colors -- the mask which becomes icolamp --
by walking self.color_basis, i.e. the basis of the color sum. The C++ selection
walks that mask over ncolor_flow entries, so a DDM build declared
icolamp[nconfig][ncolor] and read it as icolamp[nconfig][ncolor_flow]: for
g g > g g a [3][2] array read 6 wide, which reinterprets the neighbouring rows
and then runs off the end of the object. Reading icolamp the way the selection
does gave {1,2,4,5}, {0,2,3}, {0,1,5} for the three configs instead of
{0,1,3,5}, {1,2,3,4}, {0,2,4,5}.
|M|^2 does not depend on any of this, which is why the DDM port passed every
check run so far. Events do: over 3 x 100k g g > g g events the color flow
fractions came out (9.35, 9.38, 31.38, 9.15, 31.30, 9.44)% on the trace basis
against (10.54, 1.39, 32.32, 12.83, 32.46, 10.46)% on the DDM one -- 134 sigma
on the second flow -- at an unchanged cross section (1.2 sigma), since a wrong
color selection never moves the weight.
Give the exporter a color_flow_basis next to its color_basis and build
active_colors, the color_flows table and both nb_color values from it. The two
bases now write the same icolamp, and the same processes agree flow by flow
over 3 x 100k events each: g g > g g worst 2.0 sigma over 6 flows (xsec 0.1),
g g > g g g worst 1.9 sigma over 24 (xsec 0.5), and u u~ > g g, which never
leaves the trace basis, generates identical source and agrees at 0.1 sigma.
The GPU select_col_and_diag kernel had the same ncolor/ncolor_flow mismatch and
is fixed the same way; there is no CUDA toolchain here, so that path is
generated but not compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
do_multiline breaks a long statement at the last space which fits in 72
characters. When the only such space is the statement's own indentation the
first chunk is all blanks, so the emitted physical line is empty and the
continuation which follows it is attached by the compiler to the *previous*
statement.
g g > g g g on the DDM color basis is the first process to hit it: the
Kleiss-Kuijf lines rebuilding the trace JAMPs are long and contain no space
of their own, and matrix1_optim.f came out as
JAMPF(1,1)=+2D0*(+IMAG1*JAMP(6,1))
$ JAMPF(2,1)=+2D0*(-IMAG1*JAMP(3,1)-...)
which gfortran rejects with "Unclassifiable statement", so the madevent run
dies in the compilation step. Break mid-token in that case, which is what the
no-space-at-all branch next to it already does.
The condition only fires where the old code emitted a blank chunk, so any
matrix element which built before is byte for byte what it was; of the arms
generated here only g g > g g g on the DDM basis contained one.
With this, g g > g g g builds on both color bases and the two agree exactly:
same cross section, and all 10000 color columns of all 2000 events identical
at equal seed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three commits, all of which this branch wants: d718ce8 share the orbit equivariant color-flow search with the madmatrix backend -- the search moves into jamp_optimiser.JampOptimiser and the C++ writer gets it too; the fortran output is byte for byte unchanged. 467862f mg7: pick the color flow from a mask built on the color flow basis -- a DDM build declared icolamp[nconfig][ncolor] and read it ncolor_flow wide, so the colour SELECTION was wrong (134 sigma on one flow of g g > g g) at an unchanged cross section. 9f984d9 hel_recycle: never wrap a statement onto a blank physical line. Two conflicts, and the first is the interesting one: 9f984d9 and this branch fixed the SAME do_multiline bug independently. Theirs guards after the fact -- if the first chunk came out all blanks, break mid-token instead. Ours refuses to look inside the indent in the first place, by flooring the rfind at 6+len(indent). Kept ours, having checked the two agree: byte-identical output on the JAMPF line their commit message reports failing, on the short JAMPF, on a long JAMP sum and on a plain HELAS call. The second is tests/unit_tests/madevent/test_hel_recycle.py, which both sides created. The classes are disjoint -- TestAmpGather here, TestDoMultiline there -- so both are kept, 12 tests, all passing. Worth having their four even though our wrapping fix is the one that survives: they are what pins the behaviour if anyone touches do_multiline again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
input/authors.md is written at release time by bin/create_release.py and is
not tracked in git, so in any dev checkout post_banner's unguarded open()
raised FileNotFoundError.
EasterEgg.__init__ catches it (except Exception: sprint(error)), so the crash
report itself was never actually lost -- MG5_debug is written correctly. But
the swallowed error printed
DEBUG: [Errno 2] No such file or directory: .../input/authors.md
[misc.py at line 2103]
three times per crashing run -- startup EasterEgg('loading') at bin/mg5_aMC:155,
then 'error', then 'quit'. Landing that line next to "More information is found
in 'MG5_debug'" made it look like the debug write had failed.
Return "" when the file is absent, and skip lines that do not split into
exactly two fields, since a malformed file would produce the same misleading
noise on the crash path.
Verified: forced exception during output, authors.md noise 3 -> 0 with
MG5_debug still carrying the real traceback; with an authors.md present the
CONTRIBUTOR OF THE DAY banner still renders and blank/malformed lines are
skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--use_crossing was parsed by do_add only, stored on cmd._use_crossing and read from there by ExportV4Factory/ExportCPPFactory. On an output line it fell into the generic line_options dict and was silently dropped, so `output standalone_mg7 X --use_crossing=False` wrote the full crossing machinery anyway (byte-identical to the default build). Parse it in both commands through a shared pop_use_crossing_flag, and AND the output-line choice into opt['use_crossing'] in both factories. Turning the machinery off at output time is not just an exporter flag: the generation runs merge_crossing='record', so the crossed subprocesses are folded onto their base and never generated on their own. Dropping the machinery without putting them back would give a silently incomplete output -- the failure mode do_add's comment says must never be reachable from this flag. _output_folds_crossings() therefore gates the three expansion decisions, so a folding backend told to skip the crossing expands the recorded crossings back into explicit subprocesses, exactly as a non-folding backend already does. Verified byte-identical to the generate-line flag over the whole SubProcesses tree for g g > t t~ g g g (one directory) and for q q > q q, q = u d u~ d~ (one folded directory -> three expanded), on both standalone and standalone_mg7. The crossing-off mg7 source has the plain external HELAS calls and cNGoodHel loop bounds. The default build is unchanged, and the flag does not leak to a later output in the same session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…M one
`output standalone_mg7 g g > g g g g g` silently produced an unbuildable
process directory: a 0 byte coloramps.h and no CPPProcess.{cc,h} at all.
edit_coloramps asked `self.color_basis` for a colour flow decomposition
while baking the canonical colour flow codes. For a fully adjoint
(all-gluon) process standalone_mg7 auto-selects the (n-2)! Del Duca-Dixon-
Maltoni basis (madmatrix/output.py sets support_ddm_color_basis and
ddm_needs_flow_basis), whose elements are products of f's with no single
flow each, so color_flow_decomposition raises ColorBasisError by design.
Every other consumer had already moved to the trace basis carried
alongside -- self.color_flow_basis here, get_flow_basis() on the fortran
side -- and this one site was missed. Non-adjoint processes were never
affected: DDM does not engage, and get_flow_basis() returns the basis
itself.
The failure was invisible because coloramps.h was opened before any of
its content was computed, so the exception left the truncated header
behind; open it only once the content is built.
Verified on g g > {3,4,5}g: flow counts are exactly (n-1)!, coloramps.h
is byte-identical to a `set color_basis trace` build, gg>3g compiles and
check_sa gives the same |M|^2 from both bases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C++/standalone_mg7 good-helicity scan walked every crossing code that
cross_perm_ic finds structurally APPLICABLE, which is far more than an event
can ever carry: g g > t t~ g g g has 48 applicable codes and records none.
Each one costs a full ncomb-helicity calculate_jamps sweep, so the scan ran
49152 kernel calls instead of 1024 -- a one-off ~1 s of startup that
check_sa's `perf 1 32 8` amortises over only 8 iterations and therefore
reports as a 4.2x matrix-element slowdown. The per-event cost was never
affected; the tell is [Min,Max]TimeInMatrixElems, whose Min matched the
pre-crossing build all along.
_scanned_crossings now emits a cross_recorded[] table of the crossings this
ME actually folded in, the scan gates on it, and calculate_jamps carries a
runtime guard: an applicable-but-unrecorded code would otherwise find an
empty cGoodHelOfCross row, mask every helicity and return a silent zero, so
it aborts with a message instead. Structurally invalid codes keep their
existing "the denominator ASSIGNS 0" contract and are exempt.
The set is deliberately not built from _folded_crossing_flavorids: that
collapses mirror pairs (it is a demo helper) and the runtime may hand us
either member -- p p > w+ j records {4,20} but also reaches {9,22}.
This narrows a tested capability: the backend could previously be driven with
any applicable crossing code on a base that recorded nothing. Three tests
relied on that; they now use `pq pq > pq pq` (pq = g u u~), whose g g > q q~
dir genuinely records cross 3 (g u~ > g u~) and cross 23 (u u~ > g g), so the
same physics is still checked against standalone references. Two of them also
needed FPTYPE=d: the makefile default 'm' runs the colour algebra in single
precision, putting differently-ordered evaluations of the same |M|^2 ~1e-7
apart, a hundredfold above the 1e-9 tolerance they compare at.
g g > t t~ g g g is back to base timing (perf 1 32 8, measured back-to-back
under one load: base 6.05e-2, before 2.44e-1, after 6.07e-2) and matrix
elements are bit-identical on gg>ttxggg, p p > w+ j and q q > q q. The
acceptance suite is unchanged at 66 tests with the same 2 pre-existing
failures and 2 errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`output matchbox` died on every process with
TypeError: get_JAMP_lines() got an unexpected keyword argument 'proc_prefix'
because the ProcessExporterFortranMatchBox override never grew the
proc_prefix (nor symmetry_source) parameters the base signature gained. It
takes both now and forwards proc_prefix; orbit and symmetry_source are
accepted and go no further, since jamp_orbit_allowed already says no for
this exporter and the template declares neither the orbit tables nor
INIT_JAMP.
Behind that, matrix_standalone_matchbox_splitOrders_v4.inc had missed three
migrations, each only visible once the previous one was fixed:
- CF/DENOM were left undeclared in BORN (the commented-out
`REAL*8 CF(NCOLOR,NCOLOR)` is a fossil of the pre-packed 2-D colour
matrix) while the routine still substitutes color_data_lines, and
TMP_JAMP was missing there too.
- the flavor machinery: the HELAS calls take FLAVOR, rebuilt from
FLAV_IDX, and neither existed in MATRIX or BORN.
- the aloha objects: still COMPLEX*16 W(18,NWAVEFUNCS) with no
use model_object / use aloha_object, against rank-1 W(i) calls.
None of the entry points of that template carries a flavor argument, so
FLAV_IDX is pinned to the first column of the table - the canonical flavor,
which is what these routines evaluated before FLAVOR reached the HELAS
layer. Merged subprocesses can therefore only be evaluated for that one
flavor; threading the index through would change an API Herwig's Matchbox
calls, so it is left as a separate decision.
madloop_matchbox shares that template and failed differently: it supplies a
proc_prefix from the start, so the colour DATA came out as MG5_0_CF against
an unprefixed declaration. CF and DENOM are plain locals of each matchbox
routine, so color_data_prefix now writes those statements without a prefix.
Plain matchbox is unaffected (it reaches that point before its own prefix is
set) and so is every other exporter.
Verified by compiling the generated code, which the exporter's own make
never does (it is a no-op): 11 subprocesses over four LO outputs, covering
TMP_JAMP(72) and NMASK_FLAV up to 28, plus born_matrix.f from a
madloop_matchbox output of g g > t t~ [virt=QCD]. Also fixes GET_JAMP in
matrix_standalone_matchbox.inc leaving LNJAMP implicitly typed, which made
the named COMMON 40 vs 64 bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…banner Companion to the all-gluon standalone_mg7 fix: the missing input/authors.md was what made the crash noisy and misleading (3x DEBUG line next to the MG5_debug notice), though the debug report itself was always written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… line --use_crossing was parsed by do_add only and silently dropped on an output line. Parsed in both through a shared pop_use_crossing_flag, ANDed into opt['use_crossing'] in both exporter factories, and _output_folds_crossings() gates the expansion so a folding backend told to skip the crossing writes the folded subprocesses back out instead of losing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in --use_crossing on the output line (51f55cb), the colour-flow-basis fix for the flow codes (8d10421) and the authors.md post_banner guard. Conflict: _output_standalone_mg7 gained a parameter on both sides -- theirs `out_options` (output-line flags), mine `color_basis` (needed because two outputs compared number-by-number must share a colour basis, the sum being accumulated in a different order in each). Kept both; the body had already merged both additions. Their new --use_crossing tests are unaffected by the recorded-crossing narrowing in ebe7bc1: they exercise the crossing-off source and the identity crossing, and test_invalid_overlapping_swap_returns_zero drives an overlapping swap, which the guard deliberately exempts (spincol_cross == 0 keeps its "the denominator ASSIGNS 0" contract). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_matchbox_splitOrders_v4.inc pinned FLAV_IDX to 1 in
MATRIX and BORN, so only column 1 of FLAV_TABLE was reachable: for
`p p > j j QCD^2==4` that is 1 of the 28 flavor combinations of
P1_QQx_QQx, and 65 columns across the eight subprocesses collapsed to 8.
It is threaded now, with exactly the argument lists
matrix_standalone_splitOrders_v4.inc already uses, because the callers
were already written against those lists and the pinned template did not
match them:
- check_sa_born_splitOrders.f (the shared driver, written next to every
split-orders matrix element) calls SMATRIX_SPLITORDERS(P,FLAVOR,
MATELEMS) -- three arguments against a two-argument subroutine.
- loop_matrix.f of a madloop_matchbox output calls SMATRIXHEL_SPLIT-
ORDERS(P_USER,USERHEL,IC,BORNBUFF(0)) -- four against three. ANS was
bound to IC, so the Born went into an INTEGER array of ones and
BORNBUFF, which MadLoop reads back as the Born ME and as its
stability reference REF, stayed zero. The same mismatch was fixed for
the non-matchbox MadLoop in 89d968a; the matchbox template was
missed.
Both callers pass an all-ones INTEGER array where FLAV_IDX is expected,
which resolves to its first element, so the canonical flavor is what they
still get -- bit-identical to before -- and MadLoop now gets a Born at
all. A new template unit test pins the four argument lists together.
The entry points are an external API (Herwig's Matchbox binds them by
name), but the non-split matchbox template had already moved to
SMATRIX(P,FLAV_IDX,ANS) and BORN(P,NHEL,FLAVOR); keeping the split-orders
one pinned would have left Herwig with two different BORN signatures
depending on whether the process has split orders. BORN here takes the
same FLAVOR(NEXTERNAL) array and resolves it with GET_FLAVOR_INDEX,
zeroing JAMP/LNJAMP for a combination the ME does not cover.
Three things had to come with the index, none of which the pinned
template needed:
- GOODHEL/NTRY are now per flavor. Which helicity rows vanish depends
on the flavor, so one shared table would filter rows out for the
flavor that scanned first.
- BROKEN_SYM, and with it the GET_FLAVOR_INDEX/GET_FLAVOR helpers,
which the template did not emit at all. IDEN is one constant per
matrix element: the merged `_quark _quark > _quark _quark` carries
IDEN=72, right for u u > u u and a factor 2 wrong for u c > u c.
- an out-of-range FLAV_IDX returns zero instead of indexing off the
end of the per-flavor arrays.
Verified by compiling the generated code, which the matchbox exporter's
make never does, and by running it. All eight subprocesses of the LO
output compile. Their 65 flavor columns were evaluated at one phase-space
point and every one of them reproduces, to better than 1e-12 relative,
the dedicated subprocess it stands for in an `apply_flavor_grouping
False` output -- including the 6+6+12 columns that need BROKEN_SYM.
Column 1 of each is bit-identical to the pinned build. A madloop_matchbox
output of u u~ > u u~ [virt=QCD] builds and runs check_sa_born_split-
Orders, which returns 2.8276928588371737, bit-identical to the same
driver built from a plain `output standalone` of that process.
The matchbox IO test reference is regenerated. Beyond born_matrix.f, it
also picks up three files that were already stale on this branch: the
LNJAMP declaration of 73ca0f8 in the non-split matrix.f, and the
dormant crossing demo in check_sa.f plus a blank line in
f2py_matrix_wrapper.f from earlier merged work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`output matchbox` writes two drivers beside every matrix.f and neither
could be built. The exporter's `make` is a no-op, so nothing ever said so.
check_sa.f: prefixed, and cut down to what the template provides.
Matchbox names every routine after the process id (MG5_1_SMATRIX), but
write_check_sa was handed the --prefix of the output line -- empty
unless asked for -- so the driver called SMATRIX and GET_FLAVOR_INDEX
by their unprefixed names. The prefix now comes from one place,
get_proc_prefix, which write_matrix_element_v4 uses as well, so the two
cannot drift again.
That is not enough on its own: the driver also carries a density block
and a crossing demonstration calling GET_DENSITY and
GET_PDG_FOR_FLAVOR, which only the default matrix template writes.
get_matrix_template / matrix_template_provides answer what the file
being linked against actually contains, and both blocks are emitted
behind that. This is not matchbox-specific: `output standalone` on any
split-orders process (p p > j j QCD^2==4) shipped a check_sa.f that
failed on _get_pdg_for_flavor_ too.
f2py_matrix_wrapper.f: not written for matchbox.
It is written against the default template's API and calls GET_value,
GET_value_idx, GET_DENSITY and IS_BORN_HEL_SELECTED -- none of which
either matchbox template has -- and treats MATRIX as a function where
the split-orders one has a subroutine. Supplying all that means porting
the density/value stack into matchbox for a python interface nobody
uses: Herwig links the Fortran directly. write_f2py_interface turns it
off, together with flavor_dispatch.py and the matrix2py rules finalize
appends to SubProcesses/makefile for a wrapper that is no longer there.
check_sa_born_splitOrders.f and nsqso_born.inc: written where they belong.
Both went through a bare filename, i.e. into whatever directory mg5_aMC
was launched from -- nsqso_born.inc long enough ago that it has a
.gitignore entry. They now go next to the matrix element, derived from
the writer's own path, which leaves MadLoop (which chdir's first)
exactly where it was. The split-orders driver also gets the prefix, and
the makefile stops linking it over the check_sa binary.
While verifying, the newly-working driver turned up a wrong number, so:
only fold the color sum for a template that reads a folded matrix.
get_color_data_lines writes the folded color matrix -- one row per
reversal pair, off-diagonal doubled -- for whatever template is in use,
but reading it back needs the JFOLD gather that only
matrix_standalone_v4.inc and the madevent templates have. Everything
else sums CF straight, so the folded matrix was silently misread:
matchbox g g > g g 47.68 instead of 55.18
standalone g g > g g Infinity (split-orders template with
`set color_basis trace`)
MadLoop born g g > g g Infinity (`output standalone` on
g g > g g [virt=QCD])
All three now return 55.179250628823411, which is also what the default
standalone gives through the completely different DDM color basis.
The gate is on the standalone exporter, so madevent (a separate class,
and its templates do read a folded matrix) is untouched, and outputs
that were already correct are byte-identical: matrix.f is unchanged for
every subprocess of p p > j j and p p > j j QCD^2==4. The FKS born is
written from born_fks.inc, which has no gather either, through a path
get_matrix_template does not describe -- it is only covered here by
accident and needs its own fix.
Verified by compiling and running the generated code. All eight
subprocesses of the LO matchbox output build check_sa and run it; the
merged flavor columns still reproduce their dedicated (grouping-off)
matrix elements to 1e-12, with g g > g g moving to the corrected value on
both sides. `make check_sa_born_splitOrders` builds and runs in both the
LO matchbox and the madloop_matchbox directories, the latter unchanged at
2.8276928588371737. Matchbox default_opt also regains 'output_options',
without which the class could not be constructed at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CPU branch of sigmaKin runs the event-page loop under '#pragma omp parallel for default( none )', which means every variable the loop body touches has to be named in the shared() clause -- anything missing is a hard compile error, not a warning. Three sigmaKin arguments used inside the loop were absent from it: iflavorVec passed to calculate_jamps allrnddiagram the storeChannelWeights test and the diagram sampling allDiagramIdsOut the sampled diagram id written back per event so the generated CPPProcess.cc did not compile at all once OpenMP was on. All three are plain (non-const-qualified) pointer parameters, exactly like allmomenta and allChannelIds which were already listed, so none of them is predetermined-shared. Add them to _OMPLIST1. Nothing caught this because nothing ever builds the OpenMP path: it is opt-in via USEOPENMP=1 (#758), madmatrix.mk force-disables it on Darwin, and no CI job nor any contrib/ driver script sets it -- they all ship the export commented out. The new acceptance test therefore does not go through USEOPENMP either. It takes the compile command the generated makefile itself would run (from 'make -n', so it keeps following the real build flags) and re-runs it with whatever OpenMP flags the local compiler accepts, which keeps it meaningful on macOS as well as on the gcc CI runner. It then checks the object really carries an OpenMP runtime call, so it cannot go vacuous if the parallel region is ever compiled out. Verified by hand beyond the test: with the OpenMP flags forced on, g g > t t~ and g g > t t~ g build and run, and check_sa.exe gives output byte-identical to a serial build at 1, 4 and 8 threads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… folded get_color_data_lines can emit the color matrix in three forms, and two of them are a contract with the file being written, not plain DATA: the compressed encoding leaves the entries to a run-time INIT_CF, and the folded form writes one row per JAMP reversal pair with the off-diagonal doubled. The method is shared by every fortran exporter, and the FKS one fills templates that honour neither -- born_fks.inc, born_fks_hel.inc, the four split-orders ones (born, bhel, real, cnt) and the sudakov goldstone one all declare CF(NCOLOR*(NCOLOR+1)/2), sum it straight, and carry no INIT_CF call. 2822859 gated the folding on ProcessExporterFortranSA.get_jamp_folding, which asks get_matrix_template -- what write_matrix_element_v4 writes. The FKS writers do not go through it, so the gate only covered them by accident: it reported the gather-less split-orders template because an aMC@NLO process always carries split orders (amcatnlo_interface fills them with every coupling order of the model, so the list is never empty). Measured on g g > g g [real=QCD] at a fixed phase space point, against 88.00353346603369 from `output standalone` of the same born: SBORN = 88.00353346603369 as shipped, born.f carrying the full 21 entries for NCOLOR=6 -- and 17.35289594455156 with the gate bypassed, where born.f gets 6. The templates really cannot read a folded matrix; only an always-true predicate stood between that and a wrong born. The encoding is the same hazard without even the accident. INIT_CF is emitted by write_matrix_element_v4 alone, so an FKS matrix element whose basis crossed the size threshold would have shipped a CF with no DATA and no routine to fill it, i.e. silently zero. Out of reach at the NCOLOR the current templates see, unconditional once it is not. So the question is asked where the answer is known -- at the call site, which is the only place that knows which template it is about to fill. get_fks_color_data_lines passes plain=True and every entry of the upper triangle is written. Not jamp_fold = False on the exporter: it writes V0_*/born_matrix.f as well, through write_bornmatrix, and that one *is* write_matrix_element_v4 and may keep both compact forms. Not teaching get_jamp_folding the template either: one exporter instance fills five of them, so it would need this same plumbing to answer at all, for an answer that is uniformly "written out in full". Generated code is unchanged: born.f, born_hel.f and all five matrix_<i>.f come out byte-identical, and SBORN still gives 88.00353346603369. msP/msF were checked the same way and are covered by construction -- get_matrix_template names their templates outright. That only shows up in the trace basis: the default DDM basis puts g g > g g at NCOLOR=2, too small to fold, so a default msP/msF test proves nothing. With `set color_basis trace` both emit 21 entries where the default standalone emits 6 plus NCOLORFOLD. Co-Authored-By: Claude Opus 5 <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>
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