Sum the four gluon current into the cubic one carrying the same colour factor - #57
Sum the four gluon current into the cubic one carrying the same colour factor#57oliviermattelaer wants to merge 39 commits into
Conversation
Each colour structure of the four gluon vertex carries exactly the same colour factor as the diagram obtained by splitting that vertex into two cubic ones, so the two can be summed before the colour algebra is applied. Verified against the colour algebra for g g > N g, N=2..5: 3/30/405/6300 links, every one landing on a single cubic diagram, and the merged count matching (2n-5)!! throughout. Generation now runs with the quartic vertex replaced by two cubic vertices joined by an auxiliary line, and puts them back together afterwards. Since the from_group rule picks the canonical decomposition from the topology alone, never from which particle sits on a line, the diagram carrying the auxiliary line is rooted exactly like the cubic one it has to be summed with, and shares every current except that line. Partner coverage goes from 30/60 to 405/405 at six gluons. The recovered vertex only carries the colour structure the two cubic vertices reproduce, which Vertex now records. Which structure that is depends on the order ALOHA receives the legs, so it is settled against sorted_mothers rather than on the generated diagram. g g > g g g 33 -> 15 wavefunctions, ~1.42x g g > g g g g 111 -> 81 wavefunctions, ~1.12x The quartic amplitudes are also summed into their cubic partner, which shrinks the JAMP block from 1091 to 697 lines at six gluons. That one costs nothing at runtime: the JAMP optimiser was already finding those pairs. |M|^2 is unchanged, to the last bit at four and five gluons and to one ulp at six where the summation order differs. All of it is behind MG_MERGE_QUARTIC, off by default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_quartic_wavefunction_merges looks for a quartic current and the cubic current it shares a colour factor with sitting at the same node, so that the sum can be done once on the current instead of on every amplitude it feeds. Both carry the same 1/P^2, so the sum itself is exact. It finds nothing, and that is the correct answer rather than a missing case. At six gluons there are 90 candidate pairs, but the two currents do not have matching consumers: quartic current 11 is used by amplitudes 7,9,36,38,60,62 whose images under the merge map are 1,31,55, while its cubic partner 9 is used by 1,5,31,34,55,58. Summing would hand the contribution to all six consumers of the partner while only three have an amplitude being dropped, so 5,34,58 would silently gain a term. The extra consumers are the diagrams whose own last vertex is quartic, which take their contribution from a different node. Attaching the contribution to the node itself, so that every consumer is entitled to it, needs the auxiliary current to be materialised rather than collapsed back into VVVVxP0_1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Splitting the quartic vertex into two cubic ones at generation time gave one diagram per colour structure: g g > 4g went from 220 diagrams to 510, and generation from 0.070s to 0.205s. The amplitude count was unchanged (510 either way) and so was |M|^2, but the diagram list is what the user sees, and what drives the MadEvent multichannel, so fragmenting it is not a trade worth making for the currents it shared. Reverted: get_auxiliary_model, collapse_auxiliary_diagrams, the Vertex 'color_key'/'aux_pair' pinning and everything honouring it in colorize, in the two colour expansion loops and in get_base_vertex. No auxiliary particle is left anywhere. Kept: unroll_quartic_vertices and the link map it builds, verified against the colour algebra for g g > N g, N=2..5, together with the amplitude sums which still emit 405 folds and still shrink the JAMP block from 1091 to 697 lines at six gluons. g g > 4g is back to 220 diagrams in 0.063s, |M|^2 unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A diagram is a tree, and which of its vertices is written last decides which internal lines become currents: everything on the far side of the final vertex is built up as a wavefunction, while the final vertex only produces an amplitude. Re-rooting therefore changes which currents exist without changing the diagram, which is what will let a quartic current find the cubic current carrying the same colour factor. The vertices are first split into the external legs they hold and the internal lines they share, since a leg number alone does not identify a line -- a vertex reuses the smallest incoming number for the leg it produces. The tree is then walked outwards from the new root, and the line pointing back at it becomes each vertex's outgoing leg, flipped to the antiparticle where the re-rooting reverses it. Checked against UnrollDiagramTag for g g > N g, N=2..4: all 7, 65 and 755 possible rerootings give back the same diagram, none altered, none refused. They are not no-ops either -- at six gluons a diagram reaches 2, 3 or 4 distinct sets of currents depending on where it is rooted. Nothing calls this yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written so the work can be picked up in a clean session: what is established by measurement, what is already committed, the five remaining steps, and the seven pitfalls that cost time getting here. The key result it rests on is the seed rule -- forbid two 3-gluon vertices from sharing a line. Unrolling a quartic always produces two adjacent cubic vertices, so a diagram is reachable iff it has an adjacent cubic pair to contract back, which makes that seed necessary and sufficient. It reconstructs the full diagram set exactly at 4, 5, 6 and 7 gluons from 25%, 40%, 25% and 15.5% of the diagrams. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unrolling a quartic vertex always yields two cubic vertices joined by the line which replaced it, so a diagram can be put back that way exactly when two of its cubic vertices already share a line. The diagrams which have no such pair are the ones generation has to produce, and every other one is reachable from them -- necessary and sufficient, hence exact coverage. reduce_leglist now drops any combination which puts two of those cubic vertices on the same line, tracking the lines they produce by leg number. The closing vertex needs both cases: a real n->0 interaction takes its legs as lines coming in, while the identity vertex states that its two legs are the two ends of one line, which is how a 2->2 reduction ends -- missing that one left g g > g g with its full four diagrams. Measured against the full generation filtered by an independently written adjacency detector, comparing the diagrams themselves and not just counts: g g > g g 4 -> 1 g g > g g g 25 -> 10 g g > g g g g 220 -> 55 g g > 5 g 2485 -> 385 g g > 6 g 34300 -> 4165 g g > t t~ g g 123 -> 84 Behind MG_MERGE_QUARTIC, off by default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seed generated in the previous commit stands for the whole set: every
diagram left out is one of its quartic vertices replaced by the pair of cubic
vertices one of their colour structures factorises into. Expanding it back
gives the baseline diagram list again, so nothing user-visible moves, and the
unrolled diagram keeps the decomposition of the seed it came from -- which is
the property the current sum needs, a quartic current and its cubic partner
sharing every other current.
Several seeds reach the same diagram, so the expansion dedups on the diagram
itself. That has to be done on the glued form: while the identity vertex is
still there the same diagram has several spellings, and a quartic vertex
sitting just in front of it is not yet the last one -- which is what decides
how ALOHA indexes its colour structures. Gluing it in right away, rather than
at the end of generate_diagrams, settles both. Without it the expansion
deduped nothing (40 diagrams for g g > g g g instead of 25) and the recorded
colour chains disagreed with the colour algebra on 8 links out of 30.
Expanding is confluent, so running the diagrams it produces through the same
treatment adds nothing to the set but does give the link for the quartic
vertices they have left, which is the whole map for free.
process diagrams (= baseline) links |M|^2 vs baseline
g g > g g 4 3 bit identical
g g > g g g 25 30 bit identical
g g > g g g g 220 405 1.5929925846563245e-04
vs ...3380e-04
g g > 5 g 2485 6300 6.6739867626784624e-07
vs ...4560e-07
Every link agrees, target for target, with the one the colour algebra gives
independently through unroll_quartic_vertices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A decay chain keeps its identity vertex rather than gluing it in, which is what the expansion relies on to compare two diagrams and to index the colour structures of a quartic vertex. Loop amplitudes were already excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Steps 1, 2, 3 and 5 are done; step 4 is not, and the reason is a counting one rather than a bug. Expanding the seed makes 340 (seed, choice) instances for the 220 diagrams at six gluons, so a diagram is reached from several seeds, and since it carries one decomposition it can be rooted to match at most one of its quartic partners -- while the current sum needs the match at every node. Measured: of the 50 genuine current pairs at six gluons, none pass the 1:1 consumer test, and none of the 135 at seven; only five gluons, the size at which no diagram is reached twice, works. The flag as it stands is worth +4.1% at seven gluons and -0.6% at six, and nearly all of that is the amplitude sum shrinking the JAMP block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Where a quartic current and the cubic current carrying the same colour factor feed the same vertex, the two amplitudes they give differ by that one line and by nothing else. Summing the currents once and calling the amplitude on the sum gets both contributions out of a single call: W(20) = W(19) W(20)%W(:) = W(19)%W(:) + W(12)%W(:) CALL VVV1_0(W(4),W(5),W(20),GC_10,AMP(33)) instead of one VVV call for the cubic amplitude, one VVVV call for the quartic one, and an addition. The sum is shared by every amplitude reading it, so it pays for as many calls as it has users -- 60 amplitude calls for 30 sums at six gluons, 432 for 60 at seven. Substituting several mothers of the same amplitude also produces the amplitude with all of them substituted, so every subset has to be a merge into that same target weighing the product of the coefficients; the substitutions which do not pass are left as they were. That check is what keeps the count honest, and it is why the two-substitution cases at seven gluons are not taken: their other single is spelled with a different rooting and is not the same amplitude object. The slot reuse had to be told about it. reuse_outdated_wavefunctions works out when a slot is free from the diagrams alone, and the sum is an extra read it cannot see -- without that, the two currents were handed the same slot and the line came out as W(11) + W(11). process helas calls JAMP lines per-call time g g > g g g 94 -> 93 + 7 131 -> 101 34.88 -> 35.12 s g g > g g g g 637 -> 612 + 30 1082 -> 688 47.77 -> 46.02 s g g > 5 g 8159 ->7784 + 60 23672 -> 7864 42.16 -> 39.80 s so +3.8% at six gluons and +5.6% at seven, where the seed reconstruction alone had been worth -0.6% and +4.1%. |M|^2 bit-identical at four and five gluons, 1e-15 at six and seven, and unchanged for g g > t t~ g g and u u~ > g g g. With the flag off matrix.f is byte-identical to before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 4 is done where the vertex reading the sum is an amplitude, which is where it is safe: nothing sits above one, so no consumer can be handed a term it must not have. Deeper it stays blocked for the counting reason already written down. Adds the slot-reuse trap as a pitfall -- a wavefunction number is not a slot, and anything emitting an extra read has to extend the lifetime there -- and corrects pitfall 7, which turns out to be wrong at seven gluons: the JAMP block alone is worth +3.1% there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan claimed another 432 amplitude calls at seven gluons were waiting to be picked up. They are not. Substituting two mothers of one amplitude also produces the amplitude with both substituted, weighing the product of the two coefficients, and the merge map has to agree -- but the sign from diagram_colour_signature does not factorise over two contractions. Taking the double's coefficient over the known single's, no merge source into the target weighs what the missing single would have to weigh for 150 of the 432. So the ceiling is at most 282, on top of the identification problem: all 432 have exactly one of the two singles present, the other being the same diagram rooted differently. subset_is_merged already refuses these, which is what keeps the current code sound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things had to change for the optimisation to survive the madevent path.
The sum is now written as CALL SUMW_1(W(a),W(b),W(c)) rather than as two
assignments. Helicity recycling rebuilds the whole DAG from the calls alone,
so a bare assignment was invisible to it: the summed current never entered
the graph and the amplitude reading it died on a KeyError. Written as a call
it is an ordinary internal wavefunction taking two mothers and every existing
mechanism handles it. sumw_1 and subw_1 go into aloha_functions.f; the
coefficient is restricted to +-1, which is all it has ever been.
hel_recycle.add_indices could not index an AMP( opening a statement -- the
pattern ate the character in front of it, and there is none at the start of
the line -- so the fold came out as "AMP(31) = AMP( K,31) + AMP( K,1)" and
would not compile. Looking at that character rather than eating it fixes it.
Latent until now, since nothing emitted a line starting on AMP(.
AMP2 needs no change and is left as it is: the fold lines run before it, so
the channel weight is the amplitude including the four gluon contribution,
which is what it should be. The quartic diagrams never had an AMP2 entry of
their own -- get_amp2_lines skips anything with a four point vertex -- so the
folded amplitudes are not referenced anywhere.
g g > g g g through madevent, 10000 events, three seeds each:
cross section rel. error ME cpu
flag off 3.680-3.694e+07 pb 0.326% 52.7 s
flag on 3.684-3.694e+07 pb 0.300% 49.0 s
so 8% less error for 7% less cpu. Including the four gluon piece in AMP2
makes it a better channel weight, not a worse one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interesting part is not the matrix element speedup but the channel weights: get_amp2_lines skips any diagram with a four point vertex, so with the flag off four fifths of the amplitude at six gluons -- 405 of 510 pieces -- entered no AMP2 at all. Folding puts each of them in the channel whose colour factor it shares. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four gluon contributions now reach AMP2 -- 510 amplitude pieces instead
of 105 at six gluons -- and the question was whether that helps or hurts the
phase space integration. It does neither, measurably.
generate_events wall time turned out to be a bad metric: the refine stage
adapts, and the same configuration swings by a factor four between seeds
(217 s to 877 s with the flag off at six gluons). Adding a survey only run --
the same fixed number of points both ways, so the error measures the channel
weights and nothing else -- and quoting standard errors over independent
seeds settles it:
off - on significance
g g > g g g error (3 seeds) -8% 1.8 sigma
g g > g g g cpu (3 seeds) -7% 1.1 sigma
g g > g g g g survey error (6) -10% 1.1 sigma
g g > g g g g full error (4) -5% 0.8 sigma
g g > g g g g full cpu (4) -1% 0.0 sigma
Every difference points the same way and none of them is established. Cross
sections agree throughout. Calling it either way would need tens of seeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SUMW_1/SUBW_1 as C++ templates next to ALOHAOBJ in cpp_hel_amps_h.inc, and
the madmatrix helas call writer emits them the same way the Fortran one does
-- as soon as the later of the two currents is made -- and skips the
amplitudes they take away. They cannot use the INLINE macro: that is defined
by the ALOHA generated block further down the header.
The colour amplitudes had to be sorted out first, and this fixes a real bug
rather than adding a feature. get_color_amplitudes dropped every merge source
from the JAMPs, on the assumption that the caller writes out the amplitude
sums to put them back. Only the Fortran writer does that, so C++ and python
output with MG_MERGE_QUARTIC set was quietly losing four fifths of the
amplitude. It now takes merge_quartic_amplitudes, and a backend which writes
no sums keeps those amplitudes in the JAMPs, where their own colour
coefficients give the identical result -- the two carry the same colour
factor, which is the whole premise. So madmatrix gets the current sums, which
really do remove work, and leaves the rest alone.
One bug found on the way, in the shared writer: a wavefunction number can be
listed by more than one diagram in the madmatrix matrix element (two objects
for the same current with the mothers ordered differently), so the sum was
written twice -- 50 lines for 30 sums at six gluons. Harmless numerically,
both write the same value to the same slot, but wasted. Both writers now
write each sum once.
|M|^2, FPTYPE=d, against the same output without the flag:
g g > g g g 1.8740711159594241e-02 vs ...317e-02
g g > g g g g 1.5929925846563324e-04 vs ...478e-04
and byte-identical CPPProcess.cc with the flag off.
Speed is mixed, and worse than Fortran:
amp calls nwf evt/s (FPTYPE=d, sse4)
g g > g g g 45 -> 38 12 -> 26 72950 -> 66800 -8.4%
g g > g g g g 510 -> 450 51 -> 111 2702 -> 2726 +0.9%
The wavefunction array more than doubles, because the sums take a slot each
at the end and are never recycled, and there is no JAMP fold here to pay for
it. At five gluons that loses outright. Recycling the sum slots through
reuse_outdated_wavefunctions is the obvious next step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sums take a wavefunction slot each at the end and are never recycled, which more than doubles NWAVEFUNCS. Fortran absorbs that because the JAMP fold pays for it; madmatrix has no such fold and loses 8.4% at five gluons. Recycling those slots is now the first item under where to go next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each sum used to get a wavefunction slot of its own at the end, never reused,
which more than doubled NWAVEFUNCS and was what made madmatrix lose 8.4% at
five gluons. A sum is an ordinary producer -- written as soon as the later of
its two currents is made, dead after the last amplitude reading it -- so it
can go through reuse_outdated_wavefunctions with everything else. It also
lets the cubic current die at the sum rather than at the amplitude, since the
amplitude no longer reads it.
NWAVEFUNCS
flag off own slots recycled
g g > g g g 12 26 19
g g > g g g g 51 91 66
g g > g g g g (madmatrix) 51 111 86
One bug had to be fixed first, and it is not mine: the same wavefunction can
be listed by more than one diagram in the madmatrix matrix element (two
objects for the same current with the mothers ordered differently), and
reuse_outdated_wavefunctions handed it a slot again on the second listing.
The first one leaked, and once the sums shared the pool that was no longer
harmless -- g g > g g g g came out 0.2% wrong. A wavefunction now takes one
slot at its first appearance and keeps it until its last use. This changes
nothing with the flag off: matrix.f and CPPProcess.cc are byte-identical for
N=2..4 in both backends.
|M|^2 unchanged: bit-identical at four and five gluons, 1e-14 at six, and the
madevent run for g g > g g g gives the same cross section and error as before
(3.666e+07 +- 1.058e+05 pb, seed 33).
per-call time before after
fortran, 6 gluons +3.8% +3.9% (the slot count was not the bottleneck)
madmatrix, 5 gl. -8.4% -6.6%
madmatrix, 6 gl. +0.9% +1.9%
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wavefunction store is what the optimisation costs and the JAMP block is what it buys, so both are tabulated against the multiplicity. Four gluons is a wash, five loses on madmatrix, six and seven win on both and the gain grows. The relative memory cost falls the other way, from +58% at five gluons to +8% at seven, which is why the trade turns positive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UNITTEST_proc is what ./tests/test_manager.py leaves behind; two commits picked it up through git add -A. Removed and added to .gitignore so it cannot happen again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reuse_outdated_wavefunctions is a linear scan allocator over lifetimes in emission order, so NWAVEFUNCS depends on the diagram order -- the wavefunction set does not, being content addressed, but the slot count does. Emitting every seed first, as the expansion does, is the worst case for it. Placing each diagram at its last discovery rather than its first takes both the locality and the sums: 27 slots instead of 66 at six gluons, below the 51 of the unoptimised code, with all 30 sums kept, and Fortran exact at N=2..5. Not shipped because it makes g g > g g g g wrong in madmatrix, which has duplicate wavefunction listings that a directly built matrix element does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up on the diagram order (thanks @oliviermattelaer — the premise of my earlier answer was half wrong). I claimed the wavefunction count was order-independent. That is true of the wavefunction set (content addressed, Four orders built and run. A sum can only be formed when its quartic current is emitted before the target amplitude, which makes this a trade rather than a free win:
Emitting a seed followed by its own unrollings gives the locality but loses the sums — the fully-cubic target is usually claimed by an earlier seed, so the quartic current arrives after it. Placing each diagram at its last discovery instead of its first fixes that and takes both: all the sums, and a slot count below the flag-off baseline (27 vs 51 at six gluons, 219 vs 268 at seven). Fortran stays exact at N=2..5 and is marginally faster (+3.7% / +7.4%). Not in this PR: it makes Worth chasing: at six gluons it takes the wavefunction store from 6600 B to 2700 B, under the 5100 B of the unoptimised code, with every current sum kept. Written up in |
g g > 5 g in madmatrix finally finished compiling: 41.75 -> 43.33 evt/s, +3.8%, with nwf 268 -> 320 and |M|^2 agreeing to 1.6e-15. The generated CPPProcess.cc was checked byte-identical against what the committed code produces, since the build had been launched before the ordering experiments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The missing row is in.
So the shape is the same in both backends: a wash at four gluons, a loss at five in madmatrix, and a growing win from six on. Fortran gains roughly twice what madmatrix does at the same multiplicity, which is the amplitude-fold difference (madmatrix has no One caveat I checked rather than assumed: this build had been launched before the diagram-ordering experiments, so it could have been generated with an experimental order. I regenerated with the committed code and confirmed |
…c one is_unrolled_pair matched on the lines alone: same four coming in, same one going out. A quartic vertex makes one current per colour structure and all three take the same lines and make the same line, so the lines cannot tell them apart -- only the pairing can, read against sorted_mothers. Nothing moves on the shipped ordering (same 7/30/60 sums, same |M|^2 in both backends), but it removes a class of mismatch which was only not biting by luck. Also records where the reordering breaks madmatrix. Ruled out by measurement: the reordering itself, slot corruption, coloramps.h, the merge_quartic_amplitudes=False path, and the duplicate listings on their own. What is left, from replaying both generated files symbolically: the folding is ordering sensitive in madmatrix (40 of 450 amplitudes differ) and not in Fortran (0 of 450). madmatrix carries two objects for the same current and match_quartic_mothers compares mothers by number, so which copy an amplitude holds decides which pairs are matched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rged by equality Root cause, and it is a latent MG5 bug rather than one of this optimisation. The reconstruction can build the same cubic current with its two mothers in either order, and sorted_mothers leaves them alone because for two identical gluons its key ties. VVV1P0_1 is antisymmetric under exchanging its two inputs -- measured, VVV1P0_1(a,b) + VVV1P0_1(b,a) = 0 exactly -- so the two objects are negatives of each other. HelasWavefunction.__eq__ compares mothers by sorted number and calls them equal, and export_cpp renumbers wavefunctions by that equality, so the two land on one slot inside a single matrix element and one of them silently carries the wrong sign. The Fortran writer never hits it because it does not renumber by equality. With the committed diagram order the collisions happen not to matter; the reordering moves them somewhere they do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Root cause of the madmatrix breakage — found, and it is a latent MG5 bug rather than one of this optimisation. I could not do the two steps in the order asked (fix the duplication, then apply the reordering), because the "duplication" turned out not to be a duplication at all. The chain:
The Fortran writer never hits this because it does not renumber by equality. With the committed diagram order the collisions happen not to matter; the last-discovery order moves them somewhere they do. Ruled out along the way, each by measurement rather than argument: the reordering itself (exact with the sums off), slot corruption (no read-before-write, no sum aliasing its own input, I have not applied the reordering, because the fix is upstream of this PR and the direction is yours to choose:
Shipped in the meantime ( |
Flag off, the branch before this work (3b3ed9e, amplitude merges only), and today. The generated code at the time the pull request was opened is byte-identical to today in both backends for N=2..5, the only code change since being the colour structure check, so those are one column. Records too that madmatrix had no usable numbers before this work: get_color_amplitudes dropped every merge source unconditionally while only the Fortran writer wrote the sums back, so any C++ output with the flag set lost 405 of its 510 amplitudes at six gluons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… reorder
The wavefunction count went *up* under the flag, which it should not have.
Measured at six gluons: 111 -> 146, and 20 of the 35 extra are the same
current built with its two mothers in the opposite order. sorted_mothers
leaves them alone, because for two identical gluons its key ties and the sort
is stable, so they stay two objects -- and being negatives of each other
(VVV1P0_1 is antisymmetric, measured: VVV1P0_1(a,b) + VVV1P0_1(b,a) = 0) they
cannot be shared. Taking the legs of both unrolled vertices in a canonical
order removes every one of them:
wavefunctions of which order-flipped twins
g g > g g g 33 -> 39 0 -> 0
g g > g g g g 111 -> 126 (was 146) 20 -> 0
g g > 5 g 898 -> 925 (was 955) 30 -> 0
That also removes the reason the last-discovery diagram order could not be
used. Those twins are what HelasWavefunction.__eq__ calls equal while they
differ by a sign, and export_cpp renumbers wavefunctions by that equality, so
they landed on one slot inside one matrix element and one of them silently
carried the wrong sign. With no twins left there is nothing to collide, and
the order goes in: each diagram is placed at its last discovery, which puts
it after every seed which can reach it and therefore after every quartic
current which can be summed into it. All the sums survive.
NWAVEFUNCS off before now
g g > g g g 12 19 19
g g > g g g g 51 66 78
g g > 5 g 268 290 259
per call / evt/s off before now
fortran g g > g g g g 47.77 s 45.61 s 45.25 s +5.3%
fortran g g > 5 g 42.19 s 39.98 s 40.10 s +4.9%
madmatrix g g > g g g 74229 66757 67566 -9.0%
madmatrix g g > g g g g 2651 2784 2880 +8.6%
|M|^2 stays exact: bit-identical at four gluons, 1e-16 at five, 1e-14 at six
and seven, in both backends. With the flag off matrix.f and CPPProcess.cc are
byte-identical to before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You were right that the wavefunction count should not have gone up — chasing that fixed the madmatrix blocker as well. At six gluons the flag took the wavefunctions from 111 to 146. Breaking that down: 20 of the 35 extra are the same current built with its two mothers in the opposite order. Taking the legs of both unrolled vertices in a canonical order removes every one:
And those twins were exactly what broke madmatrix. They are what Where that leaves the numbersstandalone Fortran,
madmatrix,
At seven gluons the slot count is now below the unoptimised build in both backends: 259 against 268. Speed:
Six gluons roughly doubles in madmatrix (+3.1% → +8.6%) and improves in Fortran (+3.7% → +5.3%). Five gluons still loses in madmatrix — 7 sums against 7 saved amplitude calls does not cover the extra slots, and madmatrix has no amplitude folds to make up the difference.
Note the timings are taken in batches with about 1% of drift between them, so each column should be read against the |
Six orders measured. Reordering never changes which currents exist, only how long each is alive, so the wavefunction count is the same throughout. The shipped last-discovery order gives 19/78/259 and nothing beats it while keeping the sums: reversing it gives 12/54/199, far the best, and loses every sum, because it puts each target ahead of the quartic currents feeding it. A register-pressure greedy under the unrolling precedence buys 19 -> 18 and 78 -> 76 and nothing at seven gluons, for an O(n^2) pass taking generation from 0.88 s to 3.24 s there and minutes at eight gluons. Not kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MG_MERGE_QUARTIC becomes "set merge_quartic_vertices <value>", taking False (the default), speed or slots. True is accepted as a synonym for speed. The interface pushes it onto madgraph.merge_quartic_vertices, which is what the generation and the exporters read; do_add syncs it as well, since the option can also arrive from mg5_configuration.txt where the setter is not called. It cannot be an output option. The diagram order is fixed while the diagrams are generated, so by output time it is already too late. The new mode is the one the gpu question asks for. The wavefunction store is a stack frame on cpu but is per thread on gpu -- about 24 kB a thread at seven gluons -- so which way the trade goes depends on the hardware: g g > 5 g amplitude calls slots per thread off 7245 268 25.1 kB speed 6813 259 24.3 kB slots 7245 199 18.7 kB 6% more arithmetic for 23% less memory. 'slots' reverses the diagram order, which is far the best on register pressure and puts each target ahead of the quartic currents feeding it, so the current sums cannot be built -- the amplitude merges, and the JAMP block they shrink, are kept. Which one a gpu wants has not been measured: there is no device here. |M|^2 checked for all three values at five and six gluons in both backends, and with no 'set' at all the generated matrix.f is byte-identical to the unoptimised one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
merge_quartic_vertices gains 'auto': generate in the 'speed' order and let
apply_quartic_diagram_order reverse it at output time, when the backend about
to be handed the matrix elements is known. Only possible because the two modes
differ in nothing but the order, and 'slots' is 'speed' reversed.
The choice is read off the *matrix element* exporter rather than the output
format. "output madevent --me_exporter=<gpu backend>" hands one
_curr_matrix_elements to both exporters in a single export_processes call, so
its two backends cannot carry different orders -- and that is the cudacpp
production path, the one case the option exists for. Keying on the format
would hand it the cpu order; keying on the me exporter gives it 'slots',
NWAVEFUNCS=54 rather than 78 at six gluons.
Two pre-existing behaviours had to be worked around, neither of them specific
to this option:
- an export mutates the diagrams it is given, 345 of 757 vertex leg records
on g g > g g g g, and reversing mutated diagrams gives an equivalent but
differently numbered result. So the reordering starts from a copy taken
before the first export. deepcopy of the amplitudes is not available --
it drags the model along and trips the array.array assert in
color_algebra.create_copy -- but the diagrams alone copy cleanly, 0.011 s
at seven gluons.
- an export also drops the marks saying the diagrams came from a seed, so
whether an amplitude may be reordered is read before the first export
rather than at the output which wants to reorder it.
Byte-identical checks: 'auto' + standalone reproduces a native 'speed'
generation, 'auto' + standalone_mg7 a native 'slots' one, "output madevent
--me_exporter=standalone_mg7" a native 'slots' madevent, and a session going
standalone -> standalone_mg7 -> standalone reproduces its first output for
the third. Processes the seed rule never applied to are untouched. 916 unit
tests, three of them new, same two pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Defaulting merge_quartic_vertices to 'auto' was tried and reverted. It breaks
NLO generation wherever the real emission has four gluons, p p > j j [QCD]
among them:
born g g > g g real g g > g g g
link_rb_configs(born, real, 5, 4, 4)
flag off -> [2, 5, 12]
'auto' -> FKSProcessError: could not link born diagram
The generation is not at fault: with the flag on, g g > g g g still has its 25
diagrams and the same tag set under both DiagramTag and UnrollDiagramTag.
link_rb_configs is order dependent by accident -- it deduplicates real_tags
but not good_diags, then walks the two in lockstep, so they only stay aligned
while the dedup drops nothing, and which representative of a duplicated tag
survives is decided by the diagram order.
Fixing that is an NLO change and nothing here validates NLO, so the default
stays False. Nine more unit tests encode the old diagram order and would have
to be re-based as well; they are listed in the doc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Defaulting merge_quartic_vertices to 'auto' was tried and reverted. It is safe
on the paths it was built for -- UFO fortran, madmatrix, the python exporter,
each validated on |M|^2 -- but turning it on for everything found three
consumers reading the diagram or amplitude *structure* rather than the result.
1. fks born/real linking. link_rb_configs finds the vertex splitting ij into
i and j and takes it out; the unrolling re-roots the real diagrams and can
put that pair in the closing vertex, where there is nothing to take out.
p p > j j [QCD] raised FKSProcessError. FIXED here, by generating an NLO
process with the merging off in FKSMultiProcess.__init__ -- so the option
is now safe for an NLO user, not merely for the default. g g > g g [QCD]
generates byte-identically with the option set and unset, where before it
crashed.
2. the legacy FortranHelasCallWriter. Only FortranUFOHelasCallWriter emits
the amplitude folds which put the merged contributions back, so the
MG4-style writer computes AMP(1..3) from GGGGXX and then leaves them out
of the JAMPs -- silently wrong |M|^2, not a crash. NOT fixed: it wants
merge_quartic_amplitudes=False the way export_cpp and export_python do,
but get_JAMP_lines is on the exporter and does not know its writer.
3. anything pinning the diagram order, which is cosmetic but wide.
So the default stays False and defaulting it on wants an audit of those
consumers rather than another round of patching outward.
Fixed independently of all that: link_rb_configs built real_tags deduplicated
but left good_diags as it was, then walked the two in lockstep, so they only
stayed aligned while the dedup dropped nothing. A no-op on every process in
the suite, but it made the result order dependent for no reason.
The loop helas sanity checker now knows a folded amplitude is left out of the
jamps on purpose; the fks, colorize and DiagramTag tests which read a diagram
by its position say so and pin the plain order.
916 unit tests, same two pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Looked for a fourth place reading the representation rather than the result. Did not find one in the shipped tree, and the search is worth keeping because it bounds what is left. Every merged-JAMP consumer against every writer: get_color_amplitudes has four call sites and three pass merge_quartic_amplitudes=False; only export_v4's three get_JAMP_lines* take the merged default, and every fortran exporter pairs with FortranUFOHelasCallWriter, which emits the folds. The base get_amplitude_merge_lines returns [] and FortranHelasCallWriter does not override get_matrix_element_calls, so it is the one writer which drops them -- selected exactly when _model_v4_path is set. No other combination reaches merged JAMPs without folds. A mechanical audit of the generated code -- for each AMP(n), written? read? -- run with the flag off as a control, over standalone, matchbox, madevent grouped and not, a decay chain, helicity-recycled files, u u~ > g g g, g g > t t~ g g, u u~ > u u~ g g and four to six gluons. Clean everywhere, including the split order path, whose amp_orders does list folded amplitude numbers but never reaches the code because the colour amplitudes no longer mention them. The two places which weight or group results are unaffected: p p > j j groups into the same five directories with byte-identical configs.inc and coloramps.inc, and find_symmetry keeps the same equivalence classes and multiplicities, [3,3,3,6] at five gluons and [3,6,12,12,12,12,12,12,24] at six, only the representative indices renumbering. Residual risk of this class is two named things: import model_v4, and a plugin supplying its own helas_exporter paired with export_v4's merged JAMPs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A full sweep of g g > N g and g g > t t~ N g, off against speed and slots, on
generation time, source and object size, wavefunction slots, amplitude counts
and runtime. Timings are the minimum of five runs, which matters: a single run
carries about 3% of noise and most of the effects are smaller than that. The
floor of the method is 0.6%, measured on g g > t t~ where off and speed give a
byte-identical matrix.f and still time 3.975 against 4.000 us.
What the amplitude columns show, and neither was obvious before:
- the AMP array never shrinks. It is declared COMPLEX*16 AMP(NGRAPHS) at the
full diagram count whatever the mode, so all three builds allocate 113 kB
at seven gluons and speed leaves 432 entries written by nobody. Only the W
array ever gets smaller, and only under slots.
- slots mode computes every amplitude, on all eight of its rows. With no
current sums nothing is skipped, so it does the baseline's amplitude work
plus the folds and buys only a shorter JAMP block and a shorter W array.
That is why it is slower than off almost everywhere rather than a wash.
And what the optimisation is actually good for is generation time and code
size rather than speed: g g > 5 g generates in 19.4 s rather than 37.1 s, a
48% cut, with matrix.o going 5.8 MB to 3.6 MB. Runtime is a steady 5-8% above
six particles and nothing at all below.
|M|^2 agrees to 8.5e-15 or better on every row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Full sweep of both series, all three modes, on speed / memory / generation time. This supersedes the standalone timings in the description, which were single runs on
Timings are the minimum of five runs of the shipped W slots is
What the columns showThe AMP array is recycled, and it is where
What it is actually good forGeneration time and code size, more than speed. Full write-up in How far it generalisesBoth series above are gluon-rich, so a third process was measured as a control:
The payoff tracks the quartic fraction, exactly. In
So It is also the one process where |
The sweep puts the turnover of the full merging at six external legs, and shows it costing slots below that, so auto now gates on it: generate_diagrams only takes the seed rule at madgraph.merge_quartic_min_legs legs or more, six by measurement. speed and slots asked for by name stay unconditional, that being the way to get the merging on a small process anyway. What is left below the threshold is not nothing, which was worth measuring rather than assuming. The amplitude merges do not need the seed rule -- they come from the colour algebra through unroll_quartic_vertices -- so they still apply, and they shrink the JAMP block without touching the wavefunctions: g g > g g g slots JAMP temporaries per call off 12 72 87.75 us auto (merges only) 12 42 84.25 us +4.0% speed (full) 19 42 87.25 us -1% So below the threshold auto beats both: it keeps the JAMP fold, which is free, and drops the reordering, which is what costs the seven extra slots. At four and five legs elsewhere it is neutral rather than positive and never negative, at an unchanged slot count. Above the threshold nothing moves -- auto at six legs generates a byte-identical matrix.f to speed. 917 unit tests, one of them new, same two pre-existing failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AMP was declared COMPLEX*16 AMP(NGRAPHS) at the full diagram count in every
mode, so seven gluons allocated 113 kB of it and speed left 432 entries
written by nobody. get_amplitude_slots now does for AMP what
reuse_outdated_wavefunctions does for W, and the enabling change is where the
merges are written: they used to come out in one block at the end, which kept
every source alive to the end, and each is now written as soon as both of its
amplitudes exist, which frees the source.
process mode AMP entries total per call
g g > g g g g off 510 13.1 kB
speed 316 12.9 kB -2%
slots 106 7.1 kB -46%
g g > 5 g off 7245 140.4 kB
speed 5869 118.0 kB -16%
slots 946 35.0 kB -75%
slots reaches the floor and speed does not, and that is the diagram order
rather than the allocator. Only (2n-5)!! amplitudes are read by the JAMPs, 945
at seven gluons, and the rest are merge sources which could share a handful of
entries. speed emits every seed before its unrollings, so a source is born
early and its target arrives late and nothing can be reclaimed in between.
Reversing that puts each source beside its target, so slots lands one above
the floor. That is worth a lot to the case slots exists for: it used to buy
23% of the wavefunction store, and now buys 75% of the whole per-thread
working set.
It buys no time -- 2247 -> 2260 us on speed and 2347 -> 2373 on slots at six
gluons, both inside the 0.6% floor. The arrays were already cache resident at
this size. This is a memory change, not a speed one.
NGRAPHS only ever dimensioned AMP inside matrix.f so it becomes the entry
count, with ngraphs.inc keeping the diagram count. The JAMPs go through
map_color_amplitudes and AMP2 through get_amplitude_slot_map; AMP2 was the one
worth checking, since multichannel reads individual amplitudes, and it reads
only merge targets, which are the entries that stay put. Verified on a
madevent output at six gluons: 316 written, 316 read, no entry read that is
never written.
|M|^2 unchanged on every row. 918 unit tests, one new, same two pre-existing
failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Two follow-ups from the sweep: a multiplicity gate on
|
g g > g g g |
slots | JAMP temporaries | per call |
|---|---|---|---|
| off | 12 | 72 | 87.75 µs |
auto (merges only) |
12 | 42 | 84.25 µs, +4.0% |
speed (full) |
19 | 42 | 87.25 µs, −1% |
So below the threshold auto beats both — it keeps the JAMP fold, which is free, and drops the reordering, which is what costs the seven extra slots. At four and five legs elsewhere it is neutral and never negative, at an unchanged slot count. At six legs and above auto generates a byte-identical matrix.f to speed.
The AMP array is recycled
AMP was declared COMPLEX*16 AMP(NGRAPHS) at the full diagram count in every mode — 113 kB at seven gluons, with speed leaving 432 entries written by nobody. get_amplitude_slots now does for AMP what reuse_outdated_wavefunctions does for W. The enabling change is where the merges are written: they used to be emitted in one block at the very end, keeping every source alive to the end, and each is now written as soon as both of its amplitudes exist.
| process | mode | AMP entries | AMP | W slots | W | total per call |
|---|---|---|---|---|---|---|
g g > g g g g |
off | 510 | 8.0 kB | 51 | 5.2 kB | 13.1 kB |
| speed | 316 | 4.9 kB | 78 | 7.9 kB | 12.9 kB (−2%) | |
| slots | 106 | 1.7 kB | 54 | 5.5 kB | 7.1 kB (−46%) | |
g g > t t~ g g |
off | 159 | 2.5 kB | 26 | 2.6 kB | 5.1 kB |
| speed | 109 | 1.7 kB | 35 | 3.6 kB | 5.3 kB (+3%) | |
| slots | 106 | 1.7 kB | 29 | 2.9 kB | 4.6 kB (−10%) | |
g g > 5 g |
off | 7245 | 113.2 kB | 268 | 27.2 kB | 140.4 kB |
| speed | 5869 | 91.7 kB | 259 | 26.3 kB | 118.0 kB (−16%) | |
| slots | 946 | 14.8 kB | 199 | 20.2 kB | 35.0 kB (−75%) |
slots reaches the floor and speed does not, and the reason is the diagram order rather than the allocator. Only (2n−5)!! amplitudes are read by the JAMPs — 105 at six gluons, 945 at seven — and the rest are merge sources that could share a handful of entries. speed emits every seed before its unrollings, so a source is born early and its target arrives late and nothing can be reclaimed in between: 5869 rather than 945. Reversing that order puts each source beside its target, so slots lands one above the floor.
That changes the slots case substantially. It used to buy 23% of the wavefunction store for 6% more arithmetic; it now buys 75% of the whole per-call working set at seven gluons — which is the number that matters on a GPU, where this is per thread.
It buys no time, and I want to be clear about that: speed 2247 → 2260 µs and slots 2347 → 2373 µs across the change at six gluons, both inside the 0.6% floor and if anything marginally the wrong way. The arrays were already cache-resident at this size. This is a memory optimisation, not a speed one.
Correctness
NGRAPHS only ever dimensioned AMP inside matrix.f, so it becomes the entry count while ngraphs.inc keeps the diagram count. Everything reading AMP afterwards goes through the same map — the JAMPs via map_color_amplitudes, AMP2 via get_amplitude_slot_map. AMP2 was the one worth checking, since multichannel reads individual amplitudes: it reads only merge targets, which are exactly the entries that stay put. Verified on a madevent output at six gluons — 316 written, 316 read, nothing read that is never written.
|M|² unchanged on every row; 918 unit tests, two new, same two pre-existing failures.
The full sweep tables were taken before AMP was recycled, so their AMP columns described an array sized at the full diagram count, and the paragraph reading them off said the AMP array never shrinks. Both re-measured on the current code, all 24 builds again, timings the minimum of five as before. The AMP column is now the entry count rather than the amplitude count, and the two are worth telling apart -- at seven gluons slots runs 7245 amplitude calls through 946 entries. g g > 5 g generate matrix.o W slots AMP entries per call off 35.2 s 5.8 MB 268 7245 141.0 ms speed 16.6 s 3.4 MB 259 5869 132.8 ms +6% slots 18.4 s 3.4 MB 199 946 134.3 ms +5% Two cells were re-measured on their own because a single run had caught noise: g g > t t~ g generation read 5.3 s against 1.8 s over three runs, and the 0.6% floor is now 4.000 against 3.988 us on the byte-identical pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both series in the sweep are gluon-rich, so u u~ > z g g g g was measured as a
control: seven legs like g g > t t~ 3g, but with a Z and a quark line, so most
of its diagrams have no four gluon vertex at all.
It is the weakest case measured, and predictably so. In slots mode the AMP
entry count turns out to be exactly total amplitudes minus merge sources, to
within one, on every process measured:
amplitudes merge sources AMP in slots saving
g g > 5 g 7245 6300 (87%) 946 -87%
g g > g g g g 510 405 (79%) 106 -79%
g g > t t~ 3g 1890 945 (50%) 946 -50%
u u~ > z 4g 516 132 (26%) 384 -26%
So the payoff tracks the quartic fraction. Runtime follows at +4% rather than
+7 to +9%, and the working set at -8% rather than -30 or -75%.
It is also the one process where slots does not reduce the wavefunctions
either, 84 against off's 76, which breaks the pattern from the gluon-rich
processes where reversing the order always recovered them. There slots is the
worst of the three: 5% slower than off and larger in W, for an AMP count it
barely wins over speed. speed still behaves, +4% with generation down 21%,
which is what the auto gate picks at seven legs anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each colour structure of the four gluon vertex carries exactly the same colour factor as the diagram obtained by splitting that vertex into two cubic ones. So the quartic current and the cubic current carrying that factor can be summed before the colour algebra is applied, and the amplitude reading the sum gets both contributions from a single call:
The option, and the default to decide
Trueis accepted as a synonym forspeed. With nosetat all the generatedmatrix.fandCPPProcess.ccare byte-identical tomainforg g > N g, N=2..5, in both backends.speedandslotshave to besetand notoutputoptions: they fix the diagram order while the diagrams are generated.autois the exception, and only becauseslotsis thespeedorder reversed — see below.Why two modes
The two objectives pull in opposite directions, and the constraint is sharp: a current sum needs its quartic current emitted before the target amplitude, and that ordering constraint is exactly what costs slots.
g g > 5 gspeedslotsOn CPU that store is a stack frame and stays cache-resident, so the amplitude calls decide —
speedwins, measured. On GPU it is per thread, ~24 kB of local memory at seven gluons, and it is what caps occupancy — so 6% more arithmetic for 23% less memory is plausibly the better trade.I have not measured that. There is no CUDA device on the machine I ran on. Everything below is CPU/SIMD; the case for
slotsis an argument, not a measurement.auto— letting each output chooseThe wrinkle behind the default question is that the option is read at
generatetime while the backend is chosen atoutputtime, and one generation can feed both.autocloses that: it generates thespeedorder and reverses it atoutputtime, once the backend about to receive the matrix elements is known.The choice comes from the matrix element exporter, not the output format.
output madevent --me_exporter=<gpu backend>hands one_curr_matrix_elementsto both exporters in a singleexport_processescall, so its fortran driver and its gpu matrix elements cannot carry different orders. That is the cudacpp production path — the one case the option exists for. Keying on the format would hand it the cpu order; keying on the me exporter gives itslots(NWAVEFUNCS=54rather than 78 at six gluons).Verified byte for byte:
auto+standalonereproduces a nativespeedgeneration,auto+standalone_mg7a nativeslotsone,output madevent --me_exporter=standalone_mg7a nativeslotsmadevent, and a session going standalone → standalone_mg7 → standalone reproduces its first output for the third. Processes the seed rule never applied to are untouched.Two pre-existing behaviours had to be worked around, neither specific to this option:
g g > g g g g. Reversing mutated diagrams gives an equivalent but differently numbered result, so the reordering starts from a copy taken before the first export.copy.deepcopyof the amplitudes is not available (it drags the model along and tripsassert type(col_obj) != array.arrayincolor_algebra.create_copy); the diagrams alone copy cleanly, 0.011 s at seven gluons.quartic_unroll_tagshas 0 entries instead of 405 — so whether an amplitude may be reordered is read before the first export, not at the output that wants to reorder it.So what should the default be?
Still yours to pick, but
autoremoves the reason to compromise:False(what this PR does). Opt-in, no behaviour change, nothing to decide until someone runsslotson a device.auto. Each backend gets the order it wants, with no per-process choice to make. This is what I would move to once there is a gpu number.speed. Best for cpu, but makes a choice on behalf of the gpu backend.I still recommend (1) for this PR:
autois only worth defaulting to once theslotshalf of it is backed by a measurement, and there is no CUDA device on the machine I ran on — everything below is CPU/SIMD, and the case forslotsremains an argument.Results (
speedagainst off)Two runs each, reproducible to ~0.1%. Standalone is the shipped
checkdriver loopingSMATRIX; madmatrix ischeck_sa.exe perfbuiltFPTYPE=doncppsse4— the default mixed-precision build rounds both to the same value and would hide any difference.Speed
g g > g gg g > g g gg g > g g g gg g > 5 gFour gluons is a wash — there is nothing to sum, the only quartic vertex is the whole amplitude. Five gluons loses on madmatrix: 7 sums against 7 saved amplitude calls does not cover the extra slots, and madmatrix has no amplitude folds to make up the difference. Six and seven gluons win on both.
Timings were taken in batches with ~1% of drift between them, so each row should be read against the
offmeasured alongside it.Memory and work —
slots / wavefunction calls (+ sums) / amplitude callsstandalone Fortran:
speedg g > g g gg g > g g g gg g > 5 gmadmatrix — the slot counts are now the same as Fortran's:
speedg g > g g gg g > g g g gg g > 5 gHow it works
reduce_leglistdrops any combination putting two of them on the same line: 1 / 10 / 55 / 385 diagrams instead of 4 / 25 / 220 / 2485.Bugs fixed on the way
Four, all pre-existing and none of them about this optimisation:
get_color_amplitudesdropped every merge source from the JAMPs, assuming the caller writes the amplitude sums to put them back. Only the Fortran writer does — so C++/madmatrix/python output with the flag set was silently losing four fifths of the amplitude. It now takesmerge_quartic_amplitudes.hel_recycle.add_indicescould not index a statement-initialAMP(— the pattern ate the character in front of it, and there is none at the start of a line. Latent until something emittedAMP(31) = AMP(31) + AMP(1).reuse_outdated_wavefunctionshanded a second slot to a wavefunction listed by more than one diagram, leaking the first.VVV1P0_1(a,b) = −VVV1P0_1(b,a)(measured), butHelasWavefunction.__eq__compares mothers by sorted number and calls them equal, andexport_cpprenumbers wavefunctions by that equality — so the two landed on one slot inside one matrix element and one silently carried the wrong sign. Sidestepped here by building the unrolled vertices with their legs in a canonical order, so the pair never arises; the underlying__eq__/export_cppmismatch is still there for anything else that produces both orders.Validation
|M|²bit-identical at four gluons, 1e-16 at five, 1e-14 at six and seven, in both backends and for all three option values; unchanged forg g > t t~ g gandu u~ > g g g.mainfor N=2..5 in both backends.g g > g g g, 10000 events, three seeds — cross sections agree. The channel weights now see all 510 amplitude pieces instead of 105, becauseget_amp2_linesskips any diagram with a four point vertex. Measured effect on the integration: none either way — every difference is under two sigma once the seed-to-seed scatter is accounted for, andgenerate_eventswall time swings by a factor four between seeds for the same configuration, so a fixed-work survey comparison over six seeds was added.main(a test-ordering artefact and a missing scipy).Not done
AMParray to fold into — which is why five gluons still loses there.slots-- and therefore theautodefault -- is unproven.Full write-up, including the pitfalls and the measurements that closed off the alternatives, in
docs/gluon-quartic-plan.md.🤖 Generated with Claude Code