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