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
34 """ Parse command-line arguments."""
36 p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
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)')
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)')
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)
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')
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)
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')
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)')
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')
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')
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)]
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]
114 hits = glob.glob(pat)
115 files.extend(sorted(hits))
119def auto_pulser_deltaT(exp, override):
120 """ Auto-select pulser DeltaT based on exp, unless overridden."""
121 if override
is not None:
123 return 21.78
if exp < 14
else 31.86
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:
131 return any(
'.sroot' in f
for f
in files)
134def strategy_from_name(name):
135 """ Map strategy name to CAF strategy class."""
136 return {
'SingleIOV': SingleIOV,
'SequentialRunByRun': SequentialRunByRun,
'SimpleRunByRun': SimpleRunByRun}[name]
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}')
146def set_log_level(level_str):
147 """ Set basf2 log level."""
148 level = getattr(basf2.LogLevel, level_str)
149 basf2.set_log_level(level)
154def make_calibration(args, input_files, pulser_deltaT, use_seq_input):
155 """ Build and return the channel T0 calibration object."""
158 main = basf2.create_path()
162 main.add_module(
'SeqRootInput')
164 main.add_module(
'RootInput')
167 if args.fit_mode !=
'mc':
168 main.add_module(
'TOPGeometryParInitializer')
171 main.add_module(
'TOPUnpacker')
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,
186 calibrationChannel=0,
187 lookBackWindows=int(args.lookback)
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))
200 algo = TOP.TOPLocalCalFitter()
201 algo.setFitMode(args.fit_mode)
202 algo.setTTSFileName(args.tts_file)
203 algo.setFitConstraintsFileName(args.laser_mc_fit)
206 algo.fitInAmplitudeBins(bool(args.amplitude_bins))
207 except AttributeError:
209 algo.fitInAmpliduteBins(bool(args.amplitude_bins))
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.")
219 cal = Calibration(name=args.calname, collector=collector, algorithms=algo, input_files=input_files)
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)
229 local_dbs = args.localDBs.split()
230 for ldb
in reversed(local_dbs):
231 cal.use_local_database(ldb)
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)
238 cal.strategies = strategy_from_name(args.strategy)
239 return cal, global_tags, local_dbs
246 set_log_level(args.log_level)
249 basf2.conditions.prepend_globaltag(
'online')
252 if args.run_high < args.run_low:
253 B2ERROR(f
'run_high < run_low: {args.run_high} < {args.run_low}')
255 if args.lookback < 0:
256 B2ERROR(f
'lookback must be >= 0, got {args.lookback}')
260 runs = exp_run_strings(args.exp, args.run_low, args.run_high)
261 input_files = discover_input_files(args.source_dir, runs)
263 B2ERROR(f
'No (s)root files found in {args.source_dir} for exp={args.exp} runs={args.run_low}-{args.run_high}')
267 use_seq_input = auto_detect_sroot(input_files, args.sroot)
270 ensure_file(args.tts_file,
'TTS file')
271 ensure_file(args.laser_mc_fit,
'Laser MC fit file')
274 pulser_deltaT = auto_pulser_deltaT(args.exp, args.pulser_deltaT)
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)
283 B2INFO(f
"Found {len(input_files)} input files (showing up to 5):")
284 for f
in input_files[:5]:
286 if len(input_files) > 5:
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}")
296 cal, global_tags, local_dbs = make_calibration(args, input_files, pulser_deltaT, use_seq_input)
297 cal.backend_args = {
"queue": args.queue}
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,
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_'))}
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')}")
333 B2INFO(
"Dry-run requested; exiting before CAF execution.")
338 cal_fw.add_calibration(cal)
339 cal_fw.output_dir = output_dir
346if __name__ ==
'__main__':