Belle II Software development
caf_svd_dedx.py
1#!/usr/bin/env python3
2
3
10'''
11Script to perform the SVD dE/dx calibration
12'''
13from prompt import CalibrationSettings, INPUT_DATA_FILTERS
14import basf2 as b2
15
16from softwaretrigger.constants import HLT_INPUT_OBJECTS
17import modularAnalysis as ma
18import vertex as vx
19import reconstruction as re
20from reconstruction import prepare_user_cdst_analysis, DIGITS_OBJECTS
21
22settings = CalibrationSettings(
23 name="caf_svd_dedx",
24 expert_username="lisovskyi",
25 subsystem="svd",
26 description=__doc__,
27 input_data_formats=["cdst"],
28 input_data_names=["hadron_calib"],
29 input_data_filters={"hadron_calib": [INPUT_DATA_FILTERS["Data Tag"]["hadron_calib"],
30 INPUT_DATA_FILTERS["Beam Energy"]["4S"],
31 INPUT_DATA_FILTERS["Beam Energy"]["Continuum"],
32 INPUT_DATA_FILTERS["Run Type"]["physics"],
33 INPUT_DATA_FILTERS["Magnet"]["On"]]},
34
35 expert_config={
36 "isMC": False,
37 "listOfMutedCalibrations": [], # dEdxCalibration, dEdxValidation
38 "rerun_reco": False, # need to rerun reconstruction for calibration?
39 # This is needed when using release-10 calibration on older CDSTs as the dEdx calculation needs to be rerun.
40 "rerun_reco_val": False, # need to rerun reconstruction for validation?
41 # This is needed when using release-10 calibration on older CDSTs as the dEdx calculation needs to be rerun.
42 "rerun_pid_val": True, # need to rerun PID for validation?
43 # The answer is almost always "yes", but this argument is ignored if you are already rerunning the reconstruction.
44 "validation_mode": "basic", # full or basic validation; full also produces global PID performance plots
45 "MaxFilesPerRun": 10, # 15,
46 "MaxFilesPerRunValidation": 6, # be careful in MC to not exclude certain event types
47 "MinEvtsPerFile": 1, # sanity check against empty CDSTs
48 "MaxEvtsPerFile": 20000, # only if rerun the reco, to prevent jobs >10h
49 "MinEvtsPerTree": 100, # sanity check against an empty collector-output TTree
50 "NBinsP": 69, # number of momentum bins in the payload. 69 is the default historically; non-uniform binning is used.
51 "NBinsdEdx": 100, # number of dEdx bins in the payload. 100 is the default historically.
52 "dedxCutoff": 5.e6, # max dEdx value in the payload.
53 # Switch on a more precise calculation of the 1D profile for 2D
54 # histograms? Helps with the universality of dEdx:beta*gamma curves
55 # between hadrons.
56 "CustomProfile": True,
57 # Fix one of the parameters in the dEdx:beta*gamma fit, which makes the fit much more stable.
58 "FixUnstableFitParameter": True,
59 # In case of large changes in dEdx:beta*gamma trend with time, might need to set to False.
60 "NEventsToGenerate": 5e5, # how many events to generate in each momentum bin, for the new payloads?
61 "UsePionBGFunctionForEverything": False, # if the dEdx:beta*gamma fit is unstable, use the pion fit for all hadrons?
62 # (This can save us if the calibration failed due to e.g. kaon fit issue.)
63 "UseProtonBGFunctionForEverything": False, # if the dEdx:beta*gamma fit is unstable, use the proton fit for all hadrons?
64 "NumROCpoints": 175, # only for full validation: number of pionts for the ROC scan
65 "MinROCMomentum": 0., # only for full validation: in which momentum range to calculate ROC?
66 "MaxROCMomentum": 2.5, # only for full validation: in which momentum range to calculate ROC?
67 "NumEffBins": 30, # validation: how many bins for efficiency plots?
68 "MaxEffMomentum": 2.5 # validation: upper momentum boundary for efficiency plots?
69 # (No separation from SVD above 2.5 GeV or so)
70 },
71 depends_on=[],
72 produced_payloads=["SVDdEdxPDFs"])
73
74
75def create_path(rerun_reco, rerun_pid, isMC, expert_config):
76 rec_path = b2.Path()
77
78 # expert_config = kwargs.get("expert_config")
79 max_events_per_file = expert_config["MaxEvtsPerFile"]
80
81 if rerun_reco:
82 if not isMC:
83 rec_path.add_module(
84 'RootInput',
85 branchNames=HLT_INPUT_OBJECTS,
86 entrySequences=[f'0:{max_events_per_file - 1}'],
87 logLevel=b2.LogLevel.ERROR)
88 re.add_unpackers(path=rec_path)
89 else:
90 rec_path.add_module('RootInput', branchNames=list(DIGITS_OBJECTS) + [
91 'MCParticles',
92 'EventLevelTriggerTimeInfo',
93 'SoftwareTriggerResult',
94 'TRGSummary'], entrySequences=[f'0:{max_events_per_file - 1}'])
95 rec_path.add_module("Gearbox")
96 rec_path.add_module("Geometry")
97
98 re.add_reconstruction(path=rec_path, pruneTracks=False)
99 rec_path.add_module('VXDDedxPID')
100 elif rerun_pid:
101 rec_path.add_module('RootInput', entrySequences=[f'0:{max_events_per_file - 1}'])
102 prepare_user_cdst_analysis(rec_path, mc=isMC)
103 else:
104 rec_path.add_module('RootInput')
105
106 # Fill particle lists
107 ma.fillParticleList("pi+:all", "", path=rec_path)
108 ma.fillParticleList("pi+:lambda", "nCDCHits > 0", path=rec_path) # pi without track quality for reconstructing lambda
109 ma.fillParticleList("pi+:cut", "abs(dr) < 0.5 and abs(dz) < 2 and nSVDHits > 1",
110 path=rec_path) # pions for reconstructing D and Dstar
111
112 ma.fillParticleList('K-:cut', cut='abs(dr) < 0.5 and abs(dz) < 2 and nSVDHits > 1', path=rec_path) # kaon
113 ma.fillParticleList('e+:cut', cut='nSVDHits > 0', path=rec_path) # electron
114 # proton. In data, we only see background at p<0.25 GeV which motivates adding this cut.
115 ma.fillParticleList('p+:lambda', cut='nCDCHits > 0 and nSVDHits > 0 and p > 0.135', path=rec_path)
116 # ----------------------------------------------------------------------------
117 # Reconstruct D*(D0->K-pi+)pi+ and cc.
118 ma.reconstructDecay(decayString='D0:kpi -> K-:cut pi+:cut', cut='1.7 < M < 2.', path=rec_path)
119 ma.reconstructDecay(
120 decayString='D*+:myDstar -> D0:kpi pi+:all',
121 cut='1.95 < M <2.05 and massDifference(0) < 0.16',
122 path=rec_path)
123
124 # Reconstruct Lambda->p+pi- and cc.
125 ma.reconstructDecay('Lambda0:myLambda -> p+:lambda pi-:lambda', '1.1 < M < 1.3', path=rec_path)
126
127 # Reconstruct gamma->e+e- (photon conversion)
128 ma.reconstructDecay('gamma:myGamma -> e+:cut e-:cut', '0.0 < M < 0.5', path=rec_path)
129
130 # ----------------------------------------------------------------------------
131 # vertex fits
132 vx.treeFit(list_name='D*+:myDstar', conf_level=0, ipConstraint=True, updateAllDaughters=True, path=rec_path)
133 vx.treeFit(list_name='Lambda0:myLambda', conf_level=0, ipConstraint=True, updateAllDaughters=True, path=rec_path)
134 vx.treeFit(list_name='gamma:myGamma', conf_level=0, path=rec_path)
135
136 # ----------------------------------------------------------------------------
137 # Final selections on Lambda:
138 # - a tighter InvM range
139 # - a good vertex quality and a displacement requirement
140 # - a kinematic requirement p(p)>p(pi) which should always be satisfied in a true Lambda decay
141 # - a veto on the misidentified converted photons using the convertedPhotonInvariantMass tool, m(ee)>0.02 GeV
142 # - a veto on the misidentified K0S->pipi decays, vetoeing the region 0.488<m(pipi)<0.513 GeV
143
144 ma.cutAndCopyList(
145 outputListName='Lambda0:cut',
146 inputListName='Lambda0:myLambda',
147 cut=(
148 "1.10 < InvM < 1.13 and chiProb > 0.001 and distance > 1.0 and "
149 "cosAngleBetweenMomentumAndVertexVector > 0 and "
150 "formula(daughter(0,p)) > formula(daughter(1,p)) and convertedPhotonInvariantMass(0,1) > 0.02 and "
151 "[[formula((((daughter(0, px)**2+daughter(0, py)**2+daughter(0, pz)**2 + 0.13957**2)**0.5+"
152 "daughter(1, E))*((daughter(0, px)**2+daughter(0, py)**2+daughter(0, pz)**2 + 0.13957**2)**0.5+"
153 "daughter(1, E))-(daughter(0, px)+daughter(1, px))*(daughter(0, px)+daughter(1, px))-(daughter(0, py)+"
154 "daughter(1, py))*(daughter(0, py)+daughter(1, py))-(daughter(0, pz)+daughter(1, pz))*(daughter(0, pz)+"
155 "daughter(1, pz)))**0.5) < 0.488]"
156 "or [formula((((daughter(0, px)**2+daughter(0, py)**2+daughter(0, pz)**2 + 0.13957**2)**0.5+"
157 "daughter(1, E))*((daughter(0, px)**2+daughter(0, py)**2+daughter(0, pz)**2 + 0.13957**2)**0.5+"
158 "daughter(1, E))-(daughter(0, px)+daughter(1, px))*(daughter(0, px)+daughter(1, px))-(daughter(0, py)+"
159 "daughter(1, py))*(daughter(0, py)+daughter(1, py))-(daughter(0, pz)+daughter(1, pz))*(daughter(0, pz)+"
160 "daughter(1, pz)))**0.5) > 0.513]]"
161 ),
162 path=rec_path)
163
164 # ----------------------------------------------------------------------------
165 # Selections on Dstar
166 # - a tighter InvM and deltaM range
167 # - a good vertex quality
168
169 ma.cutAndCopyList(
170 outputListName='D*+:cut',
171 inputListName='D*+:myDstar',
172 cut=('massDifference(0) < 0.151 and 1.85 < daughter(0, InvM) < 1.88 and '
173 '1.95 < InvM < 2.05 and chiProb > 0.001 and '
174 'formula(daughter(0,cosAngleBetweenMomentumAndVertexVector))>-0.75'),
175 path=rec_path)
176
177 # ----------------------------------------------------------------------------
178 # Selections on gamma
179 # - a good vertex quality
180 # - the dr cut on the origin vertex coordinate of the photon conversion, that excludes the beam background
181 # - a tighter range on both the invariant mass and the convertedPhotonInvariantMass
182 # - geometric cuts on proximity of e+ and e- with convertedPhotonDelR and convertedPhotonDelZ
183
184 ma.cutAndCopyList(
185 outputListName='gamma:cut',
186 inputListName='gamma:myGamma',
187 cut=('chiProb > 0.001 and 1 < dr < 12 and InvM < 0.01'
188 ' and cosAngleBetweenMomentumAndVertexVector > 0.995'
189 ' and convertedPhotonInvariantMass(0,1) < 0.005'
190 ' and -0.05 < convertedPhotonDelR(0,1) < 0.15'
191 ' and -0.05 < convertedPhotonDelZ(0,1) < 0.05'
192 ),
193 path=rec_path)
194 return rec_path
195
196
197def get_calibrations(input_data, **kwargs):
198 """
199 Parameters:
200 input_data (dict): Should contain every name from the 'input_data_names' variable as a key.
201 Each value is a dictionary with {"/path/to/file_e1_r5.root": IoV(1,5,1,5), ...}. Useful for
202 assigning to calibration.files_to_iov
203
204 **kwargs: Configuration options to be sent in. Since this may change we use kwargs as a way to help prevent
205 backwards compatibility problems. But you could use the correct arguments in b2caf-prompt-run for this
206 release explicitly if you want to.
207
208 Currently only kwargs["requested_iov"] and kwargs["expert_config"] are used.
209
210 "requested_iov" is the IoV range of the bucket and your payloads should correspond to this range.
211 However your highest payload IoV should be open ended e.g. IoV(3,4,-1,-1)
212
213 "expert_config" is the input configuration. It takes default values from your `CalibrationSettings` but these are
214 overwritten by values from the 'expert_config' key in your input `caf_config.json` file when running ``b2caf-prompt-run``.
215
216 Returns:
217 list(caf.framework.Calibration): All of the calibration objects we want to assign to the CAF process
218 """
219 import basf2
220 # Set up config options
221
222 # In this script we want to use one sources of input data.
223 # Get the input files from the input_data variable
224 file_to_iov_hadron_calib = input_data["hadron_calib"]
225
226 expert_config = kwargs.get("expert_config")
227
228 isMC = expert_config["isMC"]
229 listOfMutedCalibrations = expert_config["listOfMutedCalibrations"]
230 rerun_reco = expert_config["rerun_reco"]
231 rerun_reco_val = expert_config["rerun_reco_val"]
232 rerun_pid_val = expert_config["rerun_pid_val"]
233 max_files_per_run = expert_config["MaxFilesPerRun"]
234 max_files_per_run_validation = expert_config["MaxFilesPerRunValidation"]
235
236 # Choose between the basic (default) or the full validation (produces more plots but depends on the global PID)
237 validation_mode = 1 if expert_config["validation_mode"] == "full" else 0
238
239 # If you are using Raw data there's a chance that input files could have zero events.
240 # This causes a B2FATAL in basf2 RootInput so the collector job will fail.
241 # Currently we don't have a good way of filtering this on the automated side, so we can check here.
242 min_events_per_file = expert_config["MinEvtsPerFile"]
243
244 from prompt.utils import filter_by_max_files_per_run
245
246 reduced_file_to_iov_hadron_calib = filter_by_max_files_per_run(file_to_iov_hadron_calib, max_files_per_run, min_events_per_file)
247 input_files_hadron_calib = list(reduced_file_to_iov_hadron_calib.keys())
248 basf2.B2INFO(f"Total number of files actually used as input for calibration = {len(input_files_hadron_calib)}")
249
250 if "dEdxValidation" not in listOfMutedCalibrations:
251 reduced_file_to_iov_hadron_validation = filter_by_max_files_per_run(
252 file_to_iov_hadron_calib, max_files_per_run_validation, min_events_per_file)
253 input_files_hadron_validation = list(reduced_file_to_iov_hadron_validation.keys())
254 basf2.B2INFO(f"Total number of files actually used as input for validation = {len(input_files_hadron_validation)}")
255 # Get the overall IoV we our process should cover. Includes the end values that we may want to ignore since our output
256 # IoV should be open ended. We could also use this as part of the input data selection in some way.
257 requested_iov = kwargs.get("requested_iov", None)
258
259 from caf.utils import IoV
260 # The actual value our output IoV payload should have. Notice that we've set it open ended.
261 output_iov = IoV(requested_iov.exp_low, requested_iov.run_low, -1, -1)
262
263
265 from ROOT import Belle2 # noqa: make the Belle2 namespace available
266 from ROOT.Belle2 import SVDdEdxCalibrationAlgorithm, SVDdEdxValidationAlgorithm
267
268 algo = SVDdEdxCalibrationAlgorithm()
269 algo.setMonitoringPlots(True)
270 algo.setNumPBins(expert_config['NBinsP'])
271 algo.setNumDEdxBins(expert_config['NBinsdEdx'])
272 algo.setDEdxCutoff(expert_config['dedxCutoff'])
273 algo.setMinEvtsPerTree(expert_config['MinEvtsPerTree'])
274 algo.setNEventsToGenerate(int(expert_config['NEventsToGenerate']))
275 algo.setUsePionBGFunctionForEverything(expert_config['UsePionBGFunctionForEverything'])
276 algo.setUseProtonBGFunctionForEverything(expert_config['UseProtonBGFunctionForEverything'])
277 algo.setCustomProfile(expert_config['CustomProfile'])
278 algo.setFixUnstableFitParameter(expert_config['FixUnstableFitParameter'])
279
280 if "dEdxValidation" not in listOfMutedCalibrations:
281 algo_val = SVDdEdxValidationAlgorithm()
282 algo_val.setMonitoringPlots(True)
283 algo_val.setMinEvtsPerTree(expert_config['MinEvtsPerTree'])
284 algo_val.setNumROCpoints(expert_config['NumROCpoints'])
285 algo_val.setMinROCMomentum(expert_config['MinROCMomentum'])
286 algo_val.setMaxROCMomentum(expert_config['MaxROCMomentum'])
287 algo_val.setNumEffBins(expert_config['NumEffBins'])
288 algo_val.setMaxEffMomentum(expert_config['MaxEffMomentum'])
289 algo_val.validationMode(validation_mode)
290
291
293
294 from caf.framework import Calibration
295
296 rec_path = create_path(rerun_reco, False, isMC, expert_config)
297 rec_path_validation = create_path(rerun_reco_val, rerun_pid_val, isMC, expert_config)
298
299 dedx_calibration = Calibration("SVDdEdxCalibration",
300 collector="SVDdEdxCollector",
301 algorithms=[algo],
302 input_files=input_files_hadron_calib,
303 pre_collector_path=rec_path)
304
305 if "dEdxValidation" not in listOfMutedCalibrations:
306 dedx_validation = Calibration("SVDdEdxValidation",
307 collector="SVDdEdxValidationCollector",
308 algorithms=[algo_val],
309 input_files=input_files_hadron_validation,
310 pre_collector_path=rec_path_validation)
311 # Do this for the default AlgorithmStrategy to force the output payload IoV
312 # It may be different if you are using another strategy like SequentialRunByRun
313 for algorithm in dedx_calibration.algorithms:
314 algorithm.params = {"apply_iov": output_iov}
315
316 # Most other options like database chain and backend args will be overwritten by b2caf-prompt-run.
317 # So we don't bother setting them.
318
319 if "dEdxValidation" not in listOfMutedCalibrations:
320 dedx_validation.depends_on(dedx_calibration)
321 # You must return all calibrations you want to run in the prompt process, even if it's only one
322 list_of_calibrations = []
323 if "dEdxCalibration" not in listOfMutedCalibrations:
324 list_of_calibrations.append(dedx_calibration)
325 if "dEdxValidation" not in listOfMutedCalibrations:
326 list_of_calibrations.append(dedx_validation)
327
328 return list_of_calibrations
329
330