Muon collider MAIA detector postprocessing - #490
Conversation
In assign_genparticles_to_obj_and_merge, unmatched genparticles are merged into the genparticle owning their best cluster. The host four-vector was rebuilt from gpdata.gen_features on every iteration while the result was written back to pt_arr/eta_arr/phi_arr/energy_arr, so a second merge into the same host computed E_host_original + E_new and discarded whatever earlier merges had accumulated. Read the host from the running arrays instead, so repeated merges add up. On the MAIA ttbar sample (10 events) one host absorbed 30 particles; the overwrite silently dropped 1981 GeV, 5.2% of the visible gen energy. Target jet pT / truth jet pT goes from 0.848 +/- 0.134 to 0.877 +/- 0.143 over 42 matched jets (21 improved, 21 unchanged, none degraded), the maximum response returns to 1.000, and 4904 GeV of target jet energy is recovered. The assert below the merge loop was meant to catch exactly this but is one-sided: the merge can only lose energy, so the expression is always negative and never trips. Left as-is for now since the remaining `continue` path would make a two-sided version fire immediately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| e=gpdata.gen_features["energy"][igp_unmatched], | ||
| ) | ||
| # read the host from the running arrays, not from gen_features, so that several | ||
| # unmatched particles merging into the same host accumulate instead of overwriting |
There was a problem hiding this comment.
Bug caught by Claude
Root cause: the merge clobbers repeat hosts
In postprocessing.py:923, vec1 is rebuilt from the original gpdata.gen_features on every iteration, but the result is written back into the running `pt_arr/eta_arr/phi_arr/energy_arr:
vec1 = vector.obj(pt=gpdata.gen_features["pt"][idx_gp_bestcluster], ...) # original, every time
vec = vec0 + vec1
energy_arr[idx_gp_bestcluster] = vec.energy # accumulated target
So the second unmatched particle merging into a given host computes E_host_original + E_new and throws away everything earlier merges added. In your 5 events: 145 particles merged into 61 hosts, 14 hosts received more than one, and one host received 30 merges — 29 of them discarded. That's 1981 GeV, 5.2% of all visible gen energy, silently gone.
Changing vec1 to read from the running arrays recovers exactly the predicted 1981.4 GeV, and the ceiling goes back to 1.000
The gp_to_hit weights mixed incompatible scales. Tracker hits contributed a literal 1.0 while calorimeter hits contributed raw CalohitMCTruthLink weights, which are proportional to the deposited energy but with a constant that differs per subdetector (it absorbs the sampling fraction): summing the weights on a hit and dividing by the reconstructed hit energy gives ~151 in the ECAL and ~603 in the HCAL on the MAIA ttbar sample. Anything reading these weights as an energy was therefore wrong. Take each genparticle's share of the total weight on its hit and scale by the calibrated hit energy instead, which is correct whatever convention the weights follow, and give tracker hits their own deposited energy. mask_visible_hit now really is the 10% energy cut its comment claimed: E_deposited/E_gen has median 0.66 and 99.6% of values in [0, 1.5], where it used to have a median of 20-230. gp_in_calo becomes meaningful for the same reason. Target jet pT / truth jet pT moves 0.877 -> 0.841 over the MAIA sample, since the cut now actually bites. The mask logic itself is unchanged from jpata#463. Comments now record what it does and does not cover: it is purely calorimetric, so it removes MIPs (6 of 7 muons here, plus 22 genparticles carrying 1071 GeV that all have a good track, which gp_in_tracker would select), and it cannot recover the ~15% of status-1 energy deposited by nothing at all, mostly photons down the beampipe. Left for discussion with the other maintainers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fractional term is a shower-containment criterion, so on its own it misses MIPs: a muon deposits a roughly constant few GeV whatever its momentum, so its energy fraction falls with energy (0.006-0.04 for the muons in the MAIA ttbar sample) and the 10% cut drops 6 of 7 of them. OR in an absolute deposit threshold, which recovers all 7. gp_in_tracker cannot serve this role here: none of these muons have a track linked above its 20% threshold, so an OR with it would have recovered none of them. Both thresholds are now module-level constants. The absolute one is a detector-dependent scale that has to sit below the MIP peak (~4 GeV for muons crossing the MAIA calorimeters, minimum observed 3.4 GeV) and above the noise; 1 GeV sits in the middle of a broad plateau, where anything from 0.1 to 2 GeV keeps all 7 muons and 84-85% of the status-1 energy. Target jet pT / truth jet pT over 42 matched jets goes 0.841 -> 0.876, recovering what the fractional cut alone had given up. Two things left for the maintainer discussion, recorded in the comments: the particles the absolute term admits deposit only ~2.6% of their generated energy, so their momentum has to come from the track, which is right for MIPs but not for the neutrals it also admits (9 of the 20 added here are photons that leaked); and neither term can recover the ~15% of status-1 energy deposited by nothing at all, mostly photons down the beampipe, which sets a floor on the achievable response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Following up on the "Investigate bias of target definition" item. Summary of what I found on the MAIA ttbar sample (10 events, 42 matched jets) — three separate issues, two of them pre-existing on The starting symptom: target jet 1. Merge overwrite (629d67f) — pre-existing, from #463In On this sample: 145 particles merged into 61 hosts, 14 hosts received more than one, and one host received 30 merges — 29 discarded. That silently dropped 1981 GeV, 5.2% of the visible gen energy. This is much worse at a 10 TeV muon collider than at CLIC/CLD simply because the jets are denser (60–120 constituents at 1–5 TeV), so clusters are shared far more and multi-merges are routine. Note the assert (np.sum(gen_features_new["energy"]) - np.sum(gpdata.gen_features["energy"])) < 1e-2The merge can only ever lose energy, so the expression is always negative and the assert can never fire (it evaluates to −444.7 here). I left it alone, because making it two-sided would immediately trip on the remaining 2.
|
| mean | median | max | |
|---|---|---|---|
| original | 0.848 | 0.894 | 0.990 |
| + merge fix | 0.877 | 0.933 | 1.000 |
| + corrected weights, 10% only | 0.841 | 0.896 | 1.000 |
| + OR MIP deposit > 1 GeV | 0.876 | 0.929 | 1.000 |
The merge fix is strictly monotonic per jet — 21 improved, 21 unchanged, none degraded — and recovers 4904 GeV of target jet energy. The response ends up close to where the merge fix alone put it, but now via a mask that actually means what it says rather than one that was accidentally passing nearly everything.
Open questions
-
The absolute term admits leaked neutrals. The 20 particles it lets in deposit only ~2.6% of their generated energy. That is right for MIPs, whose momentum comes from the track, but 9 of the 20 are photons that leaked (e.g. a 237 GeV photon depositing 3.2 GeV) and have no track — so MLPF would be asked to predict energy nothing measured. Restricting the absolute term to charged particles excludes them and gives 0.834. I did not apply that; it seems like a call for the group.
-
There is a ~15% floor that no mask can reach. 186 particles carrying 13455 GeV deposit exactly zero, mostly photons down the beampipe. Since the truth jets are clustered from all status-1 particles including those, the target can never reach 1.0. Probably wants to be addressed on the truth side (defining the reference within acceptance) rather than in the mask.
-
CLIC/CLD validation. Item 2 above touches every Key4hep detector, not just MAIA. Someone should run a before/after on a CLD sample before this merges — I have only exercised the MAIA path.
Caveat throughout: 42 matched jets from 10 events is thin, so treat the individual numbers as indicative.
The MIP peak scales with calorimeter depth, so the absolute term's usable range does too: median muon deposit is 4.22 GeV on MAIA but only 1.98 GeV on CLD. That leaves 0.1-2.0 GeV usable on MAIA and 0.05-1.0 GeV on CLD, where 2.0 GeV already eats into CLD's MIP peak and keeps 15/20 muons instead of 19/20. 1.0 GeV sat mid-plateau for MAIA but right at CLD's upper edge. 0.5 GeV is inside both with room either side, and costs nothing: CLD output is identical (0 of 302 jets change), MAIA moves 2 of 42 jets by +0.0002. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CLD validation, as promised in the previous comment. Test file from CLD does not have the pathologyWorth stating up front, because it changes how to read the MAIA numbers. On So the bias I was chasing is specific to dense TeV-scale muon-collider jets, where clusters are shared heavily and the merge overwrite compounds. It is not a general defect in the target definition. Effect of this PR on CLD, per commit289 jets matched in all four variants:
Net −0.0045 (−0.45%): 6 jets improved, 211 unchanged, 85 degraded. The merge fix is a no-op on CLD (+0.0003), exactly as expected — sparse 365 GeV jets almost never produce a host that takes more than one merge, so the bug never fires. Compare MAIA, where one host absorbed 30 merges. The regression is entirely the mask now actually cutting at 10%, about half of it recovered by the MIP OR. I want to be straight about what this means rather than dress it up: on CLD, The MIP threshold is detector-dependent, and 1.0 GeV was too close to the edgeMedian muon deposit is 4.22 GeV on MAIA but 1.98 GeV on CLD — the shallower calorimeter roughly halves it. Usable range for
1.0 GeV sat mid-plateau for MAIA but right at CLD's upper edge, so I lowered the default to 0.5 GeV (a9ff5e3), which is inside both with room either side. It costs nothing: CLD output is bit-identical (0 of 302 jets change) and MAIA moves 2 of 42 jets by +0.0002. This parameter will want revisiting for any detector with a different calorimeter depth. It is a module-level constant now, so that is at least a one-line change. One thing the jet numbers hideOn CLD, without the MIP OR only 10 of 20 muons survive the mask, versus 19 with it. That barely moves jet Two notes on the fetch script
Still openThe two items from the previous comment stand, and CLD does not change either: whether the absolute term should be restricted to charged particles (it admits leaked neutrals whose energy nothing measured), and the ~15% of MAIA status-1 energy carried by particles that deposit exactly zero, which caps the achievable response there and is probably a truth-side acceptance question. Numbers are from 50 CLD events / 10 MAIA events, so treat them as indicative. |
|
Correction to something I claimed in both comments above, plus a question about the MAIA sample. The "~15% floor" was the wrong numberI said the ~15% of status-1 energy deposited by nothing at all "sets a floor on the achievable response". That is wrong in the way that matters, because jets are clustered with
The 14.8% is dominated by four photons at It is also not a forward-acceptance effectI had assumed the forward region was the culprit. It is not:
I also checked the file: there is no forward calorimeter collection being missed, only ECal/HCal barrel+endcap and the Yoke. The actual in-acceptance effect: status-1 particles with no simulator endpoint flagsThis is what I would like a second opinion on, @alexandertuna. In Using the EDM4hep bit definitions (30=CreatedInSimulation, 29=Backscatter, 28=VertexIsNotEndpointOfParent, 27=DecayedInTracker, 26=DecayedInCalorimeter, 25=LeftDetector, 24=Stopped):
The reading that fits is that those 49 particles were never propagated through Geant4. A 329 GeV pion in the barrel depositing exactly zero is not physical, and it is hard to see how it acquires no endpoint flag if it was tracked. One more oddity in the same direction: bit 31 is set on every MAIA particle and on no CLD particle. EDM4hep only defines bits up to 30, so bit 31 has no meaning in the schema. CLD's values are the clean ones (e.g. So: is a subset of status-1 particles expected to go unsimulated in this production — a generator-level filter before Geant4, or something in the BIB-overlay chain? If it is expected, then the target definition is behaving correctly and ~3% is simply not reconstructable. If it is not expected, the sample may want regenerating, and the MAIA response numbers in my earlier comments would improve accordingly. For reference the affected particles' nearest neighbours in angle are the parent partons ( Numbers from 10 MAIA events and 20 CLD events. |

@farakiko @alexandertuna