Belle II Software development
record.py
1#!/usr/bin/env python3
2
3
10
11
12from ROOT import Belle2 # make Belle2 namespace available
13
14import os
15import sys
16
17from tracking.validation.utilities import is_primary
18
19import tracking.harvest.peelers as peelers
20import tracking.harvest.harvesting as harvesting
21import tracking.harvest.refiners as refiners
22from tracking.harvest.run import HarvestingRun
23
24import trackfindingcdc.harvest.cdc_peelers as cdc_peelers
25
26
27import logging
28
29
30def get_logger():
31 return logging.getLogger(__name__)
32
33
34CONTACT = "oliver.frost@desy.de"
35
36
38 """Harvester to read, postprocess and inspect MC events for track-segment fit validation"""
39
40
41 n_events = 10000
42
43 generator_module = "low_gun" # Rather high momentum tracks should make the tracks rather straight.
44
45 root_input_file = "low_gun.root"
46
47
48 monte_carlo = "no"
49
50 karimaki_fit = False
51
52 flight_time_estimation = "none"
53
54 flight_time_reestimation = False
55
56 use_alpha_in_drift_length = True
57
58 flight_time_mass_scale = float("nan")
59
60
61 fit_positions = "recoPos"
62
63 fit_variance = "proper"
64
65 @property
67 """Get the output ROOT filename"""
68 file_name = "karimaki" if self.karimaki_fit else "riemann"
69 file_name += "-mc-" + self.monte_carlomonte_carlo
71 file_name += "-re"
73 file_name += "-" + os.path.split(self.root_input_fileroot_input_fileroot_input_file)[1]
74 else:
75 file_name += ".root"
76
77 return file_name
78
79 def harvesting_module(self, path=None):
80 """Harvest and post-process the MC events"""
82 if path:
83 path.add_module(harvesting_module)
84 return harvesting_module
85
86 def create_argument_parser(self, **kwds):
87 """Convert command-line arguments to basf2 argument list"""
88 argument_parser = super().create_argument_parser(**kwds)
89
90 argument_parser.add_argument(
91 '-m',
92 '--monte-carlo',
93 choices=["no", "medium", "full"],
94 default=self.monte_carlomonte_carlo,
95 dest='monte_carlo',
96 help='Amount of monte carlo information to be used in the segment generation.',
97 )
98
99 argument_parser.add_argument(
100 "-k",
101 "--karimaki",
102 dest="karimaki_fit",
103 action="store_true",
104 help='Use Karimaki fit instead of Riemann fit'
105 )
106
107 argument_parser.add_argument(
108 "-fp",
109 "--fit-pos",
110 choices=["recoPos", "rlDriftCircle", "wirePos"],
111 default=self.fit_positions,
112 dest="fit_positions",
113 help=("Choose which positional information the segment fit should be used. \n"
114 "* 'wirePos' means only the wire position\n"
115 "* 'recoPos' means only the reconstructed position\n"
116 "* 'rlDriftCircle' means only the drift circle with the right left passage\n")
117 )
118
119 argument_parser.add_argument(
120 "-fv",
121 "--fit-var",
122 choices=["unit", "driftLength", "pseudo", "proper"],
123 default=self.fit_variance,
124 dest="fit_variance",
125 help=("Choose which variance information the segment fit should be used. \n"
126 "* 'unit' means equal variance of 1\n"
127 "* 'driftLength' means inserting the drift length as variance, very improper because dimension mismatch\n"
128 "* 'pseudo' means the squared dirft length + plus the drift length variance "
129 "(correct dimension, proper lower bound)\n"
130 "* 'proper' means only the drift length variance\n")
131 )
132
133 argument_parser.add_argument(
134 "-ft",
135 "--flight-time-estimation",
136 choices=["none", "outwards", "downwards"],
137 default=self.flight_time_estimation,
138 dest="flight_time_estimation",
139 help=("Choose which estimation method for the time of flight should be use. \n"
140 "* 'none' no time of flight corrections\n"
141 "* 'outwards' means the minimal time needed to travel to the wire from the interaction point \n"
142 "* 'downwards' means the minimal time needed to travel to the wire from the y = 0 plane downwards \n")
143 )
144
145 argument_parser.add_argument(
146 "-fr",
147 "--flight-time-reestimation",
148 action="store_true",
149 dest="flight_time_reestimation",
150 help=("Switch to reestimate drift length before fitting.")
151 )
152
153 argument_parser.add_argument(
154 "-fa",
155 "--use-alpha-in-drift-length",
156 action="store_true",
157 dest="use_alpha_in_drift_length",
158 help=("Switch to serve the alpha angle to the drift length translator.")
159 )
160
161 argument_parser.add_argument(
162 "-fm",
163 "--flight-time-mass-scale",
164 type=float,
165 dest="flight_time_mass_scale",
166 help=("Mass parameter to estimate the velocity in the time of flight estimation")
167 )
168
169 return argument_parser
170
171 def create_path(self):
172 """
173 Sets up a path that plays back pregenerated events or generates events
174 based on the properties in the base class.
175 """
176 path = super().create_path()
177
178 path.add_module("TFCDC_WireHitPreparer",
179 flightTimeEstimation=self.flight_time_estimation,
180 UseNLoops=1
181 )
182
183 path.add_module("TFCDC_ClusterPreparer")
184
185
186 if self.monte_carlomonte_carlo == "no":
187 # MC free - default
188 path.add_module(
189 "TFCDC_SegmentFinderFacetAutomaton",
190 # investigate=[],
191 SegmentOrientation="none",
192 )
193
194 elif self.monte_carlomonte_carlo == "medium":
195 # Medium MC - proper generation logic, but true facets and facet relations
196 path.add_module("TFCDC_SegmentFinderFacetAutomaton",
197 FacetFilter="truth",
198 FacetRelationFilter="truth",
199 SegmentOrientation="none",
200 # SegmentOrientation="outwards"
201 )
202
203 elif self.monte_carlomonte_carlo == "full":
204 # Only true monte carlo segments, but make the positions realistic
205 path.add_module("TFCDC_SegmentCreatorMCTruth",
206 reconstructedDriftLength=False,
207 reconstructedPositions=True)
208
209 else:
210 raise ValueError("Invalid degree of Monte Carlo information")
211
212 path.add_module("TFCDC_SegmentFitter",
213 inputSegments="CDCSegment2DVector",
214 karimakiFit=self.karimaki_fit,
215 fitPos=self.fit_positions,
216 fitVariance=self.fit_variance,
217 updateDriftLength=self.flight_time_reestimation,
218 useAlphaInDriftLength=self.use_alpha_in_drift_length,
219 tofMassScale=self.flight_time_mass_scale)
220
221 return path
222
223
224class SegmentFitValidationModule(harvesting.HarvestingModule):
225
226 """Module to collect information about the generated segments and
227 compose validation plots on terminate."""
228
229 def __init__(self, output_file_name):
230 """Constructor"""
231 super().__init__(foreach='CDCSegment2DVector',
232 output_file_name=output_file_name)
233
234
236
237 def initialize(self):
238 """Receive signal at the start of event processing"""
239 super().initialize()
240
241
243
245
246 def prepare(self):
247 """Initialize the MC-hit lookup method"""
249
250 def pick(self, segment):
251 """Select segments with 4 or more hits and a matching primary MC particle"""
252 mc_segment_lookup = self.mc_segment_lookup
253 mc_particle = mc_segment_lookup.getMCParticle(segment)
254
255 # Check that mc_particle is not a nullptr
256 return mc_particle and is_primary(mc_particle) and segment.size() > 3
257
258 def peel(self, segment):
259 """Aggregate the track and MC information for track-segment analysis"""
260
261 mc_segment_lookup = self.mc_segment_lookup
262 mc_hit_lookup = self.mc_hit_lookup
263
264 segment_crops = cdc_peelers.peel_segment2d(segment)
265
266 mc_particle = mc_segment_lookup.getMCParticle(segment)
267 mc_particle_crops = peelers.peel_mc_particle(mc_particle)
268
269 fit3d_truth = mc_segment_lookup.getTrajectory3D(segment)
270 curvature_truth = fit3d_truth.getCurvatureXY()
271
272 reconstructed_backward = mc_segment_lookup.isForwardOrBackwardToMCTrack(segment)
273 is_fake = mc_segment_lookup.getMCTrackId(segment) < 0
274
275 if reconstructed_backward != 1:
276 curvature_truth = -curvature_truth
277
278 truth_crops = dict(
279 tan_lambda_truth=fit3d_truth.getTanLambda(),
280 curvature_truth=curvature_truth,
281 abs_curvature_truth=abs(curvature_truth),
282 curvature_residual=segment_crops["curvature_estimate"] - curvature_truth,
283 curvature_pull=(segment_crops["curvature_estimate"] - curvature_truth) / segment_crops["curvature_variance"],
284 reconstructed_backward=reconstructed_backward,
285 is_fake=is_fake
286 )
287
288 rl_sum = 0
289 n_correct = 0
290 n_total = segment.size()
291 first_i_incorrect = float("nan")
292 last_i_incorrect = float("nan")
293 first_l_incorrect = 0
294 last_l_incorrect = 0
295
296 n_rl_switch = 0
297 last_rl_info = 0
298 for i, reco_hit2d in enumerate(segment):
299 rl_info = reco_hit2d.getRLInfo()
300 hit = reco_hit2d.getWireHit().getHit()
301 true_rl_info = mc_hit_lookup.getRLInfo(hit)
302
303 if rl_info != last_rl_info:
304 n_rl_switch += 1
305 last_rl_info = rl_info
306
307 if true_rl_info == rl_info:
308 n_correct += 1
309 else:
310 if first_i_incorrect != first_i_incorrect:
311 first_i_incorrect = i
312 first_l_incorrect = reco_hit2d.getRefDriftLength()
313 last_i_incorrect = i
314 last_l_incorrect = reco_hit2d.getRefDriftLength()
315 if rl_info == 1:
316 rl_sum += 1
317 elif rl_info == -1 or rl_info == 65535: # <- The root interface mistakes the signed enum value for an unsigned value
318 rl_sum -= 1
319
320 alias_score = segment.getAliasScore()
321
322 rl_crops = dict(
323 n_total=n_total,
324 n_correct=n_correct,
325 n_incorrect=n_total - n_correct,
326 rl_purity=n_correct / n_total,
327 first_incorrect_location=first_i_incorrect / (n_total - 1),
328 first_l_incorrect=first_l_incorrect,
329 last_incorrect_location=last_i_incorrect / (n_total - 1),
330 last_l_incorrect=last_l_incorrect,
331 n_rl_switch=n_rl_switch,
332 n_rl_asymmetry=rl_sum / n_total,
333 n_abs_rl_asymmetry=abs(rl_sum) / n_total,
334 may_alias=alias_score == 0,
335 alias_score=alias_score
336 )
337
338 segment_crops.update(truth_crops)
339 segment_crops.update(mc_particle_crops)
340 segment_crops.update(rl_crops)
341 return segment_crops
342
343 # Refiners to be executed at the end of the harvesting / termination of the module
344
345 save_histograms = refiners.save_histograms(outlier_z_score=5.0, allow_discrete=True)
346
347 save_tree = refiners.save_tree()
348
349
350 save_curvature_pull = refiners.save_pull_analysis(
351 part_name="curvature",
352 unit="1/cm",
353 absolute=False,
354 aux_names=["tan_lambda_truth", "abs_curvature_truth"],
355 groupby=[
356 "stereo_kind",
357 "superlayer_id"
358 ],
359 outlier_z_score=4.0,
360 title_postfix="")
361
362
363 save_curvature_pull_rl_pure = refiners.save_pull_analysis(
364 part_name="curvature",
365 unit="1/cm",
366 absolute=True,
367 filter_on="rl_purity",
368 filter=lambda rl_purity: rl_purity > 0.5,
369 aux_names=["tan_lambda_truth", "abs_curvature_truth"],
370 groupby=[
371 "stereo_kind",
372 "superlayer_id"
373 ],
374 outlier_z_score=4.0,
375 title_postfix="",
376 folder_name="rl_pure/{groupby_addition}"
377 )
378
379
380 save_absolute_curvature_pull = refiners.save_pull_analysis(
381 part_name="curvature",
382 unit="1/cm",
383 absolute=True,
384 aux_names=["tan_lambda_truth", "abs_curvature_truth"],
385 groupby=[
386 "stereo_kind",
387 "superlayer_id"
388 ],
389 outlier_z_score=4.0,
390 title_postfix="")
391
392
393 save_fit_quality_histograms = refiners.save_histograms(
394 outlier_z_score=5.0,
395 select={
396 "ndf": "ndf",
397 "chi2": "#chi2",
398 "p_value": "p-value",
399 "tan_lambda_truth": "true tan #lambda",
400 },
401 groupby=[None, "stereo_kind", "superlayer_id"],
402 title="Histogram of {part_name}{stacked_by_indication}{stackby}",
403 description="Distribution of {part_name} in the segment fits",
404 )
405
406
407def main():
409 run.configure_and_execute_from_commandline()
410
411
412if __name__ == "__main__":
413 logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(levelname)s:%(message)s')
414 main()
def peel_segment2d(segment, key="{part_name}")
Definition: cdc_peelers.py:14
static const CDCMCHitLookUp & getInstance()
Getter for the singletone instance.
static const CDCMCSegment2DLookUp & getInstance()
Getter for the singletone instance.
mc_hit_lookup
Method to find matching MC hits.
Definition: record.py:244
def pick(self, segment)
Definition: record.py:250
def peel(self, segment)
Definition: record.py:258
mc_segment_lookup
by default, there is no method to find matching MC track segments
Definition: record.py:235
def __init__(self, output_file_name)
Definition: record.py:229
def create_argument_parser(self, **kwds)
Definition: record.py:86
float flight_time_mass_scale
assume a very heavy particle for flight time calculation
Definition: record.py:58
monte_carlo
Degree of refinement of the segment generation.
Definition: record.py:186
bool flight_time_reestimation
do not re-estimate the flight time
Definition: record.py:54
bool use_alpha_in_drift_length
use the alpha parameter for drift length
Definition: record.py:56
bool karimaki_fit
use the Riemann fit instead of the Karimaki fit
Definition: record.py:50
str fit_positions
fit the reconstructed hit position for the segment fit
Definition: record.py:61
str fit_variance
use the drift-length variance in the segment fit
Definition: record.py:63
def harvesting_module(self, path=None)
Definition: record.py:79
str root_input_file
read generated/simulated/reconstructed events from this ROOT file
Definition: record.py:45
str flight_time_estimation
do not estimate the flight time
Definition: record.py:52
str monte_carlo
do not generate new events
Definition: record.py:48
None output_file_name
Disable the writing of an output ROOT file.
Definition: run.py:20
root_input_file
generating events, so there is no ROOT input file
None root_input_file
By default, there is no input ROOT TFile.
Definition: minimal.py:113
None output_file_name
There is no default for the name of the output TFile.
Definition: mixins.py:60
Definition: main.py:1