Skip to content

MadLoop: keep an external leg exactly at rest through improve_ps - #91

Open
oliviermattelaer wants to merge 6 commits into
mainfrom
claude/improve-ps-atrest-leg
Open

MadLoop: keep an external leg exactly at rest through improve_ps#91
oliviermattelaer wants to merge 6 commits into
mainfrom
claude/improve-ps-atrest-leg

Conversation

@oliviermattelaer

@oliviermattelaer oliviermattelaer commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

IMPROVE_PS_POINT_PRECISION destroyed an exactly-zero three-momentum. This is a
pre-existing MadLoop-core bug on main; it is the blocker recorded on #79, which is
held until this lands.

The bug

madgraph/iolibs/template_files/loop/improve_ps.inc, PSMC branch. Step (a) is
described as a rescaling, but it restored momentum conservation by absorbing the
residual into leg NEXTERNAL unconditionally:

DO I=NINITIAL+1,NEXTERNAL-1
  DO J=1,3
    PBUFF(J)=PBUFF(J)-NEWP(J,I)
  ENDDO
ENDDO
DO J=1,3
  NEWP(J,NEXTERNAL)=PBUFF(J)
ENDDO

A polarised matrix element is evaluated in a frame where the selected leg is at rest,
and boost_to_frame sets its three-momentum to exactly zero on purpose: HELAS
vxxxxx reads the spin quantisation axis off pp.eq.rZero — at exactly zero the axis
is the frame z, otherwise it is the momentum direction. When the at-rest leg was the
last one, its exact zero came back as ~5.7e-14 and the longitudinal polarisation vector
was built along rounding noise.

The fix

Absorb into the final-state leg with the largest |p| (IABSORB, one argmax loop).
That leg is never the one at rest, and it is also the smallest relative distortion.
For 2->1, and for an all-at-rest final state, it degrades to the old NEXTERNAL
choice.

Two things that are easy to get wrong here and are worth recording:

  • The template is instantiated once, in quad only. write_improve_ps sets
    mp_prefix='MP_', so there is exactly one MP_PSMC_IMPROVE_PS_POINT_PRECISION;
    the DP wrapper converts to REAL*16 and calls it, and SET_MP_PS calls the same
    routine. One edit covers both entry points.
  • ImprovePSPoint defaults to 2 = ORIG, not PSMC — but ORIG bails with
    ERRCODE=200 as soon as the beams carry transverse momentum, which is exactly what
    a boost to a final-state rest frame produces, so the rescue path runs PSMC every
    time. Both settings reach the bug. (The values in MadLoopParams.dat are
    !-commented; the real defaults are at the bottom of MadLoopParamReader.f.)

Also in this PR: a warning comment on NRotations_DP/QP in the standalone
MadLoopParams.dat, and the same note above both ROTATE_PS routines. A rotation is
an exact axis permutation, so exact zeros survive it — but it is not a symmetry of a
polarised amplitude with a leg at rest, since HELAS pins that leg's axis to the frame
axes. Enabling rotations on a polarised run gives spurious instability flags. They
default to 0.

Because these edits land in generated improve_ps.f, the 15 IOTest reference copies of
that file are regenerated in update IOTest. The diff there is confined to
improve_ps.f and is the same edit in each.

That commit also deletes two references,
IOTestFDGauge/FDgauge_standalone_fortran/{FFV6M_3,VVV1_0}.f, which is correct but
worth a sentence: they were already dead. testIO_FDgauge_standalone_fortran targets
FD_fortran/Source/DHELAS/*.f, but it generates with output standalone, which no
longer produces a Fortran Source/DHELAS/ tree — on current main it produces the
C++ src/HelAmps_sm.h layout. So those targets have matched nothing since
standalone changed meaning, and -U removed the unmatched references. The test
still reports OK with zero references, and it reports OK with a deliberately corrupted
reference too, so it has been vacuous rather than failing. That is a pre-existing
main bug, not something this PR introduces
— the fix is a separate one-word change
(standalone -> standalone_fortran at tests/acceptance_tests/test_cmd.py:5040)
plus regenerating those two goldens under the path they then land in.

Measurement

Two measurements, at different levels.

Cross-section, on g g > z{0} z{0} [noborn=QCD]. me_frame=[3] and me_frame=[4]
select the two identical Z's, so they are the same observable measured twice and have
to agree. 100 events, use_syst=False, FRAME_ID 8 and 16 read back from
Source/run_card.inc:

me_frame before after
[3] 2.543e-02 pb 2.5428e-02 +- 9.9e-05 pb
[4] 3.211e-02 pb 2.5410e-02 +- 3.1e-04 pb
[3] against [4] +26%, 34 sigma 0.07%, 0.056 sigma

[4] moves by -20.9% onto [3], which does not move at all — leg 3 is not the last
leg, so it was never the damaged one. That asymmetry is the control: the fix touches
only the frame that was broken, and it is the same conclusion the routine-level rows
below reach by a different route.

An LO control on the same final state gives bit-identical [3] and [4], so the
split was a MadLoop artefact rather than physics.

These runs are on claude/loop-induced-polarisation with this PR's
MadLoop: absorb the PS residual in the largest final-state leg cherry-picked on top,
because check_process_format on main refuses [noborn=QCD] with a polarised leg
and boost_to_frame on main does not zero the selected leg (that is the M2 fix on
#79's branch). Once this PR lands, the @unittest.skip on
test_polarised_loop_induced_me_frame_last_leg can be removed on that branch — it
asserts exactly the [4] number above.

Routine level, on main, reproducible from this PR alone, and exact rather than
statistical. The driver puts leg 3, then leg 4, exactly at rest, calls improve_ps,
and prints that leg's three-momentum back. g g > z z, sqrt(s) = 500 GeV; the columns
are entry point, leg, ImprovePSPoint, then px, py, pz.

With improve_ps.inc as it stands on main:

RESULT DP  3 1     0.0000000000000000E+00    0.0000000000000000E+00    0.0000000000000000E+00
RESULT QP  3 1     0.0000000000000000E+00    0.0000000000000000E+00    0.0000000000000000E+00
RESULT DP  3 2     0.0000000000000000E+00    0.0000000000000000E+00    0.0000000000000000E+00
RESULT QP  3 2     0.0000000000000000E+00    0.0000000000000000E+00    0.0000000000000000E+00
RESULT DP  4 1    -0.5684341886080801E-13   -0.5684341886080801E-13    0.2842170943040401E-13
RESULT QP  4 1    -0.5684341886080801E-13   -0.5684341886080801E-13    0.2842170943040401E-13
RESULT DP  4 2    -0.5684341886080801E-13   -0.5684341886080801E-13    0.2842170943040401E-13
RESULT QP  4 2    -0.5684341886080801E-13   -0.5684341886080801E-13    0.2842170943040401E-13

With this PR, all 8 rows and all 24 components are exactly
0.0000000000000000E+00.

Three things those rows show that a pass/fail would not:

  • Leg 3 is never broken, only leg 4NEXTERNAL, the leg the old code dumped the
    residual into. That is the bug's signature rather than a general loss of precision,
    and it is why a process whose polarised leg normalises away from last is
    accidentally safe.
  • All four leg-4 rows carry the identical residual, so both entry points (the DP
    wrapper and the QP routine that SET_MP_PS calls) and both ImprovePSPoint
    settings (1 = PSMC, 2 = ORIG, which falls back to PSMC through the ERRCODE=200
    rescue as soon as the beams acquire transverse momentum) all reach it. The claim
    about ORIG above is measured here, not argued.
  • The residual is a clean ulp artefactpz is exactly half of px and py
    so it does not shrink with a better phase-space point and does not average away over
    events. It is either exactly zero or it is not.

The test asserts exactly this, and it drives a compiled binary: it builds the driver
with gfortran against the generated improve_ps.f and parses the 8 RESULT rows out
of the running executable, so a compile or link failure fails the test rather than
passing it silently.

Which processes are exposed

MG5 normalises the final-state leg order, so u u~ > g z{0} and u u~ > z{0} g both
give P0_uux_z0g with the Z as leg 3. Which leg me_frame selects is therefore not
under the user's control, and exposure cannot be read off the process string — read
born_leshouche.inc:

process selected leg last? exposed
g g > z{0} z{0} [noborn=QCD], [4] leg 4 = Z yes yes
u u~ > w+{0} w-{0} [QCD], [4] leg 4 = W- yes yes
p p > j z{0} [QCD], [3] leg 3 = Z no no

#79's own example was accidentally safe, which is why its CI is green over the hole.

Test

tests/acceptance_tests/test_cmd_madloop.py::test_improve_ps_keeps_a_leg_at_rest
plus tests/input_files/improve_ps_at_rest_driver.f. It generates
g g > z z [sqrvirt=QCD] standalone and compiles a driver against improve_ps.f
alone — no CutTools link is needed, the routine wants only MadLoopParams.inc and
mp_coupl.inc, and the quad type is plain REAL*16. It asserts an exact 0.0 for
2 at-rest legs x 2 ImprovePSPoint settings x 2 entry points, 8 rows.

The test is wired into .github/workflows/acceptancetest.yml as
acceptancetest_improve_ps_at_rest. The workflow invokes tests by explicit name, so
an unwired test never runs.

🤖 Generated with Claude Code

oliviermattelaer and others added 5 commits September 2, 2026 16:24
IMPROVE_PS_POINT_PRECISION (PSMC branch) restored exact momentum
conservation by dumping the residual into leg NEXTERNAL. If that leg
was at rest -- what boost_to_frame arranges for a polarised matrix
element -- its exact zero three-momentum was replaced by a ~1e-14
residual, HELAS' 'pp.eq.0' spin-axis branch flipped, and the
longitudinal polarisation vector was built along rounding noise.

Pick the final-state leg with the largest |p| instead. It can never be
a leg at rest, and it is the smallest relative distortion available.
The template is instantiated only in quad precision, so the single fix
covers both entry points (the DP wrapper and SET_MP_PS).

Also record that ROTATE_PS is not a symmetry of a polarised amplitude
with a leg at rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drives IMPROVE_PS_POINT_PRECISION directly on a g g > z z point boosted
to the rest frame of leg 3 and of leg 4 in turn, for both ImprovePSPoint
settings and through both entry points (the DP wrapper and the quad
routine SET_MP_PS calls). The check is exact, not statistical.

Fails on the parent commit with px = -5.68e-14 on leg 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main gave IMPROVE_PS_POINT_PRECISION a KEEP_OFFSHELL_MASS flag array
(6ba177c). It is not read inside improve_ps.inc, but the driver has to
supply it to link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The acceptance workflow invokes tests by explicit name, so a test that is
not named in a job never runs. Give this one its own job next to the other
uncovered MadLoop tests; it needs restore_all for the MadLoop standalone
output and gfortran for the driver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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