Repository navigation
madspace: 2 -> 1 processes with PDFs, one energy and one PDF per beam - #154
oliviermattelaer wants to merge 9 commits into
Conversation
2 -> 1 (p p > z, p p > h): a collision Diagram may have one outgoing leg. PhaseSpaceMapping integrates dPhi_1 = 2 pi delta(s_hat - m^2) against the luminosity: s_hat is fixed at the mass and the single random number sets the rapidity. Leptonic beams or a massless final state are refused. run_card.toml [beam] now has ebeam1/ebeam2 and pdf1/pdf2. beam.e_cm and beam.pdf stay readable and settable (both beams), and cards written with e_cm/pdf are translated on read. The phase space is generated in the beams' centre-of-mass frame (s = 4 E1 E2) and boosted by 0.5 ln(E1/E2) into the lab frame before the cuts, so events are written, and eta cuts applied, in the frame the user gave. Asymmetric beams break the Integrand's mirror shortcut (mirroring accepted lab events with unswapped PDFs). With mirror_beams the orientation is drawn before the mapping, the mirror is taken in the beams' frame, and each leg takes the PDF of the beam it comes from. Identical beams keep the old path. The second beam's grid is passed to Integrand and SystematicsCalculator (pdf_grid2 / nominal_pdf2); PDF member variations are dropped when the sets differ, scale variations use both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The run card now stores pdf1/pdf2, so the `pdf = "..."` substitution matched nothing and the reference runs silently used the default NNPDF4.0 set instead of NNPDF23_lo_as_0130_qed (3jets, dy1j, ttx1j and ttx_decay off by 2-43%). Pin pdf1 and pdf2 (or a legacy pdf line) and fail loudly when nothing was pinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The initial-state mirror covers the beam-swapped copy of a subprocess by
rotating the event by pi about x (py, pz -> -py, -pz, kernel_mirror_momenta),
which moves each leg onto the other beam. With mirror_beams the mapping draws
the orientation first, so the rotation happens before the boost into the lab
frame and before the cuts. Without it the Integrand keeps the cheaper path:
generate, cut, then mirror the accepted event. Everything downstream of that
mirror then sees an orientation nothing upstream ever looked at.
Two places were reading the wrong side of it.
The matrix element. Integrand::build_common_part kept feeding the cross
section momenta_acc, the momenta as generated, while the event was written
mirrored. It now takes the mirrored set when there is one, which is what
madevent does (it flips the momentum array that then goes to both the matrix
element and the event record, super_auto_dsig_group_v4.inc "Flip momenta
(rotate around x axis)") and what SystematicsCalculator::matrix_elements
already assumed, since it reads the momenta back out of the event buffer.
This is the same one-line change as "madspace: evaluate the matrix element on
the momenta the event is written with" on the mirror-me-momenta branch,
carried onto the post-cut path that only exists here; on the mirror_beams
path momenta_acc is already the written orientation and nothing changes.
Feeding the unmirrored momenta only ever worked because |M|^2 is invariant
under the rotation -- true for any Lorentz invariant matrix element, false
for a polarised one evaluated in a frame that holds the polarised particle at
rest, where HELAS quantises the spin along the frame z axis (48% on
g q > z{+} q in the Z rest frame, 23 sigma between p p > z{+} j and
p p > z{-} j end to end).
The cuts. Nothing stated that the cuts have to be blind to the mirror, and
nothing enforced it; it happened to hold because pt, |eta|, delta_r, mass and
sqrt_s all are. Observable::mirror_invariant classifies each observable (and
the one it is ordered by -- sorting by a quantity that flips picks a different
particle), Cuts::non_mirror_invariant_cuts names the offenders, and Integrand
refuses the post-cut mirror when any cut is one, next to the asymmetric-beams
check it already had. mg7 does not hit that refusal: launch.py now turns
mirror_beams on for a subprocess whose cuts are not mirror invariant, the same
switch asymmetric beams already trigger, and logs why.
This is not latent. The run_card [cuts] table takes any observable name, and
the template even advertises "eta" -- p p > e+ e- with lepton-eta.min = 0.0:
post-cut mirror 276.3 +- 2.1 pb, 1017 of 2000 events (50.9%) written
with a lepton at eta < 0, i.e. outside the cut
orientation first 233.9 +- 2.1 pb, no event outside the cut
and the second is right: lepton-eta.max = 0.0, which a pp collider's symmetry
forces to be equal, gives 232.9 +- 2.2 pb (0.3 sigma). The old number is 18%
high because it integrates one beam orientation twice, and a single ordering
of a valence-sea pair is forward-skewed.
Nothing on the invariant-cut path moves: default cuts keep the post-cut mirror
(no switch logged) and g _quark > e+ e- _quark gives 59.0349 +- 0.0490 pb
against the 59.0235 +- 0.0287 reference, 0.2 sigma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pushed
|
| cross section | events written outside the cut | |
|---|---|---|
| post-cut mirror (before) | 276.3 +- 2.1 pb | 1017 / 2000 (50.9%) |
| orientation first (after) | 233.9 +- 2.1 pb | 0 |
and the second is the right one: lepton-eta.max = 0.0, which a pp collider's symmetry forces to be equal, gives 232.9 +- 2.2 pb — 0.3 sigma from the new number, 14 sigma from the old. The old value is 18% high because it integrates one beam orientation twice, and a single ordering of a valence-sea pair is forward-skewed.
Nothing on the invariant-cut path moves
Default cuts keep the post-cut mirror (no switch logged), and g _quark > e+ e- _quark gives 59.0349 +- 0.0490 pb against the 59.0235 +- 0.0287 reference — 0.2 sigma, 0.019% relative.
Tests
22 new tests in madspace/tests/test_beam_frame.py:
- the per-observable classification, including the ordering observable and the absent-particle case;
- a mapping-level test that with
mirror_beamsthe cuts act on the momenta that come out, for both orientations — and that a signedetacut makes the two orientations disagree (627/2000 events flip verdict) whileeta_abscannot, which is exactly why the fast path is sound for the cuts mg7 ships; - the
Integrandprecondition (refused post-cut, allowed withmirror_beams, allowed for unmirrored flavors); - graph-level tests that the matrix element consumes the
mirror_momentaoutput on the post-cut path andmomenta_accon themirror_beamspath.
I reverted each fix and rebuilt: exactly those two intended tests go red, nothing else. Full madspace suite 1569 passed / 11 skipped / 4 failed, the four being No module named 'scipy' in test_double_t.py, identical before the change.
One gap
No UpdateNotes.txt entry here. main restructured that file (new 0.3.0 section, which is where the ME fix's own entry sits), so adding one on this branch would manufacture a conflict in exactly that spot. The cut half needs a line next to it when the two meet.
🤖 Generated with Claude Code
Conflicts, all from the madspace documentation PR (#92) meeting this branch's API additions; no logic conflicted: - systematics.hpp, cuts.hpp, observable.hpp, integrand.hpp: main's Doxygen comments next to new members (_nominal_pdf2, the mirror-invariance queries, pdf2_prefix). Both kept; the new members' comments turned into Doxygen and the pdf_grid2 / nominal_pdf2 argument notes moved into the constructors' @PARAM lists. - phasespace.hpp: main's Doxygen for random_dim_for, extended with the 2 -> 1 case and @throws; beam_rapidity / mirror_beams documented as @PARAM of both constructors and mirror_index added to the documented conditions; the new accessors documented. - madspace.cpp: main's pydoc::doc() bindings, with beam_rapidity, mirror_beams, pdf_grid2 and nominal_pdf2 inserted before the docstring, main's external_masses argument name kept, and the new PhaseSpaceMapping and Cuts methods added to main's documented method chains. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the merge of origin/main (12e2e16): - Observable::mirror_invariant(ObservableOption), the static per-option overload, becomes option_mirror_invariant() in observable.cpp's anonymous namespace. Nothing outside that file called it, and as an overload with its own doc comment it gave generate_docstrings two entries for one name (Observable::mirror_invariant / #2), which is why the Python method was bound without a docstring. It now gets pydoc::doc like every other binding. - clang-format on the Integrand error message and black/isort on test_beam_frame.py, which the madspace pre-commit hooks run. pydoc keys checked against docstrings generated with doxygen (ENABLE_DOCS=ON only runs on pushes to main): all 613 doc() calls resolve. madspace suite 1605 passed, 11 skipped, 4 failed (scipy not installed, as before the merge). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts: - banner.py: both sides added an entry to _LO_SCALAR_MAP (ebeam1/ebeam2 here, scalefact -> beam.scale_factor on main). All three kept. - phasespace.cpp: main's boost_beam/boost_beam_inverse now take the external masses, so the boost of a momentum moving against it can be done in light-cone components and stay on shell. to_lab/from_lab pass lab_masses(_topology, _permutations) to every boost, including the beam-rapidity one; the mirror in between is a rotation and leaves the masses alone. madspace pytest 1657 passed / 11 skipped, run_card 19/19, mg7 launch 47/47, and the asymmetric-beam and per-beam-PDF runs reproduce their pre-merge cross sections (941.2, 1817.4, 44211, 44223 pb). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflicts: - mg7/launch.py: keep both new imports (gc from main, math from here), and both blocks added after the cuts -- the mirror-orientation choice from here and main's momentum_hists / weighted_histograms gating. - test_banner.py: main's new output_format default (lhe_npy) together with this branch's per-beam [beam] assertions (ebeam1/ebeam2/pdf1/pdf2, no e_cm). Semantic fix: main's TestForceLHEOutput.test_scan_card_survives edited an `e_cm = 13000.0` line that the per-beam card no longer writes; it now puts the scan on `ebeam1` instead, which tests the same thing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - banner.py: main gave every [beam] parameter a one-line comment= help string; keep ebeam1/ebeam2/pdf1/pdf2 with help strings in that style. - run_card.toml: one-line comments, as main now uses. - RunCardLO_to_MG7_mapping.md: deleted on main; its per-beam rows move to docs/source/madevent_to_mg7.rst, and run_card.rst documents ebeam1/ebeam2/pdf1/pdf2. - phasespace.hpp/.cpp, cuts.cpp, madspace.cpp: both sides added members (beam_rapidity/mirror_beams/cuts vs empty/_y_max_lab); keep both. - integrand.cpp: keep the branch's comment, which extends main's. Semantic fix: main's eta windows (Y = log(x1/x2)/2, the root two-body decay and the t-channel/colour-ordered clamps) read the lab eta cuts as bounds in the frame the momenta are generated in. With a beam rapidity y0 that frame is the beams' one, so every node_y_max is widened by |y0|, as chili's bound already was; the mirror only flips the sign of y there. Without it, lab-passing events fall outside the windows (test_rapidity_windows_hold_in_the_lab_frame fails in all 6 cases). test_mirror_beams_cuts_the_written_orientation compared a cut mapping with an uncut one for the same random numbers; an |eta| cut now changes the sampling, so it reads the cut mapping's own momenta.
At the launch question, set ebeam/ebeam1/ebeam2, lpp/lpp1/lpp2, pdlabel/pdlabel1/pdlabel2 and lhaid were "invalid set command" (except the ebeam ones, which reached _LO_SCALAR_MAP) and the run kept the defaults. RunCardMG7.set_lo_beam_param now maps them, a trailing 1/2 acting on that beam and the other names on both: - ebeam*: beam.ebeam1/2, with units and mass expressions. - pdlabel*/lhaid: beam.pdf1/2 through the label and lhaid tables of the run_card.dat conversion; an lhaid outside the table is looked up in the pdfsets.index of the LHAPDF data paths. 'lhapdf' alone changes nothing, 'none' on both beams is beam.leptonic = true. - lpp*: the single beam.leptonic switch (0, +-3, +-4 lepton, +-1 hadron), with a warning when the two beams were given different types. A value mg7 cannot represent is reported and leaves the card alone. from_LO keeps different pdlabel1/pdlabel2 per beam instead of reporting them as dropped.
At the mg7 launch question, settings that the run_card.dat conversion
(from_LO) handles were still lost:
- "set ebeam1 6500", "set ebeam 6800", "set lpp1 0", "set pdlabel ...",
"set lhaid ...", "set fixed_fac_scale ...", "set maxjetflavor 5" and the
per-pdg cuts ("set pt_min_pdg {6: 100}") printed "invalid set command"
and the run went on with the mg7 default;
- "set dynamical_scale_choice 3" and "set sde_strategy 2" (madevent's
integer codes) went to the mg7 key of the same name, which only takes
names, and aborted a scripted launch with InvalidCardEdition.
RunCardMG7.set_lo_param now translates them as from_LO does: the integer
codes and the et/shat/ckkw shortcuts to the mg7 names (HT and HT/n stay
with the generic editor), maxjetflavor to the jet group, the {pdg: value}
cuts to a pdg<N> group plus its cut (mxx_only_part_antipart choosing
pair_mass or sfos_pair_mass, in either order), pdlabel/lhaid to beam.pdf
through the same tables. Values with no mg7 equivalent warn and keep the
current setting instead of aborting.
mg7 has one energy, one PDF and one lepton flag for both beams, so the
per-beam settings of one launch question are combined: ebeam1 and ebeam2
give e_cm = 2 sqrt(E1 E2), lpp1 and lpp2 give beam.leptonic. What still
differs per beam is reported once, when the question closes
(MG7Selector.check_card_consistency), so "set ebeam1 X" followed by
"set ebeam2 X" stays quiet. PR #154 (one energy and one PDF per beam)
routes the beam names to its per-beam keys through set_lo_beam_param;
whichever merges second hands them over to it.
from_LO now builds the jet and pdg groups with the same helpers.
|
I propose we rebase the last two commits onto #245 and close this branch because the other one has a big merge conflict now. |
Same main commit as the PR #154 merge 96b1e38, merged as a real merge rather than cherry-picked with -m 1, so main keeps its ancestry here. Conflicts, resolved as in 96b1e38: - banner.py [beam]: main gave every parameter a one-line comment= help string; keep ebeam1/ebeam2/pdf1/pdf2 with help strings in that style. - RunCardLO_to_MG7_mapping.md: deleted on main (modified here for the per-beam rows); delete it. Its per-beam rows live in the new docs/source/madevent_to_mg7.rst, and docs/source/run_card.rst documents ebeam1/ebeam2/pdf1/pdf2 (both taken from 96b1e38). - run_card.toml [beam]: one-line comments, as main now uses (from 96b1e38). The madspace conflicts of 96b1e38 (phasespace, cuts, integrand, python bindings, test_beam_frame) do not arise here: bcedaa0 already merged that part of main, and the lab-frame eta windows are handled by lab_fractions instead of widening node_y_max by |y0|.
Summary
p p > z,p p > h):Diagramaccepts one outgoing leg for a collision.PhaseSpaceMappinggets a 2 → 1 branch:s_hat = m², one random number (the rapidity), weight2π |ln τ| / s. The flat mode accepts 3 masses. Leptonic 2 → 1 and massless final states raise a clear error, and so does the launcher.run_card.toml:[beam] ebeam1, ebeam2, pdf1, pdf2.beam.e_cm(= 2√(E1E2)) andbeam.pdfremain readable and settable; setting either sets both beams.e_cm/pdfare translated on read.set ebeam1 …works from launch scripts, and LO→MG7 conversion keeps asymmetric beams.y₀ = ½ ln(E1/E2)into the lab frame before the cuts. Events are written in the user's frame and rapidity cuts act there. pT, masses and ΔR for massless particles are boost invariant.PhaseSpaceMapping(mirror_beams=True)the orientation is amirror_indexcondition drawn before the mapping._pdfs_swapped); mirror flavours weigh 2, others 1.Integrand(pdf_grid2, globals prefixbeam2) andSystematicsCalculator(nominal_pdf2, also reloaded by the gridpack).<mgrwt>beam labels follow the beam direction.<init>carries both beam energies and both LHAPDF ids.Validation
All runs use a fixed μ = 91.188 and NNPDF4.0 LO unless stated otherwise.
p p > z, 6.5+6.5 TeVp p > e+ e-, 7+4 TeV, ptl>10, ΔR>0.4, no η cutp p > z, pdf1=NNPDF40 / pdf2=NNPDF23 vs swappedmadspace/tests/test_beam_frame.py(9 tests) covers the 2 → 1 mapping and its inverse, boosted mappings (rambo/propagator, hadronic/leptonic), lab-frame η cuts andmirror_beams.test_flow/test_mlpneed torch and were not run; lhapdf python bindings were stubbed out because they abort on this Python 3.14.test_banner.pyRunCardMG7 tests: 17/17, including new per-beam and legacy-card tests.test_mg7_launch.py: 40/40.test_keyboard_interrupt_reaches_the_error_handlingfailed once in a full run; it also fails alone on the base commit, so it's a flaky test that predates this PR.Notes
ebeam1 ≠ ebeam2is not a valid reference for η cuts. Withetal = 2.5its LHE leptons span η ∈ [−2.5, 2.5 + 2y₀], and its cross section is ~4.5% below the lab-frame value. Not touched here.fact_scale1/fact_scale2follow the legs, not the beams, for mirrored events. This only matters for fixed and different per-beam factorisation scales.DifferentialCrossSection(input_momentum_fraction=False)still reconstructs x from energies assuming symmetric beams. The launcher never uses that mode.🤖 Generated with Claude Code