176 def compute_metrics(self) -> dict[str, Any]:
177 """Run the full benchmark analysis over all events.
178
179 Returns
180 -------
181 dict
182 Dictionary of metrics compatible with compare_metrics.py format.
183 """
184 n_events = self.sim_tree.GetEntries()
185 n_reco_events = self.reco_tree.GetEntries()
186 if n_events != n_reco_events:
187 print(f"WARNING: sim has {n_events} events, reco has {n_reco_events}")
188 n_events = min(n_events, n_reco_events)
189
190
191 h_dp_over_p = ROOT.TH1D("h_dp_over_p", "#Deltap/p;(p_{reco} - p_{truth})/p_{truth};Entries", 100, -0.5, 0.5)
192 h_dp_vs_p = ROOT.TH2D(
193 "h_dp_vs_p", "#Deltap/p vs p_{truth};p_{truth} [GeV/c];#Deltap/p", 50, 0, 120, 100, -0.5, 0.5
194 )
195 h_dx = ROOT.TH1D("h_dx", "#Deltax at first hit;x_{reco} - x_{truth} [cm];Entries", 100, -5.0, 5.0)
196 h_dy = ROOT.TH1D("h_dy", "#Deltay at first hit;y_{reco} - y_{truth} [cm];Entries", 100, -5.0, 5.0)
197 h_dtx = ROOT.TH1D("h_dtx", "#Deltat_{x};t_{x,reco} - t_{x,truth};Entries", 100, -0.01, 0.01)
198 h_dty = ROOT.TH1D("h_dty", "#Deltat_{y};t_{y,reco} - t_{y,truth};Entries", 100, -0.01, 0.01)
199 h_chi2ndf = ROOT.TH1D("h_chi2ndf", "#chi^{2}/ndf;#chi^{2}/ndf;Entries", 100, 0, 20)
200 h_p_truth = ROOT.TH1D("h_p_truth", "p_{truth} (reconstructible);p [GeV/c];Entries", 50, 0, 120)
201 h_p_matched = ROOT.TH1D("h_p_matched", "p_{truth} (matched);p [GeV/c];Entries", 50, 0, 120)
202
203
204 n_reconstructible = 0
205 n_matched_mc = 0
206 n_total_reco = 0
207 n_matched_reco = 0
208 n_clone_reco = 0
209
210 for i_event in range(n_events):
211 self.sim_tree.GetEvent(i_event)
212 self.reco_tree.GetEvent(i_event)
213
214
215 reconstructible_ids: set[int] = set()
216 n_mc_tracks = len(self.sim_tree.MCTrack)
217 for mc_id in range(n_mc_tracks):
218 if self._is_reconstructible(mc_id):
219 reconstructible_ids.add(mc_id)
220 p_truth, _, _, _ = self._get_ptruth_first(mc_id)
221 if p_truth > 0:
222 h_p_truth.Fill(p_truth)
223
224 n_reconstructible += len(reconstructible_ids)
225
226
227 n_reco = self.reco_tree.FitTracks.size()
228 n_total_reco += n_reco
229
230
231 matched_mc_this_event: set[int] = set()
232
233 for i_reco in range(n_reco):
234 track = self.reco_tree.FitTracks[i_reco]
235 fit_status = track.getFitStatus()
236 if not fit_status.isFitConverged():
237 continue
238
239 ndf = fit_status.getNdf()
240 if ndf <= 0:
241 continue
242 chi2 = fit_status.getChi2() / ndf
243 h_chi2ndf.Fill(chi2)
244
245
246 mc_id = self.reco_tree.fitTrack2MC[i_reco]
247
248
249 frac, _dominant_id = self._fracMCsame(i_reco)
250
251 if frac < self.purity_cut:
252
253 continue
254
255 n_matched_reco += 1
256
257 if mc_id in reconstructible_ids:
258 if mc_id not in matched_mc_this_event:
259 matched_mc_this_event.add(mc_id)
260 else:
261 n_clone_reco += 1
262
263
264 p_truth, _, _, _ = self._get_ptruth_first(mc_id)
265 x_t, y_t, _ = self._get_truth_pos_first(mc_id)
266 tx_t, ty_t = self._get_truth_slopes(mc_id)
267
268 if p_truth > 0:
269 try:
270 fitted_state = track.getFittedState()
271 p_reco = fitted_state.getMomMag()
272 mom = fitted_state.getMom()
273 pos = fitted_state.getPos()
274
275 dp_over_p = (p_reco - p_truth) / p_truth
276 h_dp_over_p.Fill(dp_over_p)
277 h_dp_vs_p.Fill(p_truth, dp_over_p)
278
279 h_dx.Fill(pos.X() - x_t)
280 h_dy.Fill(pos.Y() - y_t)
281
282 pz_reco = mom.Z()
283 if abs(pz_reco) > 1e-10:
284 tx_reco = mom.X() / pz_reco
285 ty_reco = mom.Y() / pz_reco
286 h_dtx.Fill(tx_reco - tx_t)
287 h_dty.Fill(ty_reco - ty_t)
288
289 h_p_matched.Fill(p_truth)
290 except Exception:
291 pass
292
293 n_matched_mc += len(matched_mc_this_event)
294
295
296 n_ghost_reco = n_total_reco - n_matched_reco
297
298 efficiency = n_matched_mc / n_reconstructible if n_reconstructible > 0 else 0.0
299 efficiency_unc = wilson_interval(n_matched_mc, n_reconstructible)
300
301 clone_rate = n_clone_reco / n_matched_reco if n_matched_reco > 0 else 0.0
302 clone_rate_unc = wilson_interval(n_clone_reco, n_matched_reco)
303
304 ghost_rate = n_ghost_reco / n_total_reco if n_total_reco > 0 else 0.0
305 ghost_rate_unc = wilson_interval(n_ghost_reco, n_total_reco)
306
307
308 dp_p_sigma = h_dp_over_p.GetRMS()
309 dp_p_sigma_unc = h_dp_over_p.GetRMSError()
310 if h_dp_over_p.GetEntries() > 20:
311 fit_result = h_dp_over_p.Fit("gaus", "QS")
312 if fit_result and int(fit_result) == 0:
313 dp_p_sigma = fit_result.Parameter(2)
314 dp_p_sigma_unc = fit_result.ParError(2)
315
316 self.metrics = {
317 "tracking_benchmark": {
318 "n_events": {"value": int(n_events), "compare": "exact"},
319 "n_reconstructible": {"value": int(n_reconstructible), "compare": "exact"},
320 "n_total_reco": {"value": int(n_total_reco), "compare": "exact"},
321 "efficiency": {
322 "value": round(efficiency, 6),
323 "uncertainty": round(efficiency_unc, 6),
324 "compare": "statistical",
325 },
326 "clone_rate": {
327 "value": round(clone_rate, 6),
328 "uncertainty": round(clone_rate_unc, 6),
329 "compare": "statistical",
330 },
331 "ghost_rate": {
332 "value": round(ghost_rate, 6),
333 "uncertainty": round(ghost_rate_unc, 6),
334 "compare": "statistical",
335 },
336 "dp_over_p_sigma": {
337 "value": round(dp_p_sigma, 6),
338 "uncertainty": round(dp_p_sigma_unc, 6),
339 "compare": "statistical",
340 },
341 "dx_rms": {
342 "value": round(h_dx.GetRMS(), 6),
343 "uncertainty": round(h_dx.GetRMSError(), 6),
344 "compare": "statistical",
345 },
346 "dy_rms": {
347 "value": round(h_dy.GetRMS(), 6),
348 "uncertainty": round(h_dy.GetRMSError(), 6),
349 "compare": "statistical",
350 },
351 "dtx_rms": {
352 "value": round(h_dtx.GetRMS(), 6),
353 "uncertainty": round(h_dtx.GetRMSError(), 6),
354 "compare": "statistical",
355 },
356 "dty_rms": {
357 "value": round(h_dty.GetRMS(), 6),
358 "uncertainty": round(h_dty.GetRMSError(), 6),
359 "compare": "statistical",
360 },
361 }
362 }
363
364 self._histos = {
365 "h_dp_over_p": h_dp_over_p,
366 "h_dp_vs_p": h_dp_vs_p,
367 "h_dx": h_dx,
368 "h_dy": h_dy,
369 "h_dtx": h_dtx,
370 "h_dty": h_dty,
371 "h_chi2ndf": h_chi2ndf,
372 "h_p_truth": h_p_truth,
373 "h_p_matched": h_p_matched,
374 }
375
376 return self.metrics
377