Skip to content

Two-particle invariant mass cut that also bounds the integration - #110

Merged
oliviermattelaer merged 3 commits into
mainfrom
claude/madspace-invariant-mass-cut
Sep 10, 2026
Merged

oliviermattelaer merged 3 commits into
mainfrom
claude/madspace-invariant-mass-cut

Conversation

@oliviermattelaer

Copy link
Copy Markdown
Contributor

A cut on the invariant mass of a pair was only expressible as mass with summed momenta, which is the mass of the whole selection and happens to be a pair only when the selection holds exactly two particles. There was no way to cut on every pair a group can form, and no way to cut at all on a group larger than two.

More importantly, such a cut only ever filtered finished events. It never shaped the phase space, so asking for a hard cut meant generating the forbidden region and throwing nearly all of it away.

What this adds

obs_m_inv, a genuine pairwise observable applied to each pair the selection produces:

lepton-m_inv.min = 500.0
top-m_inv.min    = 800.0

and, where the pair is exactly what a propagator decays into, the cut becomes that propagator's floor on its own invariant mass.

That last part needed almost no new machinery. Every propagator already carries e_min, and update_mass_min_max already carries it into the s_min handed to the invariant sampler, into what the parents subtract for their maximum, and into s_hat_min. Raising e_min is therefore enough for the associated decaying particle's invariant and sqrt(s_hat) to follow on their own.

Where no single propagator carries the pair, the cut still bounds the total invariant mass: the pair contributes at least the cut and the rest of the final state at least its masses. The smallest such bound over the named pairs is used, which is the one that holds whether the cut must be satisfied by all of them or by only one.

Both are necessary consequences of the cut, so neither removes phase space the cut would have kept.

Measured

p p > e+ e- j j j with m(ee) > 200 GeV:

sigma [pb] cut efficiency RSD
before 0.15082 +- 0.00077 27.8% 41.0
after 0.15014 +- 0.00024 53.8% 10.4

The same answer to 0.8 sigma, twice the efficiency, four times the RSD. At m(ee) > 500 the contrast is sharper: before, the survey gives up with "not enough points passing cuts after 1000 batches"; after, it completes at 58% efficiency.

p p > t t~ j with m(t t~) > 800 GeV is unchanged at 1.1 sigma, 47.714 +- 0.093 against 47.576 +- 0.084, and every generated event respects the cut, the smallest being 800.01 GeV.

Testing

madspace/tests/test_m_inv_cut.py, on g g > t t~ g g whose first diagram has the top pair on a single propagator. It pins down that obs_m_inv is the pair's invariant mass and not obs_mass; that nothing below the cut is generated once the mapping knows about it; that without it more than half the points land where the cut would have discarded them; and that the volume of the cut region is the same either way, over a region that is a real fraction of the total.

Verified to fail without the feature: disabling the propagator floor drops the smallest generated m(t t~) from the cut to 346 GeV, twice the top mass.

Full madspace suite: no new failures against main. The four test_double_t failures on this branch are present on main too.

Note for review

The second commit also fixes a bug this feature exposed: Cuts indexes its per-particle tables by outgoing position assuming two incoming particles, so on a 1 -> n decay topology those tables are a different length than the outgoing legs. Reading one against the other threw out of every mapping built from a decay. A decay has no cuts to apply, so the boundary is skipped there.

The gain is largest for genuinely resonance-like pairs. For t t~ j the pair is usually not an s-channel propagator in g g -> t t~ g, so only the sqrt(s_hat) bound applies and the gain is modest.

🤖 Generated with Claude Code

oliviermattelaer and others added 2 commits September 8, 2026 15:43
A cut on the invariant mass of a pair was only expressible as "mass" with
summed momenta, which is the mass of the whole selection and happens to be
a pair only when the selection holds exactly two particles. There was no
way to ask for a cut on every pair a group can form, and no way to ask for
one at all on a group of more than two.

obs_m_inv is that cut: a genuine pairwise observable, applied to each pair
the selection produces, written in the run card as e.g.

    lepton-m_inv.min = 500.0
    top-m_inv.min    = 800.0

More importantly the cut now shapes the phase space instead of only
filtering it afterwards. Every propagator already carries e_min, a floor
on its own invariant mass, and update_mass_min_max carries that into the
s_min handed to the invariant sampler, into what the parents subtract for
their maximum, and into s_hat_min. So when the pair a cut names is exactly
what a propagator decays into, raising that propagator's e_min is all it
takes for the region the cut forbids never to be generated.

Where no single propagator carries the pair the cut still bounds the total
invariant mass, since the pair contributes at least the cut and the rest of
the final state at least its masses. The smallest such bound over the pairs
the cut names is taken, which is the one that holds whether the cut has to
be satisfied by all of them or by only one.

Both are necessary conditions of the cut, so neither removes phase space
the cut would have kept. Measured on p p > e+ e- j j j with m(ee) > 200:

                    sigma [pb]        cut eff.   RSD
    without      0.15082 +- 0.00077     27.8%   41.0
    with         0.15014 +- 0.00024     53.8%   10.4

The same answer to 0.8 sigma, twice the efficiency and four times the RSD.
At m(ee) > 500 the difference is starker: without this the survey gives up
with "not enough points passing cuts", while with it the run completes at
58% efficiency. On p p > t t~ j with m(t t~) > 800 the cross section is
unchanged at 1.1 sigma, 47.714 +- 0.093 against 47.576 +- 0.084, and every
generated event respects the cut with none within 0.01 GeV below it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cuts indexes its per-particle tables by outgoing position and counts two
incoming particles, so on a 1 -> n decay topology those tables are a
different length than the outgoing legs and reading one against the other
ran off the end. That threw out of every PhaseSpaceMapping built from a
decay, which is every test in test_decay_topology. A decay carries no cuts
to apply in the first place, so the boundary is simply skipped there.

The tests cover both halves of the feature on g g > t t~ g g, whose first
diagram has the top pair coming off a single propagator so the cut lands
exactly on one node:

  - obs_m_inv is the invariant mass of the pair, and is not obs_mass,
  - with the cut given to the mapping nothing below it is generated,
  - without it more than half the points land where the cut would have
    thrown them away, which is what makes the previous test worth having,
  - the volume of the cut region is the same either way, so the boundary
    follows from the cut rather than adding to it,
  - that region is a real fraction of the total, so the comparison above
    cannot pass for want of anything to compare.

Verified to fail without the feature: disabling the propagator floor drops
the smallest generated m(t t~) from the cut to 346 GeV, twice the top mass,
and test_cut_bounds_the_sampled_invariant catches it.

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

Copy link
Copy Markdown
Contributor

There was no way to cut on every pair a group can form, and no way to cut at all on a group larger than two.

This is not true, the system is quite flexible. For example, lepton-lepton-sum-mass should apply an invariant mass cut for pairs of leptons and lepton-jet-top-sum-mass for triples consisting of a lepton, jet and top. Of course this is quite hard to find and currently not very well documented (even the AI didn't understand it), so this will also not be clear to the user.

Therefore I think it indeed makes sense to define a separate observable for pairs since this is a really common cut. We should change the naming though. I'd propose either going with m_inv and pair_m_inv, or mass and pair_mass. mass vs m_inv is confusing since both are invariant masses.

"mass" and "m_inv" are both invariant masses, so the pair sitting under
the second name told the reader nothing about what separates them.
pair_mass says the thing that actually differs: it is taken over pairs.

Also correct what the cuts section claims about "mass". Naming a group
per member already gives one combination at a time, so
"lepton-lepton-sum-mass" was always the invariant mass of a lepton pair
and "lepton-jet-top-sum-mass" that of a triple. pair_mass is a shorter
spelling of the first, not the only one.

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

Copy link
Copy Markdown
Contributor Author

Renamed to pair_mass in 1177979, keeping mass as it is.

obs_m_inv -> obs_pair_mass, kernel_obs_m_inv -> kernel_obs_pair_mass, the run card key becomes top-pair_mass.min = 800.0, and the test file is now madspace/tests/test_pair_mass_cut.py. Rebuilt from source and checked against the built module: observable='pair_mass' constructs and reaches the phase-space floor, observable='m_inv' is now a TypeError. Full madspace suite 1512 passed, 2 skipped (test_flow / test_mlp need torch, test_double_t fails on main too).

You are right about the existing syntax, and the PR description was wrong to say there was no way to do this. Naming a group per member already gives one combination at a time, so lepton-lepton-sum-mass was always the invariant mass of a lepton pair and lepton-jet-top-sum-mass that of a triple. The cuts section of the run card now says so, and presents pair_mass as the shorter spelling of the first rather than the only one:

# possible observables: pt, eta, dR, mass, pair_mass, sqrt_s
#     (mass is the mass of one particle. Put "-sum-" before it and the momenta
#      are added up first: "lepton-sum-mass" is the mass of all the leptons
#      together, while naming a group per member gives one combination at a
#      time, so "lepton-lepton-sum-mass" is the mass of a lepton pair and
#      "lepton-jet-top-sum-mass" that of a lepton-jet-top triple.
#      pair_mass is the invariant mass of a pair, taken over every pair the
#      named groups can form, so "lepton-pair_mass" is the same cut as
#      "lepton-lepton-sum-mass". sqrt_s is for all outgoing particles)

One thing your comment turned up, which I have not changed

If lepton-lepton-sum-mass is the pre-existing spelling of the same cut, then it ought to get the same phase-space bound. It does not. Measuring the floor Cuts::m_inv_min reports for a min=500 cut on the top pair:

cut floor
top-pair_mass 500.0
top-sum-mass 500.0
top-top-sum-mass 0.0

Cuts::m_inv_min only recognises a summed selection of exactly two particles (idx.size() == 1 && idx.at(0).size() == 2), which is the top-sum-mass shape. The top-top-sum-mass shape is one index list per group, so it falls through and gets the event-level filter without the bound. That is a behaviour change rather than a rename, so it is not in this push. Happy to add it here if you would like the two spellings to stay interchangeable.

Also, the accessor is still called Cuts::m_inv_min(). It pre-dates this branch and is public Python API (Cuts.m_inv_min(), ColorOrderedMapping(m_inv_min=...)), so renaming it reaches well past this PR. Say the word if you want the vocabulary uniform.

🤖 Generated with Claude Code

@oliviermattelaer
oliviermattelaer merged commit d91c9db into main Sep 10, 2026
521 checks passed
@oliviermattelaer
oliviermattelaer deleted the claude/madspace-invariant-mass-cut branch September 10, 2026 09:16
@oliviermattelaer

Copy link
Copy Markdown
Contributor Author

Tracked the phase-space bound gap as #121 so this can merge as it is.

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