Belle II Software development
run_channelT0_laser_calibration.py
1#!/usr/bin/env python3
2
3
10
11# ---------------------------------------------------------------------------------------
12# CAF calibration script: channel T0 with laser
13# data type: local runs with laser
14#
15# usage: basf2 run_channelT0_laser_calibration.py EXP RUN_LOW RUN_HIGH -- [options]
16# ---------------------------------------------------------------------------------------
17
18import os
19import sys
20import glob
21import argparse
22import yaml
23import basf2
24from caf import backends
25from caf.framework import Calibration, CAF
26from caf.strategies import SequentialRunByRun, SingleIOV, SimpleRunByRun
27from ROOT.Belle2 import TOP
28from basf2 import B2ERROR, B2INFO, B2WARNING
29
30# ------------------------ CLI ------------------------
31
32
33def parse_args():
34 """ Parse command-line arguments."""
35
36 p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
37 # Positional arguments
38 p.add_argument('exp', type=int, help='Experiment number (int)')
39 p.add_argument('run_low', type=int, help='First run (inclusive)')
40 p.add_argument('run_high', type=int, help='Last run (inclusive)')
41
42 # Optional arguments
43 p.add_argument('--source-dir', default='/ghi/fs01/belle2/bdata/group/detector/TOP/current_data_sroot_global',
44 help='Directory containing top.<exp>.<run>.*root files')
45 p.add_argument('--localDBs', type=str,
46 default='/group/belle2/group/detector/TOP/calibration/TBC/official/TBC_2025c_Nov25/localDB/localDB.txt',
47 help='Whitespace-separated list of localDB files (priority > global tags)')
48 p.add_argument('--global-tags', type=str, default='online',
49 help='Comma-separated central DB global tags, highest priority first')
50 p.add_argument('--main-output-dir', default='./top_calibration',
51 help='Base output directory')
52 p.add_argument('--ref-channel', default=0, type=int, help='Reference channel (pixel)')
53 p.add_argument('--ref-slot', default=9, type=int, help='Reference slot')
54 p.add_argument('--fit-mode', choices=['calibration', 'monitoring', 'mc'],
55 default='calibration', help='Fitter mode')
56 p.add_argument('--strategy', choices=['SingleIOV', 'SequentialRunByRun', 'SimpleRunByRun'],
57 default='SingleIOV', help='CAF merge strategy')
58 p.add_argument('--lookback', type=int, default=28, help='lookBackWindows for RawDigitConverter (0 uses DB default)')
59
60 # Time-base calibration toggle (enabled by default)
61 tb = p.add_mutually_exclusive_group()
62 tb.add_argument('--tbc', dest='use_tbc', action='store_true', help='Enable TBC in RawDigitConverter')
63 tb.add_argument('--no-tbc', dest='use_tbc', action='store_false', help='Disable TBC in RawDigitConverter')
64 p.set_defaults(use_tbc=True)
65
66 # Additional fitter options: amplitude bins, crosstalk detection
67 p.add_argument('--amplitude-bins', action='store_true', help='Fit in amplitude bins')
68 p.add_argument('--detect-crosstalk', action='store_true', help='Enable crosstalk detection')
69
70 # Input data type (sroot/root)
71 srt = p.add_mutually_exclusive_group()
72 srt.add_argument('--sroot', action='store_true', help='Force SeqRootInput')
73 srt.add_argument('--root', dest='sroot', action='store_false', help='Force RootInput (default: auto)')
74 p.set_defaults(sroot=None) # auto-detect
75
76 # Files for fitter
77 p.add_argument('--tts-file', default='/group/belle2/group/detector/TOP/calibration/MCreferences/TTSParametrizations.root',
78 help='TTS parametrization ROOT file')
79 p.add_argument('--laser-mc-fit', default='/group/belle2/group/detector/TOP/calibration/MCreferences/laserMCFit.root',
80 help='Laser MC fit constraints ROOT file')
81
82 # Pulser DeltaT
83 p.add_argument('--pulser-deltaT', type=float, default=None,
84 help='Override pulser DeltaT (ns). Default auto: 21.78 (exp<14), 31.86 (exp>=14)')
85
86 # Backend knobs
87 p.add_argument('--queue', default='l', help='LSF queue name')
88 p.add_argument('--max-files-per-job', type=int, default=1, help='max_files_per_collector_job')
89 p.add_argument('--max-collector-jobs', type=int, default=None, help='cap number of collector jobs')
90
91 # Misc
92 p.add_argument('--calname', default='TOP_channelT0', help='Calibration name (conditions object)')
93 p.add_argument('--dry-run', action='store_true', help='List files and exit without running CAF')
94 p.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'], default='INFO',
95 help='basf2 log level')
96
97 return p.parse_args()
98
99# -------------------- Utilities ----------------------
100
101
102def exp_run_strings(exp, run_low, run_high):
103 """ Generate list of formatted run strings for given exp and run range."""
104 exp_str = f'{exp:04d}'
105 runs = [f'{exp_str}.{r:05d}' for r in range(run_low, run_high + 1)]
106 return runs
107
108
109def discover_input_files(source_dir, runs):
110 """ Discover (s)root files in source_dir matching exp and runs."""
111 patterns = [os.path.join(source_dir, f'top.{r}.*root') for r in runs]
112 files = []
113 for pat in patterns:
114 hits = glob.glob(pat)
115 files.extend(sorted(hits))
116 return files
117
118
119def auto_pulser_deltaT(exp, override):
120 """ Auto-select pulser DeltaT based on exp, unless overridden."""
121 if override is not None:
122 return override
123 return 21.78 if exp < 14 else 31.86
124
125
126def auto_detect_sroot(files, forced):
127 """ Auto-detect whether to use SeqRootInput (sroot) or RootInput (root) based on filenames, unless forced."""
128 if forced is not None:
129 return forced
130 # Heuristic: if filenames contain ".sroot" anywhere, use SeqRootInput
131 return any('.sroot' in f for f in files)
132
133
134def strategy_from_name(name):
135 """ Map strategy name to CAF strategy class."""
136 return {'SingleIOV': SingleIOV, 'SequentialRunByRun': SequentialRunByRun, 'SimpleRunByRun': SimpleRunByRun}[name]
137
138
139def ensure_file(path, label):
140 """ Ensure that a file exists, else exit with error."""
141 if not os.path.isfile(path):
142 B2ERROR(f'{label} not found: {path}')
143 sys.exit(1)
144
145
146def set_log_level(level_str):
147 """ Set basf2 log level."""
148 level = getattr(basf2.LogLevel, level_str)
149 basf2.set_log_level(level)
150
151# --------------------- Calibration -------------------
152
153
154def make_calibration(args, input_files, pulser_deltaT, use_seq_input):
155 """ Build and return the channel T0 calibration object."""
156
157 # Build the pre-collector path
158 main = basf2.create_path()
159
160 # Input
161 if use_seq_input:
162 main.add_module('SeqRootInput')
163 else:
164 main.add_module('RootInput')
165
166 # Geometry (not needed for MC mode)
167 if args.fit_mode != 'mc':
168 main.add_module('TOPGeometryParInitializer')
169
170 # TOPUnpacker
171 main.add_module('TOPUnpacker')
172
173 # TOPRawDigitConverter
174 main.add_module(
175 'TOPRawDigitConverter',
176 useChannelT0Calibration=False,
177 useSampleTimeCalibration=args.use_tbc,
178 useAsicShiftCalibration=True,
179 useModuleT0Calibration=False,
180 useCommonT0Calibration=False,
181 calpulseHeightMin=200,
182 calpulseHeightMax=700,
183 calpulseWidthMin=0.5,
184 calpulseWidthMax=3.0,
185 minPulseWidth=0.5,
186 calibrationChannel=0,
187 lookBackWindows=int(args.lookback)
188 )
189
190 # Collector
191 collector = basf2.register_module('TOPLaserCalibratorCollector')
192 collector.param('useReferencePulse', True)
193 collector.param('storeMCTruth', args.fit_mode == 'mc')
194 collector.param('refChannel', int(args.ref_channel))
195 collector.param('refSlot', int(args.ref_slot))
196 collector.param('pulserDeltaT', float(pulser_deltaT))
197 # Optionally: collector.param('pulserDeltaTTolerance', 50)
198
199 # CAF algorithm
200 algo = TOP.TOPLocalCalFitter()
201 algo.setFitMode(args.fit_mode)
202 algo.setTTSFileName(args.tts_file)
203 algo.setFitConstraintsFileName(args.laser_mc_fit)
204 # NOTE: fitInAmplitudeBins method name has a typo in older basf2 versions; the correct one is:
205 try:
206 algo.fitInAmplitudeBins(bool(args.amplitude_bins))
207 except AttributeError:
208 # Fallback for older basf2 versions; keep original to be safe
209 algo.fitInAmpliduteBins(bool(args.amplitude_bins))
210
211 # Crosstalk detection algorithm
212 if hasattr(algo, 'crosstalkDetection'):
213 algo.crosstalkDetection(bool(args.detect_crosstalk))
214 elif args.detect_crosstalk:
215 B2WARNING("Crosstalk detection requested (--detect-crosstalk) "
216 "but this TOPLocalCalFitter has no 'crosstalkDetection' method. Ignoring.")
217
218 # Calibration object
219 cal = Calibration(name=args.calname, collector=collector, algorithms=algo, input_files=input_files)
220
221 # Database priorities: local before global
222 cal.reset_database()
223 # Central DB global tags (highest priority first)
224 global_tags = [g.strip() for g in args.global_tags.split(',') if g.strip()]
225 for gt in reversed(global_tags):
226 cal.use_central_database(gt)
227
228 # Local DB files (highest priority first)
229 local_dbs = args.localDBs.split()
230 for ldb in reversed(local_dbs):
231 cal.use_local_database(ldb)
232
233 cal.pre_collector_path = main
234 cal.max_files_per_collector_job = int(args.max_files_per_job)
235 if args.max_collector_jobs is not None:
236 cal.max_collector_jobs = int(args.max_collector_jobs)
237
238 cal.strategies = strategy_from_name(args.strategy)
239 return cal, global_tags, local_dbs
240
241# ------------------------ Main -----------------------
242
243
244def main():
245 args = parse_args()
246 set_log_level(args.log_level)
247
248 # basf2 conditions: prepend highest-priority global tag for non-CAF modules (kept minimal)
249 basf2.conditions.prepend_globaltag('online') # harmless even if overridden in CAF
250
251 # Validate ranges
252 if args.run_high < args.run_low:
253 B2ERROR(f'run_high < run_low: {args.run_high} < {args.run_low}')
254 sys.exit(1)
255 if args.lookback < 0:
256 B2ERROR(f'lookback must be >= 0, got {args.lookback}')
257 sys.exit(1)
258
259 # Discover input
260 runs = exp_run_strings(args.exp, args.run_low, args.run_high)
261 input_files = discover_input_files(args.source_dir, runs)
262 if not input_files:
263 B2ERROR(f'No (s)root files found in {args.source_dir} for exp={args.exp} runs={args.run_low}-{args.run_high}')
264 sys.exit(1)
265
266 # Auto-detect reader flavor unless user forced it
267 use_seq_input = auto_detect_sroot(input_files, args.sroot)
268
269 # External files
270 ensure_file(args.tts_file, 'TTS file')
271 ensure_file(args.laser_mc_fit, 'Laser MC fit file')
272
273 # Pulser DeltaT
274 pulser_deltaT = auto_pulser_deltaT(args.exp, args.pulser_deltaT)
275
276 # Output dirs
277 expNo = f"e{args.exp:04d}"
278 run_range = f"r{args.run_low:05d}-{args.run_high:05d}"
279 output_dir = os.path.join(args.main_output_dir, f"channelT0-local-{expNo}-{run_range}")
280 os.makedirs(output_dir, exist_ok=True)
281
282 # Summary logging
283 B2INFO(f"Found {len(input_files)} input files (showing up to 5):")
284 for f in input_files[:5]:
285 B2INFO(f" {f}")
286 if len(input_files) > 5:
287 B2INFO(" ...")
288
289 B2INFO(f"Reader: {'SeqRootInput' if use_seq_input else 'RootInput'} | fit-mode: {args.fit_mode} | strategy: {args.strategy}")
290 B2INFO(f"TBC: {'ON' if args.use_tbc else 'OFF'} | amp-bins: {args.amplitude_bins} | crosstalk: {args.detect_crosstalk}")
291 B2INFO(f"Ref slot/channel: {args.ref_slot}/{args.ref_channel} | pulser DeltaT: {pulser_deltaT:.2f} ns")
292 B2INFO(f"Queue: {args.queue} | max_files_per_job: {args.max_files_per_job} | max_collector_jobs: {args.max_collector_jobs}")
293 B2INFO(f"Output dir: {output_dir}")
294
295 # Build calibration
296 cal, global_tags, local_dbs = make_calibration(args, input_files, pulser_deltaT, use_seq_input)
297 cal.backend_args = {"queue": args.queue}
298
299 # Persist setup to YAML
300 setup_dict = {
301 'Experiment': int(args.exp),
302 'Run low': int(args.run_low),
303 'Run high': int(args.run_high),
304 'Globaltags': global_tags,
305 'Local databases': local_dbs,
306 'Source directory': os.path.abspath(args.source_dir),
307 'Output directory (base)': os.path.abspath(args.main_output_dir),
308 'Resolved output dir': os.path.abspath(output_dir),
309 'Lookback windows': int(args.lookback),
310 'TTS file': os.path.abspath(args.tts_file),
311 'MC fit file': os.path.abspath(args.laser_mc_fit),
312 'Fit mode': args.fit_mode,
313 'Input files (count)': len(input_files),
314 'TBC enabled': bool(args.use_tbc),
315 'Reference slot': int(args.ref_slot),
316 'Reference channel': int(args.ref_channel),
317 'Pulser Delta T (ns)': float(pulser_deltaT),
318 'Amplitude bins': bool(args.amplitude_bins),
319 'Detect crosstalk': bool(args.detect_crosstalk),
320 'Reader': 'SeqRootInput' if use_seq_input else 'RootInput',
321 'Strategy': args.strategy,
322 'Queue': args.queue,
323 'max_files_per_collector_job': int(args.max_files_per_job),
324 'max_collector_jobs': None if args.max_collector_jobs is None else int(args.max_collector_jobs),
325 'Command': ' '.join(sys.argv),
326 'Env': {k: v for k, v in os.environ.items() if k.startswith(('BELLE2_', 'BASF2_', 'TOP_', 'PG', 'USE_'))}
327 }
328 with open(os.path.join(output_dir, 'setup.yml'), 'w') as f:
329 yaml.dump(setup_dict, f, sort_keys=False)
330 B2INFO(f"Written setup config: {os.path.join(output_dir, 'setup.yml')}")
331
332 if args.dry_run:
333 B2INFO("Dry-run requested; exiting before CAF execution.")
334 return
335
336 # CAF wiring
337 cal_fw = CAF()
338 cal_fw.add_calibration(cal)
339 cal_fw.output_dir = output_dir
340 cal_fw.backend = backends.LSF()
341
342 # Run!
343 cal_fw.run()
344
345
346if __name__ == '__main__':
347 main()
Definition main.py:1