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