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