Crossing symmetry - #49
Conversation
…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>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
|
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 <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>
madspace does not support crossing yet, so the shipped default must be the
un-crossed output everywhere. --use_crossing=True opts in; --use_crossing=False
is now redundant but still honoured. Flipped in the four places that decided it:
MadGraphCmd._use_crossing / ._output_use_crossing (class attrs and the per-process
reset), the do_add fallback when the flag is absent, and the exporter's
self.opt['use_crossing'] fallback. One .get('use_crossing', True) in
get_crossing_pdg_lines still defaulted on and is now False, so no path can
re-enable it implicitly. Help text and the output completion updated.
Verified end to end: 'generate p p > j j QCD=0; output standalone' gives the
3-directory unfolded layout by default (as on main) and 1 folded directory with
--use_crossing=True.
TESTS NOW STATE THEIR CHOICE. The crossing suite had 45 explicit
--use_crossing=False against only 2 explicit True -- everything else inherited
the default, so flipping it would have silently converted ~60 crossing-ON tests
into a second copy of the crossing-OFF ones: still green, testing nothing. Every
generate in test_standalone_cross_symmetry.py is now pinned, via a _pin_crossing
helper where the caller supplies an options string (it leaves an explicit choice
alone) and literally otherwise. Same for the two crossing-ON arms of
test_standalone_madevent_consistency.py, the standalone_mg7 backend arm, and
test_standalone_crossing_folds_qqx_subprocess, whose 'crossed' arm was the bare
default and is now --use_crossing=True. Nothing in the test suite reads the
product default in either direction any more.
Checked green with the flip: the 13 static crossing tests, the three
layout-sensitive test_cmd tests, and every IOTest job that was green in CI
(iotest_examples, modification_to_cuts, cpp_write, loop_sqso, ewsudakov,
gnuplot). Full unit sweep 1042 tests: the only failures are the 6 reserved
codegen goldens that were already red (fks, export, unit_proc), plus one local
'No module named scipy'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default flip left the accumulator behind:
self._use_crossing = self._use_crossing and use_crossing
That was right while the default was True (any line asking for False wins), but
with the class attribute now False it can never be lifted -- False and True is
False -- so --use_crossing=True was parsed, accepted, and silently ignored. No
routers, no CROSSUSE, crossing unreachable. CI caught it in the three tests whose
non-vacuity guards exist for exactly this ('no subprocess group is served by a
crossing router, so the comparison would be between two identical builds').
Resolved per line instead: an explicit True switches it on, an explicit False
switches it off for good (a multi-line definition must not end up half crossed,
which is what the old protected), and a line with no flag inherits what
the definition already chose. The sticky-off marker is reset with the rest of
the per-definition state.
Measured on 'define q1 = g u u~; generate q1 q1 > q1 q1':
no flag 0 routers
--use_crossing=True 3 routers
--use_crossing=False 0 routers
True + 'add process g g > u u~' 3 routers (inherits)
True + 'add process ... --use_crossing=False' 0 routers (sticky off)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7f50f03 ('let each output pick the diagram order') and the DDM basis work added color_basis and merge_quartic_vertices as set-options but did not extend the expected dict in test_config, which enumerates every option exhaustively. The merge of that branch into the crossing tip (921b203) therefore turned acceptancetest_5 red. Not a crossing failure and not caused by the default flip; it just landed in the same window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FLIP(I) -- the row whose helicity configuration is I's with every helicity
negated -- was rebuilt at run time by an O(NCOMB^2 * NEXTERNAL) scan over the
materialized NHEL table, in four templates. The generator already holds
everything that search rediscovers: a configuration is a mixed-radix code over
the per-leg states, so the partner is a digit-wise complement of the code.
_helstate_data now computes it alongside HELALLOW and emits DATA FLIP.
Removed from matrix_standalone_v4.inc, matrix_madevent_v4.inc,
matrix_madevent_group_v4.inc and hel_warmup_v4.inc: the search itself, the
HELSAME/JHEL/KHEL scratch, the DATA FLIP/NCOMB*0/ sentinel and the
IF (FLIP(1).EQ.0) lazy-init guard -- one less piece of order-dependent mutable
state in a routine already juggling FLAV_USE/CROSSUSE. The grouped template
keeps a latch (CSYM_DONE) for the self-paired scan that follows, which writes
CSYMBAD and is not idempotent-cheap.
Semantics are unchanged, including the two ways to land on FLIP(I)=I, which
every consumer reads as "no distinct partner, keep the de-duplication off":
a row that negates to itself, and -- the case the search could only discover by
failing -- a row whose negation is not in the allowed set, which is what
polarization produces (a leg keeps its full radix but only some states are
selected, leaving HELALLOW a non-contiguous subset).
Verified rather than assumed. The emitted table is identical to what the
removed search computes, checked directly on g g > t t~, u u~ > g g, g g > g g,
e+ e- > z z (NCOMB=36, so the 0-helicity state is exercised), p p > w+ j, and
p p > z{0} j -- the polarized case, confirmed non-contiguous HELALLOW. Same
check re-run against the generated fortran of all three backends (standalone,
madevent single, madevent grouped): emitted == search on every matrix*.f, and
an involution on the encoder-based file whose NHEL is only materialized at run
time. The standalone build links and gives Matrix element = 0.59262631132419219
for g g > t t~, matching the 0.5926263 pinned at test_cmd.py:616.
TestGoodHelCParityDedup + the crossing static suites 17/17. Full unit sweep
1042 tests: failure set byte-identical to before this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_v4.inc emitted DATA HELALLOW twice: a private copy in SMATRIX,
which matches a user-requested helicity code (USERHEL.EQ.HELALLOW(IHEL)), and
another in FILL_NHEL, which decodes the list into the NHEL table. Both are live;
neither was dead code. The list is NCOMB long and grows with the multiplicity,
and a reader had no way to tell the two tables were the same table.
FILL_NHEL now owns the single DATA and publishes it through a COMMON that
SMATRIX reads. A DATA statement cannot initialise a COMMON outside a BLOCK DATA
-- and a BLOCK DATA can be dropped by the linker when it is not referenced --
so the list stays a local DATA and is copied into the COMMON on the first call.
Every reader is ordered behind that call: SMATRIX calls FILL_NHEL at the top and
only reads the table ~30 lines later, with no RETURN or GOTO in between (checked),
and it is the only other consumer in the file. No generator change: the emitted
DATA is untouched, only who declares it.
An accessor function was the other option and is worse here: Fortran does not
guarantee short-circuit evaluation of .OR., so USERHEL.EQ.-1 .OR.
USERHEL.EQ.GET_HELALLOW(IHEL) risks a call per helicity per event in the loop.
Scope, checked rather than assumed: only this template duplicated it. The
madevent and madweight templates never mention HELALLOW; matrix_standalone_hel
has one; and the split-orders variants (matrix_standalone_splitOrders_v4.inc,
the matchbox one, split_orders_helping_functions.inc) carry neither the encoder
nor the C-parity machinery at all -- a generated split-orders matrix.f has zero
DATA HELALLOW, zero DATA FLIP and zero HELSAME.
Verified: g g > t t~ still gives 0.59262631132419219 (the value pinned at
test_cmd.py:616), and p p > z{0} j -- polarized, so HELALLOW really is
non-identity (3,4,9,10,15,16,21,22) and the changed USERHEL path is exercised --
is bit-identical against a build from the parent commit on both matrix elements.
Generated file goes from 2 DATA HELALLOW to 1. Crossing/C-parity suites 17/17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_splitOrders_v4.inc had drifted from
matrix_standalone_v4.inc: 16 routines against 23, and no reader could tell
which of the differences were meant. They are decided one by one here, and
the dividing line is the crossing -- this template has no crossing machinery
(fill_crossing_replace_dict fills holes it does not have, use_crossing_ic
excludes split orders), so what exists only to decode an extended FLAV_IDX
stays out and what is meaningful without one comes in.
CARRIED
DECODE_HEL / FILL_NHEL / the HELALLOW list. The helicity table was emitted
TWICE (SMATRIX_SPLITORDERS and GET_NHEL) and initialised a COMMON from a
DATA statement, which Fortran only allows inside a BLOCK DATA -- it worked
because gfortran accepts it. Now decoded once into PROCESS_NHEL, with the
FILL_NHEL call added to GET_DENSITY as well: it reads that table without
going through SMATRIX first, and would otherwise see zeros.
The C-parity de-duplication (FLIP/CSYM). Adapted, not copied: the pair test
and the doubling run per squared-order component rather than on their sum,
since two components could in principle cancel into an equal total. The
separate NTRY_CSYM of the default template is collapsed into NTRY -- it
exists there only so a crossed call does not advance the scan. Kept
verbatim: the verdict is all-or-nothing per flavor (a self-paired row or
one mismatched pair turns it off for good), and the pair tolerance has an
absolute floor as well as a relative one, without which two numerically
zero rows compare roundoff to itself and veto every real pair.
GET_NHEL_IDX, in its non-crossing form. A caller that reads GET_NHEL's
static IDEN to undo the averaging gets the wrong denominator for every
non-representative flavor of a merged matrix element, and merged flavors
are exactly what this template already handles (BROKEN_SYM).
LEFT OUT, each for a reason
GET_DENSITY_IDX, GET_ALL_INTER_IDX, GET_ALL_INTER_CROSSED,
GET_INTER_RESCALE. The whole quartet exists because a FLAVOR array cannot
express a crossing; with no crossing the _IDX entries duplicate their
FLAVOR-array twins and _CROSSED/RESCALE are pass-throughs. Same for
GET_PDG_FOR_FLAVOR and the f2py flav_idx wrappers, which decode nothing
else.
ENCODE_HEL. Nothing in the tree calls it -- the crossing routines that
would inline their own encode in CROSS_GHIDX. A split-orders matrix.f is
written once per MadLoop and FKS born as well as per standalone
subprocess, so a dead routine there is paid for many times over.
The canonical-code helicity label (HELCODE). The default template matches
USERHEL against the code; here it stays the ROW NUMBER, because that is
what the callers pass: MadLoop hands SMATRIXHEL_SPLITORDERS its USERHEL,
which the EW-Sudakov driver fills from `do chosen_hel=1,SDK_GET_NCOMB()`
(Template/NLO/SubProcesses/check_sudakov.f). The two labels coincide for
every unpolarized process; relabelling would silently move the Born of a
polarized NLO run.
HELSAME, named as missing: it exists nowhere in the tree, in any template.
matrix_standalone_matchbox_splitOrders_v4.inc, the further-reduced variant:
out of scope, so that this lands ahead of a queued golden regeneration.
matrix_template_provides answers by searching the template text, so a comment
naming an omitted routine would make it claim the routine exists. The
templates therefore describe what they leave out instead of naming it, and
test_splitorders_template_carries_the_non_crossing_standalone_api pins both
lists plus the two predicates that read them (the crossing demo and the
folded color sum stay off).
Also: check_sa_splitOrders.f could not compile at all against a model with
flavored couplings -- coupl.inc declares TYPE(FLV_COUPLING) and the driver
had no `use model_object`, which check_sa.f has carried since those landed.
Nothing noticed because `make` builds check_sa.f only.
Verified by compiling and running the generated code, not by `make` exiting.
* u u~ > u u~ QED^2==2, g g > t t~ QCD^2==4, e+ e- > w+{0} w- QED^2==4
(polarized, so HELALLOW is really non-identity: 4,5,6,13,14,15,...):
check_sa and check_sa_born_splitOrders byte-identical to a build from
602235d, the polarized one included.
* p p > j j QCD^2==4: all 8 subprocesses, every one of their 65 flavor
columns plus the out-of-range indices, evaluated 40 times so the fast
phase is reached, and each cross-checked against the same total rebuilt
one helicity at a time through SMATRIXHEL_SPLITORDERS (which never
de-duplicates). Byte-identical, column for column.
* The de-duplication engages and halves the loop: 1.94x on g g > g g,
1.88x on the merged QQx_QQx (28 flavors), 1.92x on gQ_gQ, 1.92x on
g g > t t~ -- the last shifting the total by 1.2e-16, the reassociation
of summing half the rows doubled, and agreeing with the per-helicity
rebuild to the same 1.2e-16. It correctly refuses u u~ > u u~ QED^2==2,
whose Z axial couplings break the pairing.
* MadLoop born_matrix.f is written from this template: u u~ > u u~
[virt=QCD] builds and its driver returns 2.8276928588371737, bit-identical
to the base build. A matchbox output builds and returns the same.
IOTests, reported rather than regenerated (the user regenerates next):
* testIO_sqso_uux_uuxuuxx: red before, red after (its golden already
predates a template reordering).
* test_export_fks: 11 tests, the same 2 red before and after
(testIO_test_ppw_fksall, testIO_test_wprod_fksew).
* test_loop_exporters: same 1 red before and after.
* test_export_v4 unit tests: same 3 red before and after, +1 new passing.
* testIO_MatchBoxOutput: green before, RED after -- its
check_sa_born_splitOrders.f golden picks up the `use model_object` line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`p p > j j QCD^2==4 --use_crossing=True` (standalone) folded 8 subprocesses onto 3 and wrote no decoder into any of them: 50 of the 65 flavor columns became unreachable, and an extended FLAV_IDX -- the only way to ask for one -- returned 0 without a word, because SMATRIX_SPLITORDERS' range guard treats an index above NFLAV as an unresolvable flavor. Same generation without the squared order folds to the same 3 directories and DOES write the machinery, so the loss was invisible unless you counted subprocesses across the two. The cause is that the decision was made per FORMAT while the machinery is written per TEMPLATE. `standalone` is in _crossing_folding_formats, so the interface kept the folded crossings; but write_matrix_element_v4 picks matrix_standalone_splitOrders_v4.inc for any process carrying split_orders, and that template has no crossing holes -- fill_crossing_replace_dict fills holes it does not have. _crossing_needs_expansion now answers per amplitude: a folding format still gets the crossings expanded back when the amplitude carries a squared order. Expanding is the same output the user gets from --use_crossing=False, so nothing is approximated and nothing is lost -- verified byte for byte: every generated source of `p p > j j QCD^2==4 --use_crossing=True` (8 dirs, 65 flavor columns) is now identical to the --use_crossing=False build, matrix.f, check_sa.f, check_sa_born_splitOrders.f, f2py_matrix_wrapper.f and flavor_dispatch.py alike. The call had to move out of the grouped path's `if not _output_folds_crossings()` for the predicate to be asked at all: `output standalone` takes the grouped branch (checked, not assumed), where the format gate short-circuited it. The rest of that block -- MG_SPLIT_CROSSING peeling, the decay-chain regeneration -- stays behind the gate and is untouched. This is the safety net, not the feature: folding a squared-order process is still worth having and needs the crossing holes plus a split-orders snippet set in that template. Until then the flag degrades to the complete explicit output instead of a wrong one. Unaffected, checked: `p p > j j --use_crossing=True` without a squared order still folds to 3 directories with the machinery written; madevent is unchanged (it is not a folding format, so its crossings were already expanded, with and without squared orders). Tests: the crossing/C-parity acceptance suite is 67/68, and the one failure (test_inclusive_crossing_xsec_matches, "no subprocess group is served by a crossing router") reproduces identically at 602235d, so it is not from this change. test_export_v4 unit tests unchanged (same 3 pre-existing reds). No IOTest golden is affected: every suite pins crossing off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
do_add parsed --use_crossing and set self._use_crossing, and THEN called
check_add. With no model imported yet, check_generate imports the Standard
Model for the user, and do_import calls clean_process(), which resets exactly
those attributes. The LOCAL merge_crossing was already computed from the flag,
so generation still folded the crossings onto their base; the exporter reads
the ATTRIBUTE, found it False, and wrote no decoder for them.
That silently drops partonic channels. `p p > j j --use_crossing=True` came out
as 3 subprocess directories reaching 15 of its 65 flavor columns; the other 50
had no entry point at all, and the extended FLAV_IDX that names them returns 0
in silence, because SMATRIX reads an index above NFLAV as an unresolvable
flavor. The same generation with `import model sm` on the line before was
correct, which is why this survived: every test in the crossing suite imports
its model explicitly.
Resolving the flag AFTER check_add is the fix. The pop has to stay where it
is -- the flag must be out of `args` before check_add validates them -- so only
the resolution and the merge_crossing it feeds moved down, past check_add and
past the `add model` early return that has no use for either.
It also turns the madevent crossing router back on, which was dark for the same
reason and not (as I first assumed) because madevent cannot fold: routing is
not folding, it needs opt['use_crossing'] and nothing more. `p p > t t~ j j
--use_crossing=True` now writes matrix<i>_router.f in P1_gq_ttxgq and
P1_qq_ttxqq, the two the acceptance suite documents, and it does the same WITH
a squared order (`p p > j j QCD^2==4` routes P1_gq_gq and P1_qq_qq). So madevent
does support crossing with squared orders; it was only ever switched off.
Verified:
* `p p > j j --use_crossing=True` with no model line now produces exactly
what the same script with `import model sm` produces, and what
602235d produces with that line -- matrix.f byte-identical per
subprocess, plus check_sa.f, f2py_matrix_wrapper.f, flavor_dispatch.py.
* Crossing/C-parity acceptance suite 68/68. It was 67/68 before this, the
failure being test_inclusive_crossing_xsec_matches, "no subprocess group
is served by a crossing router" -- which reproduced at 602235d and is
what this repairs.
* Squared-order standalone still expands rather than folds (8 dirs, 65
flavor columns), as the previous commit intends; crossing off is
untouched. test_export_v4 (same 3 pre-existing reds) and test_cmd 15/15.
Correction to the previous commit's message: its closing "p p > j j
--use_crossing=True without a squared order still folds to 3 directories with
the machinery written" was measured on this bug, so the machinery was in fact
NOT written in that run. The statement holds now, and held then for a script
with an explicit model import; the conclusion that a squared-order process must
not fold is unaffected -- rechecked here with the import in place, where
602235d folds to 3 directories and still writes no decoder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matrix_standalone_splitOrders_v4.inc now carries the crossing machinery, so a
squared-order standalone output folds its crossed subprocesses onto their base
like any other. `p p > j j QCD^2==4 --use_crossing=True` goes from 8 subprocess
directories to 3, and all 65 flavor columns stay reachable -- through the
extended FLAV_IDX rather than through a directory of their own.
The previous commit made that case EXPAND instead, because folding into a
template with no decoder dropped the crossed subprocesses outright. That was
the safety net; this is the feature, and _crossing_needs_expansion goes back to
answering on the format alone.
NO TEMPLATE SPLIT WAS NEEDED, though the option was open. Every crossing
addition sits behind a hole of its own, and with crossing off every hole
reproduces what was there before -- so the FKS and MadLoop borns, written from
this same template and always uncrossed (a perturbative process breaks crossing
symmetry, so use_crossing is False for them by construction), are untouched.
Duplicating a thousand-line template to express that would have been worse than
one fill function.
fill_crossing_replace_dict_so is that function, and it is separate from
fill_crossing_replace_dict rather than shared with it: this SMATRIX has ANS and
T as VECTORS over the squared split orders and MATRIX as a subroutine, so the
default template's snippets do not fit -- `T=MATRIX(...)`, `ANS=ANS/IDEN*...`
and `IF (T.NE.0D0)` all have to become loops or read BUFF. Sharing them would
mean a template with a hole it cannot fill.
Three things had to come with it:
- use_crossing_ic. The external NSF flag has to be read through IC(i) for a
crossed leg to flip, and this template was excluded outright. It threads a
meaningful IC (SMATRIX passes JC, MATRIX passes it on to GET_AMP), so the
exclusion was only ever "crossing is not supported here"; it is keyed on
the template now rather than on split_orders.
- NTRY_CSYM is back, in the ON fill only. The C-parity scan must not be
advanced or consulted by a crossed call -- a crossing permutes and
sign-flips the helicities, so a base-row negation is no longer the partner.
With crossing off there are no crossed calls and NTRY remains that counter,
which is why the OFF fill keeps it.
- matrix_template_has_pdg_decoder. The crossing demonstration in check_sa.f
was gated on the DEFAULT template's hole name, so it stayed dark for a
folded split-orders output that could perfectly well run it. It asks the
real question now -- "does this matrix.f define GET_PDG_FOR_FLAVOR" -- and
both holes answer it.
Crossing OFF is unchanged. The only difference in the emitted file is
FLAV_IDX -> FLAV_USE, which is the same value there (the entry guard still
rejects an index above NFLAV when there is no crossing to name), plus the
comments. Checked numerically, not just read: u u~ > u u~ QED^2==2 and
g g > t t~ QCD^2==4 give the same check_sa value to the last digit, all 65
flavor columns of p p > j j QCD^2==4 are byte-identical through the flavor
sweep, and the MadLoop born of u u~ > u u~ [virt=QCD] still returns
2.8276928588371737.
Crossing ON, validated two ways:
- test_split_orders_merged_flavor_crossing_every_flavor, the split-orders
twin of test_merged_flavor_crossing_every_flavor: every flavor of the
merged `_quark _anti_quark > g _quark _anti_quark QED^2==0` crossed onto
`_quark g > _quark _quark _anti_quark QED^2==0`, matched through
GET_FLAVOR_INDEX rather than by index, to 1e-11. Sweeping the whole table
is what catches a crossed denominator built from the representative flavor
instead of the actual one. It also asserts the crossed value is not exactly
zero, which is what a matrix element with no decoder returns.
- p p > j j QCD^2==4 end to end: the folded build's crossing demonstration
against the unfolded build, matched by signed-PDG signature. Every one of
the 8 shared signatures agrees to at most 1.6e-16 (one ulp), and the folded
build reaches 22 further signatures that returned 0 before.
Tests: crossing/C-parity acceptance suite 69/69 (68 + the new one).
test_export_fks same 2 pre-existing reds, test_loop_exporters same 1,
test_export_v4 same 3, test_cmd 15/15. IOTest goldens move again for every
split-orders matrix element (the FLAV_USE rename), on top of the
check_sa_born_splitOrders golden the first commit moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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