From 7464f0d8d255a2570b5ed8105461c124fcda11d3 Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Tue, 23 Jun 2026 13:07:48 +0200 Subject: [PATCH 01/11] mg7 merged-flavor: refuse unsupported coupling topologies with a clear error MSSM "generate p p > go go" with the default mg7 (madmatrix) output crashed in write_flv_couplings on "k1, k2 = [i for i in key if i!=0]": the merged-flavor C++ backend only models the two-merged-leg "partner" topology (the SM q q~ V case), but MSSM has single-merged-leg vertices (gluino/chargino-squark-quark, only the light quark merged), and the relevant flavored couplings are event-by-event (running-alphas) couplings that cannot be referenced by the fixed value[] pointers set once in setIndependentCouplings. Option B (guard, not the full feature): add UFOModelConverterCPP. _assert_flv_couplings_supported, called from both write_flv_couplings copies (export_cpp.py and madmatrix/model_handling.py). It raises a clear, actionable InvalidCmd when a USED flavored coupling connects a number of merged legs other than two, or is an event-by-event coupling, telling the user to use "output madevent" / "output standalone" instead. The check is scoped to the process's used couplings, so valid SM two-leg merged cases (e.g. "u u~ > j j QCD=0") still generate, and the Fortran paths are unaffected. - docs/mg7_merged_flavor_mssm_design.md: full diagnosis + the deferred Option A plan (proper single-leg + dependent-flavored-coupling support). - test_mg7_merged_flavor_unsupported_is_clean: asserts standalone_mg7 refuses MSSM p p > go go with InvalidCmd and that madevent still supports it. Pre-existing on the feat-madmatrix branch; unrelated to the goodhel merge. Co-Authored-By: Claude Opus 4.8 --- docs/mg7_merged_flavor_mssm_design.md | 138 ++++++++++++++++++++++++++ madgraph/iolibs/export_cpp.py | 42 +++++++- madmatrix/model_handling.py | 9 +- tests/acceptance_tests/test_cmd.py | 26 +++++ 4 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 docs/mg7_merged_flavor_mssm_design.md diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md new file mode 100644 index 000000000..299199326 --- /dev/null +++ b/docs/mg7_merged_flavor_mssm_design.md @@ -0,0 +1,138 @@ +# mg7/madmatrix merged-flavor support for single-merged-leg vertices (MSSM) + +## Resolution (this PR): Option B — clean guard + +This PR ships **Option B**: the madmatrix (mg7/standalone_mg7) model export now +**refuses, with a clear and actionable error**, the merged-flavor configurations +it cannot yet generate correctly, instead of crashing or emitting +wrong/uncompilable code. Concretely, `UFOModelConverterCPP` gained +`_assert_flv_couplings_supported`, called from both `write_flv_couplings` +copies (`export_cpp.py` and `madmatrix/model_handling.py`); it raises +`InvalidCmd` when a used flavored coupling either connects a number of merged +legs other than two, or is an event-by-event ("dependent", running-αs) coupling. + +Result for `generate p p > go go; output standalone_mg7`: +``` +Command "output standalone_mg7 ..." interrupted with error: +InvalidCmd : merged-flavor C++ output (mg7/standalone_mg7) does not yet support +this process: flavor coupling FLV_54 references an event-by-event (running-alphas) +coupling. ... Use 'output madevent' or 'output standalone' for this process. +``` +The guard is scoped to the process's *used* couplings (`coups_flv_dep/indep` are +filtered to `wanted_couplings`), so valid SM two-leg merged cases (e.g. +`u u~ > j j QCD=0`) still generate. `output madevent`/`standalone` keep working. + +The full feature (Option A) is **deferred**; the diagnosis and plan below are +kept for the follow-up. The rest of this note describes Option A. + +--- + +Status of Option A: **deferred / not implemented**. This note records the +diagnosis, the discovered scope, and the implementation plan, so a follow-up PR +is self-contained. + +## Symptom + +``` +./tests/test_manager.py -pA test_generation_from_file_1 -t 0 +``` +crashes (MSSM `generate p p > go go`, default `output` = `mg7` = madmatrix): + +``` +File ".../madmatrix/model_handling.py", line 942, in write_flv_couplings + k1, k2 = [i for i in key if i!=0] +ValueError: not enough values to unpack (expected 2, got 1) +``` + +This is **pre-existing on the feat-madmatrix branch** (both `default='mg7'` and +`write_flv_couplings` exist in `9d726705c`); it is not caused by the +`aloha_obj_wmerged` goodhel merge. The Fortran `output madevent` path handles +`p p > go go` fine (it uses legacy flavor grouping, `P1_qq_gogo` has no +`PARTNER`/`flv_index`), so the gap is **madmatrix/mg7-specific**. + +## Root cause: the merged-flavor model assumes two merged partner legs + +`coupl.flavors` keys are per-leg tuples; a non-zero entry is the 1-based flavor +index of a leg in a merged-particle group, `0` otherwise. The whole FLV +mechanism assumes a flavored vertex has **exactly two** merged legs forming a +partner pair (the SM `q q̄ V` topology), routing flavor `k1 → k2`: + +- `write_flv_couplings` (model side) does `k1, k2 = [i for i in key if i!=0]`. +- `MadMatrixALOHAWriter.get_coupling_def` (vertex side, `model_handling.py` + ~408-526) is hardwired to two fermions `F1`/`F2` and requires + `partner1[flv_index1] == flv_index2`. +- `FLV_COUPLING { int partner1[]; int partner2[]; cxtype* value[]; }` encodes + exactly this pairing. + +MSSM has vertices with a **single** merged leg. Confirmed by probing the model: + +``` +INTERACTION id=94 particles: [('_quark',F,81), ('x1+',F,1000024), ('su1',S,1000002)] + FLV_1 flavors = {(1, 0, 0): 'GC_385'} # only the quark (leg 0) is merged +``` +i.e. `quark(merged) · chargino(unmerged) · squark(scalar)`. The key has one +non-zero entry → the 2-tuple unpack throws. + +Semantically a single-leg coupling is a **selection/gate**: "this interaction is +active only when the single merged leg has flavor index k" — there is no second +merged leg to partner with. Each squark flavor gets its own interaction +(su1↔flavor1, su2↔flavor3, ...). + +## Discovered scope (larger than the initial framing) + +Empirically generating `p p > go go` as `standalone_mg7` after fixing the +unpack reveals **three** independent problems; all are needed for MSSM: + +1. **Single-leg topology** (this crash). + - Model side: `write_flv_couplings` must serialize a one-merged-leg key. + *(done — see below, for the independent-coupling case.)* + - Vertex side: `get_coupling_def` (and likely the generic + `aloha/aloha_writers.py`, plus the Fortran writer) need a single-leg + branch that gates on the one merged fermion and does **not** require a + second fermion partner. This needs the writer to know *which* leg is + flavored (today the `M` tag is binary — `helas_objects.py:2099` — and the + writer just assumes `F1`/`F2`). + +2. **Dependent (event-by-event) flavored couplings.** + `set_flv_couplings` is emitted inside `Parameters::setIndependentCouplings()` + and points `FLV_xx.value[k] = &GC_yyy`. Every FLV coupling in the *model* is + serialized there. MSSM flavored couplings such as `GC_106` are **dependent** + (running-αs #373 made dependent couplings event-by-event SIMD data in + `DependentCouplings_sv`), so they are not addressable as a fixed pointer in + `setIndependentCouplings`: + ``` + Parameters.cc: error: use of undeclared identifier 'GC_106' + ``` + The `cxtype* value[]` pointer mechanism is fundamentally incompatible with + per-event dependent couplings. This needs a different representation — e.g. + store a flavor → dependent-coupling *index* and have the consumer select the + per-event dependent coupling by flavor. + +3. **Per-flavor couplings are not emitted.** The FLV couplings actually used by + `p p > go go` (P1_QQx_gogo) are *all* single-leg and reference couplings such + as `GC_452`, `GC_223`, which are **not generated in `Parameters` at all**, so + the references would not even link. The merged-flavor model export must emit + the per-flavor couplings that `value[]` entries point at. + +The minimal first crash (#1) was masking #2 and #3. + +## Plan (for the Option A follow-up) + +1. [done] single-leg serialization in `write_flv_couplings`. +2. dependent flavored couplings (#2): redesign the FLV coupling value storage to + select a per-event dependent coupling by flavor index (not a fixed pointer). +3. ensure per-flavor couplings (#3) are generated/declared in `Parameters`. +4. single-leg consumer gating (#1 vertex side) in `get_coupling_def` (+ generic + aloha writers, + Fortran), threading which-leg-is-flavored to the writer. +5. validate `p p > go go`: generate `standalone_mg7`, build, and compare the + per-flavor |M|² (check_sa.exe matrix mode) against the Fortran + `output standalone` / `madevent` reference; add a regression test. + +## Open questions for review + +- Is full MSSM merged-flavor C++ support (items 2-4) in scope now, or should the + branch ship a clear "unsupported topology" guard (Option B) for models with + single-merged-leg / dependent flavored couplings while the feature is built? +- Preferred representation for per-event dependent flavored couplings. +- Whether `output` should keep defaulting to `mg7` while these gaps exist + (separate policy question; `output madevent`/`standalone` already work). diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index a82efae21..817c2c534 100755 --- a/madgraph/iolibs/export_cpp.py +++ b/madgraph/iolibs/export_cpp.py @@ -373,9 +373,49 @@ def write_set_parameters(self, params): return "\n".join(res_strings) + def _assert_flv_couplings_supported(self, params): + """Refuse, with a clear and actionable message, the merged-flavor + coupling structures the C++ (mg7/standalone_mg7) backend cannot yet + generate correctly, instead of crashing or emitting wrong/uncompilable + code: + + * a vertex with a number of merged-flavor legs other than two -- the + backend only models the two-leg "partner" topology (the SM q q~ V + case); MSSM gluino/chargino-squark-quark vertices have a single + merged leg; + * an event-by-event ("dependent", running-alphas) flavored coupling, + which cannot be referenced by the fixed value[] pointers that are + set once in setIndependentCouplings. + + The Fortran 'madevent'/'standalone' output supports these processes. + See docs/mg7_merged_flavor_mssm_design.md for the full plan to lift this + limitation. + """ + dep_names = set(c.name for c in getattr(self, 'coups_flv_dep', [])) + for coupl in params: + is_dep = coupl.name in dep_names + for key in coupl.flavors: + nb_merged = len([i for i in key if i != 0]) + if nb_merged == 2 and not is_dep: + continue + if is_dep: + reason = ("references an event-by-event (running-alphas) " + "coupling") + else: + reason = ("connects %d merged-flavor leg(s); only the " + "two-leg partner topology is supported" % nb_merged) + raise InvalidCmd( + "merged-flavor C++ output (mg7/standalone_mg7) does not yet " + "support this process: flavor coupling %s %s. This occurs " + "e.g. for MSSM gluino/chargino-squark-quark vertices. Use " + "'output madevent' or 'output standalone' for this process. " + "See docs/mg7_merged_flavor_mssm_design.md for details." + % (coupl.name, reason)) + def write_flv_couplings(self, params): """Write out the lines of independent parameters""" + self._assert_flv_couplings_supported(params) def_flv = [] # For each parameter, write name = expr; for coupl in params: @@ -383,7 +423,7 @@ def write_flv_couplings(self, params): # get first/second index k1, k2 = [i for i in key if i!=0] def_flv.append('%(name)s.partner[%(in)i] = %(out)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) - def_flv.append('%(name)s.partner2[%(out)i] = %(in)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) + def_flv.append('%(name)s.partner2[%(out)i] = %(in)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.val[%(in)i] = &%(coupl)s;' % {'name': coupl.name,'in': k1-1, 'coupl': c}) return "\n".join(def_flv) diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index 39de81b6c..1d55f9057 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -934,11 +934,18 @@ def write_parameters(self, params): def write_flv_couplings(self, params): """Write out the lines of independent parameters""" + # Refuse merged-flavor structures this backend cannot yet generate + # correctly (single-merged-leg vertices, e.g. MSSM + # gluino/chargino-squark-quark; event-by-event flavored couplings) with + # a clear message rather than crashing or emitting wrong/uncompilable + # code. See docs/mg7_merged_flavor_mssm_design.md. + self._assert_flv_couplings_supported(params) + def_flv = [] # For each parameter, write name = expr; for coupl in params: for key, c in coupl.flavors.items(): - # get first/second index + # get first/second index (two-leg "partner" topology) k1, k2 = [i for i in key if i!=0] def_flv.append('%(name)s.partner1[%(in)i] = %(out)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.partner2[%(out)i] = %(in)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) diff --git a/tests/acceptance_tests/test_cmd.py b/tests/acceptance_tests/test_cmd.py index 24b990fef..6c35b33a9 100755 --- a/tests/acceptance_tests/test_cmd.py +++ b/tests/acceptance_tests/test_cmd.py @@ -1407,6 +1407,32 @@ def get_values(output_format, check_exe): mg7 = get_values('standalone_mg7', './check_sa.exe') self._assert_me_lists_close(mg7, cpp) + def test_mg7_merged_flavor_unsupported_is_clean(self): + """The mg7/madmatrix C++ output must REFUSE, with a clear InvalidCmd, + the merged-flavor structures it cannot yet generate -- rather than + crashing with a cryptic unpack error or emitting uncompilable code. + + MSSM 'p p > go go' has gluino/chargino-squark-quark vertices where only + the light quark is in a merged group (a single merged-flavor leg), and + the relevant flavored couplings are event-by-event (running-alphas) + couplings; neither is supported by the two-merged-leg, fixed-pointer + FLV mechanism (see docs/mg7_merged_flavor_mssm_design.md). The Fortran + 'madevent' path handles the same process, so we also check that still + works. + """ + self.do('import model MSSM_SLHA2') + self.do('generate p p > go go') + # mg7 (default) and the explicit standalone_mg7 must both refuse cleanly. + self.assertRaises(InvalidCmd, self.do, + 'output standalone_mg7 %s -f' % self.out_dir) + if os.path.isdir(self.out_dir): + shutil.rmtree(self.out_dir) + # The Fortran madevent output supports the same process. + self.do('output madevent %s -f' % self.out_dir) + self.assertTrue( + os.path.isdir(os.path.join(self.out_dir, 'SubProcesses')), + 'madevent output should support MSSM p p > go go') + def test_standalone_density(self): """test that standalone density is working""" From 3de7092b6288ef4547667954011a8206b66a21c7 Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Wed, 24 Jun 2026 11:50:51 +0200 Subject: [PATCH 02/11] tests/CI: split MSSM p p > go go test into mg7 + madevent, wire into CI The combined guard test asserted both that standalone_mg7 refuses MSSM p p > go go and that madevent supports it. Split it so each side runs in its own CI workflow (there was no madevent equivalent before): * test_mssm_gogo_mg7_unsupported -> acceptancetest_mg7.yml (mg7/madmatrix must refuse the merged-flavor squark/gluino vertices with a clear InvalidCmd). * test_madevent_mssm_gogo -> acceptancetest_madevent.yml (the Fortran madevent output supports the same process; reference for the eventual mg7 fix). Both verified passing locally. --- .github/workflows/acceptancetest_madevent.yml | 15 ++++++++++ .github/workflows/acceptancetest_mg7.yml | 16 ++++++++++ tests/acceptance_tests/test_cmd.py | 29 ++++++++++++++----- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/.github/workflows/acceptancetest_madevent.yml b/.github/workflows/acceptancetest_madevent.yml index 5db4e5bb1..4b76eb9d6 100644 --- a/.github/workflows/acceptancetest_madevent.yml +++ b/.github/workflows/acceptancetest_madevent.yml @@ -983,3 +983,18 @@ jobs: cd $GITHUB_WORKSPACE cp input/.mg5_configuration_default.txt input/mg5_configuration.txt ./tests/test_manager.py test_density_mode_doublettbar -pA -t0 -l INFO + + acceptancetest_mssm_gogo: + # Fortran madevent supports MSSM p p > go go (merged-flavor squark/gluino + # vertices); the mg7/madmatrix counterpart cannot yet and refuses cleanly + # (test_mssm_gogo_mg7_unsupported in acceptancetest_mg7.yml). + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + - name: test one of the test test_madevent_mssm_gogo + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_madevent_mssm_gogo -pA -t0 -l INFO diff --git a/.github/workflows/acceptancetest_mg7.yml b/.github/workflows/acceptancetest_mg7.yml index a37a311c6..c62b2e78e 100644 --- a/.github/workflows/acceptancetest_mg7.yml +++ b/.github/workflows/acceptancetest_mg7.yml @@ -177,4 +177,20 @@ jobs: export PATH="$HOME/.cache/HEPtools/bin:$PATH" ./tests/test_manager.py test_generation_heft_mg7 -pA -t0 -l INFO + acceptancetest_mg7_mssm_gogo: + # mg7/madmatrix cannot yet generate the MSSM merged-flavor squark/gluino + # vertices (single-merged-leg / event-by-event flavored couplings); the + # output must refuse cleanly with InvalidCmd (not crash). The madevent + # counterpart is test_madevent_mssm_gogo in acceptancetest_madevent.yml. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + - name: test one of the test test_mssm_gogo_mg7_unsupported + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_mssm_gogo_mg7_unsupported -pA -t0 -l INFO + diff --git a/tests/acceptance_tests/test_cmd.py b/tests/acceptance_tests/test_cmd.py index 6c35b33a9..21b1a7e9f 100755 --- a/tests/acceptance_tests/test_cmd.py +++ b/tests/acceptance_tests/test_cmd.py @@ -1407,7 +1407,7 @@ def get_values(output_format, check_exe): mg7 = get_values('standalone_mg7', './check_sa.exe') self._assert_me_lists_close(mg7, cpp) - def test_mg7_merged_flavor_unsupported_is_clean(self): + def test_mssm_gogo_mg7_unsupported(self): """The mg7/madmatrix C++ output must REFUSE, with a clear InvalidCmd, the merged-flavor structures it cannot yet generate -- rather than crashing with a cryptic unpack error or emitting uncompilable code. @@ -1416,22 +1416,35 @@ def test_mg7_merged_flavor_unsupported_is_clean(self): the light quark is in a merged group (a single merged-flavor leg), and the relevant flavored couplings are event-by-event (running-alphas) couplings; neither is supported by the two-merged-leg, fixed-pointer - FLV mechanism (see docs/mg7_merged_flavor_mssm_design.md). The Fortran - 'madevent' path handles the same process, so we also check that still - works. + FLV mechanism (see docs/mg7_merged_flavor_mssm_design.md). + + The Fortran madevent counterpart (test_madevent_mssm_gogo) checks that + the same process is supported there. """ self.do('import model MSSM_SLHA2') self.do('generate p p > go go') - # mg7 (default) and the explicit standalone_mg7 must both refuse cleanly. self.assertRaises(InvalidCmd, self.do, 'output standalone_mg7 %s -f' % self.out_dir) - if os.path.isdir(self.out_dir): - shutil.rmtree(self.out_dir) - # The Fortran madevent output supports the same process. + + def test_madevent_mssm_gogo(self): + """The Fortran madevent output supports MSSM 'p p > go go' (merged-flavor + squark/gluino vertices with single-merged-leg / event-by-event flavored + couplings), which the mg7/madmatrix C++ output cannot yet generate + (test_mssm_gogo_mg7_unsupported). Acts as the madevent counterpart and a + reference for the eventual mg7 fix. + """ + self.do('import model MSSM_SLHA2') + self.do('generate p p > go go') self.do('output madevent %s -f' % self.out_dir) self.assertTrue( os.path.isdir(os.path.join(self.out_dir, 'SubProcesses')), 'madevent output should support MSSM p p > go go') + proc_root = os.path.join(self.out_dir, 'SubProcesses') + proc_dirs = [d for d in os.listdir(proc_root) + if d.startswith('P') and + os.path.isdir(os.path.join(proc_root, d))] + self.assertTrue(proc_dirs, + 'madevent produced no subprocess for p p > go go') def test_standalone_density(self): """test that standalone density is working""" From 7b8714f3b6b2c0b0e7b3d370b95760ce191e0c8b Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Wed, 24 Jun 2026 12:07:25 +0200 Subject: [PATCH 03/11] docs: record Fortran mechanism + single-leg consumer finding Generated the Fortran flavor_couplings.f and validated the single-leg attempt on p p > n1 n1 QCD=0 (single merged leg, independent EW couplings). Findings: - Fortran serializes a single merged leg as a two-leg partner with the unmerged leg at flavor index 1, and stores VAL as a pointer into the per-event coupling array (so dependent/running couplings work uniformly). - Serializing single-leg that way in mg7 compiles and runs but gives WRONG per-flavor |M|^2 (only flavor 0 matches Fortran). Root cause is the consumer gating in get_coupling_def (FFSxM): it is hard-wired to F1/F2 and assumes the merged fermion is F1, so it mis-indexes partner1 when the unmerged fermion is F1. The squark diagram masses are correct and goodhel is not the cause. The attempt was reverted; the guard still blocks single-leg so no wrong physics ships. Updated the Option A plan: single-leg needs a real consumer fix (parity / partner2, mirroring Fortran), then the dependent-coupling mechanism. --- docs/mg7_merged_flavor_mssm_design.md | 87 ++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md index 299199326..ae5aa1806 100644 --- a/docs/mg7_merged_flavor_mssm_design.md +++ b/docs/mg7_merged_flavor_mssm_design.md @@ -116,17 +116,86 @@ unpack reveals **three** independent problems; all are needed for MSSM: The minimal first crash (#1) was masking #2 and #3. +## How the Fortran/madevent side does it (the template to mirror) + +`output madevent`/`standalone` supports `p p > go go`. From the generated +`Source/MODEL/flavor_couplings.f`: + +```fortran +TYPE COUPPTR + DOUBLE COMPLEX, POINTER :: P +END TYPE +TYPE FLV_COUPLING + INTEGER :: PARTNER(4) + INTEGER :: PARTNER2(4) + TYPE(COUPPTR) :: VAL(4) ! pointer per flavor index +END TYPE +! single merged leg (quark flavor k), unmerged partner (gluino) carries flavor 1: +FLV_56(J)%PARTNER(3)=1 ; FLV_56(J)%PARTNER2(1)=3 ; FLV_56(J)%VAL(3)%P => GC_106(J) +``` + +Two ideas make it work: + +1. **Single-leg = two-leg with an unmerged partner of flavor 1.** Unmerged legs + carry flavor index 1 (Fortran) / 0 (C++, since `get_flavor_matrix` subtracts + 1). So a single-leg key `(k,0,..)` is serialized exactly like a two-leg one + with the partner flavor = 1: `partner1[k-1]=0, partner2[0]=k-1, value[k-1]=coup`. + Each squark is a separate diagram, gated by the merged-quark flavor. +2. **`VAL` is a pointer into the per-event coupling array** (`=> GC_106(J)`), so + running-αs ("dependent") couplings work uniformly with independent ones. + +## Empirical finding: serialization alone is NOT sufficient (consumer is wrong) + +An attempt that (a) serialized single-leg as above (`k2 = 1`) and (b) relaxed +the guard to allow single-leg *independent* couplings was validated on +`p p > n1 n1 QCD=0` (electroweak neutralino pair: single merged quark leg + +unmerged neutralino + squark, with **independent** couplings, so the dependent +gap is out of the way). It **compiles and runs** but gives **wrong** per-flavor +|M|² — only the first flavor matches: + +| flavor (q q~ > n1 n1) | standalone_mg7 | Fortran standalone | +|---|---|---| +| d d~ (idx 0) | 2.4022949e-05 | 2.4022949e-05 ✓ | +| u u~ (idx 1) | 3.5226867e-07 | 3.8121047e-04 ✗ | +| s s~ (idx 2) | 4.5463454e-07 | 2.4022949e-05 ✗ | +| c c~ (idx 3) | 3.5226867e-07 | 3.8121047e-04 ✗ | + +The generated cudacpp **does** emit separate, correctly-mass-ed squark diagrams +(`FFS1M_3(..., cIPD[3]=Msd1, ...)`, `cIPD[5]=Msd4`, `cIPD[9]=Msu1`, …), so the +propagator masses are fine and the goodhel-union filter is not the cause +(re-tested with it applied — no change). The bug is the **consumer gating** in +`MadMatrixALOHAWriter.get_coupling_def` (the `FFSxM` routine): it is hard-wired +to read `F1`/`F2` and assumes the *merged* fermion is `F1`. For these vertices +the unmerged fermion can be `F1` (flv_index 0), so `partner1[flv_index1]` indexes +the wrong slot and only flavor 0 (where `partner1[0]==0`) survives. Fortran +handles this by branching on the fermion *position parity* and using `PARTNER` +vs `PARTNER2` (aloha_writers.py ~757-786); the cudacpp port of that logic does +not correctly cover the single-merged-leg case. + +So the single-leg work is **(1) serialization [trivial, done in the attempt] + +(2) a real consumer fix** that picks the merged fermion's flavor (mirroring the +Fortran parity/`PARTNER2` branching) — NOT serialization alone. The attempt was +reverted; the guard still (correctly) blocks single-leg so no wrong physics +ships. + ## Plan (for the Option A follow-up) -1. [done] single-leg serialization in `write_flv_couplings`. -2. dependent flavored couplings (#2): redesign the FLV coupling value storage to - select a per-event dependent coupling by flavor index (not a fixed pointer). -3. ensure per-flavor couplings (#3) are generated/declared in `Parameters`. -4. single-leg consumer gating (#1 vertex side) in `get_coupling_def` (+ generic - aloha writers, + Fortran), threading which-leg-is-flavored to the writer. -5. validate `p p > go go`: generate `standalone_mg7`, build, and compare the - per-flavor |M|² (check_sa.exe matrix mode) against the Fortran - `output standalone` / `madevent` reference; add a regression test. +1. single-leg serialization in `write_flv_couplings` (trivial: unmerged partner + flavor = 1, i.e. the two-leg formula with `k2 = 1`). Validated structurally + against `flavor_couplings.f`. +2. **single-leg consumer fix** in `get_coupling_def` (+ the generic + `aloha_writers.py`): identify and index by the *merged* fermion leg (parity / + `partner2`), mirroring Fortran. This is the actual correctness fix; validate + on `p p > n1 n1 QCD=0` against the Fortran per-flavor |M|². +3. dependent flavored couplings: redesign the FLV value storage to select a + per-event dependent coupling by flavor index (idcoup into the per-event + `couplings` buffer + a `CD_ACCESS` `FLV_COUPLING` view), the analogue of + Fortran's `VAL%P => GC(J)`. This is the large piece and unblocks `p p > go go`. +4. relax the guard incrementally (single-leg once step 2 is validated; dependent + once step 3 is); keep it for any residue. +5. validate `p p > go go`: build `standalone_mg7`, compare per-flavor |M|² + against the `test_madevent_mssm_gogo` reference; convert + `test_mssm_gogo_mg7_unsupported` into a positive consistency check. ## Open questions for review From 15772303627b109ff617cc1518ae60b499fb0214 Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Wed, 24 Jun 2026 12:35:06 +0200 Subject: [PATCH 04/11] mg7 merged-flavor: support single-merged-leg flavored couplings (independent) Single-merged-leg vertices (one merged fermion + an unmerged partner + a squark/boson, e.g. the electroweak MSSM squark-quark-neutralino vertices) are now generated correctly by the mg7/madmatrix C++ backend, for flavor- independent couplings. Two parts, mirroring the Fortran side: - write_flv_couplings (export_cpp.py + madmatrix/model_handling.py): a single non-zero flavor key is serialized like Fortran flavor_couplings.f -- the unmerged partner is given flavor index 1, i.e. the two-leg formula with k2=1 (partner1[k-1]=0, partner2[0]=k-1, value[k-1]=&coup). - get_coupling_def (MadMatrixALOHAWriter): the flavored coupling is selected by the *merged* fermion leg. Unlike the Fortran routines (where F1 is the merged leg by argument-order convention), the cudacpp argument order can put the unmerged fermion first, so the consumer now picks whichever leg is the partner-populated (merged) one. The two-leg case (partner1[flv1]==flv2) reproduces the previous behaviour unchanged. The guard (_assert_flv_couplings_supported) is narrowed to allow one- and two-merged-leg INDEPENDENT couplings; it still raises a clear InvalidCmd for event-by-event ("dependent", running-alphas) flavored couplings (the remaining gap, e.g. SUSY-QCD gluino-squark-quark in p p > go go) and for >2 merged legs. Validated: p p > n1 n1 QCD=0 (t-channel squark, EW/independent) now reproduces the Fortran standalone per-flavor |M|^2 for ALL flavors (before the consumer fix only the first flavor matched). Regression test test_standalone_mg7_mssm_single_leg; the two-leg test_standalone_mg7_vs_cpp still passes. Co-Authored-By: Claude Opus 4.8 --- docs/mg7_merged_flavor_mssm_design.md | 75 ++++++++++++++++----------- madgraph/iolibs/export_cpp.py | 41 +++++++++------ madmatrix/model_handling.py | 23 ++++++-- tests/acceptance_tests/test_cmd.py | 53 +++++++++++++++++++ 4 files changed, 142 insertions(+), 50 deletions(-) diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md index ae5aa1806..bdb5ea814 100644 --- a/docs/mg7_merged_flavor_mssm_design.md +++ b/docs/mg7_merged_flavor_mssm_design.md @@ -1,35 +1,47 @@ # mg7/madmatrix merged-flavor support for single-merged-leg vertices (MSSM) -## Resolution (this PR): Option B — clean guard - -This PR ships **Option B**: the madmatrix (mg7/standalone_mg7) model export now -**refuses, with a clear and actionable error**, the merged-flavor configurations -it cannot yet generate correctly, instead of crashing or emitting -wrong/uncompilable code. Concretely, `UFOModelConverterCPP` gained -`_assert_flv_couplings_supported`, called from both `write_flv_couplings` -copies (`export_cpp.py` and `madmatrix/model_handling.py`); it raises -`InvalidCmd` when a used flavored coupling either connects a number of merged -legs other than two, or is an event-by-event ("dependent", running-αs) coupling. - -Result for `generate p p > go go; output standalone_mg7`: -``` -Command "output standalone_mg7 ..." interrupted with error: -InvalidCmd : merged-flavor C++ output (mg7/standalone_mg7) does not yet support -this process: flavor coupling FLV_54 references an event-by-event (running-alphas) -coupling. ... Use 'output madevent' or 'output standalone' for this process. -``` -The guard is scoped to the process's *used* couplings (`coups_flv_dep/indep` are -filtered to `wanted_couplings`), so valid SM two-leg merged cases (e.g. -`u u~ > j j QCD=0`) still generate. `output madevent`/`standalone` keep working. +## Status (this PR) + +Two things ship here: + +1. **Single-merged-leg flavored couplings are now supported** (the MSSM + topology: one merged fermion + an unmerged partner + a squark/boson), for + flavor-*independent* couplings. Two parts: + - **serialization** (`write_flv_couplings`, both copies): a single-leg key is + written exactly like the Fortran side — the unmerged partner gets flavor + index 1, i.e. the two-leg formula with `k2 = 1`. + - **consumer fix** (`MadMatrixALOHAWriter.get_coupling_def`): the flavored + coupling is selected by the *merged* fermion leg, which (unlike Fortran) + can be either `F1` or `F2` in the cudacpp argument order — pick whichever + leg is the partner-populated one. + + Validated: `p p > n1 n1 QCD=0` (t-channel squark, EW/independent couplings) + now reproduces the Fortran standalone per-flavor |M|² for **all** flavors + (before the consumer fix only flavor 0 matched). Regression test: + `test_standalone_mg7_mssm_single_leg`. The two-leg path is unchanged + (`test_standalone_mg7_vs_cpp` still passes). + +2. **A guard for the remaining gap.** `UFOModelConverterCPP. + _assert_flv_couplings_supported` (called from both `write_flv_couplings`) + still raises a clear `InvalidCmd` for the cases not yet handled — an + event-by-event ("dependent", running-αs) flavored coupling, or a vertex with + >2 merged legs. So `generate p p > go go; output standalone_mg7` still fails + cleanly (its SUSY-QCD couplings are dependent): + ``` + InvalidCmd : merged-flavor C++ output (mg7/standalone_mg7) does not yet support + this process: flavor coupling FLV_54 references an event-by-event (running-alphas) + coupling. ... Use 'output madevent' or 'output standalone' for this process. + ``` + The guard is scoped to the process's *used* couplings, so SM two-leg cases + and single-leg independent cases generate; `output madevent`/`standalone` + keep working. -The full feature (Option A) is **deferred**; the diagnosis and plan below are -kept for the follow-up. The rest of this note describes Option A. +**Remaining for full MSSM (`p p > go go`): the dependent-coupling mechanism** +(Step 3 below) — the large piece. --- -Status of Option A: **deferred / not implemented**. This note records the -diagnosis, the discovered scope, and the implementation plan, so a follow-up PR -is self-contained. +The rest of this note records the diagnosis and the remaining plan. ## Symptom @@ -172,11 +184,12 @@ handles this by branching on the fermion *position parity* and using `PARTNER` vs `PARTNER2` (aloha_writers.py ~757-786); the cudacpp port of that logic does not correctly cover the single-merged-leg case. -So the single-leg work is **(1) serialization [trivial, done in the attempt] + -(2) a real consumer fix** that picks the merged fermion's flavor (mirroring the -Fortran parity/`PARTNER2` branching) — NOT serialization alone. The attempt was -reverted; the guard still (correctly) blocks single-leg so no wrong physics -ships. +So the single-leg work is **(1) serialization [trivial] + (2) a real consumer +fix** that picks the merged fermion's flavor — NOT serialization alone. +**Both are now done** (see the Status section at the top): the consumer selects +whichever fermion leg is the partner-populated (merged) one, and +`p p > n1 n1 QCD=0` matches Fortran for all flavors. The guard now only blocks +the dependent-coupling case (Step 3). ## Plan (for the Option A follow-up) diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index 817c2c534..71c9cdca5 100755 --- a/madgraph/iolibs/export_cpp.py +++ b/madgraph/iolibs/export_cpp.py @@ -377,37 +377,44 @@ def _assert_flv_couplings_supported(self, params): """Refuse, with a clear and actionable message, the merged-flavor coupling structures the C++ (mg7/standalone_mg7) backend cannot yet generate correctly, instead of crashing or emitting wrong/uncompilable - code: + code. + + Supported: one- and two-merged-leg "partner" vertices with + flavor-*independent* couplings. Single-merged-leg vertices (one merged + fermion + an unmerged partner, e.g. the electroweak MSSM + squark-quark-neutralino vertices) are serialized like the Fortran side + (the unmerged partner is given flavor index 1) and gated by the merged + leg (see get_coupling_def). + + Not yet supported (raises): - * a vertex with a number of merged-flavor legs other than two -- the - backend only models the two-leg "partner" topology (the SM q q~ V - case); MSSM gluino/chargino-squark-quark vertices have a single - merged leg; * an event-by-event ("dependent", running-alphas) flavored coupling, which cannot be referenced by the fixed value[] pointers that are - set once in setIndependentCouplings. + set once in setIndependentCouplings -- this is the remaining gap, + e.g. for the SUSY-QCD MSSM gluino-squark-quark vertices; + * a vertex with more than two merged-flavor legs (never seen so far). - The Fortran 'madevent'/'standalone' output supports these processes. - See docs/mg7_merged_flavor_mssm_design.md for the full plan to lift this - limitation. + The Fortran 'madevent'/'standalone' output supports the remaining cases. + See docs/mg7_merged_flavor_mssm_design.md for the plan to lift the + dependent-coupling limitation. """ dep_names = set(c.name for c in getattr(self, 'coups_flv_dep', [])) for coupl in params: is_dep = coupl.name in dep_names for key in coupl.flavors: nb_merged = len([i for i in key if i != 0]) - if nb_merged == 2 and not is_dep: + if nb_merged in (1, 2) and not is_dep: continue if is_dep: reason = ("references an event-by-event (running-alphas) " "coupling") else: - reason = ("connects %d merged-flavor leg(s); only the " - "two-leg partner topology is supported" % nb_merged) + reason = ("connects %d merged-flavor legs; only one or two " + "are supported" % nb_merged) raise InvalidCmd( "merged-flavor C++ output (mg7/standalone_mg7) does not yet " "support this process: flavor coupling %s %s. This occurs " - "e.g. for MSSM gluino/chargino-squark-quark vertices. Use " + "e.g. for SUSY-QCD MSSM gluino-squark-quark vertices. Use " "'output madevent' or 'output standalone' for this process. " "See docs/mg7_merged_flavor_mssm_design.md for details." % (coupl.name, reason)) @@ -420,8 +427,12 @@ def write_flv_couplings(self, params): # For each parameter, write name = expr; for coupl in params: for key, c in coupl.flavors.items(): - # get first/second index - k1, k2 = [i for i in key if i!=0] + nonzero = [i for i in key if i != 0] + if len(nonzero) == 2: + k1, k2 = nonzero + else: + # single merged leg: unmerged partner has flavor index 1 + k1 = nonzero[0]; k2 = 1 def_flv.append('%(name)s.partner[%(in)i] = %(out)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.partner2[%(out)i] = %(in)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.val[%(in)i] = &%(coupl)s;' % {'name': coupl.name,'in': k1-1, 'coupl': c}) diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index 1d55f9057..f4edcaa73 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -453,7 +453,17 @@ def get_coupling_def(self): out.write(' return;\n') out.write(' }\n') if nb_coupling == 1: - out.write(' if(MCOUP.partner1[flv_index1] != flv_index2) {\n') + # A flavored coupling is indexed by the *merged* fermion leg; for + # a single merged leg the unmerged partner carries flavor index 0 + # and the merged leg can be either F1 or F2 (the cudacpp argument + # order is not guaranteed to put the merged leg first, unlike the + # Fortran side). Pick whichever leg is the merged (partner- + # populated) one. For the two-leg case partner1[flv1]==flv2 holds + # and flv_sel==flv_index1, reproducing the old behaviour. + out.write(' int flv_sel = -1;\n') + out.write(' if(MCOUP.partner1[flv_index1] == flv_index2) flv_sel = flv_index1;\n') + out.write(' else if(MCOUP.partner1[flv_index2] == flv_index1) flv_sel = flv_index2;\n') + out.write(' if(flv_sel == -1) {\n') out.write(' %s\n' % fail) out.write(' return;\n') out.write(' }\n') @@ -467,7 +477,7 @@ def get_coupling_def(self): # the coupling is a complex number but in this case it is represented as a sequence of real numbers # so, when we need to shift within the array, we need to double the shift width to account for # both real and imaginary parts - out.write(' COUP = C_ACCESS::kernelAccessConst( MCOUP.value + 2*flv_index1 );\n') + out.write(' COUP = C_ACCESS::kernelAccessConst( MCOUP.value + 2*flv_sel );\n') else: for i in range(1,nb_coupling+1): # the coupling is a complex number but in this case it is represented as a sequence of real numbers @@ -945,8 +955,13 @@ def write_flv_couplings(self, params): # For each parameter, write name = expr; for coupl in params: for key, c in coupl.flavors.items(): - # get first/second index (two-leg "partner" topology) - k1, k2 = [i for i in key if i!=0] + nonzero = [i for i in key if i != 0] + if len(nonzero) == 2: + k1, k2 = nonzero + else: + # single merged leg: unmerged partner has flavor index 1 + # (mirror Fortran flavor_couplings.f) + k1 = nonzero[0]; k2 = 1 def_flv.append('%(name)s.partner1[%(in)i] = %(out)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.partner2[%(out)i] = %(in)i;' % {'name': coupl.name,'in': k1-1, 'out': k2-1}) def_flv.append('%(name)s.value[%(in)i] = &%(coupl)s;' % {'name': coupl.name,'in': k1-1, 'coupl': c}) diff --git a/tests/acceptance_tests/test_cmd.py b/tests/acceptance_tests/test_cmd.py index 21b1a7e9f..a266de5d2 100755 --- a/tests/acceptance_tests/test_cmd.py +++ b/tests/acceptance_tests/test_cmd.py @@ -1407,6 +1407,59 @@ def get_values(output_format, check_exe): mg7 = get_values('standalone_mg7', './check_sa.exe') self._assert_me_lists_close(mg7, cpp) + def test_standalone_mg7_mssm_single_leg(self): + """Single-merged-leg flavored couplings must give the same per-flavor + |M|^2 in standalone_mg7 (madmatrix) as in the Fortran standalone. + + p p > n1 n1 QCD=0 is a t-channel-squark process with single-merged-leg + vertices (one merged light quark + an unmerged neutralino + a squark) + whose flavored couplings are *independent* (electroweak), isolating the + single-leg consumer-gating fix (the get_coupling_def merged-leg + selection) from the still-guarded dependent-coupling case. Without the + fix only the first flavor matches; with it all flavors do. + """ + energy = '1000' + devnull = open(os.devnull, 'w') + me_re = re.compile(r'Matrix element\s*=\s*([\d.eE+-]+)\s*GeV', + re.IGNORECASE) + + def get_values(output_format, check_exe, build_source=False): + if os.path.isdir(self.out_dir): + shutil.rmtree(self.out_dir) + self.do('output %s %s -f' % (output_format, self.out_dir)) + if build_source: + subprocess.call(['make'], stdout=devnull, stderr=devnull, + cwd=os.path.join(self.out_dir, 'Source')) + proc_root = os.path.join(self.out_dir, 'SubProcesses') + dirs = sorted(d for d in os.listdir(proc_root) + if d.startswith('P') and + os.path.isdir(os.path.join(proc_root, d))) + self.assertTrue(dirs, 'no subprocess for %s' % output_format) + values = [] + for d in dirs: + proc_dir = os.path.join(proc_root, d) + target = ['make', 'check'] if output_format == 'standalone' \ + else ['make'] + subprocess.call(target, stdout=devnull, stderr=devnull, + cwd=proc_dir) + log = os.path.join(proc_dir, 'check.log') + subprocess.call('%s %s' % (check_exe, energy), + stdout=open(log, 'w'), stderr=subprocess.STDOUT, + cwd=proc_dir, shell=True) + found = me_re.findall(open(log).read()) + self.assertTrue(found, '%s produced no matrix element (see %s)' + % (output_format, log)) + values.extend(float(v) for v in found) + return values + + self.do('import model MSSM_SLHA2') + self.do('generate p p > n1 n1 QCD=0') + mg7 = get_values('standalone_mg7', './check_sa.exe') + standalone = get_values('standalone', './check', build_source=True) + self.assertTrue(any(v != 0.0 for v in standalone), + 'all matrix elements vanished for p p > n1 n1') + self._assert_me_lists_close(mg7, standalone, rtol=1e-4) + def test_mssm_gogo_mg7_unsupported(self): """The mg7/madmatrix C++ output must REFUSE, with a clear InvalidCmd, the merged-flavor structures it cannot yet generate -- rather than From 1d6b170ee531f8f5ccb69f56fc3cb1bfb28d49f7 Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Wed, 24 Jun 2026 12:49:26 +0200 Subject: [PATCH 05/11] docs: refined Step 3 plan (per-event gather for dependent flavored couplings) Co-Authored-By: Claude Opus 4.8 --- docs/mg7_merged_flavor_mssm_design.md | 75 ++++++++++++++++++--------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md index bdb5ea814..9e7a49610 100644 --- a/docs/mg7_merged_flavor_mssm_design.md +++ b/docs/mg7_merged_flavor_mssm_design.md @@ -191,30 +191,55 @@ whichever fermion leg is the partner-populated (merged) one, and `p p > n1 n1 QCD=0` matches Fortran for all flavors. The guard now only blocks the dependent-coupling case (Step 3). -## Plan (for the Option A follow-up) - -1. single-leg serialization in `write_flv_couplings` (trivial: unmerged partner - flavor = 1, i.e. the two-leg formula with `k2 = 1`). Validated structurally - against `flavor_couplings.f`. -2. **single-leg consumer fix** in `get_coupling_def` (+ the generic - `aloha_writers.py`): identify and index by the *merged* fermion leg (parity / - `partner2`), mirroring Fortran. This is the actual correctness fix; validate - on `p p > n1 n1 QCD=0` against the Fortran per-flavor |M|². -3. dependent flavored couplings: redesign the FLV value storage to select a - per-event dependent coupling by flavor index (idcoup into the per-event - `couplings` buffer + a `CD_ACCESS` `FLV_COUPLING` view), the analogue of - Fortran's `VAL%P => GC(J)`. This is the large piece and unblocks `p p > go go`. -4. relax the guard incrementally (single-leg once step 2 is validated; dependent - once step 3 is); keep it for any residue. -5. validate `p p > go go`: build `standalone_mg7`, compare per-flavor |M|² - against the `test_madevent_mssm_gogo` reference; convert +## Plan + +1. [done] single-leg serialization in `write_flv_couplings`. +2. [done] single-leg consumer fix in `get_coupling_def` (select by the merged + fermion leg). Validated on `p p > n1 n1 QCD=0`. +3. **dependent flavored couplings (remaining, the large piece)** — see below. +4. relax the guard for dependent once step 3 is validated. +5. validate `p p > go go` vs the `test_madevent_mssm_gogo` reference; convert `test_mssm_gogo_mg7_unsupported` into a positive consistency check. -## Open questions for review - -- Is full MSSM merged-flavor C++ support (items 2-4) in scope now, or should the - branch ship a clear "unsupported topology" guard (Option B) for models with - single-merged-leg / dependent flavored couplings while the feature is built? -- Preferred representation for per-event dependent flavored couplings. -- Whether `output` should keep defaulting to `mg7` while these gaps exist - (separate policy question; `output madevent`/`standalone` already work). +## Step 3 design: dependent (event-by-event) flavored couplings + +Today the cudacpp FLV mechanism (`model_handling.py` ~1591-1660 + the +`CPPProcess.cc` template) **bakes** the per-flavor coupling values into a +constant device array `cIPF_value[nIPF*nMF*2]` at construction +(`tIPF_value[..] = *tFLV[i].value[j]`), and the vertex routines read a value- +based `FLV_COUPLING_VIEW`. This works only for *independent* couplings; a +running-αs coupling like `GC_106` changes per event and cannot be a fixed +pointer/constant (and isn't even in scope in `setIndependentCouplings`). + +**Chosen approach — per-event gather, reuse the consumer.** The dependent +couplings are already computed per event into `allcouplings` and exposed in the +kernel as `allCOUPs[idcoup]` (`CD_ACCESS::idcoupAccessBufferConst`). So for a +dependent flavored coupling we keep an `idcoup` per (coupling, flavor) slot and, +**per event page in `calculate_jamps`, gather** the current values into a +`dpf_value[nDPF*nMF*2]` array, then build an ordinary value-based +`FLV_COUPLING_VIEW` over it. The vertex routines and `get_coupling_def` are +**unchanged** (they already consume a value-based view) — this is the key +simplification, and it is the direct analogue of Fortran's `VAL%P => GC(J)`. + +Concrete surface: + +1. **Split** `couporderflv` into independent (existing `cIPF`) and dependent + (`cDPF`) flavored couplings (using `coups_flv_dep`). +2. **Model-side** (`model_handling.py`): emit `cDPF_partner1/2[nDPF*nMF]` (as + today) plus `cDPF_idcoup[nDPF*nMF]` — the `idcoup` of the dependent coupling + each `value[j]` slot points at (the position of that GC in `coups_dep`; + `value[j]` is null for unused slots → idcoup `-1`). +3. **Kernel** (`CPPProcess.cc`/`process_function_definitions.inc`): right after + `allCOUPs` is set up, gather + `dpf_value[i*nMF+j] = (cDPF_idcoup[i*nMF+j] >= 0) ? COUPs[cDPF_idcoup[i*nMF+j]] : 0` + (per event / SIMD page), then + `FLV_COUPLING_ARRAY flvCOUPs_dep{ cDPF_partner1, cDPF_partner2, dpf_value }`. +4. **Routing** (`model_handling.py` helas-call writer, ~2251-2287): a dependent + flavored coupling resolves to `flvCOUPs_dep[idx]` instead of `flvCOUPs[idx]`. +5. **Guard**: drop the `is_dep` rejection in `_assert_flv_couplings_supported`. +6. **Validate**: `p p > go go` standalone_mg7 vs the Fortran reference + (`test_madevent_mssm_gogo`); flip `test_mssm_gogo_mg7_unsupported` to a + positive consistency test. + +Risk: this is the deepest cudacpp codegen change (touches the SIMD/CUDA gather +and the per-coupling routing); needs CPU **and** GPU validation. From e10960b02ad31685e8dad6b65828bf58f7697d89 Mon Sep 17 00:00:00 2001 From: Daniele Massaro Date: Wed, 24 Jun 2026 15:59:28 +0200 Subject: [PATCH 06/11] mg7 merged-flavor: add per-event AoSoA plumbing for flavored couplings C++ template-side foundation for dependent (event-by-event, running-alphas) flavored couplings. Inert until the codegen emits the new arrays. - MemoryAccessCouplings.h: flv_stride = nx2*neppC (per-event AoSoA record). - MemoryAccessCouplingsFixed.h: flv_stride = nx2 (scalar complex, broadcast). - cpp_hel_amps_h.inc: FLV_COUPLING_ARRAY gains an FSTRIDE template param so the per-flavor value offset is i*FSTRIDE*STRIDE. - process_function_definitions.inc: nDPF + %(cdpfdecl)s placeholders. Co-Authored-By: Claude Opus 4.8 --- .../template_files/madmatrix/MemoryAccessCouplings.h | 5 +++++ .../madmatrix/MemoryAccessCouplingsFixed.h | 4 ++++ .../iolibs/template_files/madmatrix/cpp_hel_amps_h.inc | 9 +++++++-- .../madmatrix/process_function_definitions.inc | 9 +++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplings.h b/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplings.h index a12433c87..fd3fb80c6 100644 --- a/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplings.h +++ b/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplings.h @@ -164,6 +164,11 @@ namespace mg5amcCpu static constexpr auto idcoupAccessBuffer = MemoryAccessCouplingsBase::idcoupAccessBuffer; static constexpr auto idcoupAccessBufferConst = MemoryAccessCouplingsBase::idcoupAccessBufferConst; + // Per-flavor stride (in fptype's) between two consecutive flavor slots of a flavored coupling value buffer. + // For dependent (event-by-event, running-alphas) couplings the value is an AOSOA record [nx2][neppC] + // (real and imaginary SIMD lanes), so consecutive flavor slots are nx2*neppC fptype's apart. + static constexpr int flv_stride = MemoryAccessCouplingsBase::neppC * mgOnGpu::nx2; + // Expose selected functions from MemoryAccessCouplings static constexpr auto ieventAccessRecordConst = MemoryAccessCouplings::ieventAccessRecordConst; diff --git a/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplingsFixed.h b/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplingsFixed.h index 79de14ae9..757de7b6f 100644 --- a/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplingsFixed.h +++ b/madgraph/iolibs/template_files/madmatrix/MemoryAccessCouplingsFixed.h @@ -60,6 +60,10 @@ namespace mg5amcCpu // Expose selected functions from MemoryAccessCouplingsFixedBase static constexpr auto iicoupAccessBufferConst = MemoryAccessCouplingsFixedBase::iicoupAccessBufferConst; + // Per-flavor stride (in fptype's) between two consecutive flavor slots of a flavored coupling value buffer. + // For fixed (independent) couplings the value is a single scalar complex (real,imag): nx2 fptype's, broadcast across the SIMD vector. + static constexpr int flv_stride = mgOnGpu::nx2; + // Locate a field (output) in a memory buffer (input) from a kernel event-indexing mechanism (internal) and the given field indexes (input) // [Signature (const, SCALAR OR VECTOR) ===> cxtype_sv kernelAccessConst( const fptype* buffer ) <===] static __host__ __device__ inline const cxtype_sv diff --git a/madgraph/iolibs/template_files/madmatrix/cpp_hel_amps_h.inc b/madgraph/iolibs/template_files/madmatrix/cpp_hel_amps_h.inc index 2bf8a1759..8dde0fff4 100644 --- a/madgraph/iolibs/template_files/madmatrix/cpp_hel_amps_h.inc +++ b/madgraph/iolibs/template_files/madmatrix/cpp_hel_amps_h.inc @@ -59,11 +59,16 @@ namespace mg5amcCpu : partner1(p1), partner2(p2), value(v) {} }; - template + // FSTRIDE is the number of fptype's used to store one flavor slot of the value buffer: + // - independent (fixed) flavored couplings: FSTRIDE = nx2 = 2 (a single scalar complex, broadcast across the SIMD vector) + // - dependent (event-by-event, running-alphas) flavored couplings: FSTRIDE = nx2*neppC (an AOSOA SIMD record) + // It must match C_ACCESS::flv_stride of the access type the consuming vertex routine is instantiated with. + template class FLV_COUPLING_ARRAY { static_assert(SIZE >= 0, "flvCOUPs SIZE must be non-negative"); static_assert(STRIDE > 0, "flvCOUPs STRIDE must be positive"); + static_assert(FSTRIDE > 0, "flvCOUPs FSTRIDE must be positive"); const int* const partner1; const int* const partner2; const fptype* const value; @@ -78,7 +83,7 @@ namespace mg5amcCpu return FLV_COUPLING_VIEW{ partner1 + i*STRIDE, partner2 + i*STRIDE, - value + i*2*STRIDE + value + i*FSTRIDE*STRIDE }; } }; diff --git a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc index a1e961c88..1ba9c07b7 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc @@ -109,11 +109,14 @@ namespace mg5amcCpu // nIPF are the number of SM independent flavor couplings, of type FLV_COUPLING constexpr int nMF = FLV_COUPLING::max_flavor; constexpr int nIPF = %(nipf)i; + // nDPF are the number of dependent (event-by-event, running-alphas) flavor couplings + constexpr int nDPF = %(ndpf)i; static_assert( nIPC <= nicoup ); static_assert( nIPD >= 0 ); // Hack to avoid build warnings when nIPD==0 is unused static_assert( nIPC >= 0 ); // Hack to avoid build warnings when nIPC==0 is unused static_assert( nMF >= 0 ); // Hack to avoid build warnings when nMF ==0 is unused static_assert( nIPF >= 0 ); // Hack to avoid build warnings when nIPF==0 is unused + static_assert( nDPF >= 0 ); // Hack to avoid build warnings when nDPF==0 is unused #ifdef MGONGPU_HARDCODE_PARAM %(cipdhrdcod)s %(cipchrdcod)s @@ -130,6 +133,12 @@ namespace mg5amcCpu #endif #endif + // Dependent (event-by-event, running-alphas) flavor couplings: partner indices and + // the per-flavor idcoup are pure compile-time constants (the complex values are + // gathered per event page in calculate_jamps), so they are emitted the same way in + // all build modes (no CUDA constant memory copy needed). + %(cdpfdecl)s + // AV Jan 2024 (PR #625): this ugly #define was the only way I found to avoid creating arrays[nBsm] in CPPProcess.cc if nBsm is 0 // The problem is that nBsm is determined when generating Parameters.h, which happens after CPPProcess.cc has already been generated // For simplicity, keep this code hardcoded also for SM processes (a nullptr is needed as in the case nBsm == 0) From 2b712328801c67353ffd3a285d80225f65e8ec3c Mon Sep 17 00:00:00 2001 From: Daniele Massaro Date: Wed, 24 Jun 2026 15:59:28 +0200 Subject: [PATCH 07/11] mg7 merged-flavor: support dependent (running-alphas) flavored couplings Step 3 of the MSSM merged-flavor plan: SUSY-QCD squark-quark-gluino vertices (GC_106/GC_110 ~ g_s) are running-alphas couplings and cannot be addressed by the fixed value[] pointers of setIndependentCouplings. They are gathered per event page into an AoSoA dpf_value buffer and consumed by the *same* templated vertex routines instantiated with CD_ACCESS instead of CI_ACCESS -- the direct analogue of Fortran's FLV_xx%VAL(k)%P => GC_yyy(J). - model_handling.py: format_coupling splits flavored couplings into independent (flvCOUPs / couporderflv, CI_ACCESS) vs dependent (flvCOUPs_dep / couporderflv_dep, CD_ACCESS) via model.is_running_coupling; get_process_function_definitions emits cDPF_partner1/2 + cDPF_idcoup (symbolic Parameters_dependentCouplings::idcoup_) and the per-event gather into flvCOUPs_dep; get_coupling_def uses C_ACCESS::flv_stride instead of the hardcoded 2; set_flv_couplings serializes only independent couplings. - export_cpp.py: drop the is_dep guard (now only >2 merged legs is rejected); write_flv_couplings restricted to independent flavored couplings. Co-Authored-By: Claude Opus 4.8 --- madgraph/iolibs/export_cpp.py | 41 +++++------ madmatrix/model_handling.py | 127 ++++++++++++++++++++++++++++++---- 2 files changed, 130 insertions(+), 38 deletions(-) diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index 71c9cdca5..b9b51f549 100755 --- a/madgraph/iolibs/export_cpp.py +++ b/madgraph/iolibs/export_cpp.py @@ -306,8 +306,10 @@ def generate_parameters_class_files(self): self.write_set_parameters(self.params_dep) replace_dict['set_dependent_couplings'] = \ self.write_set_parameters(list(self.coups_dep.values())) + # Only independent flavored couplings use the FLV_COUPLING value[] pointer mechanism; + # dependent (running-alphas) ones are gathered event-by-event (see model_handling / Step 3). replace_dict['set_flv_couplings'] = \ - self.write_flv_couplings(self.coups_flv_dep+self.coups_flv_indep) + self.write_flv_couplings(self.coups_flv_indep) replace_dict['print_independent_parameters'] = \ self.write_print_parameters(self.params_indep) @@ -379,45 +381,34 @@ def _assert_flv_couplings_supported(self, params): generate correctly, instead of crashing or emitting wrong/uncompilable code. - Supported: one- and two-merged-leg "partner" vertices with - flavor-*independent* couplings. Single-merged-leg vertices (one merged - fermion + an unmerged partner, e.g. the electroweak MSSM - squark-quark-neutralino vertices) are serialized like the Fortran side - (the unmerged partner is given flavor index 1) and gated by the merged - leg (see get_coupling_def). + Supported: one- and two-merged-leg "partner" vertices, with either + flavor-*independent* or *dependent* (event-by-event, running-alphas) + couplings. Single-merged-leg vertices (one merged fermion + an unmerged + partner, e.g. the electroweak MSSM squark-quark-neutralino vertices) are + serialized like the Fortran side (the unmerged partner is given flavor + index 1) and gated by the merged leg (see get_coupling_def). Dependent + flavored couplings (e.g. the SUSY-QCD MSSM gluino-squark-quark vertices) + are gathered event-by-event into cDPF_* / flvCOUPs_dep (Step 3). Not yet supported (raises): - * an event-by-event ("dependent", running-alphas) flavored coupling, - which cannot be referenced by the fixed value[] pointers that are - set once in setIndependentCouplings -- this is the remaining gap, - e.g. for the SUSY-QCD MSSM gluino-squark-quark vertices; * a vertex with more than two merged-flavor legs (never seen so far). The Fortran 'madevent'/'standalone' output supports the remaining cases. - See docs/mg7_merged_flavor_mssm_design.md for the plan to lift the - dependent-coupling limitation. + See docs/mg7_merged_flavor_mssm_design.md. """ - dep_names = set(c.name for c in getattr(self, 'coups_flv_dep', [])) for coupl in params: - is_dep = coupl.name in dep_names for key in coupl.flavors: nb_merged = len([i for i in key if i != 0]) - if nb_merged in (1, 2) and not is_dep: + if nb_merged in (1, 2): continue - if is_dep: - reason = ("references an event-by-event (running-alphas) " - "coupling") - else: - reason = ("connects %d merged-flavor legs; only one or two " - "are supported" % nb_merged) raise InvalidCmd( "merged-flavor C++ output (mg7/standalone_mg7) does not yet " - "support this process: flavor coupling %s %s. This occurs " - "e.g. for SUSY-QCD MSSM gluino-squark-quark vertices. Use " + "support this process: flavor coupling %s connects %d " + "merged-flavor legs; only one or two are supported. Use " "'output madevent' or 'output standalone' for this process. " "See docs/mg7_merged_flavor_mssm_design.md for details." - % (coupl.name, reason)) + % (coupl.name, nb_merged)) def write_flv_couplings(self, params): """Write out the lines of independent parameters""" diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index f4edcaa73..7e6ed0338 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -477,13 +477,16 @@ def get_coupling_def(self): # the coupling is a complex number but in this case it is represented as a sequence of real numbers # so, when we need to shift within the array, we need to double the shift width to account for # both real and imaginary parts - out.write(' COUP = C_ACCESS::kernelAccessConst( MCOUP.value + 2*flv_sel );\n') + # the per-flavor stride is C_ACCESS::flv_stride (nx2 for independent + # couplings = scalar broadcast; nx2*neppC for dependent ones = AOSOA + # SIMD record), so the same routine body works for both instantiations + out.write(' COUP = C_ACCESS::kernelAccessConst( MCOUP.value + C_ACCESS::flv_stride*flv_sel );\n') else: for i in range(1,nb_coupling+1): # the coupling is a complex number but in this case it is represented as a sequence of real numbers # so, when we need to shift within the array, we need to double the shift width to account for # both real and imaginary parts - out.write(' if(zero_coup%i ==0) { COUP%i = C_ACCESS::kernelAccessConst( MCOUP%i.value + 2*flv_index1 ); }\n' % (i,i,i)) + out.write(' if(zero_coup%i ==0) { COUP%i = C_ACCESS::kernelAccessConst( MCOUP%i.value + C_ACCESS::flv_stride*flv_index1 ); }\n' % (i,i,i)) else: incoming = [i+1 for i in range(len(self.particles)) if i+1 != self.outgoing and self.particles[self.outgoing-1] == 'F'][0] if incoming %2 == 1: @@ -532,7 +535,7 @@ def get_coupling_def(self): # the coupling is a complex number but in this case it is represented as a sequence of real numbers # so, when we need to shift within the array, we need to double the shift width to account for # both real and imaginary parts - out.write(' %s = C_ACCESS::kernelAccessConst( M%s.value + 2*flv_index1 );\n' % (name, name)) + out.write(' %s = C_ACCESS::kernelAccessConst( M%s.value + C_ACCESS::flv_stride*flv_index1 );\n' % (name, name)) return out.getvalue() # AV - modify aloha_writers.ALOHAWriterForCPP method (improve formatting) @@ -1165,7 +1168,12 @@ def super_generate_parameters_class_files(self): replace_dict['set_independent_parameters'] = '\n'.join( set_params_indep ) replace_dict['set_independent_parameters'] += self.super_write_set_parameters_onlyfixMajorana( hardcoded=False ) # add fixes for Majorana particles only in the aS-indep parameters #622 replace_dict['set_independent_parameters'] += '\n // BSM parameters that do not depend on alphaS but are needed in the computation of alphaS-dependent couplings;' # NB this is now done also for 'sm' processes (no check on model name, see PR #824) - replace_dict['set_flv_couplings'] = self.write_flv_couplings(self.coups_flv_dep+self.coups_flv_indep) + # Only the independent flavored couplings are serialized via the FLV_COUPLING + # value[] pointer mechanism in setIndependentCouplings: a dependent (running-alphas) + # coupling is not addressable as a fixed pointer here (it is computed event-by-event). + # Dependent flavored couplings are handled separately via cDPF_* + the per-event + # gather in calculate_jamps (see get_process_function_definitions / Step 3). + replace_dict['set_flv_couplings'] = self.write_flv_couplings(self.coups_flv_indep) if len(bsmparam_indep_real_used) + len(bsmparam_indep_complex_used) > 0: for ipar, par in enumerate( bsmparam_indep_real_used ): replace_dict['set_independent_parameters'] += '\n mdl_bsmIndepParam[%i] = %s;' % ( ipar, par ) @@ -1660,6 +1668,52 @@ def get_process_function_definitions(self, write=True): replace_dict['cipfhrdcod'] = """__device__ const int* cIPF_partner1 = nullptr; // unused as nIPF=0' __device__ const int* cIPF_partner2 = nullptr; // unused as nIPF=0' __device__ const fptype* cIPF_value = nullptr; // unused as nIPF=0'""" + + # dependent (running-alphas, event-by-event) flavor couplings -> cDPF_* (Step 3). + # Unlike cIPF, these have NO baked-in value array: partner1/partner2 and the + # per-flavor idcoup (the index of the underlying dependent coupling in the + # event-by-event allcouplings buffer) are pure codegen constants. The actual + # complex values are gathered per event page in calculate_jamps (see + # super_get_matrix_element_calls). The single-leg serialization mirrors the + # Fortran side / write_flv_couplings (the unmerged partner has flavor index 1). + flv_couplings_dep = [''] * len(self.couporderflv_dep) + for flv_coup, pos in self.couporderflv_dep.items(): + flv_couplings_dep[pos] = flv_coup + replace_dict['ndpf'] = len(flv_couplings_dep) + if len(flv_couplings_dep): + nMF = max(len(ids) for ids in self.model['merged_particles'].values()) + flv_map = self.helas_call_writer.flv_couplings_map + partner1_vals, partner2_vals, idcoup_vals = [], [], [] + for name in flv_couplings_dep: + coupl = flv_map[name] + p1 = [-1] * nMF + p2 = [-1] * nMF + idc = ['-1'] * nMF + for key, gc in coupl.flavors.items(): + nonzero = [i for i in key if i != 0] + if len(nonzero) == 2: + k1, k2 = nonzero + else: + # single merged leg: unmerged partner has flavor index 1 + k1 = nonzero[0]; k2 = 1 + p1[k1-1] = k2-1 + p2[k2-1] = k1-1 + # symbolic idcoup: resolves to the position of this dependent coupling + # in the event-by-event allcouplings buffer (== COUPs index), defined in + # Parameters_dependentCouplings (Parameters_.h) + idc[k1-1] = '(int)Parameters_dependentCouplings::idcoup_%s' % gc + partner1_vals += [str(v) for v in p1] + partner2_vals += [str(v) for v in p2] + idcoup_vals += idc + cdpfdecl = '__device__ const int cDPF_partner1[nMF * nDPF] = { %s };\n' % ', '.join(partner1_vals) + cdpfdecl += ' __device__ const int cDPF_partner2[nMF * nDPF] = { %s };\n' % ', '.join(partner2_vals) + cdpfdecl += ' __device__ const int cDPF_idcoup[nMF * nDPF] = { %s };' % ', '.join(idcoup_vals) + replace_dict['cdpfdecl'] = cdpfdecl + else: + replace_dict['cdpfdecl'] = """__device__ const int* cDPF_partner1 = nullptr; // unused as nDPF=0 + __device__ const int* cDPF_partner2 = nullptr; // unused as nDPF=0 + __device__ const int* cDPF_idcoup = nullptr; // unused as nDPF=0""" + # FIXME! Here there should be different code generated depending on MGONGPUCPP_NBSMINDEPPARAM_GT_0 (issue #827) replace_dict['all_helicities'] = self.get_helicity_matrix(self.matrix_elements[0]) replace_dict['all_helicities'] = replace_dict['all_helicities'] .replace('helicities', 'tHel') @@ -1732,6 +1786,7 @@ def get_all_sigmaKin_lines(self, color_amplitudes, class_name): assert len(self.matrix_elements) == 1 or len(self.matrix_elements) == 2 # how to handle if this is not true? self.couplings2order = self.helas_call_writer.couplings2order self.couporderflv = self.helas_call_writer.couporderflv + self.couporderflv_dep = self.helas_call_writer.couporderflv_dep self.params2order = self.helas_call_writer.params2order ret_lines.append(""" // Evaluate QCD partial amplitudes jamps for this given helicity from Feynman diagrams @@ -2217,7 +2272,8 @@ def format_coupling(self, call): if not hasattr(self, 'couporderdep'): self.couporderdep = {} self.couporderindep = {} - self.couporderflv = {} + self.couporderflv = {} # independent (fixed) flavored couplings -> flvCOUPs + self.couporderflv_dep = {} # dependent (running-alphas) flavored couplings -> flvCOUPs_dep for coup in re.findall(self.findcoupling, call): if coup == 'ZERO': ###call = call.replace('pars->ZERO', '0.') @@ -2242,15 +2298,31 @@ def format_coupling(self, call): aliastxt = 'COUPD' name = 'cIPC' elif coup.startswith("FLV"): - if coup not in [coup.name for coup in self.wanted_ordered_flv_couplings]: - flv_coup = self.flv_couplings_map[coup] + flv_coup = self.flv_couplings_map[coup] + # Classify the whole flavored coupling as dependent (running-alphas, + # event-by-event) or independent, mirroring export_cpp.prepare_couplings + # (which buckets it into coups_flv_dep/coups_flv_indep using one of its + # underlying couplings). + is_flv_dep = model.is_running_coupling(flv_coup.get_one_coupling()) + if coup not in [c.name for c in self.wanted_ordered_flv_couplings]: self.wanted_ordered_flv_couplings.append(flv_coup) - for indep_coup in set(flv_coup.flavors.values()): - if indep_coup not in self.wanted_ordered_indep_couplings: - self.wanted_ordered_indep_couplings.append(indep_coup) - alias = self.couporderflv - aliastxt = 'flvCOUP' - name = 'flvCOUPs' + # NB: the underlying couplings are added to the *independent* wanted + # list even when they are running. prepare_couplings still routes the + # running ones into coups_dep (by the 'aS' model key), but keeping + # them out of wanted_ordered_dep_couplings preserves the alignment + # between couporderdep and the coups_dep/idcoup ordering used by the + # ordinary (non-flavored) dependent couplings. + for ud_coup in set(flv_coup.flavors.values()): + if ud_coup not in self.wanted_ordered_indep_couplings: + self.wanted_ordered_indep_couplings.append(ud_coup) + if is_flv_dep: + alias = self.couporderflv_dep + aliastxt = 'flvCOUPdep' + name = 'flvCOUPs_dep' + else: + alias = self.couporderflv + aliastxt = 'flvCOUP' + name = 'flvCOUPs' else: if coup not in self.wanted_ordered_indep_couplings: self.wanted_ordered_indep_couplings.append(coup) @@ -2284,6 +2356,14 @@ def format_coupling(self, call): call = call.replace('CI_ACCESS', 'CD_ACCESS') call = call.replace('m_pars->%s%s' % (sign, coup), 'COUPs[%s], %s' % (alias[coup], '1.0' if not sign else '-1.0')) + elif name == 'flvCOUPs_dep': + # dependent (running-alphas) flavored coupling: instantiate the vertex + # routine with CD_ACCESS (the default in the call is CI_ACCESS, as for the + # ordinary running couplings) so get_coupling_def reads the per-event AOSOA + # values gathered into flvCOUPs_dep with the right per-flavor stride. + call = call.replace('CI_ACCESS', 'CD_ACCESS') + call = call.replace('m_pars->%s%s' % (sign, coup), + '%s[%s], %s' % (name, alias[coup], '1.0' if not sign else '-1.0')) elif name == 'flvCOUPs': call = call.replace('CD_ACCESS', 'CI_ACCESS') call = call.replace('m_pars->%s%s' % (sign, coup), @@ -2365,6 +2445,27 @@ def super_get_matrix_element_calls(self, matrix_element, color_amplitudes, multi // Create an array of views over the Flavor Couplings FLV_COUPLING_ARRAY flvCOUPs{ cIPF_partner1, cIPF_partner2, cIPF_value }; + // Dependent (event-by-event, running-alphas) flavor couplings (Step 3): the per-flavor + // values are NOT baked in (they run per event). Gather the current values of the + // underlying dependent couplings for this event page into an AOSOA buffer dpf_value + // (one nx2*neppC SIMD record per (coupling,flavor) slot, matching CD_ACCESS), then build + // an ordinary value-based view over it. The flavor index is constant across a SIMD lane + // (guaranteed by the phase-space integrator), so each lane gets its own running value + // while sharing the same flavor selection. This is the direct analogue of Fortran's + // FLV_xx%VAL(k)%P => GC_yyy(J). The vertex routines are instantiated with CD_ACCESS so + // get_coupling_def reads dpf_value with the right per-flavor stride (CD_ACCESS::flv_stride). + constexpr int ndpfbuf = ( nDPF > 0 ? nDPF * nMF * CD_ACCESS::flv_stride : 1 ); + alignas( mgOnGpu::cppAlign ) fptype dpf_value[ndpfbuf]{}; + for( int idpf = 0; idpf < nDPF; idpf++ ) + for( int imf = 0; imf < nMF; imf++ ) + { + const int idc = cDPF_idcoup[idpf * nMF + imf]; + if( idc >= 0 ) + CD_ACCESS::kernelAccess( dpf_value + ( idpf * nMF + imf ) * CD_ACCESS::flv_stride ) = + CD_ACCESS::kernelAccessConst( COUPs[idc] ); + } + FLV_COUPLING_ARRAY flvCOUPs_dep{ cDPF_partner1, cDPF_partner2, dpf_value }; + // Reset color flows (reset jamp_sv) at the beginning of a new event or event page for( int i = 0; i < ncolor; i++ ) { jamp_sv[i] = cxzero_sv(); } From 3dabe55633d4dfb352e70c4bad6dcfedf6d0bc79 Mon Sep 17 00:00:00 2001 From: Daniele Massaro Date: Wed, 24 Jun 2026 15:59:28 +0200 Subject: [PATCH 08/11] tests: positive p p > go go mg7 consistency check Convert test_mssm_gogo_mg7_unsupported into the positive test_standalone_mg7_mssm_gogo: standalone_mg7 now reproduces the Fortran standalone per-flavor |M|^2 for both subprocesses (~1e-7). Update the test_madevent_mssm_gogo docstring and the CI workflow test-name/comments to match. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/acceptancetest_madevent.yml | 4 +- .github/workflows/acceptancetest_mg7.yml | 4 +- tests/acceptance_tests/test_cmd.py | 78 ++++++++++++++----- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/.github/workflows/acceptancetest_madevent.yml b/.github/workflows/acceptancetest_madevent.yml index 4b76eb9d6..4724d92f6 100644 --- a/.github/workflows/acceptancetest_madevent.yml +++ b/.github/workflows/acceptancetest_madevent.yml @@ -986,8 +986,8 @@ jobs: acceptancetest_mssm_gogo: # Fortran madevent supports MSSM p p > go go (merged-flavor squark/gluino - # vertices); the mg7/madmatrix counterpart cannot yet and refuses cleanly - # (test_mssm_gogo_mg7_unsupported in acceptancetest_mg7.yml). + # vertices); the mg7/madmatrix counterpart now also supports it and is + # checked per-flavor (test_standalone_mg7_mssm_gogo in acceptancetest_mg7.yml). runs-on: ubuntu-24.04 if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true steps: diff --git a/.github/workflows/acceptancetest_mg7.yml b/.github/workflows/acceptancetest_mg7.yml index c62b2e78e..529c255eb 100644 --- a/.github/workflows/acceptancetest_mg7.yml +++ b/.github/workflows/acceptancetest_mg7.yml @@ -188,9 +188,9 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/checkout_mg5 - uses: ./.github/actions/restore-pip-cache - - name: test one of the test test_mssm_gogo_mg7_unsupported + - name: test one of the test test_standalone_mg7_mssm_gogo run: | cd $GITHUB_WORKSPACE - ./tests/test_manager.py test_mssm_gogo_mg7_unsupported -pA -t0 -l INFO + ./tests/test_manager.py test_standalone_mg7_mssm_gogo -pA -t0 -l INFO diff --git a/tests/acceptance_tests/test_cmd.py b/tests/acceptance_tests/test_cmd.py index a266de5d2..8fcac7fd5 100755 --- a/tests/acceptance_tests/test_cmd.py +++ b/tests/acceptance_tests/test_cmd.py @@ -1460,31 +1460,73 @@ def get_values(output_format, check_exe, build_source=False): 'all matrix elements vanished for p p > n1 n1') self._assert_me_lists_close(mg7, standalone, rtol=1e-4) - def test_mssm_gogo_mg7_unsupported(self): - """The mg7/madmatrix C++ output must REFUSE, with a clear InvalidCmd, - the merged-flavor structures it cannot yet generate -- rather than - crashing with a cryptic unpack error or emitting uncompilable code. - - MSSM 'p p > go go' has gluino/chargino-squark-quark vertices where only - the light quark is in a merged group (a single merged-flavor leg), and - the relevant flavored couplings are event-by-event (running-alphas) - couplings; neither is supported by the two-merged-leg, fixed-pointer - FLV mechanism (see docs/mg7_merged_flavor_mssm_design.md). - - The Fortran madevent counterpart (test_madevent_mssm_gogo) checks that - the same process is supported there. + def test_standalone_mg7_mssm_gogo(self): + """Dependent (event-by-event, running-alphas) flavored couplings must + give the same per-flavor |M|^2 in standalone_mg7 (madmatrix) as in the + Fortran standalone. + + MSSM 'p p > go go' has single-merged-leg squark/gluino-quark vertices + (one merged light quark + an unmerged gluino + a squark) whose flavored + couplings are *dependent* (SUSY-QCD, running-alphas): the squark-quark- + gluino coupling GC_106/GC_110 ~ g_s changes per event. These are not + addressable as fixed value[] pointers, so they are gathered event-by- + event into cDPF_* / flvCOUPs_dep (Step 3 of + docs/mg7_merged_flavor_mssm_design.md). This is the dependent-coupling + counterpart of test_standalone_mg7_mssm_single_leg (independent flavored + couplings) and the consistency check matching test_madevent_mssm_gogo. + + The energy (sqrt(s)) is chosen above the gluino-pair threshold (Mgo ~ + 608 GeV) so neither check driver auto-bumps it, i.e. both evaluate the + same phase-space point. """ + energy = '3000' + devnull = open(os.devnull, 'w') + me_re = re.compile(r'Matrix element\s*=\s*([\d.eE+-]+)\s*GeV', + re.IGNORECASE) + + def get_values(output_format, check_exe, build_source=False): + if os.path.isdir(self.out_dir): + shutil.rmtree(self.out_dir) + self.do('output %s %s -f' % (output_format, self.out_dir)) + if build_source: + subprocess.call(['make'], stdout=devnull, stderr=devnull, + cwd=os.path.join(self.out_dir, 'Source')) + proc_root = os.path.join(self.out_dir, 'SubProcesses') + dirs = sorted(d for d in os.listdir(proc_root) + if d.startswith('P') and + os.path.isdir(os.path.join(proc_root, d))) + self.assertTrue(dirs, 'no subprocess for %s' % output_format) + values = [] + for d in dirs: + proc_dir = os.path.join(proc_root, d) + target = ['make', 'check'] if output_format == 'standalone' \ + else ['make'] + subprocess.call(target, stdout=devnull, stderr=devnull, + cwd=proc_dir) + log = os.path.join(proc_dir, 'check.log') + subprocess.call('%s %s' % (check_exe, energy), + stdout=open(log, 'w'), stderr=subprocess.STDOUT, + cwd=proc_dir, shell=True) + found = me_re.findall(open(log).read()) + self.assertTrue(found, '%s produced no matrix element (see %s)' + % (output_format, log)) + values.extend(float(v) for v in found) + return values + self.do('import model MSSM_SLHA2') self.do('generate p p > go go') - self.assertRaises(InvalidCmd, self.do, - 'output standalone_mg7 %s -f' % self.out_dir) + mg7 = get_values('standalone_mg7', './check_sa.exe') + standalone = get_values('standalone', './check', build_source=True) + self.assertTrue(any(v != 0.0 for v in standalone), + 'all matrix elements vanished for p p > go go') + self._assert_me_lists_close(mg7, standalone, rtol=1e-4) def test_madevent_mssm_gogo(self): """The Fortran madevent output supports MSSM 'p p > go go' (merged-flavor squark/gluino vertices with single-merged-leg / event-by-event flavored - couplings), which the mg7/madmatrix C++ output cannot yet generate - (test_mssm_gogo_mg7_unsupported). Acts as the madevent counterpart and a - reference for the eventual mg7 fix. + couplings). The mg7/madmatrix C++ output now also supports it and is + checked to agree per-flavor in test_standalone_mg7_mssm_gogo; this acts + as the madevent counterpart. """ self.do('import model MSSM_SLHA2') self.do('generate p p > go go') From a808b0a14ec3e95d1df369a81069c84f30edae49 Mon Sep 17 00:00:00 2001 From: Daniele Massaro Date: Wed, 24 Jun 2026 15:59:28 +0200 Subject: [PATCH 09/11] docs: record Step 3 (dependent flavored couplings) done Mark the Status and Step 3 sections of the merged-flavor design note as implemented and CPU/SIMD-validated (GPU path still unverified). Co-Authored-By: Claude Opus 4.8 --- docs/mg7_merged_flavor_mssm_design.md | 152 +++++++++++++++----------- 1 file changed, 87 insertions(+), 65 deletions(-) diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md index 9e7a49610..5835c2a00 100644 --- a/docs/mg7_merged_flavor_mssm_design.md +++ b/docs/mg7_merged_flavor_mssm_design.md @@ -1,12 +1,14 @@ # mg7/madmatrix merged-flavor support for single-merged-leg vertices (MSSM) -## Status (this PR) +## Status -Two things ship here: +Full MSSM merged-flavor support for single-merged-leg vertices, with both +flavor-*independent* and *dependent* (event-by-event, running-αs) couplings, now +ships in the mg7/madmatrix C++ (`standalone_mg7`) backend. -1. **Single-merged-leg flavored couplings are now supported** (the MSSM - topology: one merged fermion + an unmerged partner + a squark/boson), for - flavor-*independent* couplings. Two parts: +1. **Single-merged-leg flavored couplings** (the MSSM topology: one merged + fermion + an unmerged partner + a squark/boson), for flavor-*independent* + couplings. Two parts: - **serialization** (`write_flv_couplings`, both copies): a single-leg key is written exactly like the Fortran side — the unmerged partner gets flavor index 1, i.e. the two-leg formula with `k2 = 1`. @@ -16,32 +18,31 @@ Two things ship here: leg is the partner-populated one. Validated: `p p > n1 n1 QCD=0` (t-channel squark, EW/independent couplings) - now reproduces the Fortran standalone per-flavor |M|² for **all** flavors - (before the consumer fix only flavor 0 matched). Regression test: - `test_standalone_mg7_mssm_single_leg`. The two-leg path is unchanged - (`test_standalone_mg7_vs_cpp` still passes). - -2. **A guard for the remaining gap.** `UFOModelConverterCPP. - _assert_flv_couplings_supported` (called from both `write_flv_couplings`) - still raises a clear `InvalidCmd` for the cases not yet handled — an - event-by-event ("dependent", running-αs) flavored coupling, or a vertex with - >2 merged legs. So `generate p p > go go; output standalone_mg7` still fails - cleanly (its SUSY-QCD couplings are dependent): - ``` - InvalidCmd : merged-flavor C++ output (mg7/standalone_mg7) does not yet support - this process: flavor coupling FLV_54 references an event-by-event (running-alphas) - coupling. ... Use 'output madevent' or 'output standalone' for this process. - ``` - The guard is scoped to the process's *used* couplings, so SM two-leg cases - and single-leg independent cases generate; `output madevent`/`standalone` - keep working. - -**Remaining for full MSSM (`p p > go go`): the dependent-coupling mechanism** -(Step 3 below) — the large piece. + reproduces the Fortran standalone per-flavor |M|² for **all** flavors + (regression test `test_standalone_mg7_mssm_single_leg`). The two-leg path is + unchanged (`test_standalone_mg7_vs_cpp`). + +2. **Dependent (event-by-event, running-αs) flavored couplings** — Step 3 below, + the large piece, now **done**. The SUSY-QCD MSSM squark-quark-gluino vertices + (`GC_106`/`GC_110` ∝ `g_s`) cannot be addressed by fixed `value[]` pointers in + `setIndependentCouplings`, so they are gathered event-by-event into + `cDPF_*` / `flvCOUPs_dep` and consumed by the *same* vertex routines + instantiated with `CD_ACCESS` (per-event SIMD access) instead of `CI_ACCESS`. + + Validated: `p p > go go` `standalone_mg7` reproduces the Fortran standalone + per-flavor |M|² for both subprocesses (`P1_QQx_gogo`, `P1_gg_gogo`) to ~1e-7 + (the mg7 mixed-precision float floor). Regression test: + `test_standalone_mg7_mssm_gogo` (the dependent-coupling counterpart of + `test_standalone_mg7_mssm_single_leg`, and the consistency check matching the + Fortran `test_madevent_mssm_gogo`). + +3. **The guard** (`UFOModelConverterCPP._assert_flv_couplings_supported`) now + only rejects vertices with **>2 merged-flavor legs** (never seen so far); + both independent and dependent one-/two-leg cases generate. --- -The rest of this note records the diagnosis and the remaining plan. +The rest of this note records the diagnosis and the (now implemented) plan. ## Symptom @@ -196,12 +197,12 @@ the dependent-coupling case (Step 3). 1. [done] single-leg serialization in `write_flv_couplings`. 2. [done] single-leg consumer fix in `get_coupling_def` (select by the merged fermion leg). Validated on `p p > n1 n1 QCD=0`. -3. **dependent flavored couplings (remaining, the large piece)** — see below. -4. relax the guard for dependent once step 3 is validated. -5. validate `p p > go go` vs the `test_madevent_mssm_gogo` reference; convert - `test_mssm_gogo_mg7_unsupported` into a positive consistency check. +3. [done] **dependent flavored couplings (the large piece)** — see below. +4. [done] relax the guard for dependent (now only rejects >2 merged legs). +5. [done] validate `p p > go go` vs the Fortran standalone; converted + `test_mssm_gogo_mg7_unsupported` into the positive `test_standalone_mg7_mssm_gogo`. -## Step 3 design: dependent (event-by-event) flavored couplings +## Step 3 design: dependent (event-by-event) flavored couplings [DONE] Today the cudacpp FLV mechanism (`model_handling.py` ~1591-1660 + the `CPPProcess.cc` template) **bakes** the per-flavor coupling values into a @@ -211,35 +212,56 @@ based `FLV_COUPLING_VIEW`. This works only for *independent* couplings; a running-αs coupling like `GC_106` changes per event and cannot be a fixed pointer/constant (and isn't even in scope in `setIndependentCouplings`). -**Chosen approach — per-event gather, reuse the consumer.** The dependent -couplings are already computed per event into `allcouplings` and exposed in the -kernel as `allCOUPs[idcoup]` (`CD_ACCESS::idcoupAccessBufferConst`). So for a -dependent flavored coupling we keep an `idcoup` per (coupling, flavor) slot and, -**per event page in `calculate_jamps`, gather** the current values into a -`dpf_value[nDPF*nMF*2]` array, then build an ordinary value-based -`FLV_COUPLING_VIEW` over it. The vertex routines and `get_coupling_def` are -**unchanged** (they already consume a value-based view) — this is the key -simplification, and it is the direct analogue of Fortran's `VAL%P => GC(J)`. - -Concrete surface: - -1. **Split** `couporderflv` into independent (existing `cIPF`) and dependent - (`cDPF`) flavored couplings (using `coups_flv_dep`). -2. **Model-side** (`model_handling.py`): emit `cDPF_partner1/2[nDPF*nMF]` (as - today) plus `cDPF_idcoup[nDPF*nMF]` — the `idcoup` of the dependent coupling - each `value[j]` slot points at (the position of that GC in `coups_dep`; - `value[j]` is null for unused slots → idcoup `-1`). -3. **Kernel** (`CPPProcess.cc`/`process_function_definitions.inc`): right after - `allCOUPs` is set up, gather - `dpf_value[i*nMF+j] = (cDPF_idcoup[i*nMF+j] >= 0) ? COUPs[cDPF_idcoup[i*nMF+j]] : 0` - (per event / SIMD page), then - `FLV_COUPLING_ARRAY flvCOUPs_dep{ cDPF_partner1, cDPF_partner2, dpf_value }`. -4. **Routing** (`model_handling.py` helas-call writer, ~2251-2287): a dependent - flavored coupling resolves to `flvCOUPs_dep[idx]` instead of `flvCOUPs[idx]`. -5. **Guard**: drop the `is_dep` rejection in `_assert_flv_couplings_supported`. -6. **Validate**: `p p > go go` standalone_mg7 vs the Fortran reference - (`test_madevent_mssm_gogo`); flip `test_mssm_gogo_mg7_unsupported` to a - positive consistency test. - -Risk: this is the deepest cudacpp codegen change (touches the SIMD/CUDA gather -and the per-coupling routing); needs CPU **and** GPU validation. +**Implemented approach — per-event AoSoA gather + reuse the (templated) +consumer.** The dependent couplings are already computed per event into +`allcouplings` and exposed in the kernel as `allCOUPs[idcoup]` +(`CD_ACCESS::idcoupAccessBufferConst`). For a dependent flavored coupling we keep +an `idcoup` per (coupling, flavor) slot and, **per event page in +`calculate_jamps`, gather** the running values into an AoSoA `dpf_value` buffer +(one `nx2*neppC` SIMD record per slot), then build a `FLV_COUPLING_ARRAY` view +over it. The key realisation: the vertex routines (`FFSxM`, …) are *templated* +on the coupling access type (`template`), and the access type is chosen **at the call site**. So the *same* +routine body is instantiated with `CD_ACCESS` for dependent flavored calls and +`CI_ACCESS` for independent ones — `get_coupling_def` is essentially unchanged +(only the per-flavor stride `2` becomes `C_ACCESS::flv_stride`, which is `nx2` +for the broadcast `CI_ACCESS` and `nx2*neppC` for the per-event AoSoA +`CD_ACCESS`). This is the direct analogue of Fortran's `VAL%P => GC(J)`, and it +keeps the independent path numerically identical. NB: by construction the +phase-space integrator gives all SIMD lanes the same flavor index, so each lane +selects the same flavor but reads its own per-event running value. + +Concrete surface (as implemented): + +1. **Split** `couporderflv` into independent (`flvCOUPs`/`cIPF`) and dependent + (`flvCOUPs_dep`/`cDPF`) orderings in the helas-call writer `format_coupling`, + classifying via `model.is_running_coupling(flv_coup.get_one_coupling())`. + Dependent flavored calls keep `CD_ACCESS` (the independent ones swap to + `CI_ACCESS`). +2. **Model-side** (`model_handling.py` `get_process_function_definitions`): emit + compile-time constant `cDPF_partner1/2[nMF*nDPF]` plus `cDPF_idcoup[nMF*nDPF]` + — the `idcoup` of the dependent coupling each `value[j]` slot points at, + emitted *symbolically* as `Parameters_dependentCouplings::idcoup_` (`-1` + for unused slots). No baked-in value array (the values run per event). +3. **Kernel** (`CPPProcess.cc`/`process_function_definitions.inc`): after + `allCOUPs`/`COUPs` are set up, gather (per event page) + `CD_ACCESS::kernelAccess(dpf_value + (i*nMF+j)*CD_ACCESS::flv_stride) = CD_ACCESS::kernelAccessConst(COUPs[cDPF_idcoup[i*nMF+j]])` + for `idcoup>=0`, then + `FLV_COUPLING_ARRAY flvCOUPs_dep{ cDPF_partner1, cDPF_partner2, dpf_value }`. + `FLV_COUPLING_ARRAY` grew a 3rd template param `FSTRIDE` (default `nx2`) so the + value offset is `i*FSTRIDE*STRIDE`; `dpf_value` is `alignas(cppAlign)` for the + SIMD reinterpret. +4. **Serialization**: `write_flv_couplings`/`set_flv_couplings` now serializes + *only* the independent flavored couplings (the dependent ones have no fixed + `value[]` pointer). +5. **Guard**: dropped the `is_dep` rejection in + `_assert_flv_couplings_supported` (now only >2 merged legs). +6. **Validated** on CPU/SIMD (`cppavx2`): `p p > go go` standalone_mg7 vs Fortran + standalone, per-flavor |M|² agree to ~1e-7 for both `P1_QQx_gogo` and + `P1_gg_gogo` (`test_standalone_mg7_mssm_gogo`). + +Remaining/risk: GPU validation was **not** run here (no GPU in the dev +environment). The gather/routing was written to be CPU+GPU uniform (CUDA +`__constant__`-free `__device__ const` cDPF arrays; `CD_ACCESS` kernel access), +but a GPU build+run cross-check is still advisable before relying on the CUDA +path. From 693dcc04924f45f0e74f17fc8f5c1553d4ba4c81 Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Thu, 25 Jun 2026 20:29:02 +0200 Subject: [PATCH 10/11] mg7: pin gogo generation test to madevent + add red mg7 xsec tracker test_generation_from_file_1 drives generate_events from the input file tests/input_files/test_mssm_generation, which used a bare `output` that now defaults to mg7. mg7 has no event-generation pipeline for this MSSM p p > go go process, so the run crashed. Pin the input file to `output madevent` (the MadEvent reference cross-sections / banner / LHE assertions are MadEvent-specific), matching the branch-wide convention for generate_events tests. Add the missing mg7 counterpart as an explicit (intentionally red) tracker: test_generation_from_file_1_mg7 runs the full mg7 (madspace) pipeline for p p > go go and pins the cross-section to the madevent reference (4.541638 pb, run_01 of test_generation_from_file_1). standalone_mg7 already reproduces the per-flavor |M|^2 (test_standalone_mg7_mssm_gogo), but full mg7 event generation for merged-flavor processes is not wired up yet (the madspace integrator does not produce the cross-section, cf. the SM tracker test_madevent_merged_flavor_uq_mg7). Left undecorated so the gap stays visible. New CI job acceptancetest_mg7_mssm_gogo_xsec runs it (self-skips without the madspace + LHAPDF stack). Also refresh the stale acceptancetest_mg7_mssm_gogo comment: mg7 no longer refuses MSSM gogo with InvalidCmd; it generates the matrix element and is checked per-flavor. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/acceptancetest_mg7.yml | 32 ++++++++++++++++--- tests/acceptance_tests/test_cmd_madevent.py | 35 +++++++++++++++++++-- tests/input_files/test_mssm_generation | 2 +- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/.github/workflows/acceptancetest_mg7.yml b/.github/workflows/acceptancetest_mg7.yml index 529c255eb..ed8e882fe 100644 --- a/.github/workflows/acceptancetest_mg7.yml +++ b/.github/workflows/acceptancetest_mg7.yml @@ -178,10 +178,13 @@ jobs: ./tests/test_manager.py test_generation_heft_mg7 -pA -t0 -l INFO acceptancetest_mg7_mssm_gogo: - # mg7/madmatrix cannot yet generate the MSSM merged-flavor squark/gluino - # vertices (single-merged-leg / event-by-event flavored couplings); the - # output must refuse cleanly with InvalidCmd (not crash). The madevent - # counterpart is test_madevent_mssm_gogo in acceptancetest_madevent.yml. + # mg7/madmatrix now generates the MSSM merged-flavor squark/gluino vertices + # (single-merged-leg / event-by-event flavored couplings); standalone_mg7 + # reproduces the Fortran standalone per-flavor |M|^2 (~1e-4). This is a + # matrix-element (standalone) check only -- full mg7 event generation for + # merged-flavor processes is not wired up yet (see test_madevent_merged_ + # flavor_uq_mg7). The madevent counterpart is test_madevent_mssm_gogo in + # acceptancetest_madevent.yml. runs-on: ubuntu-24.04 if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true steps: @@ -193,4 +196,25 @@ jobs: cd $GITHUB_WORKSPACE ./tests/test_manager.py test_standalone_mg7_mssm_gogo -pA -t0 -l INFO + acceptancetest_mg7_mssm_gogo_xsec: + # KNOWN-FAILING (not xfail): standalone_mg7 reproduces the per-flavor |M|^2 + # for MSSM p p > go go (test_standalone_mg7_mssm_gogo), but full mg7 event + # generation for merged-flavor processes is not wired up yet -- the madspace + # integrator does not produce the cross-section. This pins the mg7 result to + # the madevent reference (4.541638 pb, run_01 of test_generation_from_file_1) + # and is expected to be red until the mg7 integrator handles merged-flavor + # p p > go go. Self-skips if the madspace + LHAPDF(NNPDF23) stack is absent. + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + - uses: ./.github/actions/restore_heptools_lhapdf + - name: test one of the test test_generation_from_file_1_mg7 + run: | + cd $GITHUB_WORKSPACE + export PATH="$HOME/.cache/HEPtools/bin:$PATH" + ./tests/test_manager.py test_generation_from_file_1_mg7 -pA -t0 -l INFO + diff --git a/tests/acceptance_tests/test_cmd_madevent.py b/tests/acceptance_tests/test_cmd_madevent.py index 7d90de8c4..4d6961201 100755 --- a/tests/acceptance_tests/test_cmd_madevent.py +++ b/tests/acceptance_tests/test_cmd_madevent.py @@ -2541,8 +2541,39 @@ def test_generation_from_file_1(self): self.assertEqual(run_card['ptheavy'], 50) for event in events: event.check() - - + + def test_generation_from_file_1_mg7(self): + """mg7 (madspace) cross-section for MSSM p p > go go, pinned to the + madevent reference from test_generation_from_file_1. + + KNOWN-FAILING, intentionally NOT marked xfail: standalone_mg7 already + reproduces the per-flavor |M|^2 for p p > go go + (test_standalone_mg7_mssm_gogo, ~1e-4), but full mg7 event generation + for merged-flavor processes is not wired up yet -- the madspace + integrator does not currently produce the cross-section (cf. the SM + tracker test_madevent_merged_flavor_uq_mg7, which segfaults). This pins + the mg7 cross-section to the madevent reference (run_01 of + test_generation_from_file_1, 4.541638 pb) and is expected to fail until + the mg7 integrator handles merged-flavor p p > go go; it is left + undecorated so the gap stays visible in the mg7 workflow rather than + being silently swallowed by expectedFailure. (The mg7 default + run_card.toml uses a dynamical HT/2 scale rather than the madevent + run_card_matching.dat settings, so a residual scale-driven difference is + expected even once the integrator works.) Self-skips where the mg7 + runtime stack is unavailable. + """ + datadir = _mg7_datadir_or_skip(self) + cross, error = _run_mg7_xsec(self, + ['set automatic_html_opening False --no_save', + 'import model MSSM_SLHA2', + 'generate p p > go go'], + pjoin(self.path, 'MG7_mssm_gogo'), datadir) + # madevent reference (run_01 in test_generation_from_file_1) + target = 4.541638 + self.assertLess(abs(cross - target) / target, 0.10, + 'mg7 p p > go go cross-section %s far from madevent reference %s' + % (cross, target)) + def test_contur_from_file(self): """check that contur runs as expected""" diff --git a/tests/input_files/test_mssm_generation b/tests/input_files/test_mssm_generation index 036d12b5c..c8d16dc53 100644 --- a/tests/input_files/test_mssm_generation +++ b/tests/input_files/test_mssm_generation @@ -2,7 +2,7 @@ import model MSSM_SLHA2 set automatic_html_opening False --no-save set notification_center False --no-save generate p p > go go -output %(dir_name)s +output madevent %(dir_name)s launch -i set automatic_html_opening False --no-save set madanalysis_path None --no_save From 73c7c1e3fc5339b22073772985a59701f499b5cb Mon Sep 17 00:00:00 2001 From: Olivier Mattelaer Date: Thu, 2 Jul 2026 15:13:42 +0200 Subject: [PATCH 11/11] remove claude instructions --- docs/mg7_merged_flavor_mssm_design.md | 267 -------------------------- 1 file changed, 267 deletions(-) delete mode 100644 docs/mg7_merged_flavor_mssm_design.md diff --git a/docs/mg7_merged_flavor_mssm_design.md b/docs/mg7_merged_flavor_mssm_design.md deleted file mode 100644 index 5835c2a00..000000000 --- a/docs/mg7_merged_flavor_mssm_design.md +++ /dev/null @@ -1,267 +0,0 @@ -# mg7/madmatrix merged-flavor support for single-merged-leg vertices (MSSM) - -## Status - -Full MSSM merged-flavor support for single-merged-leg vertices, with both -flavor-*independent* and *dependent* (event-by-event, running-αs) couplings, now -ships in the mg7/madmatrix C++ (`standalone_mg7`) backend. - -1. **Single-merged-leg flavored couplings** (the MSSM topology: one merged - fermion + an unmerged partner + a squark/boson), for flavor-*independent* - couplings. Two parts: - - **serialization** (`write_flv_couplings`, both copies): a single-leg key is - written exactly like the Fortran side — the unmerged partner gets flavor - index 1, i.e. the two-leg formula with `k2 = 1`. - - **consumer fix** (`MadMatrixALOHAWriter.get_coupling_def`): the flavored - coupling is selected by the *merged* fermion leg, which (unlike Fortran) - can be either `F1` or `F2` in the cudacpp argument order — pick whichever - leg is the partner-populated one. - - Validated: `p p > n1 n1 QCD=0` (t-channel squark, EW/independent couplings) - reproduces the Fortran standalone per-flavor |M|² for **all** flavors - (regression test `test_standalone_mg7_mssm_single_leg`). The two-leg path is - unchanged (`test_standalone_mg7_vs_cpp`). - -2. **Dependent (event-by-event, running-αs) flavored couplings** — Step 3 below, - the large piece, now **done**. The SUSY-QCD MSSM squark-quark-gluino vertices - (`GC_106`/`GC_110` ∝ `g_s`) cannot be addressed by fixed `value[]` pointers in - `setIndependentCouplings`, so they are gathered event-by-event into - `cDPF_*` / `flvCOUPs_dep` and consumed by the *same* vertex routines - instantiated with `CD_ACCESS` (per-event SIMD access) instead of `CI_ACCESS`. - - Validated: `p p > go go` `standalone_mg7` reproduces the Fortran standalone - per-flavor |M|² for both subprocesses (`P1_QQx_gogo`, `P1_gg_gogo`) to ~1e-7 - (the mg7 mixed-precision float floor). Regression test: - `test_standalone_mg7_mssm_gogo` (the dependent-coupling counterpart of - `test_standalone_mg7_mssm_single_leg`, and the consistency check matching the - Fortran `test_madevent_mssm_gogo`). - -3. **The guard** (`UFOModelConverterCPP._assert_flv_couplings_supported`) now - only rejects vertices with **>2 merged-flavor legs** (never seen so far); - both independent and dependent one-/two-leg cases generate. - ---- - -The rest of this note records the diagnosis and the (now implemented) plan. - -## Symptom - -``` -./tests/test_manager.py -pA test_generation_from_file_1 -t 0 -``` -crashes (MSSM `generate p p > go go`, default `output` = `mg7` = madmatrix): - -``` -File ".../madmatrix/model_handling.py", line 942, in write_flv_couplings - k1, k2 = [i for i in key if i!=0] -ValueError: not enough values to unpack (expected 2, got 1) -``` - -This is **pre-existing on the feat-madmatrix branch** (both `default='mg7'` and -`write_flv_couplings` exist in `9d726705c`); it is not caused by the -`aloha_obj_wmerged` goodhel merge. The Fortran `output madevent` path handles -`p p > go go` fine (it uses legacy flavor grouping, `P1_qq_gogo` has no -`PARTNER`/`flv_index`), so the gap is **madmatrix/mg7-specific**. - -## Root cause: the merged-flavor model assumes two merged partner legs - -`coupl.flavors` keys are per-leg tuples; a non-zero entry is the 1-based flavor -index of a leg in a merged-particle group, `0` otherwise. The whole FLV -mechanism assumes a flavored vertex has **exactly two** merged legs forming a -partner pair (the SM `q q̄ V` topology), routing flavor `k1 → k2`: - -- `write_flv_couplings` (model side) does `k1, k2 = [i for i in key if i!=0]`. -- `MadMatrixALOHAWriter.get_coupling_def` (vertex side, `model_handling.py` - ~408-526) is hardwired to two fermions `F1`/`F2` and requires - `partner1[flv_index1] == flv_index2`. -- `FLV_COUPLING { int partner1[]; int partner2[]; cxtype* value[]; }` encodes - exactly this pairing. - -MSSM has vertices with a **single** merged leg. Confirmed by probing the model: - -``` -INTERACTION id=94 particles: [('_quark',F,81), ('x1+',F,1000024), ('su1',S,1000002)] - FLV_1 flavors = {(1, 0, 0): 'GC_385'} # only the quark (leg 0) is merged -``` -i.e. `quark(merged) · chargino(unmerged) · squark(scalar)`. The key has one -non-zero entry → the 2-tuple unpack throws. - -Semantically a single-leg coupling is a **selection/gate**: "this interaction is -active only when the single merged leg has flavor index k" — there is no second -merged leg to partner with. Each squark flavor gets its own interaction -(su1↔flavor1, su2↔flavor3, ...). - -## Discovered scope (larger than the initial framing) - -Empirically generating `p p > go go` as `standalone_mg7` after fixing the -unpack reveals **three** independent problems; all are needed for MSSM: - -1. **Single-leg topology** (this crash). - - Model side: `write_flv_couplings` must serialize a one-merged-leg key. - *(done — see below, for the independent-coupling case.)* - - Vertex side: `get_coupling_def` (and likely the generic - `aloha/aloha_writers.py`, plus the Fortran writer) need a single-leg - branch that gates on the one merged fermion and does **not** require a - second fermion partner. This needs the writer to know *which* leg is - flavored (today the `M` tag is binary — `helas_objects.py:2099` — and the - writer just assumes `F1`/`F2`). - -2. **Dependent (event-by-event) flavored couplings.** - `set_flv_couplings` is emitted inside `Parameters::setIndependentCouplings()` - and points `FLV_xx.value[k] = &GC_yyy`. Every FLV coupling in the *model* is - serialized there. MSSM flavored couplings such as `GC_106` are **dependent** - (running-αs #373 made dependent couplings event-by-event SIMD data in - `DependentCouplings_sv`), so they are not addressable as a fixed pointer in - `setIndependentCouplings`: - ``` - Parameters.cc: error: use of undeclared identifier 'GC_106' - ``` - The `cxtype* value[]` pointer mechanism is fundamentally incompatible with - per-event dependent couplings. This needs a different representation — e.g. - store a flavor → dependent-coupling *index* and have the consumer select the - per-event dependent coupling by flavor. - -3. **Per-flavor couplings are not emitted.** The FLV couplings actually used by - `p p > go go` (P1_QQx_gogo) are *all* single-leg and reference couplings such - as `GC_452`, `GC_223`, which are **not generated in `Parameters` at all**, so - the references would not even link. The merged-flavor model export must emit - the per-flavor couplings that `value[]` entries point at. - -The minimal first crash (#1) was masking #2 and #3. - -## How the Fortran/madevent side does it (the template to mirror) - -`output madevent`/`standalone` supports `p p > go go`. From the generated -`Source/MODEL/flavor_couplings.f`: - -```fortran -TYPE COUPPTR - DOUBLE COMPLEX, POINTER :: P -END TYPE -TYPE FLV_COUPLING - INTEGER :: PARTNER(4) - INTEGER :: PARTNER2(4) - TYPE(COUPPTR) :: VAL(4) ! pointer per flavor index -END TYPE -! single merged leg (quark flavor k), unmerged partner (gluino) carries flavor 1: -FLV_56(J)%PARTNER(3)=1 ; FLV_56(J)%PARTNER2(1)=3 ; FLV_56(J)%VAL(3)%P => GC_106(J) -``` - -Two ideas make it work: - -1. **Single-leg = two-leg with an unmerged partner of flavor 1.** Unmerged legs - carry flavor index 1 (Fortran) / 0 (C++, since `get_flavor_matrix` subtracts - 1). So a single-leg key `(k,0,..)` is serialized exactly like a two-leg one - with the partner flavor = 1: `partner1[k-1]=0, partner2[0]=k-1, value[k-1]=coup`. - Each squark is a separate diagram, gated by the merged-quark flavor. -2. **`VAL` is a pointer into the per-event coupling array** (`=> GC_106(J)`), so - running-αs ("dependent") couplings work uniformly with independent ones. - -## Empirical finding: serialization alone is NOT sufficient (consumer is wrong) - -An attempt that (a) serialized single-leg as above (`k2 = 1`) and (b) relaxed -the guard to allow single-leg *independent* couplings was validated on -`p p > n1 n1 QCD=0` (electroweak neutralino pair: single merged quark leg + -unmerged neutralino + squark, with **independent** couplings, so the dependent -gap is out of the way). It **compiles and runs** but gives **wrong** per-flavor -|M|² — only the first flavor matches: - -| flavor (q q~ > n1 n1) | standalone_mg7 | Fortran standalone | -|---|---|---| -| d d~ (idx 0) | 2.4022949e-05 | 2.4022949e-05 ✓ | -| u u~ (idx 1) | 3.5226867e-07 | 3.8121047e-04 ✗ | -| s s~ (idx 2) | 4.5463454e-07 | 2.4022949e-05 ✗ | -| c c~ (idx 3) | 3.5226867e-07 | 3.8121047e-04 ✗ | - -The generated cudacpp **does** emit separate, correctly-mass-ed squark diagrams -(`FFS1M_3(..., cIPD[3]=Msd1, ...)`, `cIPD[5]=Msd4`, `cIPD[9]=Msu1`, …), so the -propagator masses are fine and the goodhel-union filter is not the cause -(re-tested with it applied — no change). The bug is the **consumer gating** in -`MadMatrixALOHAWriter.get_coupling_def` (the `FFSxM` routine): it is hard-wired -to read `F1`/`F2` and assumes the *merged* fermion is `F1`. For these vertices -the unmerged fermion can be `F1` (flv_index 0), so `partner1[flv_index1]` indexes -the wrong slot and only flavor 0 (where `partner1[0]==0`) survives. Fortran -handles this by branching on the fermion *position parity* and using `PARTNER` -vs `PARTNER2` (aloha_writers.py ~757-786); the cudacpp port of that logic does -not correctly cover the single-merged-leg case. - -So the single-leg work is **(1) serialization [trivial] + (2) a real consumer -fix** that picks the merged fermion's flavor — NOT serialization alone. -**Both are now done** (see the Status section at the top): the consumer selects -whichever fermion leg is the partner-populated (merged) one, and -`p p > n1 n1 QCD=0` matches Fortran for all flavors. The guard now only blocks -the dependent-coupling case (Step 3). - -## Plan - -1. [done] single-leg serialization in `write_flv_couplings`. -2. [done] single-leg consumer fix in `get_coupling_def` (select by the merged - fermion leg). Validated on `p p > n1 n1 QCD=0`. -3. [done] **dependent flavored couplings (the large piece)** — see below. -4. [done] relax the guard for dependent (now only rejects >2 merged legs). -5. [done] validate `p p > go go` vs the Fortran standalone; converted - `test_mssm_gogo_mg7_unsupported` into the positive `test_standalone_mg7_mssm_gogo`. - -## Step 3 design: dependent (event-by-event) flavored couplings [DONE] - -Today the cudacpp FLV mechanism (`model_handling.py` ~1591-1660 + the -`CPPProcess.cc` template) **bakes** the per-flavor coupling values into a -constant device array `cIPF_value[nIPF*nMF*2]` at construction -(`tIPF_value[..] = *tFLV[i].value[j]`), and the vertex routines read a value- -based `FLV_COUPLING_VIEW`. This works only for *independent* couplings; a -running-αs coupling like `GC_106` changes per event and cannot be a fixed -pointer/constant (and isn't even in scope in `setIndependentCouplings`). - -**Implemented approach — per-event AoSoA gather + reuse the (templated) -consumer.** The dependent couplings are already computed per event into -`allcouplings` and exposed in the kernel as `allCOUPs[idcoup]` -(`CD_ACCESS::idcoupAccessBufferConst`). For a dependent flavored coupling we keep -an `idcoup` per (coupling, flavor) slot and, **per event page in -`calculate_jamps`, gather** the running values into an AoSoA `dpf_value` buffer -(one `nx2*neppC` SIMD record per slot), then build a `FLV_COUPLING_ARRAY` view -over it. The key realisation: the vertex routines (`FFSxM`, …) are *templated* -on the coupling access type (`template`), and the access type is chosen **at the call site**. So the *same* -routine body is instantiated with `CD_ACCESS` for dependent flavored calls and -`CI_ACCESS` for independent ones — `get_coupling_def` is essentially unchanged -(only the per-flavor stride `2` becomes `C_ACCESS::flv_stride`, which is `nx2` -for the broadcast `CI_ACCESS` and `nx2*neppC` for the per-event AoSoA -`CD_ACCESS`). This is the direct analogue of Fortran's `VAL%P => GC(J)`, and it -keeps the independent path numerically identical. NB: by construction the -phase-space integrator gives all SIMD lanes the same flavor index, so each lane -selects the same flavor but reads its own per-event running value. - -Concrete surface (as implemented): - -1. **Split** `couporderflv` into independent (`flvCOUPs`/`cIPF`) and dependent - (`flvCOUPs_dep`/`cDPF`) orderings in the helas-call writer `format_coupling`, - classifying via `model.is_running_coupling(flv_coup.get_one_coupling())`. - Dependent flavored calls keep `CD_ACCESS` (the independent ones swap to - `CI_ACCESS`). -2. **Model-side** (`model_handling.py` `get_process_function_definitions`): emit - compile-time constant `cDPF_partner1/2[nMF*nDPF]` plus `cDPF_idcoup[nMF*nDPF]` - — the `idcoup` of the dependent coupling each `value[j]` slot points at, - emitted *symbolically* as `Parameters_dependentCouplings::idcoup_` (`-1` - for unused slots). No baked-in value array (the values run per event). -3. **Kernel** (`CPPProcess.cc`/`process_function_definitions.inc`): after - `allCOUPs`/`COUPs` are set up, gather (per event page) - `CD_ACCESS::kernelAccess(dpf_value + (i*nMF+j)*CD_ACCESS::flv_stride) = CD_ACCESS::kernelAccessConst(COUPs[cDPF_idcoup[i*nMF+j]])` - for `idcoup>=0`, then - `FLV_COUPLING_ARRAY flvCOUPs_dep{ cDPF_partner1, cDPF_partner2, dpf_value }`. - `FLV_COUPLING_ARRAY` grew a 3rd template param `FSTRIDE` (default `nx2`) so the - value offset is `i*FSTRIDE*STRIDE`; `dpf_value` is `alignas(cppAlign)` for the - SIMD reinterpret. -4. **Serialization**: `write_flv_couplings`/`set_flv_couplings` now serializes - *only* the independent flavored couplings (the dependent ones have no fixed - `value[]` pointer). -5. **Guard**: dropped the `is_dep` rejection in - `_assert_flv_couplings_supported` (now only >2 merged legs). -6. **Validated** on CPU/SIMD (`cppavx2`): `p p > go go` standalone_mg7 vs Fortran - standalone, per-flavor |M|² agree to ~1e-7 for both `P1_QQx_gogo` and - `P1_gg_gogo` (`test_standalone_mg7_mssm_gogo`). - -Remaining/risk: GPU validation was **not** run here (no GPU in the dev -environment). The gather/routing was written to be CPU+GPU uniform (CUDA -`__constant__`-free `__device__ const` cDPF arrays; `CD_ACCESS` kernel access), -but a GPU build+run cross-check is still advisable before relying on the CUDA -path.