Skip to content

madmatrix: squared split orders, so the interference syntax stops returning the total - #125

Merged
oliviermattelaer merged 3 commits into
mainfrom
claude/madmatrix-interference-syntax-63a130
Sep 11, 2026
Merged

oliviermattelaer merged 3 commits into
mainfrom
claude/madmatrix-interference-syntax-63a130

Conversation

@oliviermattelaer

Copy link
Copy Markdown
Contributor

Stacked on #60 — please merge that first. Base is claude/dixon-maltoni-gluon-basis-69e1d7, not main.

The bug

madmatrix ignored the ^2 (interference) syntax and returned the full amplitude squared.

Every C++ exporter contracted one jamp vector per helicity with the colour matrix once, so the |M|^2 it returned was the sum over every squared-order component. That is the requested number whenever the constraint keeps all of them, and it is silently wrong when it drops one — which is exactly the interference case, since a cross term cannot be reached by dropping diagrams at generation.

u u~ > u u~ QED^2==2 asks for the QCD-EW cross term alone (-5.58e-02) and got the whole +2.7756 back, from generated code that differed from the unconstrained process by one comment line.

The fix

jamp_sv becomes njampso = ncolor * nampso long — one vector per amplitude split order — and the colour sum pairs them, mirroring the Fortran GET_MATRIX(JAMP(NCOLOR,NAMPSO) -> RES(NSQAMPSO)) contract. The pairing lives in a dedicated color_sum_splitorders.cc because the sum changes shape rather than gaining a hole; everything else is a hole whose empty fill is the text that was already there.

The trap, for reviewers: the loop over amplitude-order pairs runs over all ordered pairs, never a triangle. The colour contraction keeps its triangular matrix with the off-diagonal doubled — for a single jamp vector that is just the statement that the (i,j) and (j,i) terms are conjugates, but for two different vectors they are not. Only (m,n) and (n,m) together equal the truth. Masking stays safe because sqSoIndex is symmetric (a squared order is the sum of the two amplitude orders), so a pair and its transpose are always kept or dropped together.

Switched off while the jamps are split: the jamp CSE/orbit (its shared partial sums would mix amplitudes of different orders into one vector) and the BLAS colour sum (it takes a single jamp vector). Colour choice is deliberately not split or masked — jampso_sv[icol] = sum_M jamp_M[icol] only picks which flow to write out.

GPU refuses at compile time: the device jamp buffers are sized ncolor, not njampso, and the backend is a make-time flag, so color_sum_splitorders.cc #errors under MGONGPUCPP_GPUIMPL. The exporter also says so at generation, rather than letting a GPU build be the first the user hears of it.

Why it is stacked on #60 rather than main

Only #60 carries the jamp CSE/orbit and the BLAS colour sum that this has to switch off. On main those do not exist, so the port would drop half its own safety logic and then silently mis-sum once #60 landed.

The third commit is the port adaptation, kept separate: the formats renamed to standalone/standalone_fortran, and the colour reflection folding (ncolorfold/colorFoldRep[]) that #60 removed, so the sum runs over ncolor directly.

Worth flagging: an unrecognised format name on the output line is not an error — it is taken as the directory name. The original test drove output standalone_mg7, which after the renaming silently exercised the default format instead.

Validation

u u~ > u u~, FPTYPE=d, standard RAMBO point:

component madmatrix output standalone_fortran
QED=0 2.8276928588371750e+00
QED=2 -5.5828746494657258e-02 -5.5828746494657265e-02
QED=4 3.7810076327210495e-03
sum 2.7756451199752387e+00
QED^2<=4 2.7756451199752394e+00 (computed independently)

The interference now matches Fortran to 1.3e-16, and the sum rule closes to 3 ulp. u u~ > d d~ g (ncolor=4) reproduces its three components too, with the sum rule at 2.8e-16.

The sum rule is the check that pins the pair loop: it is internal to one backend, so momenta and parameters cancel and it sees the interference algebra alone. A triangle over the pairs gives roughly 2x.

Regressions:

  • Byte-identical output to the base commit for u u~ > u u~, g g > t t~ and p p > j j QCD^2==4 (PYTHONHASHSEED=0, ignoring *.pkl/*.gz). The last one carries a ^2 constraint that resolves to a single amplitude order, so the new path correctly stays off.
  • Unit suite unchanged: 1581 tests, 2 failures, 94 errors on both the base commit and this branch, and diffing the failure names shows no difference. All 96 are pre-existing on Cheaper colour sum: DDM colour basis, Kleiss-Kuijf flows, and the same for madmatrix #60.
  • New acceptance test test_standalone_split_orders_interference passes.

🤖 Generated with Claude Code

oliviermattelaer and others added 3 commits September 10, 2026 13:58
`output standalone_mg7` accepted `^2` syntax and wrote a matrix element with no
squared-order machinery in it at all -- `grep split_orders madmatrix/` is still 0
hits, and so is `grep split_orders madgraph/iolibs/export_cpp.py`. That is not
wrong everywhere, which is why it went unnoticed: this backend contracts one jamp
vector per helicity with the color matrix once, so the |M|^2 it returns is the sum
over every squared-order component, and that IS the requested number whenever the
constraint keeps all of them.

It is a wrong number when the constraint drops one. MG5 resolves what it can at
generation -- `u u~ > t t~ QED^2==4` prunes to 2 diagrams and NSQAMPSO=1 -- but a
component that needs two amplitudes cannot be reached that way. `u u~ > t t~
QED^2==2` keeps all three diagrams and asks for the QCD-EW cross term alone:

    fortran, masked   -3.4453535703428253e-18
    madmatrix          6.1741265892435804e-01   = the whole QCD^2 + int + EW^2

silently, from generated code differing from the unconstrained process by one
comment line. `u u~ > u u~ QED^2==2` is the same failure with the sign visible:
-5.5828747824035893e-02 asked for, +2.7756451 returned.

WHAT IS SUPPORTED, and is byte-identical to before:

  NSQAMPSO==1, the constraint resolved at generation. `p p > j j QCD^2==4` (all 8
  subprocesses build and run under FPTYPE=d), `u u~ > t t~ QED^2==4`, `QED^2==0`.

  NSQAMPSO>1 with every component kept, e.g. `u u~ > t t~ QED^2<=4`: the total is
  what was asked for. madmatrix 6.1741265892435804e-01 against the Fortran
  ANS(0) 0.61741265892435848, ~3 ulp. It now says out loud that the components
  behind that total are not available here, rather than leaving the user to
  assume they were applied.

WHAT IS REFUSED: a constraint that drops a component. The message names the
components, which are kept and which dropped, and points at `output standalone`.

WHY REFUSE RATHER THAN MASK. Applying the mask needs the jamps split by amplitude
order and paired in the color sum -- the Fortran GET_MATRIX contract,
JAMP(NCOLOR,NAMPSO) -> RES(NSQAMPSO). Masking alone would keep the scalar
signature, so it is not the return type that blocks it; it is that all three color
sums (color_sum_cpu, the CUDA kernel, the BLAS path) take a single jamp vector per
helicity, as do the color folding, the C-parity de-duplication and the
good-helicity scan reading their output. The per-component vector on top of that
would also need an ME buffer with a component index, which the SIMD lane layout
and every consumer of allMEs assume away.

madspace needs nothing and got nothing: MatrixElementOutput has no component kind
and the umami metadata keys carry no squared-order count, so there is no ABI slot
and no consumer for a component vector. The gate covers `output mg7` too, so
madspace can never be handed a process whose mask it cannot honour.

The predicate is factored out of set_chosen_SO_index as
export_v4.chosen_squared_orders so the two cannot drift; the Fortran matrix.f,
CHOSEN_SO_CONFIGS included, is byte-identical.

NOT DONE, deliberately: the other C++ exporters share the gap. `output
standalone_cpp` of `u u~ > t t~ QED^2==2` still returns 6.1741266e-01 (measured).
Gating ProcessExporterCPP would cover standalone_cpp, mg7_v5, matchbox_cpp and
pythia8 in one line, but matchbox_cpp has IOTest goldens and a golden
regeneration is in flight, so that is left as its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit refused a '^2' constraint that drops a squared-order
component, because this backend had no way to compute one. It has one now, and
the refusal goes with it: `u u~ > u u~ QED^2==2` returns the QCD-EW cross term
-5.5828746494657258e-02 where it used to return the whole +2.7756451.

THE SHAPE, from the Fortran GET_MATRIX contract
JAMP(NCOLOR,NAMPSO) -> RES(NSQAMPSO). jamp_sv becomes njampso = ncolor*nampso
long, one vector per amplitude split order, and each amplitude is written into
the vector of the order it belongs to. The color sum pairs them.

THE PAIR LOOP RUNS OVER ALL nampso*nampso ORDERED PAIRS, and that is the one
thing in here that is easy to get wrong. The color contraction keeps the
triangular matrix with its off-diagonal doubled, which for a single jamp vector
is just the statement that the (i,j) and (j,i) terms are conjugates. For two
DIFFERENT jamp vectors they are not, so what the triangle computes for one
ordered pair (m,n) is NOT that pair's contribution -- only (m,n) and (n,m)
together are, and their sum is right. Halving the loop the way the color indices
are halved gives roughly twice the answer. Masking stays safe for the same
reason it is symmetric: a squared order is the SUM of the two amplitude orders
(sqSoIndex[m][n] == sqSoIndex[n][m]), so a pair and its transpose are always
kept or dropped together and the masked total stays real.

A DEDICATED TEMPLATE, color_sum_splitorders.cc, because the sum changes shape
rather than gaining a hole. Everything else is a hole whose empty fill is the
text that was there before -- process_class.inc, process_matrix.inc,
process_function_definitions.inc, process_sigmaKin_function.inc -- so a process
without split orders is byte-for-byte unchanged, checked on
`p p > j j QCD^2==4` (8 subprocesses), `u u~ > t t~ QED^2==4` and plain
`u u~ > t t~`.

SWITCHED OFF WHILE THE JAMPS ARE SPLIT

  The jamp CSE/orbit optimisation. Its shared sub-expressions are partial sums
  of amplitudes, and two amplitudes of different orders must not land in the
  same one; the flows are written out one amplitude at a time instead.

  The BLAS color sum, which multiplies a single jamp vector by the color matrix.

  Color choice is NOT split: jampso_sv[icol] = sum_M jamp_M[icol] is the color
  flow of the whole amplitude, and deliberately unmasked -- it picks which flow
  to write out, it is not an observable. With one amplitude order it is exactly
  today's jamp2.

GPU REFUSES AT COMPILE TIME. The device jamp buffers are sized ncolor, not
njampso, and the backend is a make-time flag rather than an output-time one, so
the refusal cannot live in the exporter: color_sum_splitorders.cc #errors under
MGONGPUCPP_GPUIMPL. The exporter says so at generation as well, so a GPU build
is not the first the user hears of it.

VERIFIED by building and running, at the standard RAMBO point with FPTYPE=d
(mixed precision hides differences here), on `u u~ > u u~`:

  component   madmatrix                 Fortran split-order driver     rel
  QED=0        2.8276928588371750e+00    2.8276928588371737e+00      4.7e-16
  QED=2       -5.5828746494657258e-02   -5.5828747824035893e-02      2.4e-08
  QED=4        3.7810076327210495e-03    3.7810071013307570e-03      1.4e-07

  The two EW rows do not agree to machine precision, and that is not new: the
  EW couplings differ between the Fortran and C++ builds at ~1e-7 relative.
  QED^2==4 prunes to a single amplitude order and never reaches any of this
  code, yet shows the same 1.4e-7 -- which is how one can tell. It is why the
  cross-backend tests in this tree compare at atol=1e-7 rather than in ulps.

  The check that does pin the interference algebra uses one backend, one set of
  momenta and one set of parameters: the three components sum to the
  unconstrained total to 1.6e-16 relative (2.7756451199752390 against
  2.7756451199752394).

  And the pair loop with nothing masked reproduces the old single-jamp color sum
  bit for bit: `u u~ > t t~ QED^2<=4` gives 6.1741265892435804e-01 either way.

  Repeated on `u u~ > d d~ g`, where ncolor is 4 rather than 2 so the color
  triangle inside the pair loop is a real one: QED=0 2.0061172866596953e-04
  (1.4e-15 from Fortran), QED=2 -1.5150831345723654e-05, QED=4
  5.9366352531549790e-06. The two EW rows are 1.8e-11 and 1.2e-12 from Fortran
  in absolute terms -- 1.2e-6 and 2.0e-7 relative, the interference amplifying
  the coupling difference because it is a cancellation. The three components sum
  to the independently computed QED^2<=4 total BIT-IDENTICALLY.

STILL NOT DONE, and unchanged by this: the other C++ exporters have no
squared-order machinery either (`grep split_orders madgraph/iolibs/export_cpp.py`
is 0 hits), so `output standalone_cpp` of `u u~ > t t~ QED^2==2` still builds and
returns 6.1741266e-01. That wants either this treatment or a refusal, in its own
change -- matchbox_cpp shares its base class and has IOTest goldens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two commits above were written on a base that predates the big output
format renaming and the removal of the color reflection folding, so replaying
them here needs three adjustments that are not part of the feature:

  'standalone_mg7' is now spelled 'standalone' (the Fortran one is
  'standalone_fortran'). The exporter named itself by the old name, which only
  reached the user through the informational message, and the test drove the
  old name on the 'output' line -- where it is not an error but a directory
  name, so the test silently exercised the default format instead.

  color_sum_splitorders.cc summed over ncolorfold with a colorFoldRep[] map.
  There is no folding on this branch, so it sums over ncolor directly, which is
  the same code the folding degenerated to when it found nothing to fold.

  The expected interference is re-measured here: -5.5828746494657265e-02 from
  standalone_fortran on this base, which the C++ reproduces to 1.3e-16 relative
  rather than the ~1e-7 the couplings used to differ by.

Verified on u u~ > u u~ at the standard RAMBO point, FPTYPE=d:

  QED=0   2.8276928588371750e+00
  QED=2  -5.5828746494657258e-02   (Fortran: -5.5828746494657265e-02)
  QED=4   3.7810076327210495e-03
  sum     2.7756451199752387e+00
  total   2.7756451199752394e+00   (QED^2<=4, computed independently)

and the no-split-orders path is byte-identical to the base commit for
u u~ > u u~, g g > t t~ and p p > j j QCD^2==4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from claude/dixon-maltoni-gluon-basis-69e1d7 to main September 10, 2026 16:19
@oliviermattelaer
oliviermattelaer merged commit a32cfea into main Sep 11, 2026
521 checks passed
@oliviermattelaer
oliviermattelaer deleted the claude/madmatrix-interference-syntax-63a130 branch September 11, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant