Skip to content

NLO+PS polarisation: put the MC-counterterm azimuth in the me_frame - #79

Draft
oliviermattelaer wants to merge 9 commits into
claude/nlo-polarised-boost-assessment-d97794from
claude/polarised-nlo-ps
Draft

NLO+PS polarisation: put the MC-counterterm azimuth in the me_frame#79
oliviermattelaer wants to merge 9 commits into
claude/nlo-polarised-boost-assessment-d97794from
claude/polarised-nlo-ps

Conversation

@oliviermattelaer

Copy link
Copy Markdown
Contributor

Polarised cross-sections at NLO were validated at fixed order only. This makes the
NLO+PS (MC@NLO) path frame-consistent as well, and adds a test with a verified
negative control.

The bug

get_mbar (Template/NLO/SubProcesses/montecarlocounter.f) took borntilde from a
boosted sborn_frame but built the azimuthal phase entirely in the partonic c.m. —
azifact from unboosted emission spinors, cphi_mother=±1 from the mother lying on
the beam axis. borntilde is the ±gluon-helicity interference, so the mother's
little-group phase enters it, and that phase only cancels against the one in
azifact if both are computed in the same frame. fks_singular.f had already solved
this at fixed order with azifact_me_frame.

Nothing blocked NLO+PS: no guard distinguishes fixed order from event generation,
and generate_events ran end to end on the unfixed code. The failure was silent.

Measured on p p > z{0} j [QCD], me_frame=[3]: the ISR phase error was a constant
37.4°, 63.4° and 156.6° on three collinear scans, flat down to 1-y = 1e-10. It never
converged. After the fix it falls off like sqrt(1-y) (10.07° → 0.97° → 0.097° →
0.0003°).

The change

Three routines in boost_to_frame.fme_frame_born_boost, azifact_from_spinors,
azifact_mc_frame — and if (me_boosted) … else <legacy verbatim> endif at both
get_mbar sites, plus the same edit in montecarlocounter_alt.f (dead code, but
leaving the two copies inconsistent is a trap). The R_y(pi) flip is skipped when a
frame is requested: it only put a j_fks=2 mother back on +z, and for a one-leg
frame it would also flip the polarisation axis of the leg at rest — harmless for {0},
but it would swap {+}/{-}.

The collinear branch is now taken only at exactly 1-y <= 0. At any kt>0 the spinor
route is exact and the kperp route is an approximation, so there is no case for
preferring the approximation.

Validation

generate_events aMC@NLO --parton -f, 1000 events, iseed=33, nn23lo1, fixed scales
91.188, ptj 30, etaj 4.0, me_frame=[3] (FRAME_ID = 8 read back from
Source/run_card.inc):

before after
total 2.172e+03 ± 8.3e+00 pb 2.189e+03 ± 8.2e+00 pb
absolute 4.837e+03 ± 1.2e+01 pb 4.378e+03 ± 9.8e+00 pb
ratio 2.2273 1.9999

The total must not move and does not (1.4 sigma) — the MC counterterm cancels between
the S and H event. The absolute cross-section must move and does, by 30 sigma: it
measures how local the subtraction is, which is what the azimuth controls. Repeated on
a fresh pair with iseed=7717: 2.2220 → 2.0060, totals agreeing at 0.02 sigma.

me_frame=[3,4] on p p > z{0} z{0} j [QCD]: 3.134e-01 ± 2.2e-03 pb, agreeing with
the fixed-order value at 1.3 sigma, 20/20 poles in all 12 P dirs.

No frame requested stays bit-identical. Two copies of one output dir differing only
in the changed files: res_0/1/2.txt and the 2.1 MB events.lhe.gz compare byte for
byte.

Test

test_polarised_nlo_ps_me_frame, wired in as acceptancetest_106 (the workflow
invokes tests by explicit name, so an unwired test never runs). It passes on the
branch, and with montecarlocounter.f alone reverted it fails on the intended
assertion — AssertionError: 2.22732091828047 not less than 2.15 — reproduced
independently by the reviewer.

Caveats, stated rather than omitted

  • The PYTHIA8 handoff is unexercised here. The local MG5aMC_PY8_interface was
    built against v3.5.15 and its C++ link fails on arm64, strictly after events.lhe
    is written. Everything this PR touches is upstream of the handover, but no showered
    polarised event was produced on this machine.
  • test_MC is vacuous upstream and passed on the broken code — it is not evidence
    for this change. test_soft_col_limits.f assigns the same xmcsubt_wrap result to
    both sides of the comparison, so checkres2 sees a ratio of exactly 1 and returns
    PASS unconditionally. Two further breaks in the same area (the split-order sub-check
    comparing one ME under two jacobians, and ilim=0 aborting on MCcntcalled) are
    recorded in the M5 doc. All three are pre-existing on main and are being handled
    separately.

oliviermattelaer and others added 9 commits August 21, 2026 10:14
get_mbar() took borntilde from a boosted sborn_frame but kept azifact and
the mother azimuth in the partonic c.m., so the phase multiplying the
spin-correlated Born belonged to a different frame than the Born itself.
fks_singular.f solved the same problem with azifact_me_frame; the MC
counterterms are evaluated at generic y_ij_fks, where <ij>/[ij] is regular
and can simply be recomputed on boosted momenta.

Measured on p p > z{0} j [QCD], me_frame=[3], against the covariant
collinear reference: the ISR phase error was a constant 37/63/157 degrees
all the way to 1-y = 1e-10; it now falls off like sqrt(1-y).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds test_polarised_nlo_ps_me_frame (generate_events on p p > z{0} j [QCD],
me_frame=[3]) and wires it in as acceptancetest_106. It asserts on the ratio
of the absolute to the total cross-section, which is what the MC-counterterm
azimuth controls: 2.23 before the fix, 2.00 after, while the total itself is
unchanged at 1.4 sigma.

Records M5 in the plan, including the pre-existing upstream bug that makes
test_MC pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- azifact_mc_frame's header no longer claims the legacy convention
  unqualified: the collinear branch pre-cancels the caller's mother factor,
  so azifact is <ij>/[ij] only in the generic one.
- state the identity that makes the two branches agree,
  lim_{kt->0} <ij>/[ij] = -exp(2 i psi) exp(2 i phi_m).
- the collinear branch is now taken at kt exactly zero rather than below
  1-y = 1e-12. The spinors are exact at any kt>0, while the kperp
  reconstruction carries an O((kt/E)/betaT) error that the betamin guard
  does not bound (it bounds |beta|, not betaT). Boosted path only.
- record why the legacy inline copies of the spinor azimuth stay.
- cut the three new routine headers; comment share of the Template diff
  goes from 34% to 27%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reak

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oliviermattelaer
oliviermattelaer marked this pull request as draft August 21, 2026 21:57
@oliviermattelaer

Copy link
Copy Markdown
Contributor Author

On hold — do not merge yet.

Review of a follow-up branch found a pre-existing MadLoop-core bug that this PR can reach. IMPROVE_PS_POINT_PRECISION overwrites the last external leg's three-momentum with the momentum-conservation residual (madgraph/iolibs/template_files/loop/improve_ps.inc:707-714), unconditionally. If the me_frame leg held at rest is that last leg, the exact zero becomes ~1e-14, vxxxxx takes its pp.eq.rZero branch on rounding noise, and the longitudinal polarisation axis is built along that noise.

Measured on g g > z{0} z{0} [noborn=QCD], where me_frame=[3] and [4] must agree pointwise: 0.02543 pb versus 0.03211 pb, +26%, 34 sigma. Restoring the exact zero inside the shared quad routine closes it to 0.06 sigma. An LO control on the same final state gives bit-identical [3] and [4], so the split is purely the MadLoop artefact.

This PR is reachable in principle — any run_card NLO mode whose polarised massive particle is the last Born leg, e.g. p p > j z{0} [QCD] with me_frame=[4]. Both acceptance tests here happen to dodge it ([3,4] leaves no leg at rest; [3] has the jet last), so CI is green and the hole is invisible.

Held on Olivier's call until the improve_ps fix lands on its own branch with its own MadLoop validation. Nothing is wrong with the code in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant