fix(nanoevents): FsrPhoton.matched_muon resolves against Muon (not Jet) in dask mode - #1581
fix(nanoevents): FsrPhoton.matched_muon resolves against Muon (not Jet) in dask mode#1581NJManganelli wants to merge 6 commits into
Conversation
The dask path of FsrPhoton.matched_muon called Jet._apply_global_index instead of Muon._apply_global_index, a copy-paste error that made the property silently return Jet records (with Jet fields/values) instead of Muon records in dask mode, while the eager/virtual paths correctly resolved against Muon. Point the dask path at the Muon collection to match the eager path, and add a regression test covering eager, virtual, and dask modes that asserts matched_muon resolves to Muon-type records and matches the eager result. Partially addresses scikit-hep#1578 (critical bug: FsrPhoton.matched_muon resolves against wrong type in Dask). Assisted-by: Claude Fable 5
|
By-eye review is clear, running the bot to check for knock on effects and any other less-clear problems. |
lgray
left a comment
There was a problem hiding this comment.
🤖 AI text below 🤖
Summary: The one-line fix is correct and complete. On master, FsrPhoton.matched_muon's dask branch called Jet._apply_global_index(dask_array.muonIdxG) and silently returned Jet records indexed by FsrPhoton_muonIdx — I reproduced this: with the test in place and only nanoaod.py reverted to master, the dask variant fails with Jet fields (area, btagDeepB, ...) where Muon fields belong, with no exception raised; eager and virtual pass (virtual rides the eager code path, so it was never affected). The fixed line (src/coffea/nanoevents/methods/nanoaod.py:547) now mirrors the eager path (:543) and matches the sibling matched_* pattern exactly (e.g. Muon.matched_jet :399, Jet.matched_muons :616). I also swept every _apply_global_index call pair in the file: this was the only eager/dask collection mismatch — the fix class is complete. At the PR head, the new test passes in all three modes and the full tests/test_nanoevents.py passes (21 passed, 1 network test deselected). Recommend merging after one small test improvement below.
Should fix
tests/test_nanoevents.py:159— the value assertion is vacuous on this sample:nano_dy.rootcontains zero FsrPhotons across its 40 events (measured vianFsrPhoton), soak.all(...)over the empty comparison is triviallyTrueregardless of which collection is resolved, and the "Values must match the eager result" comment overstates what's exercised. The field-set assertions (:154–157) are form-level, so the test does discriminate as-is (proven by the revert run) — but switching the sample tonano_dimuon.root(already used by neighboring tests) makes the value check real: it has 1 FsrPhoton withmuonIdx=0, and the matched muon pt (22.46) differs from the Jet pt at the same index (28.58). One-line change (nano_dy.root→nano_dimuon.rootat :132).
Nits/Optional
- The full field-set equality at :154 already subsumes the two spot checks at :156–157 and would also catch a swap to any other collection (e.g. Electron, which lacks
pfRelIso04_all) — the spot checks are fine to keep as readable documentation of the failure mode. - Out of scope for this PR, but the sweep surfaced two pre-existing oddities in the same file worth a follow-up under #1578:
nanoaod.py:828(AssociatedPFCand.jetdask path callsdask_array.events()— no such method on arrays, only the factory hasevents(), so this raises AttributeError) andnanoaod.py:860–866(AssociatedSV.jetindexesself._events()[collection]wherecollectionis already an array). Both fail loudly and are PFNano-only, unlike the silent bug fixed here.
Test coverage: The regression test is non-vacuous and discriminating — verified by reverting only nanoaod.py to master and observing the dask variant fail at the field-set assertion while eager/virtual pass, then restoring the fix and observing 3/3 pass. It covers all three execution modes, checks field membership against the eager reference, and reuses the tests_directory fixture, local sample files, and the pytest.importorskip("dask_awkward") convention consistently with neighboring tests. The one weakness is the vacuous value assertion noted above.
Pydantic: Not applicable — the diff touches no pydantic code (0 grep matches).
|
Actually a good catch - @NJManganelli do we have any nanoevents samples with actual FSRPhotons in them? Probably just need to skim some out of a Zmumu nanoaod sample. |
|
What I was seeing for example regarding excessive testing from LLMs is that I think for example this PR does not need a single test. It's a one-line fix. It's not like we're going to accidentally revert that such that we need a regression test. |
|
And what if we do and miss it (again)? |
|
I'd hope that we are not stupid enough to revert this. What I think would be way more valuable is a test that loops over all of those I'm not saying I want any changes here, just pointing out what I meant when I said that I don't like how LLMs often add useless tests that just add lines of code. Just my personal opinion. |
|
I hope we are not that stupid either, but I also don't bet against stupidity winning eventually. It really tends to. |
|
I'd say the right thing to do is actually test intended mapping more generally: matched_X should probably test that e.g. the index being fed in has some relation to the object you match against; testing we don't revert this one bug is kinda useless, but testing we match intended cross-references? Genuinely useful. Without looking at branch names, I expect that's trivial. For the FSR Photons, maybe the nanov15 test files that Alejandro added in one of the PRs for JECs or Type1-MET? |
|
I agree with @ikrommyd, DRY and YAGNI principles should apply to tests too |
|
I agree with those principles too, however, we (apparently) needed it. 🤷 |
…ent target type Replace the single FsrPhoton.matched_muon regression with a parametrized sweep over the matched_*/parent/child cross-references, asserting eager (known-correct), virtual and dask all resolve to the same record type and fields. This catches the whole class of _apply_global_index owner mixups (per review discussion on scikit-hep#1581) rather than pinning one line; the FsrPhoton-matched_muon case still fails on the unfixed code. Assisted-by: Claude Opus 4.8
…ent target type Replace the single FsrPhoton.matched_muon regression with a parametrized sweep over the matched_*/parent/child cross-references, asserting eager (known-correct), virtual and dask all resolve to the same record type and fields. This catches the whole class of _apply_global_index owner mixups (per review discussion on scikit-hep#1581) rather than pinning one line; the FsrPhoton-matched_muon case still fails on the unfixed code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5db20b0 to
c27bc20
Compare
…tself Instead of a hand-maintained pair list, enumerate the _apply_global_index mixin properties directly from coffea.nanoevents.methods.nanoaod so newly added cross-references are covered automatically. A module-scoped fixture loads each mode once. Oracle is unchanged: eager/virtual/dask must agree on the resolved record type and fields, which stays discriminating on the FsrPhoton.matched_muon dask bug without asserting an absolute target type. Assisted-by: Claude Opus 4.8
The mode-consistency sweep cannot catch a cross-reference wired to the wrong collection in every mode. Add a static companion that parses each cross-ref's eager and dask source bodies and asserts the _apply_global_index owner matches a hand-declared target table, with completeness assertions binding the table to auto-discovery. Catches e.g. Muon.matched_jet -> Electron and flags the FsrPhoton dask bug on unfixed code without loading a sample. Assisted-by: Claude Opus 4.8
|
Reworked the test per the discussion here (thanks @ikrommyd / @nsmith- / @lgray). Rather than pin this one line, the PR now carries two complementary, low-maintenance tests:
On the "do we even need a test" thread: I think this lands where YAGNI/DRY and @lgray's "what if we revert and miss it" both want to be — no one-off regression, but real coverage of the mechanism. Re @lgray's sample request: #1537 (@alefisico) is adding |
Drop the issue-tracker reference and pre-fix bug narration from the cross-reference target-type test docstring; state what the sweep verifies. Assisted-by: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XeYa8sEdeLGa1VX2frvoNz
8fca917 to
7ec09dc
Compare
Part of #1578 — critical bug 2: FsrPhoton.matched_muon resolves against wrong type in Dask.
FsrPhoton.matched_muonhad a copy-paste bug in its dask code path: it calledJet._apply_global_indexinstead ofMuon._apply_global_index. As a result, in dask mode the property silently returned Jet records — indexed byFsrPhoton_muonIdx— carrying Jet fields and values, whereas the eager and virtual paths correctly resolved against the Muon collection. No error is raised, so downstream analyses usingFsrPhoton.matched_muonunder Dask read the wrong physics objects.The fix points the dask path at the Muon collection to match the eager path. A regression test (
test_fsrphoton_matched_muon_type, parametrized over eager/virtual/dask) asserts thatmatched_muonhas Muon-type fields and values matching the eager result across all three execution modes; it fails onmaster(dask variant) and passes with this fix. Fulltests/test_nanoevents.pypasses and pre-commit is clean.🤖 Generated with Claude Code