Skip to content

MLM merging - #72

Merged
oliviermattelaer merged 113 commits into
mainfrom
feat-mlm
Oct 29, 2026
Merged

oliviermattelaer merged 113 commits into
mainfrom
feat-mlm

Conversation

@theoheimel

Copy link
Copy Markdown
Contributor

Overview

This branch implements MLM merging in madspace. The core part is a clustering algorithm that takes momenta as its input and returns:

  • renormalization scale
  • factorizations scales
  • scales assigned to outgoing jets
  • a diagram index that will be used to assign color flows, resonances etc
    The first two affect the cross section and PS distributions, whereas the last two are relevant for the LHE output.

The two central parts are:

  • madspace/src/phasespace/mlm_clustering.cpp: takes the diagram topologies for the given subprocess as input and compiles them into an efficient binary representation of the possible clustering histories. This representation will also nicely generalize to Amplicol.
  • madspace/src/kernels/mlm.hpp: the actual compute kernel run during event generation for CPU and GPU. Based on the previously compiled state machine, it determines a clustering and then assigns the scales. The functions djb_clus, dj_clus, compute_scale, update_momenta are directly ported from the NLO fortran code. The actual clustering logic happens in mlm_clustering. The actual scale computation (after a clustering has been determined) is mainly based on my conversations with Rikkert when he was at CERN, and should be similar to what the NLO code does.

Things to discuss

Rikkert might have opinions here. I don't really know the physics well enough...

  • how to deal with resonances: MG5 (both NLO and LO) does that based on the SDE integration channel which is not an option with madspace/madnis. The current implementation does it by prioritizing resonant clusterings over non-resonant ones using a cutoff. This might not be ideal and result in non-continuous PS distributions.
  • what should the actual scale look like: The current version resembles the NLO one (without the Sudakov correction). Some things are quite different from LO. For instance, the factorization scale is assigned to be the softest clustering, no matter if it is actually linked to the initial state.

Missing pieces

  • this branch has not been touched in a while, so we should probably merge some of the recent changes
  • madspace/src/phasespace/mlm_clustering.cpp: right now the metadata whether a given vertex is QCD, an output is a jet, propagator masses etc. does not get populated. Instead massless QCD is assumed. In principle all the necessary information should be there, so this should be trivial to change
  • pass on the selected diagram index to the matrix element. There is a currently unused UMAMI input key for that, but it will require some changes on the madmatrix side to support a per-event diagram input (@Qubitol has worked on this). Not so urgent since this only affects the LHE output, not the actual scales. So maybe we can do this once we have merged some other stuff (especially @stloufra's backend separation)

Validation

Here is a three-step plan. The challenging part of validation is that it is not always clear what the baseline is (see things to discuss), also given that to some extent there is no right and wrong here.

Unit tests

  • check clustering result at the level of individual phase-space points. A simple "smoke test" is already there
  • baseline suggested by Rikkert: scales extracted from a MG5 NLO run, but without the Sudakov corrections. Will require some hacking. Goal: identical output to NLO (if this is what we want...). Problem: works for ren/fac scale, but we don't really have a baseline for the scales assigned to the jets since this works differently in FxFx and there is only one scale passed to the shower

Parton level

  • do cross sections look reasonable compared to MG5 MLM?
  • are the scales reasonably distributed?
  • what does reasonable even mean? is this something we can systematically test?

Shower level

  • compare merged observable histograms after shower
  • do we get smooth distributions?
  • are there processes where things are likely to fail?

@Qubitol

Qubitol commented Aug 19, 2026

Copy link
Copy Markdown
Member

Thank you.

The changes required in MadMatrix are those in madgraph4gpu#1086.

Also, let's not forget madgraph4gpu#1090, for the related changes in MadtRex.

oliviermattelaer and others added 10 commits September 5, 2026 10:07
Bring the MLM merging branch up to date with main (699 commits).

Notable conflict resolutions:

* The generated instruction-set headers (compgraphs/*.inc, cpu/gpu
  runtime mixins, python/instruction_set.hpp) are no longer checked in:
  main generates them from instruction_set.yaml at build time. The
  branch's edits to those files are dropped; only the two new
  mlm_clustering_{hadronic,leptonic} entries in instruction_set.yaml
  are kept, and the headers regenerate from them.

* madevent.py was split into launch.py / run_interface.py. The MLM
  hunks (init_beam scale selection, EnergyScale construction, matrix
  element diagram input) are re-applied to launch.py. The matrix
  element is now built in a loop over backends, so me_inputs is
  computed once before that loop.

* run_card.toml is now a template filled from banner.py, so the "mlm"
  option and the new beam.jet_radius parameter are declared there.

* LHECompleter was refactored into helper methods on main; main's
  version is taken. The branch's TODO about the permutation direction
  is dropped: main fixed exactly that by inverting the permutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clustering compiled and ran but the histories it produced were not
usable. Fixes, in rough order of impact:

* update_momenta was called with its i_remove/i_keep arguments swapped.
  The state machine names the merged pseudo-particle by the lowest bit of
  the combined mask, i.e. by particle1, so particle1 is the slot that has
  to survive; keeping particle2 left the merged momentum where no later
  state looks, and made the "i_keep < 2" initial-state branch dead and
  inconsistent with the caller's "is_initial = particle1 < 2". The
  Fortran settles the convention: in cluster_one_step the removed index
  runs from 3 and the kept one runs 1..i-1, so the kept index is the
  lower one and "jwin <= 2" means the kept particle is a beam.

* The winner selection was only partly reset between clustering steps.
  win_resonant and win_next_state survived, so after a resonant step no
  non-resonant candidate could win the next one, and the walk would then
  jump to the previous step's target.

* Nothing guaranteed a winner. A NaN measure never compares less than
  anything, so a step whose candidates were all NaN left win_next_state
  at -1 and the kernel read outside the state machine - an actual crash,
  reproducible at a few events per thousand once the initial-state
  branch of update_momenta became reachable, since its boost and
  rotation can leave a pair exactly collinear. dj_clus now zeroes a NaN
  result the way the Fortran does, and the selection takes the first
  candidate unconditionally.

* find_clusterings expanded a state once per path reaching it, appending
  a duplicate copy of its transitions each time.

* bw_cutoff was threaded into the kernel and never read: the resonance
  window was mass +- width instead of mass +- bw_cutoff * width.

* external_masses is indexed by external leg but was built in topology
  slot order. The permutation is now applied, and diagrams that disagree
  on a leg's mass throw instead of silently picking one.

* alive was hardcoded to 0xFFFFFF regardless of the particle count, and
  a dead-end state would have been read out of bounds.

The vertex metadata (is_qcd, is_jet1/2, mass_index, massive_in/out) was
hardcoded, so massless QCD was assumed everywhere and the resonance
branch was unreachable. Masses and widths now come from Topology::Decay
and from the t-channel propagator lists; Topology gained
t_propagator_pdg_ids() alongside them, since the flavour of a t-channel
line changes along the chain. The external pdg ids are not in Topology
at all, so MLMClustering takes them as a new optional argument, which
launch.py fills from the pids it already computes. Omitting it keeps the
old "everything is a QCD jet" behaviour.

tests/test_mlm.py replaces the smoke test with 50 tests covering the
structure of the compiled state machine, the kernel outputs, the
symmetries of the clustering, the resonance window, and the clustering
measures against transcriptions of the Fortran. Where a property does
not hold exactly, the test pins how far off it is and says why:

* the jet-scale assignment flips for a few percent of events under a
  symmetry transformation, because degenerate candidates make the argmin
  unstable;
* the choice of clustering is not longitudinally boost invariant,
  because the 1.000001 "against the beam" penalty tests sign(pz);
* scales are not exactly homogeneous in the momenta, because
  update_momenta keeps the Fortran's absolute 100 GeV^2 guard against
  boosting into a degenerate frame.

docs/mlm-merging-status.md records all of this along with the known
differences from madevent and what is still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
p p > e+ ve j at 13 TeV with matched cuts, PDF set and beam energy, 5000
events on each side. The hardest-parton pT distributions agree, so what
is left is the scale definitions.

Where both codes assign a jet a real clustering scale they agree
exactly: pt_clust / pT_jet is 1.000 at every quantile. mu_R differs by a
factor 0.61, which follows from the definitions in closed form and is
reproduced event by event from the LHE momenta for the q qbar
subprocesses.

The finding worth acting on is that mg7 leaves 27.2% of jets with
pt_clust = 0, where madevent leaves none. It comes from the s-channel
g q -> q* -> (W)(q) topology, whose only clusterings are the W formation
and (W, jet) - the latter has a colourless parent, so it is not a QCD
vertex and books no jet scale. madevent avoids this with an explicit
fallback to sqrt(s), which it also applies to non-jet legs, where mg7
writes 0. Since pt_clust is what an MLM veto compares against qcut, a
jet reported at zero is indistinguishable from a lepton.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pt_clust is what an MLM veto compares against qcut, so a leg that no QCD
clustering assigned a scale to has to come out at a value the veto can
never trip on. madevent does this with the "ptclus = etot" fallback in
Template/LO/SubProcesses/reweight.f; the kernel wrote 0, which a veto of
the form "reject if any pt_clust < qcut" would reject on every event.

The fallback value is the collider energy, which the kernel did not
have: it is not derivable from the momenta, since those give the
partonic energy. So MLMClustering takes it as a constructor argument
(cm_energy, filled by launch.py from process.e_cm) and it is threaded
through as a new input of the two mlm_clustering instructions.

Rerunning the p p > e+ ve j comparison, jets reported at zero go from
27.2% to 0%, and the non-jet legs now read 13000 exactly, the same as
madevent. Where both codes assign a real scale they still agree exactly
(pt_clust / pT_jet is 1.000 at every quantile), and the assigned-scale
medians are 29.50 against 30.67.

This is the convention fix, not the underlying one: mg7 still reaches
the fallback for 26.4% of jets against madevent's 10.1%, because the
s-channel g q -> q* -> (W)(q) history contains no QCD splitting at all.
docs/mlm-merging-status.md records what would actually close that gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pt_clust_i is the shower starting scale: an MLM run turns on
Beams:setProductionScalesFromLHEF (madevent_interface.py), so Pythia
reads it into event[i].scale() and limits the shower off that parton
with it. The jet-matching veto never reads it, and only FxFx uses the
per-particle scale, as a tag.

madevent assigns each leg the hardest vertex on the parton *line* it
belongs to - the scale at which the line was produced - by following the
line with ipartupdate and taking a max. The kernel instead froze the
scale at the first clustering the bare leg took part in, which is the
last ME emission off that line. That is the CKKW question of where to
restart the shower: madevent's choice lets the shower attempt the
emissions the MLM veto then rejects, and those trials are the Sudakov
suppression; freezing at the softest vertex never attempts them.

Both are now selectable with beam.jet_scale_scheme, defaulting to
"production" (madevent's). Getting there needed the parton line followed
through the clustering, so:

* the compiler works out per transition, from the colour representations
  of the mother and the two daughters, which daughter the line continues
  into - the first, the second, the harder one, or both after g -> q qbar
  - and stores it as a third word in the state machine;
* the kernel keeps the resulting representative legs per slot, the
  equivalent of ipart, and compares transverse momenta in the original
  event when it has to pick the harder daughter, as the Fortran does;
* colour representations come from the pdg_color_types map already
  exported in the subprocess metadata, with an SM fallback, and are
  inferred from the daughters when a propagator's flavour is unknown.

Two fixes fell out of running p p > e+ ve j j, which crashed before:

* find_clusterings created a transition whenever *some* diagram allowed
  the clustering, not whenever one of the diagrams still in play did.
  With none left the target state could never be expanded, and the walk
  had nowhere to go. Such transitions are now skipped, and a state whose
  expansion turns up nothing is dropped rather than pointed at. This
  blocked every process above 2 -> 3.
* is_colored_pdg treated the merged-flavour placeholders 82 and 83 as
  coloured; clean_pids maps them to an electron and a neutrino.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The differential jet rates that say whether the merging works live on
their own branch, since reconstructing them from a showered event record
is as useful for madevent as it is for madspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MLM: merge main into feat-mlm (base of #98 and #99)
Reverting each fix in turn and rerunning the suite showed one that no
test noticed: indexing external_masses by topology slot instead of by
external leg. The shipped fixtures cannot see the difference, because
their permutations only exchange identical gluons, so the two orderings
agree.

Add a topology whose permutations move the top onto a slot another
permutation gives to a gluon. The two orderings then disagree about the
mass of leg 2, which the kernel cannot represent, so the constructor has
to refuse it rather than silently pick one - and with the slot-order bug
put back the refusal does not happen and the test fails.

Two other reverts also went unnoticed: removing dj_clus's NaN guard, and
removing the "take the first candidate" guard. Those are deliberately
redundant with the SCALE_MAX sanitisation, so dropping either on its own
leaves the others covering it. Say so in the docstring of the test that
covers the behaviour they exist for, rather than implying each guard is
individually pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MLM merging needs the matrix element to stop describing radiation the
parton shower is meant to produce. madevent enforces that with xqcut;
madspace had no equivalent, so its samples reached down to whatever the
jet cuts allowed and Pythia's matching veto had to throw the difference
away.

The rule is madevent's, from the "Check xqcut for vertices with jet
daughters only" block of Template/LO/SubProcesses/reweight.f: walking the
clustering history that was just selected, any step whose daughter is
still a bare final-state jet has to be at or above xqcut, or the event is
dropped. The test does not ask whether the vertex was a QCD one, and it
reuses the same "still a bare external leg" bookkeeping the emission
scheme already maintains, so it costs one branch in the loop that is
already there.

The clustering only runs for events that already passed the phase-space
cuts, so the veto comes back as a weight - 1 or 0 - that multiplies the
event weight rather than as one of the cuts. An event below xqcut ends up
at weight zero and is never unweighted. It is a new trailing output of
the two mlm_clustering instructions, which leaves every existing
positional index alone.

p p > t t~ plus t t~ j at 13 TeV, everything else held fixed: 1462 +- 20
pb with no cut, 1181 +- 17 at 30 GeV, 815 +- 13 at 60 GeV. Off by
default, so turning it on is the only thing that can change an existing
result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SCALUP holds one number and the two codes disagree about which: unwgt.f
writes sqrt(max(q2fact)), i.e. mu_F, while madspace writes ren_scale.
Comparing that field between them is apples to oranges, which is how the
earlier claim that the mu_R definition explains the high-multiplicity
cross-section difference came to rest on nothing.

Emit both explicitly as the standard LHEF3 attributes - mur, muf, and
muf1/muf2 since madspace can carry a different factorisation scale per
beam - so a comparison does not have to guess. Nothing that was written
before changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oliviermattelaer and others added 7 commits October 27, 2026 15:08
…er setclscales

Review follow-ups to the pdfrwt fix:
- rewgt now sets every external identity of the clustered graph to row
  IPSEL right after setclscales (the jet-only reset further down and the
  pre-setclscales writes on a possibly stale igraphs(1) are gone), and the
  leading pdfrwt entry is read from it.
- drop the 81 branches that used FLAVOR(:) as a pdg (ickkw<=0 booking and
  s_rwfact): FLAVOR is the position of the coupling-class representative,
  and leshouche rows always carry physical pdgs, so ickkw<=0 books idup.
- cluster.f: say what the FLAVOR mapping really gives (the class
  representative, not the event's flavour).
- acceptance test test_mlm_pdfrwt_flavour (p p > z + 0,1j, ickkw=1), run in
  acceptancetest_50; it fails on the old code (5720 wrong entries / 20k).

Same-seed samples are identical to the previous commit for ickkw=1
(p p > z + 0,1,2j) and ickkw=0 (p p > z + 0,1j): kinematics, mgrwt and
every weight.
- rewgt: set only the incoming partons and the jets of the clustered graph
  to row IPSEL (as before the previous commit), so non-QCD legs no longer
  carry the previous event's flavours into the next event's clustering.
- rewgt: stop with a clear message if a leshouche row ever holds a merged
  id (81-83) for an incoming leg, instead of handing it to pdg2pdf.
- cluster.f: map flavour position 6 as well (top merged into 81 when
  massless), consistent with the comment.
- test_mlm_pdfrwt_flavour: identify the beams by the pz sign and check
  that every ladder quark flavour is present in the event.

Same-seed p p > z + 0,1,2j (ickkw=1) sample identical to the previous
commit (kinematics, mgrwt, every weight); the acceptance test passes.
…valuate

Under clustering_history = "diagram" or "madevent" every accepted event went
through the matrix element twice: once at the scales of the history over
every diagram (which also gives the diagram weights the history is picked
from), and again at the scales of the history it settles on. Only the
scales change between the two, and the matrix element sees them only
through alpha_s. With one power n of alpha_s for every diagram,
|M|^2 = C alpha_s^n and the cross section is linear in each beam density,
so the second evaluation is the first one times
(alpha_s' / alpha_s)^n f1' f2' / (f1 f2) - to rounding, and exactly where
the history did not change, which under "madevent" is most events. The
colour, helicity and diagram weights of the first call stay valid: their
probabilities are ratios of terms of the same power.

Integrand takes the power as mlm_history_qcd_power (launch.py passes the
subprocess's qcd_power); -1 (mixed powers, e.g. gluon fusion with VBF)
keeps the second evaluation. A first density of exactly zero keeps its
zero weight.

t t~ + 0,1,2j, 100k events, wall time: madevent history 80 s -> 46 s
(all_diagrams: 40 s). Cross sections unchanged: t t~ nominal 500k
2157.10 -> 2157.23 pb, W+-+0,1,2j madevent-like 14656.8 -> 14656.9 pb
(MC errors 0.04-0.1 %). No unit test compares the two paths event by
event: the Integrand draws its own random numbers, unseeded from Python.
beam.clustering_mean_pdf_ladder (off by default) walks madevent's pdf
ladder under clustering_mean: the beam densities, the per-beam
factorisation scale they end at and the ladder slots are madevent's,
while mu_R, the alpha_s vertex scales, the jet scales and the xqcut veto
stay clustering_mean's. Without it clustering_mean evaluates both
densities once at the softest clustering scale, which is what moves the
W+jets 1-jet rate near qcut by ~25% against madevent.

The kernel runs the madevent beam-line walk when pdf_reweighting == 2
and restores the clustering_mean outputs after it; native systematics
vary the ladder in that case too.

Co-Authored-By: Claude <noreply@anthropic.com>
clustering_mean_pdf_ladder now defaults to true, in the run card and in
the madspace API. Against madevent (Pythia8 DJRs) it closes the 12-15%
hole above qcut that the single softest-scale density leaves for W+jets
and W Z+jets, and halves the tt~+jets excess; it made no sample worse.
The unit tests about clustering_mean's own factorisation scale switch it
off explicitly.

Co-Authored-By: Claude <noreply@anthropic.com>
With two massless partons left in the final state they balance in pT, so
clustering either into the beam it travels along scores the same mT. The
walk compared rounded values, so an ulp decided which beam took the parton,
and a rotation about the beam axis could change it. Which beam it is decides
where that beam's line stops being a QCD line, i.e. jcentral, and so the
madevent mu_F: 831.8 vs 1195.4 GeV for one ttgg test event in 200, under
scale_scheme=madevent and clustering_mean with the pdf ladder alike.

madevent's cluster.f has the same tie and settles it by rounding too; in
exact arithmetic it keeps the first candidate of its loop order, which is
(particle2, particle1) order on madspace's slots. Treat measures within 1e-12
as equal and take that order. It subsumes the earlier initial-beats-final
rule for the DJ massless-massive tie, which was madevent-measure only.

Add rotation and longitudinal-boost tests for the two beam-line schemes.
@oliviermattelaer
oliviermattelaer marked this pull request as ready for review October 28, 2026 13:41
oliviermattelaer and others added 4 commits October 28, 2026 14:41
…variance

MLM: settle equal clustering candidates in cluster.f's loop order (mu_F rotation invariance)
…pdf-ladder

MLM: pdf ladder under scale_scheme = clustering_mean, on by default (+ #347)
madevent MLM systematics: leading <pdfrwt> entry used the flavour class, not the event's s/c flavour
@oliviermattelaer

Copy link
Copy Markdown
Contributor

@theoheimel will merge this as soon as the CI is green.

oliviermattelaer and others added 12 commits October 28, 2026 21:01
# Conflicts:
#	madspace/src/phasespace/integrand.cpp
#222 leaves weight_after_cuts out of the common part's weight product
unless flavor or mirror sampling contribute to it, as it is one by
construction otherwise. On feat-mlm build_channel_part also multiplies
in the merging weights (alpha_s vertices, pdf ladder and its vetoes) and
the scale floor veto when the history is over every diagram, so a
single-flavor, unmirrored MLM sample silently lost all of them.
test_a_density_too_small_to_divide_by_drops_the_event caught it: the
events the pdf ladder vetoes came back with a non-zero weight.

Co-Authored-By: Claude <noreply@anthropic.com>
… rule, tests

- run card: the MLM clustering options other than max_jet_flavor
  (jet_radius, jet_scale_scheme, scale_scheme, parton_line_scheme,
  alphas_reweighting, pdf_reweighting, clustering_mean_pdf_ladder,
  clustering_measure, clustering_history) are hidden. RunCardMG7.write
  adds a hidden parameter at the end of its section once it is user set
  or differs from its declared default, so a set value (also from
  from_LO, which sets scale_scheme without user=True) survives a rewrite.
  The pdf ladder comment no longer quotes sample-specific numbers.
- Integrand: has_channel_weight_after_cuts() is the one statement of
  when build_channel_part multiplies a weight into weight_after_cuts.
  The common part uses it, and build_channel_part throws at graph build
  if it pushes a weight the helper does not announce, instead of the
  common part dropping it silently (what the #222 merge did to the MLM
  weights).
- launch.py: the systematics' ladder decision is mlm_pdf_ladder(), with
  a unit test of its cases.
- scale.hpp: the mlm_pdf_reweighting() comment no longer says only
  scale_scheme madevent walks the ladder.
- test_mlm: the ladder veto through the whole Integrand also under
  clustering_mean + ladder (single flavour, no mirror: the case the
  weight_after_cuts merge bug hit), and the systematics' pdf product
  under clustering_mean with and without the ladder.

Co-Authored-By: Claude <noreply@anthropic.com>
Under clustering_mean + pdf ladder the beam-line walk used to overwrite
mu_R, the alpha_s vertex scales, the jet scales and the merging-jet flags
and restore a snapshot of them afterwards. Each of those writes now sits
under one madevent_scales flag inside the walk, and madevent's mu_R is
kept in a local (walk_ren_scale) that the mu_F fallbacks still use, so
nothing has to be put back: a new write in the walk is next to the flag
rather than relying on a restore list. The snapshot arrays (about 21
values per event, filled for every scheme) are gone.

test_the_ladder_leaves_every_other_clustering_mean_output_alone checks
on ttgg and ttggg, for every clustering history and both jet scale
schemes, that the ladder leaves every non-pdf output equal to plain
clustering_mean's and mu_F and the ladder outputs equal to madevent's.
Mutation checks: ungating mu_R fails it in all 13 cases, ungating the
merging jets and jet-scale booking fails all 12 new cases (the t-channel
test alone missed that one). An ungated alpha_s step cannot be seen: the
entries madevent rewrites (the central mT, the raised last scale) are
never alpha_s-reweighted vertices in any fixture (0/6000 W+jet events
differ either).

Co-Authored-By: Claude <noreply@anthropic.com>
…r Pythia8

- RunCardMG7.create_default_for_process: a process generated with
  several multiplicities, the higher ones only adding jets, on hadronic
  beams, is set up as an MLM merged sample, as madevent sets ickkw = 1:
  dynamical_scale_choice = "mlm", xqcut = 30, no jet-jet and jet-lepton
  delta_r cut. The "only jets added" test is mlm_matching_needed(),
  shared with RunCardLO.
- Pythia8: for an MLM sample Main:InternalAnalysis is switched on (new
  hidden PY8Card parameters, a user setting wins), so main164 writes the
  differential jet rates to djrs.dat.
- HwUList.from_pythia8_main164 reads that file: main164 writes
  <run id="0""> (no XML parser takes it) and two columns. Each job of a
  split shower is scaled by 1e9 n_job / n_total (main164 normalises every
  job to the full cross section, in mb), the serial shower by 1e9. A DJR
  read error only warns instead of aborting the shower.
- create_plot: ickkw is read through __getitem__ (an mg7 run card answers
  the legacy key without containing it, so the plots were never made), and
  an observable without data (main164 writes no pts.dat) is skipped rather
  than stopping before the rendering.

Checked end to end: p p > e+ ve + 0,1j output mg7, launched with only
shower=pythia8, runs MLM (xqcut 30, qCut 45), reports the matched cross
section (6157 pb) and writes HTML/<run>/tag_1_PY8_plots with the DJR PDF;
the d01 integral equals the matched cross section (5915 vs 5953 +- 55 pb).

Co-Authored-By: Claude <noreply@anthropic.com>
HwUList.from_pythia8_main164 also reads <jethistograms njet="n"> blocks
(returned titled |JETSAMPLE@n, as the plots expect) and a third column
with the bin uncertainty. main164 writes neither today; a patched main164
(per-sample histograms from JetMatchingMadgraph::nMEpartons(), errors via
Hist::table(..., printError), the <run id> quote fixed) does, and the DJR
plots then show the 0/1/2-jet contributions. The former
MG5aMC_PY8_interface format is now told apart by the header attribute of
its <run>. Today's main164 output reads as before.

Co-Authored-By: Claude <noreply@anthropic.com>
…ia8 does it

Stock main164 (Pythia8 8.317) histograms the jet rates of its internal
analysis summed over the jet samples, so an MLM run's DJR plots cannot show
the 0/1/2-jet contributions the former MG5aMC_PY8_interface plotted.
Until a Pythia8 release can (a merge request is being prepared), an MLM
shower builds a patched copy of main164 when the installed one cannot:

- misc.patch_pythia8_main164_djrs: tags each event with the number of
  matrix-element partons of the MLM matching (JetMatchingMadgraph::
  nMEpartons, the former interface's tag), writes the histograms per jet
  sample in <jethistograms njet="n"> blocks with the bin errors, and fixes
  the '<run id="0"">' of Pythia8 <= 8.317. Pythia8's own main164.cc is
  never modified; a main164.cc the patch does not know (anchors checked on
  8.316 and 8.317) or one that already splits per jet sample is used as is.
- misc.get_pythia8_main164(jet_sample_djrs=True): builds that copy as a
  variant next to the HepMC ones, <examples>/main164[_hepmcN]_djr (or
  <proc>/lib/PY8_main164[_hepmcN]_djr for a read-only Pythia8), stamped
  with the patch version.
- do_pythia8 picks the main164 build after the card setup, so that the
  MLM run's Main:InternalAnalysis selects it; a failed patched build falls
  back to the stock main164 with a warning. DJRoutput:jetSamples (hidden
  pythia8_card switch, on by default, not a Pythia8 setting) turns it off.
- install pythia8 prebuilds the patched main164.

Checked end to end: p p > e+ ve + 0,1j output mg7, launch with only
shower=pythia8, builds examples/main164_djr once and writes DJR plots with
the 0- and 1-jet samples and their statistical errors.

Co-Authored-By: Claude <noreply@anthropic.com>
The kt measure min(pt1^2, pt2^2) * 2 * (cosh(deta) - cos(dphi)) / D^2 of
cluster.f subtracts two numbers close to one for a nearly collinear pair
and lost up to eight digits there (1.2e-8 relative on the 128 events of
test_kt_measure_matches_the_fortran, 2.2e-6 on 20000 g g > g g g events).
Scalar libm and SIMD SLEEF round cosh/cos differently in the last bit, so
the two gave different scales: test_kt_measure_matches_the_fortran failed
in the simd_256/avx512y/simd_512 wheel tests (1.5e-9 > 1e-9).

It is now evaluated as 4 * (sinh^2(deta/2) + sin^2(dphi/2)), with eta =
asinh(pz/pt) (no cancellation for a forward parton, unlike
log((|p| + pz)/(|p| - pz))) and dphi = atan2(cross, dot); the Durham
measure 2 min(E1^2, E2^2) (1 - cos_theta) likewise as 4 min(E^2)
sin^2(theta/2) with theta = atan2(|p1 x p2|, p1.p2). Identical to cluster.f
in exact arithmetic.

- On identical momenta, scalar and NEON (simd_128) now give bit-identical
  scales (200000 values).
- Scalar: 539 of 200000 values move by more than 1e-9; every one that could be
  checked against a 60-digit evaluation (97) moved towards it: old error
  up to 2.2e-6, new at most 1.2e-12.
- test_mlm: the Python reference uses the same form;
  test_a_nearly_collinear_pair_keeps_its_clustering_scale (kt and Durham,
  pairs 1e-5 apart) fails with the old formula in any mode (1e-6 off).
- madspace suite: 1995 passed in scalar and in simd_128.

Co-Authored-By: Claude <noreply@anthropic.com>
The interim patch of main164 now applies exactly the change prepared for
Pythia8 (commit 89d24fa4 of a local clone of Pythia8 8.318, to be
proposed upstream): the setting InternalAnalysis:jetSamples (default off)
writes the DJR histograms per jet sample in <jetsample njet="n"> blocks,
two columns, with the jet sample tagged for kt-MLM (nMEpartons().first),
shower-kT (getProcessSubset().size()), FxFx (npNLO()) and merging
(Merging:nRequested - Merging:nRecluster), and the <run id> quote fixed.
On 8.318 the patched file is identical to that commit apart from the
two-line MadGraph7 header; it also applies to 8.316 and 8.317.

- PY8Card: InternalAnalysis:jetSamples (hidden, default off) replaces the
  MadGraph7-only DJRoutput:jetSamples. An MLM run sets it on (a user
  setting wins); a main164 that already knows it is used as is, one that
  does not is patched, and an unpatchable one does not get the setting in
  its command file (Pythia8 would refuse it).
- HwUList.from_pythia8_main164 reads <jetsample> blocks (and still the
  <jethistograms> ones of the first interim patch).
- Tests follow; one checks the patch against the installed Pythia8.

End to end: p p > e+ ve + 0,1j, output mg7, launch with shower=pythia8
only: main164_djr rebuilt, InternalAnalysis:jetSamples=on in the command
file, per-sample DJR plots.

Co-Authored-By: Claude <noreply@anthropic.com>
test_main164_djr_patch_is_the_proposed_pythia8_change checked the patch
against the main164.cc of a Pythia8 under ~/Documents/HEPTools, and was
skipped where there is none, as on CI. tests/test_manager.py exits 1 on
any skipped test, so unittest_bundle_7 failed although all its tests
passed. The patch content stays covered by test_main164_djr_patch; the
real Pythia8 8.316-8.318 sources were checked by hand.

Co-Authored-By: Claude <noreply@anthropic.com>
zeniheisser pushed a commit to zeniheisser/MadGraph7 that referenced this pull request Oct 29, 2026
GitHub retires the macos-14 hosted image on 2026-11-02, with brownouts
(jobs fail) on 2026-10-29 and 2026-10-30, 14:00-24:00 UTC, and reduced
capacity before that: on 2026-10-28 the macOS wheel job of MadGraphTeam#72 was never
given a runner. macos-15 is the arm64 replacement GitHub recommends;
macos-latest is avoided because it now means macOS 26 and keeps moving.
MACOSX_DEPLOYMENT_TARGET stays 14.5, so the wheels still install on
macOS 14.5 and later.

Co-Authored-By: Claude <noreply@anthropic.com>
MLM review follow-ups: hidden run-card options, one weight_after_cuts rule, tests
MLM automatic for a human user: merging on by default, DJR plots after Pythia8
@oliviermattelaer
oliviermattelaer merged commit 4b77387 into main Oct 29, 2026
191 checks passed
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.

3 participants