10KLMEventT0ValidationModule
11==========================
12A basf2.Module that runs after KLMEventT0Estimator in the same path and fills
13validation histograms from the estimator's results. It does not recompute them:
14it reads what the estimator stores in each event.
16 per muon of the ParticleList (extra info; <C> = BKLMScint, BKLMRPC, EKLMScint)
17 klmTrackT0<C>, klmTrackT0Err<C>, klmTrackNHits<C>,
18 klmTrackSector<C>, klmTrackSection<C>, klmTrackT0All, klmTrackT0ErrAll
20 klmEventT0<C>, klmEventT0Err<C> (EventExtraInfo)
21 the KLM entry of EventT0 (final KLM event T0 and its uncertainty)
23Only the charge histograms read the hits of the track: the charge of each
24digit, before the estimator's charge selection.
26Histograms (directory structure):
28 h_nhits_pertrk_{bklm_scint,bklm_rpc,eklm_scint}
29 h_err_pertrk_{bklm_scint,bklm_rpc,eklm_scint} per-track uncertainty
30 h_digitQ_{bklm_scint,eklm_scint}
31 h_dimuon_{all,bklm_scint,bklm_rpc,eklm_scint} T0(mu+) - T0(mu-)
32 h_per_track_resolution core width of h_dimuon_* / sqrt(2)
34 h_t0evt_klm_final, h_t0evt_klm_final_err
36 h_pull_{bklm_scint,bklm_rpc,eklm_scint} (T0(mu+) - T0(mu-)) / sqrt(err+^2 + err-^2)
37 h_pull_{B_vs_E,B_vs_R,E_vs_R} two categories of the same track
38 h_evt_pull_{B_vs_E,B_vs_R,E_vs_R} per-event results of two categories
39 sector/{bklm_scint,bklm_rpc,eklm}/ same-category pulls by sector pair
40 sector/h2_pull_{bklm_scint,bklm_rpc,eklm}_{mean,sigma}
42 the same as pulls/, in ns
44Sector pairs: (i, j) has the mu+ in sector i and the mu- in sector j. BKLM sectors 1-8
45(both halves together); EKLM sectors B1-B4 (backward) and F1-F4 (forward).
50 import modularAnalysis as ma
51 from KLMEventT0Validation import KLMEventT0ValidationModule
53 ma.fillParticleList('mu+:forT0', 'p > 1.5 and abs(d0) < 2 and abs(z0) < 4', path=path)
54 path.add_module('KLMEventT0Estimator', MuonListName='mu+:forT0')
55 path.add_module(KLMEventT0ValidationModule(muon_list_name='mu+:forT0',
56 output_file='KLMEventT0Validation.root'))
64from ROOT
import Belle2, TFile, TH1D, TH2D
66ROOT.gROOT.SetBatch(
True)
69CATEGORIES = [(
'BKLMScint',
'bklm_scint',
'B'),
70 (
'BKLMRPC',
'bklm_rpc',
'R'),
71 (
'EKLMScint',
'eklm_scint',
'E')]
73CROSS_PAIRS = [(
'B',
'E'), (
'B',
'R'), (
'E',
'R')]
75N_SECTOR_BINS = {
'B': 8,
'R': 8,
'E': 8}
78def _sector_index(label, track):
79 """Sector-pair index of a track: BKLM sector - 1; EKLM 4 * (section - 1) + (sector - 1)."""
81 return 4 * (track[
'section'] - 1) + (track[
'sector'] - 1)
82 return track[
'sector'] - 1
85def _sector_name(label, index):
86 """Readable sector name for histogram names and titles."""
88 return (
'B' if index < 4
else 'F') + str(index % 4 + 1)
93 """Single Gaussian fit to the core: start at the median with width IQR/1.349 and fit
94 within +-2.5 of that width. Returns (mean, mean error, sigma, sigma error) or None."""
95 if hist
is None or hist.GetEntries() < 50:
97 probs = array.array(
'd', [0.25, 0.5, 0.75])
98 q = array.array(
'd', [0.0, 0.0, 0.0])
99 hist.GetQuantiles(3, q, probs)
100 sigma0 = (q[2] - q[0]) / 1.349
103 f = ROOT.TF1(
'core_fit',
'gaus', q[1] - 2.5 * sigma0, q[1] + 2.5 * sigma0)
104 f.SetParameters(hist.GetMaximum(), q[1], sigma0)
105 if int(hist.Fit(f,
'RQN0')) != 0:
107 return f.GetParameter(1), f.GetParError(1), abs(f.GetParameter(2)), f.GetParError(2)
111 """Validation histograms for KLMEventT0Estimator, filled from the estimator's stored results."""
113 def __init__(self, muon_list_name='mu+:forT0
', output_file='KLMEventT0Validation.root
',
114 opposite_charges_only=True):
119 ParticleList given to KLMEventT0Estimator as MuonListName.
121 ROOT file for the histograms.
122 opposite_charges_only : bool
123 Use only opposite-charge track pairs for the same-category pulls, residuals and sector maps.
141 """Open the output file and book the histograms."""
147 """Fill the summary histograms, write and close the file."""
149 self.
_outfile.Write(
'', ROOT.TObject.kOverwrite)
151 basf2.B2INFO(f
'KLMEventT0ValidationModule: wrote {self._output_file_path}')
155 """Create (if needed) and change to a directory of the output file."""
157 for part
in path.split(
'/'):
158 sub = d.GetDirectory(part)
159 d = sub
if sub
else d.mkdir(part)
163 """Book all histograms; the binning follows the histograms used for the KLM event T0 note."""
166 self.
_cd(
'diagnostics')
167 for name, sfx, lab
in CATEGORIES:
168 h[f
'nhits_{lab}'] = TH1D(f
'h_nhits_pertrk_{sfx}', f
'Hits per track ({sfx});hits', 100, -0.5, 99.5)
169 h[f
'err_{lab}'] = TH1D(f
'h_err_pertrk_{sfx}', f
'Per-track T0 uncertainty ({sfx});uncertainty [ns]',
171 h[f
'dimuon_{lab}'] = TH1D(f
'h_dimuon_{sfx}', f
'Di-muon T0(#mu^{{+}}) - T0(#mu^{{-}}) ({sfx});#DeltaT_{{0}} [ns]',
173 h[
'dimuon_all'] = TH1D(
'h_dimuon_all',
'Di-muon T0(#mu^{+}) - T0(#mu^{-}) (all categories);#DeltaT_{0} [ns]',
175 h[
'digitQ_B'] = TH1D(
'h_digitQ_bklm_scint',
'Digit charge (BKLM scintillator);charge [ADC counts]', 100, 0.0, 800.0)
176 h[
'digitQ_E'] = TH1D(
'h_digitQ_eklm_scint',
'Digit charge (EKLM scintillator);charge [ADC counts]', 100, 0.0, 800.0)
177 h[
'resolution'] = TH1D(
'h_per_track_resolution',
'Per-track resolution from di-muon #DeltaT_{0};;#sigma/#sqrt{2} [ns]',
179 for i, lab
in enumerate([
'all',
'bklm_scint',
'bklm_rpc',
'eklm_scint'], 1):
180 h[
'resolution'].GetXaxis().SetBinLabel(i, lab)
183 h[
'final'] = TH1D(
'h_t0evt_klm_final',
'Final KLM event T0;T0 [ns]', 800, -100, 100)
184 h[
'final_err'] = TH1D(
'h_t0evt_klm_final_err',
'Final KLM event T0 uncertainty;uncertainty [ns]', 800, 0.0, 20.0)
186 for kind, title, nb, lo, hi
in ((
'pull',
'Pull', 200, -10, 10), (
'res',
'Residual', 500, -25, 25)):
187 top =
'pulls' if kind ==
'pull' else 'residuals'
188 unit =
'' if kind ==
'pull' else ' [ns]'
190 for name, sfx, lab
in CATEGORIES:
191 h[f
'{kind}_{lab}'] = TH1D(f
'h_{kind}_{sfx}', f
'{title} ({sfx});T0(#mu^{{+}}) - T0(#mu^{{-}}){unit}',
193 for a, b
in CROSS_PAIRS:
194 h[f
'{kind}_{a}_vs_{b}'] = TH1D(f
'h_{kind}_{a}_vs_{b}', f
'{title}, same track;T0({a}) - T0({b}){unit}',
196 h[f
'evt_{kind}_{a}_vs_{b}'] = TH1D(f
'h_evt_{kind}_{a}_vs_{b}', f
'{title}, per event;T0({a}) - T0({b}){unit}',
199 snb, slo, shi = (200, -10, 10)
if kind ==
'pull' else (200, -50, 50)
200 for name, sfx, lab
in CATEGORIES:
201 self.
_cd(f
'{top}/sector/{sfx}')
202 n = N_SECTOR_BINS[lab]
206 si, sj = _sector_name(lab, i), _sector_name(lab, j)
207 grid[(i, j)] = TH1D(f
'h_{kind}_{sfx}_{si}_vs_{sj}',
208 f
'{title} ({sfx}), #mu^{{+}} in {si}, #mu^{{-}} in {sj};'
209 f
'T0(#mu^{{+}}) - T0(#mu^{{-}}){unit}', snb, slo, shi)
210 h[f
'{kind}_sector_{lab}'] = grid
211 self.
_cd(f
'{top}/sector')
212 for stat
in (
'mean',
'sigma'):
213 h2 = TH2D(f
'h2_{kind}_{sfx}_{stat}', f
'{title} {stat} ({sfx});#mu^{{+}} sector;#mu^{{-}} sector',
214 n, 0.5, n + 0.5, n, 0.5, n + 0.5)
216 h2.GetXaxis().SetBinLabel(k + 1, _sector_name(lab, k))
217 h2.GetYaxis().SetBinLabel(k + 1, _sector_name(lab, k))
218 h[f
'{kind}_sector_{lab}_{stat}'] = h2
222 """Read the estimator's results for this event and fill the histograms."""
225 if not plist.isValid():
228 tracks = {lab: []
for _, _, lab
in CATEGORIES}
230 for i
in range(plist.obj().getListSize()):
231 particle = plist.obj().getParticle(i)
232 charge = particle.getCharge()
233 for name, sfx, lab
in CATEGORIES:
234 if not particle.hasExtraInfo(
'klmTrackT0' + name):
236 t = {
'mu': particle.getExtraInfo(
'klmTrackT0' + name),
237 'err': particle.getExtraInfo(
'klmTrackT0Err' + name),
238 'n': int(particle.getExtraInfo(
'klmTrackNHits' + name)),
239 'sector': int(particle.getExtraInfo(
'klmTrackSector' + name)),
240 'section': int(particle.getExtraInfo(
'klmTrackSection' + name)),
241 'charge': charge,
'index': i}
242 tracks[lab].append(t)
243 h[f
'nhits_{lab}'].Fill(t[
'n'])
244 h[f
'err_{lab}'].Fill(t[
'err'])
245 if particle.hasExtraInfo(
'klmTrackT0All'):
246 all_tracks.append((charge, particle.getExtraInfo(
'klmTrackT0All')))
252 klm = evt_t0.obj().getTemporaryEventT0s(Belle2.Const.KLM)
254 h[
'final'].Fill(klm.back().eventT0)
255 h[
'final_err'].Fill(klm.back().eventT0Uncertainty)
258 for name, sfx, lab
in CATEGORIES:
262 for a, b
in CROSS_PAIRS:
265 if tb[
'index'] == ta[
'index']:
266 self.
_fill_diff(ta[
'mu'], ta[
'err'], tb[
'mu'], tb[
'err'], f
'_{a}_vs_{b}')
272 for name, sfx, lab
in CATEGORIES:
273 if extra.obj().hasExtraInfo(
'klmEventT0' + name):
274 evt[lab] = (extra.obj().getExtraInfo(
'klmEventT0' + name),
275 extra.obj().getExtraInfo(
'klmEventT0Err' + name))
276 for a, b
in CROSS_PAIRS:
277 if a
in evt
and b
in evt:
278 self.
_fill_diff(evt[a][0], evt[a][1], evt[b][0], evt[b][1], f
'_{a}_vs_{b}', prefix=
'evt_')
281 def _last(pairs, sign):
282 vals = [mu
for q, mu
in pairs
if q * sign > 0]
283 return vals[-1]
if vals
else None
284 pos, neg = _last(all_tracks, 1), _last(all_tracks, -1)
285 if pos
is not None and neg
is not None:
286 h[
'dimuon_all'].Fill(pos - neg)
287 for name, sfx, lab
in CATEGORIES:
288 pairs = [(t[
'charge'], t[
'mu'])
for t
in tracks[lab]]
289 pos, neg = _last(pairs, 1), _last(pairs, -1)
290 if pos
is not None and neg
is not None:
291 h[f
'dimuon_{lab}'].Fill(pos - neg)
294 def _fill_diff(self, mu_a, err_a, mu_b, err_b, suffix, prefix=''):
295 """Fill residual and pull of a - b."""
297 if not (math.isfinite(mu_a)
and math.isfinite(mu_b)):
299 h[f
'{prefix}res{suffix}'].Fill(mu_a - mu_b)
300 den2 = err_a * err_a + err_b * err_b
302 h[f
'{prefix}pull{suffix}'].Fill((mu_a - mu_b) / math.sqrt(den2))
305 """Same-category pairs of tracks, the mu+ first."""
307 for i
in range(len(trk)):
308 for j
in range(i + 1, len(trk)):
309 a, b = trk[i], trk[j]
312 if a[
'charge'] < 0 < b[
'charge']:
314 den2 = a[
'err'] ** 2 + b[
'err'] ** 2
317 d = a[
'mu'] - b[
'mu']
318 pull = d / math.sqrt(den2)
319 h[f
'res_{lab}'].Fill(d)
320 h[f
'pull_{lab}'].Fill(pull)
321 ki, kj = _sector_index(lab, a), _sector_index(lab, b)
322 n = N_SECTOR_BINS[lab]
323 if 0 <= ki < n
and 0 <= kj < n:
324 h[f
'res_sector_{lab}'][(ki, kj)].Fill(d)
325 h[f
'pull_sector_{lab}'][(ki, kj)].Fill(pull)
328 """Charge of the track's scintillator digits before the charge selection (good digits, normal channels)."""
329 track = particle.getTrack()
335 return (
not status.isValid()
or
336 status.obj().getChannelStatus(digit.getUniqueChannelID()) == Belle2.KLMChannelStatus.c_Normal)
338 for hit2d
in track.getRelationsTo[
'Belle2::KLMHit2d'](
'KLMHit2ds'):
339 if hit2d.getSubdetector() == Belle2.KLMElementNumbers.c_EKLM:
340 for digit
in hit2d.getRelationsTo[
'Belle2::KLMDigit'](
'KLMDigits'):
341 if digit.isGood()
and normal(digit):
342 self.
_h[
'digitQ_E'].Fill(digit.getCharge())
343 elif not hit2d.inRPC()
and not hit2d.isOutOfTime():
344 for hit1d
in hit2d.getRelationsTo[
'Belle2::BKLMHit1d'](
'BKLMHit1ds'):
345 for digit
in hit1d.getRelationsTo[
'Belle2::KLMDigit'](
'KLMDigits'):
346 if not digit.inRPC()
and digit.isGood()
and normal(digit):
347 self.
_h[
'digitQ_B'].Fill(digit.getCharge())
350 """Core-fit mean and width of every sector pair, and the per-track resolution."""
352 for kind
in (
'pull',
'res'):
353 for name, sfx, lab
in CATEGORIES:
354 for (i, j), hh
in h[f
'{kind}_sector_{lab}'].items():
358 h[f
'{kind}_sector_{lab}_mean'].SetBinContent(i + 1, j + 1, fit[0])
359 h[f
'{kind}_sector_{lab}_mean'].SetBinError(i + 1, j + 1, fit[1])
360 h[f
'{kind}_sector_{lab}_sigma'].SetBinContent(i + 1, j + 1, fit[2])
361 h[f
'{kind}_sector_{lab}_sigma'].SetBinError(i + 1, j + 1, fit[3])
362 for b, key
in enumerate([
'dimuon_all',
'dimuon_B',
'dimuon_R',
'dimuon_E'], 1):
363 fit = core_fit(h[key])
365 h[
'resolution'].SetBinContent(b, fit[2] / math.sqrt(2.0))
366 h[
'resolution'].SetBinError(b, fit[3] / math.sqrt(2.0))
Class to access a DBObjPtr from Python.
a (simplified) python wrapper for StoreObjPtr.
_fill_diff(self, mu_a, err_a, mu_b, err_b, suffix, prefix='')
_opp_charges_only
Opposite-charge pairs only.
_muon_list_name
ParticleList name.
_channel_status
KLM channel status, for the charge histograms.
__init__(self, muon_list_name='mu+:forT0', output_file='KLMEventT0Validation.root', opposite_charges_only=True)
dict _h
Histograms by key.
_fill_charges(self, particle)
_fill_pairs(self, lab, trk)
_output_file_path
Output file name.