Skip to content

MadSpin: six fixes for the frame-projected polarised density - #119

Merged
oliviermattelaer merged 4 commits into
claude/nlo-polarised-boost-assessment-d97794from
claude/madspin-density-fixes-for-65
Sep 15, 2026
Merged

oliviermattelaer merged 4 commits into
claude/nlo-polarised-boost-assessment-d97794from
claude/madspin-density-fixes-for-65

Conversation

@oliviermattelaer

Copy link
Copy Markdown
Contributor

Targets #65's branch, so it lands with the polarisation feature it completes.

These six fixes are what make a polarised NLO production decayable at all. Before them, p p > z{0} z{0} [QCD] + MadSpin ran at ~450 s per event and was killed at event 40 of 4000. After, the same 20000 production events decay in 15.6 s — a factor ~1.3e5 — and the sample closes on the physics: f00 = 0.9995 +- 0.0228 for a pure-longitudinal production, against 1.

They also fill in the "polarised ME + MadSpin" column of the three-route comparison in #65's own study, which had to be left empty. Every entry now agrees with the other two routes within 1.3 sigma.

The six

  1. Silent wrong physics. _production_polarization parsed the process line with MadSpin's tree-level mg5cmd, which refuses a perturbation-order bracket. MadSpin only warned, so the density was left unrestricted and every NLO polarised production got unpolarised decay angles — with the right cross section and no crash. The bracket is irrelevant to the polarisation, so it is stripped before parsing.
  2. _frame_boost called get_momenta without merged_map, raising ValueError under flavour grouping.
  3. get_density called get_pdg — which matches particles by exact momentum equality — on the boosted momenta, so nothing was found. The pdgs are now read off the lab momenta, before the boost.
  4. get_density's cached branch recomputed the tag with a bare get_tag_and_order(), losing the 1->N antiparticle fallback that get_pdir owns. It now caches the tag get_pdir resolved.
  5. The load-bearing one. The offshell mass-stage weight divided a me_frame-boosted Tr(rho_off) by a lab-frame |M_prod|^2. A helicity-restricted matrix element is not Lorentz invariant, so the two are different projections; for an unpolarised production the trace is invariant and they agree, which is why only polarised runs were affected. Reverting this one alone puts the mass-stage bound back to 7.9e6 while leaving both angular bounds bit-identical — that is the whole difference between 450 s/event and 15.6 s/20000.
  6. The same frame fix on the joint path.

_onshell_production_norm returns calculate_matrix_element unchanged when there is no frame boost, so unpolarised runs are bit-for-bit identical.

Safety of fix 6 on the joint path

That call site is also on the unpolarised polarisation-weight path, so it had to be shown safe rather than assumed. Over 5414 production events the denominator was bit-identical across every joint trial of the same event — it is a per-production-event constant, and a constant cancels out of an accept/reject, so the accepted decay distribution is unchanged in law, not merely in measurement. End to end, redecaying 150000 events moves every published fraction by <= 0.5 sigma and leaves the nine-row ms_pol closure intact.

The test commit

The fixes were written against PR #103's head; #65's head has nine commits on top, three of which added stubs the fixes outgrow. All three are the double falling behind the real class, so the stubs move, not the production code:

  • _FrameStub gains _revert_merged — MadSpinInterface carries it as a class attribute, and the stub does not inherit from it. Guarding the production side with getattr would have hidden a real attribute behind a test-only default.
  • _MomentaEvent.get_momenta gains merged_map=None, matching lhe_parser.Event.get_momenta, which has always had it.
  • _offshell_fixture gains _onshell_production_norm, delegating to the calculate_matrix_element it already fakes — exactly what the real helper does with no frame boost, which is the only case a boost-free stub can represent. Borrowing the real helper would have dragged _frame_boost and get_density into the stub's hand-kept borrow list.

502 unit tests OK — the same count and result as the base without the fixes, so nothing was skipped to get there.

For the reviewer

Upstream 3.8.0 carries an independent version of fix 5 (mg5amcnlo#407) and of a related joint symmetry factor (#408, #409). When MadGraph7 merges 3.8.0 (#117) these need reconciling rather than both being applied — and MadGraph7's get_density signature carries flavour-grouping arguments 3.8.0 has no equivalent of, so it will not be a clean either/or. Flagging it here because the commit history is the only place that context survives.

Known limit: p p > z z j [QCD] still aborts, on a separate pre-existing flavour-ordering bug that reproduces identically on the unpolarised process. #114 fixes that one; it is unrelated to these six.

🤖 Generated with Claude Code

oliviermattelaer and others added 3 commits September 9, 2026 23:07
Rescued verbatim from the worktree they were developed in, on the base
they were written against (PR #103's head). Not yet ported to main --
that base is ~100 commits behind and the gap contains PR #63, the
MadSpin performance rewrite, so the port is a real one and is kept
separate from preserving the work.

Together these are what make a polarised NLO production decayable at
all; before them `p p > z{0} z{0} [QCD]` + MadSpin ran at ~450 s per
event and was killed at event 40 of 4000. After, 20000 events decay in
15.6 s.

1. _production_polarization parsed the process line with MadSpin's
   tree-level mg5cmd, which refuses the perturbation-order bracket.
   MadSpin only warned, so the density was left unrestricted and every
   NLO polarised production got UNPOLARISED decay angles -- with the
   right cross section and no crash. Strip the bracket; it is
   irrelevant to the polarisation.

2. _frame_boost called get_momenta without merged_map, raising
   ValueError under flavour grouping.

3. get_density called get_pdg (which matches by exact momentum
   equality) on the BOOSTED momenta, so no particle was found. Read the
   pdgs off the lab momenta before boosting.

4. get_density's cached branch recomputed the tag with a bare
   get_tag_and_order(), losing get_pdir's 1->N antiparticle fallback.
   Cache the tag get_pdir resolved instead.

5. The offshell mass-stage weight and the joint weight divided a
   me_frame-boosted Tr(rho_off) by a LAB-frame |M_prod|^2. A
   helicity-restricted matrix element is not Lorentz invariant, so the
   two are different projections; for an unpolarised production the
   trace is invariant and they agree, which is why only polarised runs
   were affected. _onshell_production_norm returns calculate_matrix_
   element unchanged when there is no frame boost -- so unpolarised
   runs are bit-for-bit identical -- and otherwise the trace of the
   on-shell rho in the same frame. This is the load-bearing one:
   reverting it alone puts the mass-stage bound back to 7.9e6 while
   leaving the two angular bounds bit-identical.

6. The same frame fix applied to the joint path. Verified safe there:
   over 5414 production events the denominator was bit-identical across
   every joint trial of the same event, so it cancels out of the
   accept/reject; Route B's published numbers move by <= 0.5 sigma.

Upstream 3.8.0 carries an independent version of fix 5 (mg5amcnlo#407)
and of the joint symmetry factor (#408/#409). When MadGraph7 merges
3.8.0 these need reconciling rather than both being applied -- the
MadGraph7 get_density signature carries flavour-grouping arguments that
3.8.0 has no equivalent of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The six density fixes were written against PR #103's head; #65's head
has nine commits on top, three of which added stubs that the fixes now
outgrow. All three are the double falling behind the real class, not a
problem with the fixes, so the stubs move rather than the production
code:

* _FrameStub gains _revert_merged. MadSpinInterface carries it as a
  class attribute ({} unless flavour grouping is on) and _frame_boost
  now passes it to get_momenta as merged_map; the stub does not inherit
  from MadSpinInterface, so it did not have it. Guarding the production
  side with getattr would have hidden a real attribute behind a
  test-only default.

* _MomentaEvent.get_momenta gains merged_map=None, matching
  lhe_parser.Event.get_momenta, which has carried it all along.

* _offshell_fixture gains _onshell_production_norm, delegating to the
  calculate_matrix_element it already fakes. That is exactly what the
  real helper does when there is no frame boost, which is the only case
  a stub with no boost can represent -- so the fixture's own comment,
  "|M_prod|^2 on shell, the denominator of the offshell mass-set
  weight", still describes what the test measures. Borrowing the real
  helper instead would have dragged _frame_boost and get_density into
  the stub's hand-kept borrow list.

502 tests, OK -- the same count and result as the base without the
fixes, so nothing was skipped to get there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#65 has merged main, which brings upstream mg5amcnlo 3.8.0 and with it
mg5amcnlo#407 -- an independent implementation of this branch's fix 5.
Three conflicts, all in interface_madspin, and in every one the
executable code on both sides is byte-identical: only the prose differs.
So this reconciles text, not behaviour.

* _onshell_production_norm: kept upstream's docstring, which is the
  more accurate of the two (it names both the sequential mass-set
  weight and the joint weight, and the polarisation-weight trigger),
  and appended the one paragraph this branch had that upstream does
  not: the 1.4e-7 agreement with calculate_matrix_element when the
  boost is left out, and the still-unexplained boosted-trace deficit.
  Keeping upstream's wording for the shared part means future 3.8.0
  merges do not re-conflict on it.

* sequential call site: upstream's comment, identical call.

* joint call site: kept this branch's comment, which carries the
  evidence that the site is not polarised-only and is safe anyway --
  the denominator bit-identical across every joint trial of the same
  event over 5414 events, and the end-to-end redecay numbers. Upstream
  has no equivalent.

After the merge there is exactly one definition of the helper and both
call sites use it. The other five fixes are untouched: they address
flavour grouping, which 3.8.0 has no counterpart for.

637 unit tests OK (test_madspin, test_lhe_parser).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oliviermattelaer
oliviermattelaer requested a lite review from Copilot September 14, 2026 20:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Improves correctness and runtime of MadSpin when decaying polarised NLO productions by making production-density parsing and normalization frame-consistent (and fixing flavour-grouping interactions).

Changes:

  • Strip perturbation-order brackets from NLO process lines before MadSpin’s tree-level parser reads polarisation braces.
  • Fix flavour-grouping / merged-pdg handling in _frame_boost() and get_density() (merged map propagation + cached tag).
  • Ensure get_density() uses lab-frame momenta for exact-momentum PDG matching, and align mass-stage/joint-path normalization with the numerator frame.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
MadSpin/interface_madspin.py Core fixes for process parsing, flavour-grouping momentum retrieval, density tagging, and frame-consistent normalization.
tests/unit_tests/madspin/test_madspin.py Updates stubs/fixtures to match new production behavior API expectations (merged map + normalization helper).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread MadSpin/interface_madspin.py
Comment thread MadSpin/interface_madspin.py
Comment thread MadSpin/interface_madspin.py Outdated
Comment thread MadSpin/interface_madspin.py Outdated
- Test the perturbation-order bracket stripping in
  _production_polarization: the stub parser now refuses a bracket the way
  MadSpin's tree-level mg5cmd does, and the new test fails ({} != ...)
  with the stripping reverted.
- Trim the long benchmark comments on the sequential mass stage, the joint
  path and _onshell_production_norm down to the invariant; drop the
  reference to a FINDINGS.md that is not in the repository. The
  measurements stay in the PR description.
- get_tag_and_order: pass the merged map by keyword.
- p_lab: say why a reference is enough (_boost_momenta returns new tuples
  and never mutates its input) instead of copying.

madspin unit tests: 609 OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oliviermattelaer
oliviermattelaer merged commit d6378cc into claude/nlo-polarised-boost-assessment-d97794 Sep 15, 2026
517 checks passed
oliviermattelaer added a commit that referenced this pull request Sep 15, 2026
One conflict, in MadSpin/interface_madspin.py get_density. Main (#111/#112,
b84acfb) now projects the aMC@NLO partons back onto the massless shell in
the lab and only then applies the frame boost; the branch (#119) boosted
first and kept a p_lab reference so get_pdg could still match the event's
momenta exactly. Taking main's order: the boost moves after
project_massless_partons (keeping it before as well would boost twice and
would project the boosted momenta, which is what made Tr rho frame
dependent), and p_lab goes because p is the lab momentum when get_pdg runs.
The resolved get_density equals main's apart from #119's tag caching.

Auto-merged files touched on both sides were checked: main brings no
polarisation hunk into madgraph_interface.py (tutorial and check precision
only); the RunCardNLO parton_shower default moving to PYTHIA8 does not reach
the polarised tests (test_polarised_nlo_me_frame is fixed order and
test_polarised_nlo_ps_me_frame pins PYTHIA8 itself).

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.

2 participants