Belle II Software development
KLMEventT0ValidationModule Class Reference
Inheritance diagram for KLMEventT0ValidationModule:

Public Member Functions

 __init__ (self, str muon_list_name='mu+:forT0', str output_file='KLMEventT0Validation.root', bool opposite_charges_only=True, float adc_cut_bklm_scint_min=30.0, float adc_cut_bklm_scint_max=320.0, float adc_cut_eklm_scint_min=40.0, float adc_cut_eklm_scint_max=350.0)
 
 initialize (self)
 
 beginRun (self)
 
 terminate (self)
 
 event (self)
 

Protected Member Functions

 _book_histograms (self)
 
 _book_pairwise (self, rootfile, dirpath, name_tmpl, title_tmpl, n, nb, lo, hi, offset=0)
 
 _book_2d_summary (self, name, title, n, lo=-0.5)
 
 _prevent_gc (self)
 
 _build_ext_maps (self, track)
 
 _accumulate_eklm (self, track, scint_map)
 
 _accumulate_bklm_scint (self, track, scint_map)
 
 _accumulate_bklm_rpc (self, track, rpc_map, accept_phi)
 
 _fill_pairs (self, trk_list, h_pull, h_res, ppw_pull, ppw_res, bklm)
 
 _combine_track_categories (self, *categories)
 
 _fit_summary_histograms (self)
 

Static Protected Member Functions

 _safe_cd (rootfile, dirpath)
 
 _midpoint (a, b)
 

Protected Attributes

 _muon_list_name = muon_list_name
 Muon ParticleList name.
 
 _output_file_path = output_file
 Output ROOT file path.
 
 _opp_charges_only = opposite_charges_only
 Only use opposite-charge track pairs for pull/residual histograms.
 
 _adc_bklm_min = adc_cut_bklm_scint_min
 Lower ADC cut for BKLM scintillator digits.
 
 _adc_bklm_max = adc_cut_bklm_scint_max
 Upper ADC cut for BKLM scintillator digits.
 
 _adc_eklm_min = adc_cut_eklm_scint_min
 Lower ADC cut for EKLM scintillator digits.
 
 _adc_eklm_max = adc_cut_eklm_scint_max
 Upper ADC cut for EKLM scintillator digits.
 
 _outfile = None
 Output TFile.
 
dict _h = {}
 All booked histograms, keyed by short name.
 
 _geo_bklm = None
 BKLM geometry parameters (initialised in initialize()).
 
 _geo_eklm = None
 EKLM geometry data (initialised in initialize()).
 
 _transform_eklm = None
 EKLM coordinate transform helper (initialised in initialize()).
 
 _elem_num = None
 EKLM element-number helper (initialised in initialize()).
 
float _delay_eklm = 0.0
 Propagation-delay constant for EKLM strips (ns/cm), refreshed in beginRun().
 
float _delay_bklm = 0.0
 Propagation-delay constant for BKLM scintillator strips (ns/cm), refreshed in beginRun().
 
float _delay_rpc_phi = 0.0
 Propagation-delay constant for BKLM RPC phi strips (ns/cm), refreshed in beginRun().
 
float _delay_rpc_z = 0.0
 Propagation-delay constant for BKLM RPC z strips (ns/cm), refreshed in beginRun().
 
 _db_cable_delay = None
 Cable-delay PyDBObj used to query per-channel offsets, refreshed in beginRun().
 
 _db_channel_status = None
 Channel-status PyDBObj used to skip bad channels, refreshed in beginRun().
 

Detailed Description

Full validation module for KLMEventT0Estimator.

Recomputes per-track T0 using the same correction chain as the C++
estimator (Trec - Tcable - Tprop - Tfly) and fills the complete set
of validation histograms that were present in the monolithic Trash
module: per-track/per-event T0, timing components, pulls, residuals,
cross-detector ΔT0, dimuon ΔT0, pairwise sector histograms and their
2D Gaussian-fit summaries.

Definition at line 183 of file KLMEventT0Validation.py.

Constructor & Destructor Documentation

◆ __init__()

__init__ ( self,
str muon_list_name = 'mu+:forT0',
str output_file = 'KLMEventT0Validation.root',
bool opposite_charges_only = True,
float adc_cut_bklm_scint_min = 30.0,
float adc_cut_bklm_scint_max = 320.0,
float adc_cut_eklm_scint_min = 40.0,
float adc_cut_eklm_scint_max = 350.0 )
Initialise the module with histogram booking parameters and ADC cuts.

Definition at line 195 of file KLMEventT0Validation.py.

202 adc_cut_eklm_scint_max: float = 350.0):
203 """Initialise the module with histogram booking parameters and ADC cuts."""
204 super().__init__()
205 self.set_name('KLMEventT0ValidationModule')
206
207 self._muon_list_name = muon_list_name
208
209 self._output_file_path = output_file
210
211 self._opp_charges_only = opposite_charges_only
212
213 self._adc_bklm_min = adc_cut_bklm_scint_min
214
215 self._adc_bklm_max = adc_cut_bklm_scint_max
216
217 self._adc_eklm_min = adc_cut_eklm_scint_min
218
219 self._adc_eklm_max = adc_cut_eklm_scint_max
220
221
222 self._outfile = None
223
224 self._h = {}
225
226
227 self._geo_bklm = None
228
229 self._geo_eklm = None
230
231 self._transform_eklm = None
232
233 self._elem_num = None
234
235
236 self._delay_eklm = 0.0
237
238 self._delay_bklm = 0.0
239
240 self._delay_rpc_phi = 0.0
241
242 self._delay_rpc_z = 0.0
243
244 self._db_cable_delay = None
245
246 self._db_channel_status = None
247

Member Function Documentation

◆ _accumulate_bklm_rpc()

_accumulate_bklm_rpc ( self,
track,
rpc_map,
accept_phi )
protected
Accumulate BKLM RPC hits for *track*, filtered by readout direction.

Parameters
----------
accept_phi : bool
    True  → accumulate phi-plane digits only.
    False → accumulate z-plane digits only.

Returns
-------
results : list of dict (same fields as _accumulate_eklm)

Definition at line 913 of file KLMEventT0Validation.py.

913 def _accumulate_bklm_rpc(self, track, rpc_map, accept_phi):
914 """
915 Accumulate BKLM RPC hits for *track*, filtered by readout direction.
916
917 Parameters
918 ----------
919 accept_phi : bool
920 True → accumulate phi-plane digits only.
921 False → accumulate z-plane digits only.
922
923 Returns
924 -------
925 results : list of dict (same fields as _accumulate_eklm)
926 """
927 results = []
928 cable_db = self._db_cable_delay
929 status_db = self._db_channel_status
930 delay = self._delay_rpc_phi if accept_phi else self._delay_rpc_z
931
932 for hit2d in track.getRelationsTo['Belle2::KLMHit2d']('KLMHit2ds'):
933 if hit2d.getSubdetector() != Belle2.KLMElementNumbers.c_BKLM:
934 continue
935 if not hit2d.inRPC():
936 continue
937 if hit2d.isOutOfTime():
938 continue
939
940 mod = self._geo_bklm.findModule(
941 hit2d.getSection(), hit2d.getSector(), hit2d.getLayer())
942 pos2d = hit2d.getPosition()
943
944 for b1d in hit2d.getRelationsTo['Belle2::BKLMHit1d']('BKLMHit1ds'):
945 is_phi = b1d.isPhiReadout()
946 if accept_phi and not is_phi:
947 continue
948 if not accept_phi and is_phi:
949 continue
950
951 for digit in b1d.getRelationsTo['Belle2::KLMDigit']('KLMDigits'):
952 if not digit.inRPC():
953 continue
954 if not digit.isGood():
955 continue
956
957 cid = digit.getUniqueChannelID()
958 if (status_db.isValid() and
959 status_db.obj().getChannelStatus(cid) !=
960 Belle2.KLMChannelStatus.c_Normal):
961 continue
962
963 # RPC: matched by module key
964 module_key = self._elem_num.moduleNumber(
965 digit.getSubdetector(),
966 digit.getSection(), digit.getSector(), digit.getLayer())
967 pair = rpc_map.get(module_key)
968 if pair is None:
969 continue
970 entry_ext, exit_ext = pair
971
972 Tfly = 0.5 * (entry_ext.getTOF() + exit_ext.getTOF())
973 pos_ext = self._midpoint(entry_ext.getPosition(), exit_ext.getPosition())
974
975 loc_ext = mod.globalToLocal(
976 ROOT.CLHEP.Hep3Vector(pos_ext.X(), pos_ext.Y(), pos_ext.Z()),
977 True)
978 loc_hit = mod.globalToLocal(
979 ROOT.CLHEP.Hep3Vector(pos2d.X(), pos2d.Y(), pos2d.Z()),
980 True)
981 diff = loc_ext - loc_hit
982 if (abs(diff.z()) > mod.getZStripWidth() or
983 abs(diff.y()) > mod.getPhiStripWidth()):
984 continue
985
986 # RPC propagation distance (Hep3Vector, pick y or z component)
987 propa_v = mod.getPropagationDistance(loc_ext)
988 dist_cm = propa_v.y() if is_phi else propa_v.z()
989
990 Trec = digit.getTime()
991 Tcable = (cable_db.obj().getTimeDelay(cid)
992 if cable_db.isValid() else 0.0)
993 Tprop = dist_cm * delay
994 t0_est = Trec - Tcable - Tprop - Tfly
995
996 if not math.isfinite(t0_est):
997 continue
998
999 results.append({
1000 't0_est': t0_est,
1001 'Trec': Trec, 'Tcable': Tcable,
1002 'Tprop': Tprop, 'Tfly': Tfly,
1003 'section': hit2d.getSection(),
1004 'sector': hit2d.getSector(),
1005 })
1006
1007 return results
1008

◆ _accumulate_bklm_scint()

_accumulate_bklm_scint ( self,
track,
scint_map )
protected
Accumulate BKLM scintillator hits for *track*.

Returns
-------
results : list of dict (same fields as _accumulate_eklm, plus
          'sector' as BKLM sector 1-8)

Definition at line 825 of file KLMEventT0Validation.py.

825 def _accumulate_bklm_scint(self, track, scint_map):
826 """
827 Accumulate BKLM scintillator hits for *track*.
828
829 Returns
830 -------
831 results : list of dict (same fields as _accumulate_eklm, plus
832 'sector' as BKLM sector 1-8)
833 """
834 results = []
835 cable_db = self._db_cable_delay
836 status_db = self._db_channel_status
837 delay = self._delay_bklm
838
839 for hit2d in track.getRelationsTo['Belle2::KLMHit2d']('KLMHit2ds'):
840 if hit2d.getSubdetector() != Belle2.KLMElementNumbers.c_BKLM:
841 continue
842 if hit2d.inRPC():
843 continue
844 if hit2d.isOutOfTime():
845 continue
846
847 mod = self._geo_bklm.findModule(
848 hit2d.getSection(), hit2d.getSector(), hit2d.getLayer())
849 pos2d = hit2d.getPosition()
850
851 for b1d in hit2d.getRelationsTo['Belle2::BKLMHit1d']('BKLMHit1ds'):
852 is_phi = b1d.isPhiReadout()
853
854 for digit in b1d.getRelationsTo['Belle2::KLMDigit']('KLMDigits'):
855 if digit.inRPC() or not digit.isGood():
856 continue
857
858 cid = digit.getUniqueChannelID()
859 if (status_db.isValid() and
860 status_db.obj().getChannelStatus(cid) !=
861 Belle2.KLMChannelStatus.c_Normal):
862 continue
863
864 charge = digit.getCharge()
865 if self._h['digitQ_B']:
866 self._h['digitQ_B'].Fill(charge)
867
868 if not (self._adc_bklm_min <= charge <= self._adc_bklm_max):
869 continue
870
871 pair = scint_map.get(cid)
872 if pair is None:
873 continue
874 entry_ext, exit_ext = pair
875
876 Tfly = 0.5 * (entry_ext.getTOF() + exit_ext.getTOF())
877 pos_ext = self._midpoint(entry_ext.getPosition(), exit_ext.getPosition())
878
879 # Gate in local coordinates (position consistency check)
880 loc_ext = mod.globalToLocal(
881 ROOT.CLHEP.Hep3Vector(pos_ext.X(), pos_ext.Y(), pos_ext.Z()),
882 True)
883 loc_hit = mod.globalToLocal(
884 ROOT.CLHEP.Hep3Vector(pos2d.X(), pos2d.Y(), pos2d.Z()),
885 True)
886 diff = loc_ext - loc_hit
887 if (abs(diff.z()) > mod.getZStripWidth() or
888 abs(diff.y()) > mod.getPhiStripWidth()):
889 continue
890
891 # Propagation distance (in cm, same units as delay constant)
892 dist_cm = mod.getPropagationDistance(loc_ext, digit.getStrip(), is_phi)
893
894 Trec = digit.getTime()
895 Tcable = (cable_db.obj().getTimeDelay(cid)
896 if cable_db.isValid() else 0.0)
897 Tprop = dist_cm * delay
898 t0_est = Trec - Tcable - Tprop - Tfly
899
900 if not math.isfinite(t0_est):
901 continue
902
903 results.append({
904 't0_est': t0_est,
905 'Trec': Trec, 'Tcable': Tcable,
906 'Tprop': Tprop, 'Tfly': Tfly,
907 'section': hit2d.getSection(), # 0=bwd,1=fwd
908 'sector': hit2d.getSector(), # 1-8
909 })
910
911 return results
912

◆ _accumulate_eklm()

_accumulate_eklm ( self,
track,
scint_map )
protected
Accumulate EKLM scintillator hits for *track*.

Returns
-------
results : list of dict, each with:
    t0_est, Trec, Tcable, Tprop, Tfly, section, sector

Definition at line 739 of file KLMEventT0Validation.py.

739 def _accumulate_eklm(self, track, scint_map):
740 """
741 Accumulate EKLM scintillator hits for *track*.
742
743 Returns
744 -------
745 results : list of dict, each with:
746 t0_est, Trec, Tcable, Tprop, Tfly, section, sector
747 """
748 results = []
749 cable_db = self._db_cable_delay
750 status_db = self._db_channel_status
751 delay = self._delay_eklm
752
753 for hit2d in track.getRelationsTo['Belle2::KLMHit2d']('KLMHit2ds'):
754 if hit2d.getSubdetector() != Belle2.KLMElementNumbers.c_EKLM:
755 continue
756
757 for digit in hit2d.getRelationsTo['Belle2::KLMDigit']('KLMDigits'):
758 if not digit.isGood():
759 continue
760
761 cid = digit.getUniqueChannelID()
762 if (status_db.isValid() and
763 status_db.obj().getChannelStatus(cid) !=
764 Belle2.KLMChannelStatus.c_Normal):
765 continue
766
767 # ADC charge fill (before cut)
768 charge = digit.getCharge()
769 if self._h['digitQ_E']:
770 self._h['digitQ_E'].Fill(charge)
771
772 # ADC cut
773 if not (self._adc_eklm_min <= charge <= self._adc_eklm_max):
774 continue
775
776 # Match to ExtHit pair
777 pair = scint_map.get(cid)
778 if pair is None:
779 continue
780 entry_ext, exit_ext = pair
781
782 Tfly = 0.5 * (entry_ext.getTOF() + exit_ext.getTOF())
783
784 # Propagation distance for EKLM strip
785 # strip_length is in CLHEP mm; convert to basf2 cm
786 strip_len_cm = (self._geo_eklm.getStripLength(digit.getStrip())
787 * _MM_TO_BASF2)
788
789 # Global hit position in CLHEP mm
790 pos = self._midpoint(entry_ext.getPosition(), exit_ext.getPosition())
791 hit_global = ROOT.HepGeom.Point3D('double')(
792 pos.X() * _BASF2_TO_MM,
793 pos.Y() * _BASF2_TO_MM,
794 pos.Z() * _BASF2_TO_MM)
795
796 # Apply strip local transform to get local x
797 tr = self._transform_eklm.getStripGlobalToLocal(digit)
798 hit_local = tr * hit_global
799 # hit_local.x() is in CLHEP mm → convert to cm
800 local_x_cm = hit_local.x() * _MM_TO_BASF2
801
802 dist_cm = 0.5 * strip_len_cm - local_x_cm
803
804 # Full correction chain
805 Trec = digit.getTime()
806 Tcable = (cable_db.obj().getTimeDelay(cid)
807 if cable_db.isValid() else 0.0)
808 Tprop = dist_cm * delay
809 t0_est = Trec - Tcable - Tprop - Tfly
810
811 if not math.isfinite(t0_est):
812 continue
813
814 # EKLM section: 1=bwd, 2=fwd → convert to 0-indexed for matching
815 results.append({
816 't0_est': t0_est,
817 'Trec': Trec, 'Tcable': Tcable,
818 'Tprop': Tprop, 'Tfly': Tfly,
819 'section': digit.getSection(), # 1=bwd, 2=fwd
820 'sector': digit.getSector(), # 1-4
821 })
822
823 return results
824

◆ _book_2d_summary()

_book_2d_summary ( self,
name,
title,
n,
lo = -0.5 )
protected
Book a TH2D for pairwise Gaussian-fit summaries.

Definition at line 615 of file KLMEventT0Validation.py.

615 def _book_2d_summary(self, name, title, n, lo=-0.5):
616 """Book a TH2D for pairwise Gaussian-fit summaries."""
617 hi = n + lo
618 return TH2D(name, title, n, lo, hi, n, lo, hi)
619

◆ _book_histograms()

_book_histograms ( self)
protected
Create all validation histograms inside the output ROOT file.

Definition at line 296 of file KLMEventT0Validation.py.

296 def _book_histograms(self):
297 """Create all validation histograms inside the output ROOT file."""
298 h = self._h
299 f = self._outfile
300
301 def H1(name, title, nb, lo, hi):
302 return TH1D(name, title, nb, lo, hi)
303
304 def H1I(name, title, nb, lo, hi):
305 return TH1I(name, title, nb, lo, hi)
306
307 # --- per_track/ ---
308 f.mkdir('per_track').cd()
309 h['t0trk_B'] = H1('h_t0trk_bklm_scint',
310 'Per-track T_{0} (BKLM Scint);T_{0} [ns]', 800, -100, 100)
311 h['t0trk_R'] = H1('h_t0trk_bklm_rpc',
312 'Per-track T_{0} (BKLM RPC);T_{0} [ns]', 800, -100, 100)
313 h['t0trk_E'] = H1('h_t0trk_eklm_scint',
314 'Per-track T_{0} (EKLM Scint);T_{0} [ns]', 800, -100, 100)
315
316 # --- per_event/ ---
317 f.mkdir('per_event').cd()
318 h['t0evt_B'] = H1('h_t0evt_bklm_scint',
319 'Per-event T_{0} (BKLM Scint);T_{0} [ns]', 800, -100, 100)
320 h['t0evt_R'] = H1('h_t0evt_bklm_rpc',
321 'Per-event T_{0} (BKLM RPC);T_{0} [ns]', 800, -100, 100)
322 h['t0evt_E'] = H1('h_t0evt_eklm_scint',
323 'Per-event T_{0} (EKLM Scint);T_{0} [ns]', 800, -100, 100)
324 h['t0evt_all'] = H1('h_t0evt_all',
325 'Per-event T_{0} (all KLM);T_{0} [ns]', 800, -100, 100)
326 h['final_source'] = H1I('h_final_source', 'Final KLM source;;events', 7, 0.5, 7.5)
327 for ib, lab in enumerate(['B only', 'E only', 'R only',
328 'B+E', 'B+R', 'E+R', 'B+E+R'], 1):
329 h['final_source'].GetXaxis().SetBinLabel(ib, lab)
330
331 # --- diagnostics/ ---
332 f.mkdir('diagnostics').cd()
333 h['nhits_B'] = H1I('h_nhits_pertrk_bklm_scint',
334 'Hits per track (BKLM Scint);N_{hits}', 50, 0, 50)
335 h['nhits_E'] = H1I('h_nhits_pertrk_eklm_scint',
336 'Hits per track (EKLM Scint);N_{hits}', 50, 0, 50)
337 h['sem_B'] = H1('h_sem_pertrk_bklm_scint',
338 'SEM per track (BKLM Scint);SEM [ns]', 200, 0.0, 10.0)
339 h['sem_E'] = H1('h_sem_pertrk_eklm_scint',
340 'SEM per track (EKLM Scint);SEM [ns]', 200, 0.0, 10.0)
341 h['digitQ_B'] = H1('h_digitQ_bklm_scint',
342 'KLMDigit charge (BKLM Scint);ADC (a.u.)', 100, 0, 800)
343 h['digitQ_E'] = H1('h_digitQ_eklm_scint',
344 'KLMDigit charge (EKLM Scint);ADC (a.u.)', 100, 0, 800)
345 h['sample_type'] = H1I('h_sample_type', 'Sample type;;events', 2, 0.5, 2.5)
346 h['sample_type'].GetXaxis().SetBinLabel(1, 'Data')
347 h['sample_type'].GetXaxis().SetBinLabel(2, 'MC')
348
349 # Timing components per detector type
350 _timing_cfg = {
351 'B': ('BKLM Scint', -5000, -4000, -5000, -4000),
352 'R': ('BKLM RPC', -800, -500, -800, -500),
353 'E': ('EKLM Scint', -5000, -4000, -5000, -4000),
354 }
355 for key, (label, r0, r1, rc0, rc1) in _timing_cfg.items():
356 h[f'Trec_{key}'] = H1(f'h_Trec_{key.lower()}',
357 f'T_{{rec}} ({label});time [ns]', 800, r0, r1)
358 h[f'Tcable_{key}'] = H1(f'h_Tcable_{key.lower()}',
359 f'T_{{cable}} ({label});time [ns]', 800, rc0, rc1)
360 h[f'Tprop_{key}'] = H1(f'h_Tprop_{key.lower()}',
361 f'T_{{prop}} ({label});time [ns]', 800, -50, 50)
362 h[f'Tfly_{key}'] = H1(f'h_Tfly_{key.lower()}',
363 f'T_{{fly}} ({label});time [ns]', 800, -100, 100)
364
365 # --- dimuon (inside diagnostics/, matching C++ module layout) ---
366 self._safe_cd(f, 'diagnostics')
367 h['dimuon_all'] = H1('h_dimuon_all',
368 'Dimuon #DeltaT_{0} (all KLM);T_{0}(#mu^{+})-T_{0}(#mu^{-}) [ns]',
369 400, -50, 50)
370 h['dimuon_B'] = H1('h_dimuon_bklm_scint',
371 'Dimuon #DeltaT_{0} (BKLM Scint);#DeltaT_{0} [ns]', 400, -50, 50)
372 h['dimuon_R'] = H1('h_dimuon_bklm_rpc',
373 'Dimuon #DeltaT_{0} (BKLM RPC);#DeltaT_{0} [ns]', 400, -50, 50)
374 h['dimuon_E'] = H1('h_dimuon_eklm_scint',
375 'Dimuon #DeltaT_{0} (EKLM Scint);#DeltaT_{0} [ns]', 400, -50, 50)
376 h['dimuon_scint'] = H1('h_dimuon_scint_only',
377 'Dimuon #DeltaT_{0} (Scint only);#DeltaT_{0} [ns]', 400, -50, 50)
378 h['dimuon_rpc'] = H1('h_dimuon_with_rpc',
379 'Dimuon #DeltaT_{0} (With RPC);#DeltaT_{0} [ns]', 400, -50, 50)
380 h['dimuon_rpcdir'] = H1('h_dimuon_with_rpc_dir',
381 'Dimuon #DeltaT_{0} (With RPC dir);#DeltaT_{0} [ns]', 400, -50, 50)
382 # Resolution summary (filled in terminate() from Gaussian fits)
383 # 6 categories: BKLM Scint, BKLM RPC, EKLM Scint, Scint Only, With RPC, With RPC Dir
384 h['resolution'] = H1('h_per_track_resolution',
385 'Per-Track T_{0} Resolution (#sigma_{#DeltaT0}/#sqrt{2});'
386 'Category;Resolution [ns]', 6, 0.5, 6.5)
387 for ib, lab in enumerate(
388 ['BKLM Scint', 'BKLM RPC', 'EKLM Scint', 'Scint Only', 'With RPC', 'With RPC Dir'], 1):
389 h['resolution'].GetXaxis().SetBinLabel(ib, lab)
390
391 # --- pulls/ ---
392 f.mkdir('pulls').cd()
393 h['pull_B'] = H1('h_pull_bklm_scint',
394 'Pull (BKLM Scint);(T_{0,i}-T_{0,j})/#sigma', 200, -10, 10)
395 h['pull_E'] = H1('h_pull_eklm_scint',
396 'Pull (EKLM Scint);(T_{0,i}-T_{0,j})/#sigma', 200, -10, 10)
397 h['pull_Rphi'] = H1('h_pull_rpc_phi',
398 'Pull (BKLM RPC Phi);(T_{0,i}-T_{0,j})/#sigma', 200, -10, 10)
399 h['pull_Rz'] = H1('h_pull_rpc_z',
400 'Pull (BKLM RPC Z);(T_{0,i}-T_{0,j})/#sigma', 200, -10, 10)
401 h['pull_R'] = H1('h_pull_bklm_rpc',
402 'Pull (BKLM RPC combined);(T_{0,i}-T_{0,j})/#sigma', 200, -10, 10)
403 h['pull_BvE'] = H1('h_pull_B_vs_E',
404 'Pull (BKLM Scint vs EKLM Scint);pull', 200, -10, 10)
405 h['pull_BvR'] = H1('h_pull_B_vs_R',
406 'Pull (BKLM Scint vs BKLM RPC);pull', 200, -10, 10)
407 h['pull_EvR'] = H1('h_pull_E_vs_R',
408 'Pull (EKLM Scint vs BKLM RPC);pull', 200, -10, 10)
409 h['pull_smean'] = H1('h_pull_summary_mean',
410 'Pull Mean;Category;#mu', 4, 0.5, 4.5)
411 h['pull_swidth'] = H1('h_pull_summary_width',
412 'Pull Sigma;Category;#sigma', 4, 0.5, 4.5)
413 for ib, lab in enumerate(['BKLM Scint', 'EKLM Scint', 'RPC Phi', 'RPC Z'], 1):
414 h['pull_smean'].GetXaxis().SetBinLabel(ib, lab)
415 h['pull_swidth'].GetXaxis().SetBinLabel(ib, lab)
416
417 # Pairwise sector pull histograms (each in its own subdirectory)
418 h['ppw_B'] = self._book_pairwise(
419 f, 'pulls/sector/bklm_scint', 'h_pull_B_s{i}_vs_s{j}',
420 'BKLM Scint Pull: Sec {i} vs Sec {j};pull',
421 _N_BKLM_SECTORS, 200, -10, 10)
422 h['ppw_E'] = self._book_pairwise(
423 f, 'pulls/sector/eklm_scint_fwd_vs_bwd',
424 'h_pull_E_fwd{i}_vs_bwd{j}',
425 'EKLM Pull: Fwd Sec {i} vs Bwd Sec {j};pull',
426 _N_EKLM_SECTORS, 200, -10, 10, offset=1)
427 h['ppw_Rphi'] = self._book_pairwise(
428 f, 'pulls/sector/rpc_phi', 'h_pull_Rphi_s{i}_vs_s{j}',
429 'RPC Phi Pull: Sec {i} vs Sec {j};pull',
430 _N_BKLM_SECTORS, 200, -10, 10)
431
432 h['ppw_Rz'] = self._book_pairwise(
433 f, 'pulls/sector/rpc_z', 'h_pull_Rz_s{i}_vs_s{j}',
434 'RPC Z Pull: Sec {i} vs Sec {j};pull',
435 _N_BKLM_SECTORS, 200, -10, 10)
436
437 # 2D summary histograms live in pulls/sector/ (matching C++ layout)
438 self._safe_cd(f, 'pulls/sector')
439 h['ppw_2d_B_mean'] = self._book_2d_summary(
440 'h2_pull_bklm_scint_mean', 'Pull Mean (BKLM Scint);Sec 1;Sec 2',
441 _N_BKLM_SECTORS)
442 h['ppw_2d_B_sigma'] = self._book_2d_summary(
443 'h2_pull_bklm_scint_sigma', 'Pull Sigma (BKLM Scint);Sec 1;Sec 2',
444 _N_BKLM_SECTORS)
445 h['ppw_2d_E_mean'] = self._book_2d_summary(
446 'h2_pull_eklm_fwd_bwd_mean', 'Pull Mean (EKLM Fwd vs Bwd);Fwd Sec;Bwd Sec',
447 _N_EKLM_SECTORS, lo=0.5)
448 h['ppw_2d_E_sigma'] = self._book_2d_summary(
449 'h2_pull_eklm_fwd_bwd_sigma', 'Pull Sigma (EKLM Fwd vs Bwd);Fwd Sec;Bwd Sec',
450 _N_EKLM_SECTORS, lo=0.5)
451 h['ppw_2d_Rphi_mean'] = self._book_2d_summary(
452 'h2_pull_rpc_phi_mean', 'Pull Mean (RPC Phi);Sec 1;Sec 2', _N_BKLM_SECTORS)
453 h['ppw_2d_Rphi_sigma'] = self._book_2d_summary(
454 'h2_pull_rpc_phi_sigma', 'Pull Sigma (RPC Phi);Sec 1;Sec 2', _N_BKLM_SECTORS)
455 h['ppw_2d_Rz_mean'] = self._book_2d_summary(
456 'h2_pull_rpc_z_mean', 'Pull Mean (RPC Z);Sec 1;Sec 2', _N_BKLM_SECTORS)
457 h['ppw_2d_Rz_sigma'] = self._book_2d_summary(
458 'h2_pull_rpc_z_sigma', 'Pull Sigma (RPC Z);Sec 1;Sec 2', _N_BKLM_SECTORS)
459
460 # --- residuals/ ---
461 f.mkdir('residuals').cd()
462 h['res_B'] = H1('h_residual_bklm_scint',
463 'Residual (BKLM Scint);#DeltaT_{0} [ns]', 200, -50, 50)
464 h['res_E'] = H1('h_residual_eklm_scint',
465 'Residual (EKLM Scint);#DeltaT_{0} [ns]', 200, -50, 50)
466 h['res_Rphi'] = H1('h_residual_rpc_phi',
467 'Residual (BKLM RPC Phi);#DeltaT_{0} [ns]', 200, -50, 50)
468 h['res_Rz'] = H1('h_residual_rpc_z',
469 'Residual (BKLM RPC Z);#DeltaT_{0} [ns]', 200, -50, 50)
470 h['res_R'] = H1('h_residual_bklm_rpc',
471 'Residual (BKLM RPC combined);#DeltaT_{0} [ns]', 200, -50, 50)
472 h['res_BvE'] = H1('h_residual_B_vs_E',
473 'Residual BKLM Scint - EKLM Scint;#DeltaT_{0} [ns]', 200, -50, 50)
474 h['res_BvR'] = H1('h_residual_B_vs_R',
475 'Residual BKLM Scint - BKLM RPC;#DeltaT_{0} [ns]', 200, -50, 50)
476 h['res_EvR'] = H1('h_residual_E_vs_R',
477 'Residual EKLM Scint - BKLM RPC;#DeltaT_{0} [ns]', 200, -50, 50)
478 h['res_smean'] = H1('h_residual_summary_mean',
479 'Residual Mean;Category;#mu [ns]', 4, 0.5, 4.5)
480 h['res_swidth'] = H1('h_residual_summary_width',
481 'Residual Sigma;Category;#sigma [ns]', 4, 0.5, 4.5)
482 for ib, lab in enumerate(['BKLM Scint', 'EKLM Scint', 'RPC Phi', 'RPC Z'], 1):
483 h['res_smean'].GetXaxis().SetBinLabel(ib, lab)
484 h['res_swidth'].GetXaxis().SetBinLabel(ib, lab)
485
486 # Pairwise sector residual histograms (each in its own subdirectory)
487 h['rpw_B'] = self._book_pairwise(
488 f, 'residuals/sector/bklm_scint', 'h_residual_B_s{i}_vs_s{j}',
489 'BKLM Scint Residual: Sec {i} vs Sec {j};#DeltaT_{{0}} [ns]',
490 _N_BKLM_SECTORS, 200, -50, 50)
491 h['rpw_E'] = self._book_pairwise(
492 f, 'residuals/sector/eklm_scint_fwd_vs_bwd',
493 'h_residual_E_fwd{i}_vs_bwd{j}',
494 'EKLM Residual: Fwd Sec {i} vs Bwd Sec {j};#DeltaT_{{0}} [ns]',
495 _N_EKLM_SECTORS, 200, -50, 50, offset=1)
496 h['rpw_Rphi'] = self._book_pairwise(
497 f, 'residuals/sector/rpc_phi', 'h_residual_Rphi_s{i}_vs_s{j}',
498 'RPC Phi Residual: Sec {i} vs Sec {j};#DeltaT_{{0}} [ns]',
499 _N_BKLM_SECTORS, 200, -50, 50)
500 h['rpw_Rz'] = self._book_pairwise(
501 f, 'residuals/sector/rpc_z', 'h_residual_Rz_s{i}_vs_s{j}',
502 'RPC Z Residual: Sec {i} vs Sec {j};#DeltaT_{{0}} [ns]',
503 _N_BKLM_SECTORS, 200, -50, 50)
504
505 # 2D summary histograms live in residuals/sector/ (matching C++ layout)
506 self._safe_cd(f, 'residuals/sector')
507 h['rpw_2d_B_mean'] = self._book_2d_summary(
508 'h2_residual_bklm_scint_mean', 'Residual Mean (BKLM Scint);Sec 1;Sec 2',
509 _N_BKLM_SECTORS)
510 h['rpw_2d_B_sigma'] = self._book_2d_summary(
511 'h2_residual_bklm_scint_sigma', 'Residual Sigma (BKLM Scint);Sec 1;Sec 2',
512 _N_BKLM_SECTORS)
513 h['rpw_2d_E_mean'] = self._book_2d_summary(
514 'h2_residual_eklm_fwd_bwd_mean',
515 'Residual Mean (EKLM Fwd vs Bwd);Fwd Sec;Bwd Sec',
516 _N_EKLM_SECTORS, lo=0.5)
517 h['rpw_2d_E_sigma'] = self._book_2d_summary(
518 'h2_residual_eklm_fwd_bwd_sigma',
519 'Residual Sigma (EKLM Fwd vs Bwd);Fwd Sec;Bwd Sec',
520 _N_EKLM_SECTORS, lo=0.5)
521 h['rpw_2d_Rphi_mean'] = self._book_2d_summary(
522 'h2_residual_rpc_phi_mean', 'Residual Mean (RPC Phi);Sec 1;Sec 2',
523 _N_BKLM_SECTORS)
524 h['rpw_2d_Rphi_sigma'] = self._book_2d_summary(
525 'h2_residual_rpc_phi_sigma', 'Residual Sigma (RPC Phi);Sec 1;Sec 2',
526 _N_BKLM_SECTORS)
527 h['rpw_2d_Rz_mean'] = self._book_2d_summary(
528 'h2_residual_rpc_z_mean', 'Residual Mean (RPC Z);Sec 1;Sec 2',
529 _N_BKLM_SECTORS)
530 h['rpw_2d_Rz_sigma'] = self._book_2d_summary(
531 'h2_residual_rpc_z_sigma', 'Residual Sigma (RPC Z);Sec 1;Sec 2',
532 _N_BKLM_SECTORS)
533
534 # --- cross_detector/ 4x4 matrix ---
535 f.mkdir('cross_detector').cd()
536 _reg_names = ['EKLM_Bwd', 'EKLM_Fwd', 'BKLM_RPC', 'BKLM_Scint']
537 _reg_labels = ['EKLM Backward', 'EKLM Forward', 'BKLM RPC', 'BKLM Scint']
538 h['dt0'] = {}
539 for ri in range(_N_REGIONS):
540 h['dt0'][ri] = {}
541 for rj in range(_N_REGIONS):
542 h['dt0'][ri][rj] = H1(
543 f'h_deltaT0_{_reg_names[ri]}_vs_{_reg_names[rj]}',
544 f'#DeltaT_{{0}}: {_reg_labels[ri]} vs {_reg_labels[rj]};'
545 f'#DeltaT_{{0}} [ns]', 200, -50, 50)
546 h['dt0_2d_mean'] = TH2D('h2_deltaT0_mean',
547 '#DeltaT_{0} Mean by Region;Region 1;Region 2',
548 _N_REGIONS, -0.5, _N_REGIONS - 0.5,
549 _N_REGIONS, -0.5, _N_REGIONS - 0.5)
550 h['dt0_2d_sigma'] = TH2D('h2_deltaT0_sigma',
551 '#DeltaT_{0} Sigma by Region;Region 1;Region 2',
552 _N_REGIONS, -0.5, _N_REGIONS - 0.5,
553 _N_REGIONS, -0.5, _N_REGIONS - 0.5)
554 h['dt0_2d_entries'] = TH2D('h2_deltaT0_entries',
555 '#DeltaT_{0} Entries by Region;Region 1;Region 2',
556 _N_REGIONS, -0.5, _N_REGIONS - 0.5,
557 _N_REGIONS, -0.5, _N_REGIONS - 0.5)
558 for ri, lab in enumerate(_reg_labels, 1):
559 for hh in (h['dt0_2d_mean'], h['dt0_2d_sigma'], h['dt0_2d_entries']):
560 hh.GetXaxis().SetBinLabel(ri, lab)
561 hh.GetYaxis().SetBinLabel(ri, lab)
562
563 # --- final/ ---
564 f.mkdir('final').cd()
565 h['final_scint'] = H1('h_t0evt_final_scint_only',
566 'Final KLM T_{0} (Scint only);T_{0} [ns]', 800, -100, 100)
567 h['final_rpc'] = H1('h_t0evt_final_with_rpc',
568 'Final KLM T_{0} (Scint+RPC);T_{0} [ns]', 800, -100, 100)
569 h['final_rpcdir'] = H1('h_t0evt_final_with_rpc_dir',
570 'Final KLM T_{0} (Scint+RPC dir);T_{0} [ns]', 800, -100, 100)
571
572 # Prevent ROOT garbage-collecting objects not owned by directories
573 self._prevent_gc()
574 f.cd()
575

◆ _book_pairwise()

_book_pairwise ( self,
rootfile,
dirpath,
name_tmpl,
title_tmpl,
n,
nb,
lo,
hi,
offset = 0 )
protected
Book an n×n grid of TH1D inside *dirpath* and return as a dict-of-dict.

*offset* shifts sector indices in name/title (use 1 for EKLM sectors 1–4).

Definition at line 576 of file KLMEventT0Validation.py.

577 n, nb, lo, hi, offset=0):
578 """
579 Book an n×n grid of TH1D inside *dirpath* and return as a dict-of-dict.
580
581 *offset* shifts sector indices in name/title (use 1 for EKLM sectors 1–4).
582 """
583 # Navigate step-by-step, creating each sub-directory if needed.
584 # rootfile.mkdir(dirpath).cd() is unreliable in cppyy when
585 # intermediate directories already exist (mkdir may return None).
586 d = rootfile
587 for part in dirpath.split('/'):
588 sub = d.GetDirectory(part)
589 if not sub:
590 sub = d.mkdir(part)
591 d = sub
592 d.cd()
593 grid = {}
594 for i in range(n):
595 grid[i] = {}
596 for j in range(n):
597 si, sj = i + offset, j + offset
598 grid[i][j] = TH1D(
599 name_tmpl.format(i=si, j=sj),
600 title_tmpl.format(i=si, j=sj),
601 nb, lo, hi)
602 return grid
603

◆ _build_ext_maps()

_build_ext_maps ( self,
track )
protected
Build channel→ExtHit maps for *track*.

Returns
-------
scint_map : dict  channel_key → (entry_ExtHit, exit_ExtHit)
rpc_map   : dict  module_key  → (entry_ExtHit, exit_ExtHit)

where entry = minimum TOF, exit = maximum TOF among all ExtHits
with the same key.  This mirrors the C++ matchExt() logic.

Definition at line 645 of file KLMEventT0Validation.py.

645 def _build_ext_maps(self, track):
646 """
647 Build channel→ExtHit maps for *track*.
648
649 Returns
650 -------
651 scint_map : dict channel_key → (entry_ExtHit, exit_ExtHit)
652 rpc_map : dict module_key → (entry_ExtHit, exit_ExtHit)
653
654 where entry = minimum TOF, exit = maximum TOF among all ExtHits
655 with the same key. This mirrors the C++ matchExt() logic.
656 """
657 scint_raw = {} # key → list of ExtHit
658 rpc_raw = {}
659
660 _muids = track.getRelationsTo['Belle2::KLMMuidLikelihood']('KLMMuidLikelihoods')
661 muid = _muids[0] if _muids.size() > 0 else None
662 elem = self._elem_num
663
664 for ext in track.getRelationsTo['Belle2::ExtHit']('ExtHits'):
665 if ext.getStatus() != Belle2.EXT_EXIT:
666 continue
667
668 det = ext.getDetectorID()
669 is_bklm = (det == Belle2.Const.EDetector.BKLM)
670 is_eklm = (det == Belle2.Const.EDetector.EKLM)
671 if not is_bklm and not is_eklm:
672 continue
673
674 copy_id = ext.getCopyID()
675
676 if is_bklm:
677 # Decode BKLM channel number → element numbers
678 # copy_id is int but KLMChannelNumber is uint16_t;
679 # C++ narrows implicitly, Python/cppyy does not.
680 fwd = ctypes.c_int(0)
681 sec = ctypes.c_int(0)
682 lay = ctypes.c_int(0)
683 pla = ctypes.c_int(0)
684 strip = ctypes.c_int(0)
686 copy_id & 0xFFFF, fwd, sec, lay, pla, strip)
687 fwd, sec, lay, pla, strip = (fwd.value, sec.value,
688 lay.value, pla.value, strip.value)
689
690 # Check layer was crossed
691 if muid:
692 if not muid.isExtrapolatedBarrelLayerCrossed(lay - 1):
693 continue
694
695 is_rpc = (lay >= Belle2.BKLMElementNumbers.c_FirstRPCLayer)
696 if is_rpc:
697 key = elem.moduleNumber(
698 Belle2.KLMElementNumbers.c_BKLM, fwd, sec, lay)
699 rpc_raw.setdefault(key, []).append(ext)
700 else:
701 key = elem.channelNumber(
702 Belle2.KLMElementNumbers.c_BKLM, fwd, sec, lay, pla, strip)
703 scint_raw.setdefault(key, []).append(ext)
704
705 else: # is_eklm
706 # Decode EKLM strip number → element numbers
707 fwd = ctypes.c_int(0)
708 lay = ctypes.c_int(0)
709 sec = ctypes.c_int(0)
710 pla = ctypes.c_int(0)
711 strip = ctypes.c_int(0)
712 Belle2.EKLMElementNumbers.Instance().stripNumberToElementNumbers(
713 copy_id, fwd, lay, sec, pla, strip)
714 fwd, lay, sec, pla, strip = (fwd.value, lay.value,
715 sec.value, pla.value, strip.value)
716
717 if muid:
718 if not muid.isExtrapolatedEndcapLayerCrossed(lay - 1):
719 continue
720
721 key = elem.channelNumber(
722 Belle2.KLMElementNumbers.c_EKLM, fwd, sec, lay, pla, strip)
723 scint_raw.setdefault(key, []).append(ext)
724
725 # Reduce each list to (entry, exit) pair by TOF
726 def _entry_exit(exts):
727 en = min(exts, key=lambda e: e.getTOF())
728 ex = max(exts, key=lambda e: e.getTOF())
729 return en, ex
730
731 scint_map = {k: _entry_exit(v) for k, v in scint_raw.items()}
732 rpc_map = {k: _entry_exit(v) for k, v in rpc_raw.items()}
733 return scint_map, rpc_map
734
static void channelNumberToElementNumbers(KLMChannelNumber channel, int *section, int *sector, int *layer, int *plane, int *strip)
Get element numbers by channel number.
static const EKLMElementNumbers & Instance()
Instantiation.

◆ _combine_track_categories()

_combine_track_categories ( self,
* categories )
protected
Combine multiple per-track category lists into a single list,
merging by charge (one entry per charge sign, weighted mean of T0).

Definition at line 1347 of file KLMEventT0Validation.py.

1347 def _combine_track_categories(self, *categories):
1348 """
1349 Combine multiple per-track category lists into a single list,
1350 merging by charge (one entry per charge sign, weighted mean of T0).
1351 """
1352 by_charge = {}
1353 for cat in categories:
1354 for t in cat:
1355 q = t['charge']
1356 if q not in by_charge:
1357 by_charge[q] = []
1358 by_charge[q].append((t['mu'], t['sem']))
1359
1360 result = []
1361 for q, pairs in by_charge.items():
1362 mu, se = _weighted_mean(pairs)
1363 if mu is not None and math.isfinite(mu):
1364 result.append({'mu': mu, 'sem': se or 0.0, 'charge': q,
1365 'sector': -1, 'section': -1})
1366 return result
1367

◆ _fill_pairs()

_fill_pairs ( self,
trk_list,
h_pull,
h_res,
ppw_pull,
ppw_res,
bklm )
protected
Fill same-detector pair histograms for one category.

For BKLM (bklm=True): sector key = sector (0-indexed for 8 sectors).
For EKLM (bklm=False): sector key = sector-1 (0-indexed for 4 sectors).

Definition at line 1288 of file KLMEventT0Validation.py.

1288 def _fill_pairs(self, trk_list, h_pull, h_res, ppw_pull, ppw_res, bklm):
1289 """
1290 Fill same-detector pair histograms for one category.
1291
1292 For BKLM (bklm=True): sector key = sector (0-indexed for 8 sectors).
1293 For EKLM (bklm=False): sector key = sector-1 (0-indexed for 4 sectors).
1294 """
1295 n = len(trk_list)
1296 for i in range(n):
1297 ti = trk_list[i]
1298 if not math.isfinite(ti['mu']):
1299 continue
1300 for j in range(i + 1, n):
1301 tj = trk_list[j]
1302 if not math.isfinite(tj['mu']):
1303 continue
1304 if self._opp_charges_only and ti['charge'] * tj['charge'] >= 0:
1305 continue
1306
1307 dt = ti['mu'] - tj['mu']
1308 if not math.isfinite(dt):
1309 continue
1310
1311 if h_res:
1312 h_res.Fill(dt)
1313
1314 si = ti['sem']
1315 sj = tj['sem']
1316 denom2 = si * si + sj * sj if (si and sj) else 0.0
1317 if denom2 > 0.0:
1318 pull = dt / math.sqrt(denom2)
1319 if h_pull and math.isfinite(pull):
1320 h_pull.Fill(pull)
1321
1322 # Pairwise sector histograms
1323 if ppw_pull or ppw_res:
1324 if bklm:
1325 # BKLM sectors 1-8 → 0-indexed key
1326 ki = ti['sector'] - 1
1327 kj = tj['sector'] - 1
1328 n_max = _N_BKLM_SECTORS
1329 else:
1330 # EKLM sectors 1-4 → 0-indexed key
1331 # Pairwise is Fwd (section=2) vs Bwd (section=1)
1332 # Use section to assign which side each track is on
1333 ki = ti['sector'] - 1
1334 kj = tj['sector'] - 1
1335 n_max = _N_EKLM_SECTORS
1336
1337 if 0 <= ki < n_max and 0 <= kj < n_max:
1338 if ppw_pull:
1339 pg = ppw_pull.get(ki, {}).get(kj)
1340 if pg and math.isfinite(pull):
1341 pg.Fill(pull)
1342 if ppw_res:
1343 rg = ppw_res.get(ki, {}).get(kj)
1344 if rg and math.isfinite(dt):
1345 rg.Fill(dt)
1346

◆ _fit_summary_histograms()

_fit_summary_histograms ( self)
protected
Fit Gaussian to pull/residual/dimuon histograms and fill
summary histograms (mean, sigma per category; 2D pairwise summaries;
per-track T0 resolution from dimuon ΔT0).

Definition at line 1372 of file KLMEventT0Validation.py.

1372 def _fit_summary_histograms(self):
1373 """
1374 Fit Gaussian to pull/residual/dimuon histograms and fill
1375 summary histograms (mean, sigma per category; 2D pairwise summaries;
1376 per-track T0 resolution from dimuon ΔT0).
1377 """
1378 h = self._h
1379
1380 # Pull summary (4 categories: B, E, Rphi, Rz)
1381 for ib, (key, label) in enumerate([('pull_B', 'BKLM Scint'),
1382 ('pull_E', 'EKLM Scint'),
1383 ('pull_Rphi', 'RPC Phi'),
1384 ('pull_Rz', 'RPC Z')], 1):
1385 mu, muE, sig, sigE = _gauss_fit(h.get(key), -5.0, 5.0)
1386 if mu is not None:
1387 h['pull_smean'].SetBinContent(ib, mu)
1388 h['pull_smean'].SetBinError(ib, muE)
1389 h['pull_swidth'].SetBinContent(ib, sig)
1390 h['pull_swidth'].SetBinError(ib, sigE)
1391 basf2.B2INFO(f'KLMEventT0Validation: {label} pull fit:'
1392 f' mean={mu:.3f}±{muE:.3f} sigma={sig:.3f}±{sigE:.3f}')
1393
1394 # Residual summary (4 categories)
1395 for ib, (key, label) in enumerate([('res_B', 'BKLM Scint'),
1396 ('res_E', 'EKLM Scint'),
1397 ('res_Rphi', 'RPC Phi'),
1398 ('res_Rz', 'RPC Z')], 1):
1399 mu, muE, sig, sigE = _gauss_fit(h.get(key), -25.0, 25.0)
1400 if mu is not None:
1401 h['res_smean'].SetBinContent(ib, mu)
1402 h['res_smean'].SetBinError(ib, muE)
1403 h['res_swidth'].SetBinContent(ib, sig)
1404 h['res_swidth'].SetBinError(ib, sigE)
1405
1406 # Pairwise sector pull summaries
1407 _pairwise_cfgs = [
1408 ('ppw_B', 'ppw_2d_B_mean', 'ppw_2d_B_sigma', _N_BKLM_SECTORS),
1409 ('ppw_E', 'ppw_2d_E_mean', 'ppw_2d_E_sigma', _N_EKLM_SECTORS),
1410 ('ppw_Rphi', 'ppw_2d_Rphi_mean', 'ppw_2d_Rphi_sigma', _N_BKLM_SECTORS),
1411 ('ppw_Rz', 'ppw_2d_Rz_mean', 'ppw_2d_Rz_sigma', _N_BKLM_SECTORS),
1412 ]
1413 for pw_key, mean_key, sig_key, n in _pairwise_cfgs:
1414 pw = h.get(pw_key, {})
1415 h2m = h.get(mean_key)
1416 h2s = h.get(sig_key)
1417 for i in range(n):
1418 for j in range(n):
1419 mu, _, sig, _ = _gauss_fit(pw.get(i, {}).get(j), -5.0, 5.0)
1420 if mu is not None:
1421 if h2m:
1422 h2m.SetBinContent(i + 1, j + 1, mu)
1423 if h2s:
1424 h2s.SetBinContent(i + 1, j + 1, sig)
1425
1426 # Pairwise sector residual summaries
1427 _rpairwise_cfgs = [
1428 ('rpw_B', 'rpw_2d_B_mean', 'rpw_2d_B_sigma', _N_BKLM_SECTORS),
1429 ('rpw_E', 'rpw_2d_E_mean', 'rpw_2d_E_sigma', _N_EKLM_SECTORS),
1430 ('rpw_Rphi', 'rpw_2d_Rphi_mean', 'rpw_2d_Rphi_sigma', _N_BKLM_SECTORS),
1431 ('rpw_Rz', 'rpw_2d_Rz_mean', 'rpw_2d_Rz_sigma', _N_BKLM_SECTORS),
1432 ]
1433 for pw_key, mean_key, sig_key, n in _rpairwise_cfgs:
1434 pw = h.get(pw_key, {})
1435 h2m = h.get(mean_key)
1436 h2s = h.get(sig_key)
1437 for i in range(n):
1438 for j in range(n):
1439 mu, _, sig, _ = _gauss_fit(pw.get(i, {}).get(j), -25.0, 25.0)
1440 if mu is not None:
1441 if h2m:
1442 h2m.SetBinContent(i + 1, j + 1, mu)
1443 if h2s:
1444 h2s.SetBinContent(i + 1, j + 1, sig)
1445
1446 # Cross-detector ΔT0 2D summary
1447 dt0_h = h.get('dt0', {})
1448 h2m = h.get('dt0_2d_mean')
1449 h2s = h.get('dt0_2d_sigma')
1450 h2e = h.get('dt0_2d_entries')
1451 for ri in range(_N_REGIONS):
1452 for rj in range(_N_REGIONS):
1453 hh = dt0_h.get(ri, {}).get(rj)
1454 if hh is None:
1455 continue
1456 if h2e:
1457 h2e.SetBinContent(ri + 1, rj + 1, hh.GetEntries())
1458 mu, _, sig, _ = _gauss_fit(hh, -25.0, 25.0)
1459 if mu is not None:
1460 if h2m:
1461 h2m.SetBinContent(ri + 1, rj + 1, mu)
1462 if h2s:
1463 h2s.SetBinContent(ri + 1, rj + 1, sig)
1464
1465 # Per-track T0 resolution from dimuon ΔT0: σ_track = σ(ΔT0)/√2
1466 # 6 categories matching resolution histogram bin labels
1467 h_res = h.get('resolution')
1468 for ib, dm_key in enumerate(
1469 ['dimuon_B', 'dimuon_R', 'dimuon_E',
1470 'dimuon_scint', 'dimuon_rpc', 'dimuon_rpcdir'], 1):
1471 _, _, sig, sigE = _gauss_fit(h.get(dm_key), -40.0, 40.0)
1472 if sig is not None and h_res:
1473 per_track = sig / math.sqrt(2.0)
1474 per_track_err = sigE / math.sqrt(2.0)
1475 h_res.SetBinContent(ib, per_track)
1476 h_res.SetBinError(ib, per_track_err)

◆ _midpoint()

_midpoint ( a,
b )
staticprotected
Return the midpoint of two XYZVector-like objects.

Definition at line 635 of file KLMEventT0Validation.py.

635 def _midpoint(a, b):
636 """Return the midpoint of two XYZVector-like objects."""
637 return ROOT.Math.XYZVector(0.5 * (a.X() + b.X()),
638 0.5 * (a.Y() + b.Y()),
639 0.5 * (a.Z() + b.Z()))
640

◆ _prevent_gc()

_prevent_gc ( self)
protected
Call ROOT.SetOwnership(False) on all booked histograms.

Definition at line 620 of file KLMEventT0Validation.py.

620 def _prevent_gc(self):
621 """Call ROOT.SetOwnership(False) on all booked histograms."""
622 def _traverse(obj):
623 if isinstance(obj, dict):
624 for v in obj.values():
625 _traverse(v)
626 elif hasattr(obj, 'IsA'): # it's a ROOT object
627 ROOT.SetOwnership(obj, False)
628 for v in self._h.values():
629 _traverse(v)
630

◆ _safe_cd()

_safe_cd ( rootfile,
dirpath )
staticprotected
Navigate *rootfile* to *dirpath*, creating sub-dirs as needed.

Definition at line 605 of file KLMEventT0Validation.py.

605 def _safe_cd(rootfile, dirpath):
606 """Navigate *rootfile* to *dirpath*, creating sub-dirs as needed."""
607 d = rootfile
608 for part in dirpath.split('/'):
609 sub = d.GetDirectory(part)
610 if not sub:
611 sub = d.mkdir(part)
612 d = sub
613 d.cd()
614

◆ beginRun()

beginRun ( self)
Cache run-dependent DB constants from KLMTimeConstants payload.

Definition at line 269 of file KLMEventT0Validation.py.

269 def beginRun(self):
270 """Cache run-dependent DB constants from KLMTimeConstants payload."""
271 tc = Belle2.PyDBObj('KLMTimeConstants', Belle2.KLMTimeConstants.Class())
272 if tc.isValid():
273 obj = tc.obj()
274 self._delay_eklm = obj.getDelay(Belle2.KLMTimeConstants.c_EKLM)
275 self._delay_bklm = obj.getDelay(Belle2.KLMTimeConstants.c_BKLM)
276 self._delay_rpc_phi = obj.getDelay(Belle2.KLMTimeConstants.c_RPCPhi)
277 self._delay_rpc_z = obj.getDelay(Belle2.KLMTimeConstants.c_RPCZ)
278 else:
279 basf2.B2WARNING('KLMEventT0Validation: KLMTimeConstants not available; '
280 'propagation delays set to zero.')
281 self._delay_eklm = self._delay_bklm = 0.0
282 self._delay_rpc_phi = self._delay_rpc_z = 0.0
283
Class to access a DBObjPtr from Python.
Definition PyDBObj.h:50

◆ event()

event ( self)
Read muon tracks, compute per-track T0, fill all histograms.

Definition at line 1013 of file KLMEventT0Validation.py.

1013 def event(self):
1014 """Read muon tracks, compute per-track T0, fill all histograms."""
1015 h = self._h
1016
1017 # Sample type: check for MCInitialParticles
1018 mc_ip = Belle2.PyStoreObj('MCInitialParticles')
1019 is_mc = mc_ip.isValid()
1020 if h.get('sample_type'):
1021 h['sample_type'].Fill(2 if is_mc else 1)
1022
1023 muon_list = Belle2.PyStoreObj(self._muon_list_name)
1024 if not muon_list.isValid():
1025 return
1026 n_particles = muon_list.obj().getListSize()
1027 if n_particles == 0:
1028 return
1029
1030 # Per-track results keyed by category:
1031 # Each entry is a dict:
1032 # mu, sem, charge, n_hits, dom_sector, dom_section
1033 trk_B, trk_E, trk_R, trk_Rphi, trk_Rz = [], [], [], [], []
1034
1035 for i in range(n_particles):
1036 particle = muon_list.obj().getParticle(i)
1037 if not particle:
1038 continue
1039 track = particle.getTrack()
1040 if not track:
1041 continue
1042 charge = int(particle.getCharge())
1043
1044 # Build ExtHit maps for this track
1045 scint_map, rpc_map = self._build_ext_maps(track)
1046
1047 # Accumulate per-subdetector
1048 hits_E = self._accumulate_eklm(track, scint_map)
1049 hits_B = self._accumulate_bklm_scint(track, scint_map)
1050 hits_Rphi = self._accumulate_bklm_rpc(track, rpc_map, accept_phi=True)
1051 hits_Rz = self._accumulate_bklm_rpc(track, rpc_map, accept_phi=False)
1052
1053 # Fill timing-component diagnostics
1054 for det_key, hits in (('E', hits_E), ('B', hits_B),
1055 ('R', hits_Rphi + hits_Rz)):
1056 for hd in hits:
1057 for comp in ('Trec', 'Tcable', 'Tprop', 'Tfly'):
1058 hh = h.get(f'{comp}_{det_key}')
1059 if hh:
1060 hh.Fill(hd[comp])
1061
1062 # Per-track T0 from each category
1063 def _trk_result(hits, h_nhits, h_sem):
1064 """Compute (mu, sem, dom_sector, dom_section) for a hit list."""
1065 if not hits:
1066 return None
1067 vals = [hd['t0_est'] for hd in hits]
1068 mu, sem = _mu_sem(vals)
1069 if mu is None or not math.isfinite(mu):
1070 return None
1071 n = len(hits)
1072 dom_sec = int(round(sum(hd['sector'] for hd in hits) / n))
1073 dom_section = int(round(sum(hd['section'] for hd in hits) / n))
1074 if h_nhits:
1075 h_nhits.Fill(n)
1076 if h_sem and sem is not None:
1077 h_sem.Fill(sem)
1078 return mu, (sem if sem is not None else 0.0), dom_sec, dom_section
1079
1080 res_B = _trk_result(hits_B, h.get('nhits_B'), h.get('sem_B'))
1081 res_E = _trk_result(hits_E, h.get('nhits_E'), h.get('sem_E'))
1082 res_Rphi = _trk_result(hits_Rphi, None, None)
1083 res_Rz = _trk_result(hits_Rz, None, None)
1084
1085 # Combined RPC (phi + z)
1086 hits_R = hits_Rphi + hits_Rz
1087 res_R = _trk_result(hits_R, None, None)
1088
1089 if res_B:
1090 mu, se, sec, section = res_B
1091 if h.get('t0trk_B'):
1092 h['t0trk_B'].Fill(mu)
1093 trk_B.append({'mu': mu, 'sem': se, 'charge': charge,
1094 'sector': sec, 'section': section})
1095
1096 if res_E:
1097 mu, se, sec, section = res_E
1098 if h.get('t0trk_E'):
1099 h['t0trk_E'].Fill(mu)
1100 trk_E.append({'mu': mu, 'sem': se, 'charge': charge,
1101 'sector': sec, 'section': section})
1102
1103 if res_R:
1104 mu, se, sec, section = res_R
1105 if h.get('t0trk_R'):
1106 h['t0trk_R'].Fill(mu)
1107 trk_R.append({'mu': mu, 'sem': se, 'charge': charge,
1108 'sector': sec, 'section': section})
1109
1110 if res_Rphi:
1111 mu, se, sec, section = res_Rphi
1112 trk_Rphi.append({'mu': mu, 'sem': se, 'charge': charge,
1113 'sector': sec, 'section': section})
1114
1115 if res_Rz:
1116 mu, se, sec, section = res_Rz
1117 trk_Rz.append({'mu': mu, 'sem': se, 'charge': charge,
1118 'sector': sec, 'section': section})
1119
1120 if not any([trk_B, trk_E, trk_R]):
1121 return
1122
1123 # --- Per-event T0 (track average per category) ---
1124 def _evt_t0(trk_list):
1125 pairs = [(t['mu'], t['sem']) for t in trk_list]
1126 return _weighted_mean(pairs)
1127
1128 muB, seB = _evt_t0(trk_B)
1129 muE, seE = _evt_t0(trk_E)
1130 muR, seR = _evt_t0(trk_R)
1131 muRphi, seRphi = _evt_t0(trk_Rphi)
1132 muRz, seRz = _evt_t0(trk_Rz)
1133
1134 def _safe_fill(key, val):
1135 hh = h.get(key)
1136 if hh and val is not None and math.isfinite(val):
1137 hh.Fill(val)
1138
1139 _safe_fill('t0evt_B', muB)
1140 _safe_fill('t0evt_E', muE)
1141 _safe_fill('t0evt_R', muR)
1142
1143 # Overall event T0 from all categories
1144 all_parts = [(mu, se) for mu, se in ((muB, seB), (muE, seE), (muR, seR))
1145 if mu is not None and math.isfinite(mu)]
1146 muAll, _ = _weighted_mean(all_parts)
1147 _safe_fill('t0evt_all', muAll)
1148
1149 # Final source audit
1150 useB = muB is not None and math.isfinite(muB)
1151 useE = muE is not None and math.isfinite(muE)
1152 useR = muR is not None and math.isfinite(muR)
1153 if any((useB, useE, useR)):
1154 src = (useB, useE, useR)
1155 src_map = {
1156 (True, False, False): 1,
1157 (False, True, False): 2,
1158 (False, False, True): 3,
1159 (True, True, False): 4,
1160 (True, False, True): 5,
1161 (False, True, True): 6,
1162 (True, True, True): 7,
1163 }
1164 sb = src_map.get(src)
1165 if sb and h.get('final_source'):
1166 h['final_source'].Fill(sb)
1167
1168 # --- Final T0 combinations ---
1169 def _combine(parts):
1170 """Inverse-variance combine non-None (mu, se) pairs."""
1171 valid = [(mu, se) for mu, se in parts
1172 if mu is not None and math.isfinite(mu)]
1173 return _weighted_mean(valid)
1174
1175 t0_scint, _ = _combine([(muB, seB), (muE, seE)])
1176 t0_rpc, _ = _combine([(muB, seB), (muE, seE), (muR, seR)])
1177 t0_rpcdir, _ = _combine([(muB, seB), (muE, seE),
1178 (muRphi, seRphi), (muRz, seRz)])
1179 _safe_fill('final_scint', t0_scint)
1180 _safe_fill('final_rpc', t0_rpc)
1181 _safe_fill('final_rpcdir', t0_rpcdir)
1182
1183 # --- Same-detector pair histograms (pulls and residuals) ---
1184 self._fill_pairs(trk_B, h.get('pull_B'), h.get('res_B'),
1185 h.get('ppw_B'), h.get('rpw_B'), bklm=True)
1186 self._fill_pairs(trk_E, h.get('pull_E'), h.get('res_E'),
1187 h.get('ppw_E'), h.get('rpw_E'), bklm=False)
1188 self._fill_pairs(trk_Rphi, h.get('pull_Rphi'), h.get('res_Rphi'),
1189 h.get('ppw_Rphi'), h.get('rpw_Rphi'), bklm=True)
1190 self._fill_pairs(trk_Rz, h.get('pull_Rz'), h.get('res_Rz'),
1191 h.get('ppw_Rz'), h.get('rpw_Rz'), bklm=True)
1192 self._fill_pairs(trk_R, h.get('pull_R'), h.get('res_R'),
1193 None, None, bklm=True)
1194
1195 # --- Cross-detector pulls (event-level) ---
1196 def _cross_pull(muX, seX, muY, seY, h_pull, h_res):
1197 if muX is None or muY is None:
1198 return
1199 if not (math.isfinite(muX) and math.isfinite(muY)):
1200 return
1201 dt = muX - muY
1202 if h_res and math.isfinite(dt):
1203 h_res.Fill(dt)
1204 if (seX is not None and seY is not None and
1205 seX > 0 and seY > 0):
1206 denom2 = seX * seX + seY * seY
1207 pull = dt / math.sqrt(denom2)
1208 if h_pull and math.isfinite(pull):
1209 h_pull.Fill(pull)
1210
1211 _cross_pull(muB, seB, muE, seE, h.get('pull_BvE'), h.get('res_BvE'))
1212 _cross_pull(muB, seB, muR, seR, h.get('pull_BvR'), h.get('res_BvR'))
1213 _cross_pull(muE, seE, muR, seR, h.get('pull_EvR'), h.get('res_EvR'))
1214
1215 # --- Cross-detector 4x4 ΔT0 matrix ---
1216 # Map detector regions to per-event T0 values
1217 # Regions: 0=EKLM Bwd, 1=EKLM Fwd, 2=BKLM RPC, 3=BKLM Scint
1218 def _eklm_region_t0(trk_list, target_section):
1219 """Per-event T0 from tracks in a specific EKLM section."""
1220 filtered = [t for t in trk_list if t['section'] == target_section]
1221 if not filtered:
1222 return None, None
1223 return _weighted_mean([(t['mu'], t['sem']) for t in filtered])
1224
1225 reg_t0 = {
1226 _REG_EKLM_BWD: _eklm_region_t0(trk_E, _EKLM_SECTION_BWD),
1227 _REG_EKLM_FWD: _eklm_region_t0(trk_E, _EKLM_SECTION_FWD),
1228 _REG_BKLM_RPC: (muR, seR),
1229 _REG_BKLM_SCINT: (muB, seB),
1230 }
1231 dt0_h = h.get('dt0', {})
1232 for ri in range(_N_REGIONS):
1233 mu_i, se_i = reg_t0[ri]
1234 if mu_i is None or not math.isfinite(mu_i):
1235 continue
1236 for rj in range(_N_REGIONS):
1237 mu_j, se_j = reg_t0[rj]
1238 if mu_j is None or not math.isfinite(mu_j):
1239 continue
1240 dt = mu_i - mu_j
1241 hh = dt0_h.get(ri, {}).get(rj)
1242 if hh and math.isfinite(dt):
1243 hh.Fill(dt)
1244
1245 # --- Dimuon ΔT0 ---
1246 def _dimuon_fill(trk_list, h_hist):
1247 if not h_hist:
1248 return
1249 t0p = t0m = None
1250 for t in trk_list:
1251 if t['charge'] > 0:
1252 t0p = t['mu']
1253 elif t['charge'] < 0:
1254 t0m = t['mu']
1255 if (t0p is not None and t0m is not None and
1256 math.isfinite(t0p) and math.isfinite(t0m)):
1257 h_hist.Fill(t0p - t0m)
1258
1259 _dimuon_fill(trk_B, h.get('dimuon_B'))
1260 _dimuon_fill(trk_E, h.get('dimuon_E'))
1261 _dimuon_fill(trk_R, h.get('dimuon_R'))
1262
1263 # All KLM combined (use first available T0 per charge sign: B > E > R)
1264 trk_all_by_charge = {}
1265 for lst in (trk_B, trk_E, trk_R):
1266 for t in lst:
1267 q = t['charge']
1268 if q not in trk_all_by_charge:
1269 trk_all_by_charge[q] = t
1270 _dimuon_fill(list(trk_all_by_charge.values()), h.get('dimuon_all'))
1271
1272 # Scint-only combined per track: B+E hits combined
1273 trk_scint = self._combine_track_categories(trk_B, trk_E)
1274 _dimuon_fill(trk_scint, h.get('dimuon_scint'))
1275
1276 # With-RPC combined per track: B+E+R hits combined (RPC as single category)
1277 trk_all_cats = self._combine_track_categories(trk_B, trk_E, trk_R)
1278 _dimuon_fill(trk_all_cats, h.get('dimuon_rpc'))
1279
1280 # With-RPC-dir combined per track: B+E+Rphi+Rz as separate categories
1281 trk_rpcdir_cats = self._combine_track_categories(trk_B, trk_E, trk_Rphi, trk_Rz)
1282 _dimuon_fill(trk_rpcdir_cats, h.get('dimuon_rpcdir'))
1283
a (simplified) python wrapper for StoreObjPtr.
Definition PyStoreObj.h:67

◆ initialize()

initialize ( self)
Open output file, book histograms, and initialise geometry.

Definition at line 252 of file KLMEventT0Validation.py.

252 def initialize(self):
253 """Open output file, book histograms, and initialise geometry."""
254 self._outfile = TFile(self._output_file_path, 'RECREATE')
255 self._book_histograms()
256
257 self._geo_bklm = Belle2.bklm.GeometryPar.instance()
258 self._geo_eklm = Belle2.EKLM.GeometryData.Instance()
259 self._transform_eklm = Belle2.EKLM.TransformData(
260 True, Belle2.EKLM.TransformData.c_None)
261 self._elem_num = Belle2.KLMElementNumbers.Instance()
262
263 # Register DB dependencies
264 self._db_cable_delay = Belle2.PyDBObj(
265 'KLMTimeCableDelay', Belle2.KLMTimeCableDelay.Class())
266 self._db_channel_status = Belle2.PyDBObj(
267 'KLMChannelStatus', Belle2.KLMChannelStatus.Class())
268
static const GeometryData & Instance(enum DataSource dataSource=c_Database, const GearDir *gearDir=nullptr)
Instantiation.
Transformation data.
static const KLMElementNumbers & Instance()
Instantiation.
static GeometryPar * instance(void)
Static method to get a reference to the singleton GeometryPar instance.

◆ terminate()

terminate ( self)
Fit summary histograms, write all histograms, and close the file.

Definition at line 284 of file KLMEventT0Validation.py.

284 def terminate(self):
285 """Fit summary histograms, write all histograms, and close the file."""
286 self._fit_summary_histograms()
287 if self._outfile:
288 self._outfile.Write('', ROOT.TObject.kOverwrite)
289 self._outfile.Close()
290 basf2.B2INFO(f'KLMEventT0ValidationModule: wrote {self._output_file_path}')
291

Member Data Documentation

◆ _adc_bklm_max

_adc_bklm_max = adc_cut_bklm_scint_max
protected

Upper ADC cut for BKLM scintillator digits.

Definition at line 215 of file KLMEventT0Validation.py.

◆ _adc_bklm_min

_adc_bklm_min = adc_cut_bklm_scint_min
protected

Lower ADC cut for BKLM scintillator digits.

Definition at line 213 of file KLMEventT0Validation.py.

◆ _adc_eklm_max

_adc_eklm_max = adc_cut_eklm_scint_max
protected

Upper ADC cut for EKLM scintillator digits.

Definition at line 219 of file KLMEventT0Validation.py.

◆ _adc_eklm_min

_adc_eklm_min = adc_cut_eklm_scint_min
protected

Lower ADC cut for EKLM scintillator digits.

Definition at line 217 of file KLMEventT0Validation.py.

◆ _db_cable_delay

_db_cable_delay = None
protected

Cable-delay PyDBObj used to query per-channel offsets, refreshed in beginRun().

Definition at line 244 of file KLMEventT0Validation.py.

◆ _db_channel_status

_db_channel_status = None
protected

Channel-status PyDBObj used to skip bad channels, refreshed in beginRun().

Definition at line 246 of file KLMEventT0Validation.py.

◆ _delay_bklm

float _delay_bklm = 0.0
protected

Propagation-delay constant for BKLM scintillator strips (ns/cm), refreshed in beginRun().

Definition at line 238 of file KLMEventT0Validation.py.

◆ _delay_eklm

float _delay_eklm = 0.0
protected

Propagation-delay constant for EKLM strips (ns/cm), refreshed in beginRun().

Definition at line 236 of file KLMEventT0Validation.py.

◆ _delay_rpc_phi

float _delay_rpc_phi = 0.0
protected

Propagation-delay constant for BKLM RPC phi strips (ns/cm), refreshed in beginRun().

Definition at line 240 of file KLMEventT0Validation.py.

◆ _delay_rpc_z

float _delay_rpc_z = 0.0
protected

Propagation-delay constant for BKLM RPC z strips (ns/cm), refreshed in beginRun().

Definition at line 242 of file KLMEventT0Validation.py.

◆ _elem_num

_elem_num = None
protected

EKLM element-number helper (initialised in initialize()).

Definition at line 233 of file KLMEventT0Validation.py.

◆ _geo_bklm

_geo_bklm = None
protected

BKLM geometry parameters (initialised in initialize()).

Definition at line 227 of file KLMEventT0Validation.py.

◆ _geo_eklm

_geo_eklm = None
protected

EKLM geometry data (initialised in initialize()).

Definition at line 229 of file KLMEventT0Validation.py.

◆ _h

dict _h = {}
protected

All booked histograms, keyed by short name.

Definition at line 224 of file KLMEventT0Validation.py.

◆ _muon_list_name

_muon_list_name = muon_list_name
protected

Muon ParticleList name.

Definition at line 207 of file KLMEventT0Validation.py.

◆ _opp_charges_only

_opp_charges_only = opposite_charges_only
protected

Only use opposite-charge track pairs for pull/residual histograms.

Definition at line 211 of file KLMEventT0Validation.py.

◆ _outfile

_outfile = None
protected

Output TFile.

Definition at line 222 of file KLMEventT0Validation.py.

◆ _output_file_path

_output_file_path = output_file
protected

Output ROOT file path.

Definition at line 209 of file KLMEventT0Validation.py.

◆ _transform_eklm

_transform_eklm = None
protected

EKLM coordinate transform helper (initialised in initialize()).

Definition at line 231 of file KLMEventT0Validation.py.


The documentation for this class was generated from the following file: