Belle II Software  release-05-01-25
validationcomparison.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 
4 """ Compare ROOT objects and perform e.g. chi2 tests.
5 A small command line interface for testing/debugging purposes is included.
6 Run `python3 validationcomparison.py --help` for more information. """
7 
8 # std
9 from abc import ABC, abstractmethod
10 import argparse
11 import numpy
12 import os.path
13 from typing import Optional
14 
15 # 3rd
16 import ROOT
17 
18 # ours
19 from metaoptions import MetaOptionParser
20 
21 # Unfortunately doxygen has some trouble with inheritance of attributes, so
22 # we disable it.
23 # @cond SUPPRESS_DOXYGEN
24 
25 
26 # ==============================================================================
27 # Custom Exceptions
28 # ==============================================================================
29 
30 
31 class ComparisonFailed(Exception):
32  """
33  The comparison failed for some reason. For example
34  because ROOT was not able to compute the Chi^2 properly
35  """
36  pass
37 
38 
39 class ObjectsNotSupported(Exception):
40  """
41  The type and/or combination of provided ROOT objects
42  is not supported for comparison
43  """
44  pass
45 
46 
47 class DifferingBinCount(Exception):
48  """
49  The two ROOT objects provided have a different bin count
50  and therefor, cannot be compared using the Chi2 test
51  """
52  pass
53 
54 
55 class TooFewBins(Exception):
56  """
57  Not sufficient bins to perform the Chi^2 test
58  """
59  pass
60 
61 
62 # ==============================================================================
63 # Comparison class selector
64 # ==============================================================================
65 
66 
67 def get_comparison(
68  object_1,
69  object_2,
70  mop: Optional[MetaOptionParser]
71 ) -> "ComparisonBase":
72  """ Uses the metaoptions to determine which comparison algorithm is used
73  and initializes the corresponding subclass of :class:`ComparisonBase` that
74  implements the actual comparison and holds the results.
75  @param object_1 ROOT TObject
76  @param object_2 ROOT TObject
77  @param mop Metaoption parser
78  """
79  if mop.has_option("kolmogorov"):
80  tester = KolmogorovTest
81  elif mop.has_option("andersondarling"):
82  tester = AndersonDarlingTest
83  else:
84  tester = Chi2Test
85 
86  test = tester(
87  object_1,
88  object_2,
89  mop=mop
90  )
91 
92  return test
93 
94 
95 # ==============================================================================
96 # Comparison Base Class
97 # ==============================================================================
98 
99 class ComparisonBase(ABC):
100  """
101  Base class for all comparison implementations.
102 
103  Follows 3 steps:
104 
105  1. Initialize the class together with two ROOT objects of different
106  revisions (that are to be compared) and the metaoptions (given in the
107  corresponding validation (steering) file), that determine how to compare
108  them.
109 
110  2. The Comparison class saves the ROOT objects and the metaoptions
111  internally, but does not compute anything yet
112 
113  3. If :meth:`ensure_compute` is called, or any property is accessed that
114  depends on computation, the internal implementation :meth:`_compute`
115  (to be implemented in the subclass) is called.
116 
117  4. :meth:`_compute` ensures that all values, like chi2, p-value etc. are
118  computed
119 
120  5. Two properties :meth:`comparison_result` (pass/warning/error) and
121  :meth:`comparison_result_long` (longer description of the comparison result)
122  allow to access the results.
123  """
124 
125  def __init__(self, object_a, object_b,
126  mop: Optional[MetaOptionParser] = None, debug=False):
127  """
128  Initialize ComparisonBase class
129 
130  :param object_a:
131  :param object_b:
132  :param mop: MetaOptionParser
133  :param debug (bool): Debug mode enabled?
134  """
135 
136  self.object_a = object_a
137 
138 
139  self.object_b = object_b
140 
141 
142  if mop is None:
143  mop = MetaOptionParser()
144  self.mop = mop
145 
146 
147  self.debug = debug
148 
149 
150  self.computed = False
151 
152 
153  self._comparison_result = "not_compared"
154 
156  self._comparison_result_long = ""
157 
158  def ensure_compute(self):
159  """
160  Ensure all required quantities get computed and are cached inside the
161  class
162  """
163  if self.computed:
164  return
165 
166  if self.mop.has_option("nocompare"):
167  # is comparison disabled for this plot ?
168  self._comparison_result_long = 'Testing is disabled for this plot'
169  return
170 
171  fail_message = "Comparison failed: "
172 
173  # Note: default for comparison_result is "not_compared"
174  try:
175  self._compute()
176  except ObjectsNotSupported as e:
177  self._comparison_result_long = fail_message + str(e)
178  except DifferingBinCount as e:
179  self._comparison_result = "error"
180  self._comparison_result_long = fail_message + str(e)
181  except TooFewBins as e:
182  self._comparison_result_long = fail_message + str(e)
183  except ComparisonFailed as e:
184  self._comparison_result = "error"
185  self._comparison_result_long = fail_message + str(e)
186  except Exception as e:
187  self._comparison_result = "error"
188  self._comparison_result_long = "Unknown error occurred. Please " \
189  "submit a bug report. " + str(e)
190  else:
191  # Will be already set in case of errors above and we don't want
192  # to overwrite this.
193  self._comparison_result_long = self._get_comparison_result_long()
194  self._comparison_result = self._get_comparison_result()
195 
196  self.computed = True
197 
198  @abstractmethod
199  def _get_comparison_result(self) -> str:
200  """ Used to format the value of :attr:`_comparison_result`. """
201  pass
202 
203  @abstractmethod
204  def _get_comparison_result_long(self) -> str:
205  """ Used to format the value of :attr:`_comparison_result_long`. """
206  pass
207 
208  @property
209  def comparison_result(self):
210  """ Comparison result, i.e. pass/warning/error """
211  self.ensure_compute()
212  return self._comparison_result
213 
214  @property
215  def comparison_result_long(self):
216  """ Longer description of the comparison result """
217  self.ensure_compute()
218  return self._comparison_result_long
219 
220  @abstractmethod
221  def _compute(self):
222  """ This method performs the actual computations. """
223  pass
224 
225  def can_compare(self):
226  """
227  @return: True if the two objects can be compared, False otherwise
228  """
229  return self._has_correct_types() and self._has_compatible_bins()
230 
231  def _has_correct_types(self) -> bool:
232  """
233  @return: True if the two objects have a) a type supported for
234  comparison and b) can be compared with each other
235  """
236  if self.object_a is None or self.object_b is None:
237  return False
238 
239  # check if the supplied object inherit from one of the supported types
240  # and if they are of the same type
241  supported_types = ["TProfile", "TH1D", "TH1F", "TEfficiency"]
242  if self.object_a.ClassName() != self.object_b.ClassName():
243  return False
244  if self.object_a.ClassName() not in supported_types:
245  return False
246 
247  if self.object_a.ClassName() == "TEfficiency":
248  # can only handle TEfficiencies with dimension one atm
249  if self.object_a.GetDimension() > 1:
250  return False
251 
252  return True
253 
254  def _raise_has_correct_types(self) -> None:
255  """
256  Raise Exception if not the two objects have a) a type supported for
257  comparison and b) can be compared with each other
258  @return: None
259  """
260  if not self._has_correct_types():
261  msg = "Comparison of {} (Type {}) with {} (Type {}) not " \
262  "supported.\nPlease open a JIRA issue (validation " \
263  "component) if you need this supported. "
264  raise ObjectsNotSupported(
265  msg.format(
266  self.object_a.GetName(),
267  self.object_a.ClassName(),
268  self.object_b.GetName(),
269  self.object_b.ClassName()
270  )
271  )
272 
273  def _has_compatible_bins(self) -> bool:
274  """
275  Check if both ROOT obeject have the same amount of bins
276  @return: True if the bins are equal, otherwise False
277  """
278  if self.object_a.ClassName() == "TEfficiency" == \
279  self.object_b.ClassName():
280  nbins_a = self.object_a.GetTotalHistogram().GetNbinsX()
281  nbins_b = self.object_b.GetTotalHistogram().GetNbinsX()
282  else:
283  nbins_a = self.object_a.GetNbinsX()
284  nbins_b = self.object_b.GetNbinsX()
285 
286  return nbins_a == nbins_b
287 
288  def _raise_has_compatible_bins(self) -> None:
289  """
290  Raise Exception if not both ROOT obeject have the same amount of bins
291  @return: None
292  """
293  if not self._has_compatible_bins():
294  msg = "The objects have differing x bin count: {} has {} vs. {} " \
295  "has {}."
296  raise DifferingBinCount(
297  msg.format(
298  self.object_a.GetName(),
299  self.object_a.GetNbinsX(),
300  self.object_b.GetName(),
301  self.object_b.GetNbinsX()
302  )
303  )
304 
305  @staticmethod
306  def _convert_teff_to_hist(teff_a):
307  """
308  Convert the content of a TEfficiency plot to a histogram and set
309  the bin content and errors
310  """
311  conv_hist = teff_a.GetTotalHistogram()
312  xbin_count = conv_hist.GetNbinsX()
313  xbin_low = conv_hist.GetXaxis().GetXmin()
314  xbin_max = conv_hist.GetXaxis().GetXmax()
315 
316  th1 = ROOT.TH1D(
317  teff_a.GetName() + "root_conversion",
318  teff_a.GetName(),
319  xbin_count,
320  xbin_low,
321  xbin_max
322  )
323  # starting from the first to the last bin, ignoring the under/overflow
324  # bins
325  for i in range(1, xbin_count):
326  th1.SetBinContent(i, teff_a.GetEfficiency(i))
327  th1.SetBinError(i, teff_a.GetEfficiencyErrorLow(i))
328 
329  return th1
330 
331 
332 class PvalueTest(ComparisonBase):
333  """ Test with a pvalue """
334 
335 
337  _default_pvalue_warn = 1.0
338 
339 
341  _default_pvalue_error = 0.01
342 
343  def __init__(self, *args, **kwargs):
344  """ Initialize Pvalue test
345 
346  Args:
347  *args: Positional arguments to ComparisonBase
348  **kwargs: Keyword arguments to ComparisonBase
349  """
350  super().__init__(*args, **kwargs)
351 
352  self._pvalue = None
353 
354  self._pvalue_warn = self.mop.pvalue_warn()
355 
356  self._pvalue_error = self.mop.pvalue_error()
357 
358  if self._pvalue_warn is None:
359  self._pvalue_warn = self._default_pvalue_warn
360  if self._pvalue_error is None:
361  self._pvalue_error = self._default_pvalue_error
362 
363  def _get_comparison_result(self) -> str:
364  if self._pvalue is None:
365  return "error"
366 
367  if self._pvalue < self._pvalue_error:
368  return "error"
369  elif self._pvalue < self._pvalue_warn:
370  return "warning"
371  else:
372  return "equal"
373 
374  @abstractmethod
375  def _compute(self):
376  pass
377 
378  @abstractmethod
379  def _get_comparison_result_long(self):
380  pass
381 
382 
383 # ==============================================================================
384 # Implementation of specific comparison algorithms
385 # ==============================================================================
386 
387 # ------------------------------------------------------------------------------
388 # Chi2 Test
389 # ------------------------------------------------------------------------------
390 
391 class Chi2Test(PvalueTest):
392 
393  """
394  Perform a Chi2Test for ROOT objects. The chi2 test method is e.g. described
395  in the documentation of TH1::Chi2Test. Basically this class wraps around
396  this Chi2Test function, and takes care that we can call perform these
397  tests for a wider selection of ROOT objects.
398  """
399 
400  def __init__(self, *args, **kwargs):
401  """
402  Initialize Chi2Test.
403  :param args: See arguments of :class:`ComparisonBase`
404  :param kwargs: See arguments of :class:`ComparisonBase`
405  """
406  super().__init__(*args, **kwargs)
407 
408  # The following attributes will be set in :meth:`_compute`
409 
410 
411  self._chi2 = None
412 
413  self._chi2ndf = None
414 
415  self._ndf = None
416 
417  def _ensure_zero_error_has_no_content(self, a, b):
418  """
419  Ensure there are no bins which have a content set, but 0 error
420  This bin content will be set to 0 to disable this bin completely during
421  the comparison
422  """
423  nbins = a.GetNbinsX()
424  for ibin in range(1, nbins + 1):
425  if a.GetBinError(ibin) <= 0.0 and b.GetBinError(ibin) <= 0.0:
426  # set the bin content of the profile plots to zero so ROOT
427  # will ignore this bin in its comparison
428  a.SetBinContent(ibin, 0.0)
429  b.SetBinContent(ibin, 0.0)
430  if self.debug:
431  print("DEBUG: Warning: Setting bin content of bin {} to "
432  "zero for both histograms, because both histograms "
433  "have vanishing errors there.".format(ibin))
434 
435  def _compute(self) -> None:
436  """
437  Performs the actual Chi^2 test
438  @return: None
439  """
440  self._raise_has_correct_types()
441  self._raise_has_compatible_bins()
442 
443  local_object_a = self.object_a
444  local_object_b = self.object_b
445 
446  # very special handling for TEfficiencies
447  if self.object_a.ClassName() == "TEfficiency":
448  local_object_a = self._convert_teff_to_hist(self.object_a)
449  local_object_b = self._convert_teff_to_hist(self.object_b)
450  if self.debug:
451  print("Converting TEfficiency objects to histograms.")
452 
453  nbins = local_object_a.GetNbinsX()
454 
455  if nbins < 2:
456  raise TooFewBins("{} bin(s) is too few to perform the Chi2 "
457  "test.".format(nbins))
458 
459  weighted_types = ["TProfile", "TH1D", "TH1F"]
460  comp_weight_a = local_object_a.ClassName() in weighted_types
461  comp_weight_b = local_object_b.ClassName() in weighted_types
462 
463  # clone, because possibly some content of profiles will
464  # be set to zero
465  first_obj = local_object_a.Clone()
466  second_obj = local_object_b.Clone()
467 
468  if comp_weight_a and not comp_weight_b:
469  # switch histograms, because ROOT can only have the first one
470  # to be unweighted
471  first_obj, second_obj = second_obj, first_obj
472  if self.debug:
473  print("Debug: Warning: Switching the two objects, because "
474  "ROOT can only have the first one to be unweighted")
475 
476  # Construct the option string for the Chi2Test call
477  comp_options = "P " # for debugging output
478  if comp_weight_a and comp_weight_b:
479  comp_options += "WW"
480  elif comp_weight_a or comp_weight_b:
481  comp_options += "UW"
482  else:
483  comp_options += "UU"
484 
485  if comp_weight_a and comp_weight_b:
486  self._ensure_zero_error_has_no_content(first_obj, second_obj)
487 
488  # use numpy arrays to support ROOT's pass-by-reference interface here
489  res_chi2 = numpy.array([1], numpy.float64)
490  res_igood = numpy.array([1], numpy.int32)
491  res_ndf = numpy.array([1], numpy.int32)
492 
493  res_pvalue = first_obj.Chi2TestX(
494  second_obj,
495  res_chi2,
496  res_ndf,
497  res_igood,
498  comp_options
499  )
500 
501  if self.debug:
502  print("Performing our own chi2 test, with bin-by-bin results: ")
503  print()
504  print_contents_and_errors(first_obj, second_obj)
505  print()
506  print("Here's what ROOT's Chi2Test gave us (comp_options: '{}'):"
507  " ".format(comp_options))
508 
509  tp = TablePrinter(3, width=(10, 10, 40))
510  print()
511  tp.print_divider()
512  tp.print(["Key", "Value", "Comment"])
513  tp.print_divider()
514  tp.print(["chi2", numpy.asscalar(res_chi2),
515  "Should roughly match above 'Total chi2'"])
516  tp.print(["ndf", numpy.asscalar(res_ndf), "#Non-empty bins - 1"])
517  tp.print(["chi2/ndf", numpy.asscalar(res_chi2 / res_ndf), ""])
518  tp.print(["igood", numpy.asscalar(res_igood),
519  "a debug indicator, 0 if all good"])
520  tp.print(["pvalue", res_pvalue, ""])
521  tp.print_divider()
522  print()
523  print("See https://root.cern.ch/doc/master/classTH1.html for more "
524  "information.")
525  print()
526 
527  if res_ndf < 1:
528  msg = "Comparison failed, no Chi^2 could be computed. For " \
529  "debugging, you can use the CLI of " \
530  "'validation/scripts/validationcomparison.py' on your root " \
531  "file and the reference. Run 'validationcomparison.py " \
532  "--help' for info. If problem persists, please open " \
533  "JIRA issue (validation component)."
534  raise ComparisonFailed(msg)
535 
536  res_chi2ndf = res_chi2 / res_ndf
537 
538  self._pvalue, self._chi2, self._chi2ndf, self._ndf = \
539  res_pvalue, res_chi2[0], res_chi2ndf[0], res_ndf[0]
540 
541  def _get_comparison_result_long(self) -> str:
542  if self._pvalue is None or self._chi2ndf is None or self._chi2 is None:
543  return r"Could not perform $\chi^2$-Test between {{revision1}} " \
544  r"and {{revision2}} due to an unknown error. Please " \
545  r"submit a bug report."
546 
547  return r'Performed $\chi^2$-Test between {{revision1}} ' \
548  r'and {{revision2}} ' \
549  r'($\chi^2$ = {chi2:.4f}; NDF = {ndf}; ' \
550  r'$\chi^2/\text{{{{NDF}}}}$ = {chi2ndf:.4f}).' \
551  r' <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, ' \
552  r'p-value error: {pvalue_error})'.format(
553  chi2=self._chi2, ndf=self._ndf, chi2ndf=self._chi2ndf,
554  pvalue=self._pvalue, pvalue_warn=self._pvalue_warn,
555  pvalue_error=self._pvalue_error
556  )
557 
558 # ------------------------------------------------------------------------------
559 # Kolmogorov Test
560 # ------------------------------------------------------------------------------
561 
562 
563 class KolmogorovTest(PvalueTest):
564  """ Kolmogorov-Smirnov Test """
565 
566  def __init__(self, *args, **kwargs):
567  """
568  Initialize Kolmogorov test.
569  @param args: See arguments of :class:`ComparisonBase`
570  @param kwargs: See arguments of :class:`ComparisonBase`
571  """
572  super().__init__(*args, **kwargs)
573 
574  def _compute(self):
575  """
576  Perform the actual test
577  @return: None
578  """
579  self._raise_has_correct_types()
580  self._raise_has_compatible_bins()
581 
582  local_object_a = self.object_a
583  local_object_b = self.object_b
584 
585  # very special handling for TEfficiencies
586  if self.object_a.ClassName() == "TEfficiency":
587  local_object_a = self._convert_teff_to_hist(self.object_a)
588  local_object_b = self._convert_teff_to_hist(self.object_b)
589  if self.debug:
590  print("Converting TEfficiency objects to histograms.")
591 
592  option_str = "UON"
593  if self.debug:
594  option_str += "D"
595 
596  self._pvalue = local_object_a.KolmogorovTest(local_object_b, option_str)
597 
598  def _get_comparison_result_long(self) -> str:
599  if self._pvalue is None:
600  return r"Could not perform Kolmogorov test between {{revision1}} " \
601  r"and {{revision2}} due to an unknown error. Please submit " \
602  r"a bug report."
603 
604  return r'Performed Komlogorov test between {{revision1}} ' \
605  r'and {{revision2}} ' \
606  r' <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, ' \
607  r'p-value error: {pvalue_error})'.format(
608  pvalue=self._pvalue, pvalue_warn=self._pvalue_warn,
609  pvalue_error=self._pvalue_error
610  )
611 
612 # ------------------------------------------------------------------------------
613 # Anderson Darling Test
614 # ------------------------------------------------------------------------------
615 
616 
617 class AndersonDarlingTest(PvalueTest):
618  """ Anderson-Darling test"""
619 
620  def __init__(self, *args, **kwargs):
621  """
622  Initialize Kolmogorov test.
623  @param args: See arguments of :class:`ComparisonBase`
624  @param kwargs: See arguments of :class:`ComparisonBase`
625  """
626  super().__init__(*args, **kwargs)
627 
628  def _compute(self):
629  """
630  Perform the actual test
631  @return: None
632  """
633  self._raise_has_correct_types()
634  # description on
635  # https://root.cern.ch/doc/master/classTH1.html#aa6b386786876dc304d73ab6b2606d4f6
636  # sounds like we don't have to have the same bins
637 
638  local_object_a = self.object_a
639  local_object_b = self.object_b
640 
641  # very special handling for TEfficiencies
642  if self.object_a.ClassName() == "TEfficiency":
643  local_object_a = self._convert_teff_to_hist(self.object_a)
644  local_object_b = self._convert_teff_to_hist(self.object_b)
645  if self.debug:
646  print("Converting TEfficiency objects to histograms.")
647 
648  option_str = ""
649  if self.debug:
650  option_str += "D"
651 
652  self._pvalue = local_object_a.KolmogorovTest(local_object_b, option_str)
653 
654  def _get_comparison_result_long(self) -> str:
655  if self._pvalue is None:
656  return r"Could not perform Anderson Darling test between " \
657  r"{{revision1}} and {{revision2}} due to an unknown error." \
658  r" Please support a bug report."
659 
660  return r'Performed Anderson Darling test between {{revision1}} ' \
661  r'and {{revision2}} ' \
662  r' <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, ' \
663  r'p-value error: {pvalue_error})'.format(
664  pvalue=self._pvalue, pvalue_warn=self._pvalue_warn,
665  pvalue_error=self._pvalue_error
666  )
667 
668 # ==============================================================================
669 # Helpers
670 # ==============================================================================
671 
672 
673 class TablePrinter(object):
674  """ A tiny class to print columns of fixed width numbers. """
675 
676  def __init__(self, ncols, width=None):
677  """
678  Constructor.
679  @param ncols: Number of columns
680  @param width: Width of each column. Either int or list.
681  """
682 
683  self.ncols = ncols
684  if not width:
685  width = 10
686  if isinstance(width, int):
687 
688  self.widths = [width] * ncols
689  elif isinstance(width, list) or isinstance(width, tuple):
690  # let's hope this is a list then.
691  self.widths = width
692 
693  @property
694  def tot_width(self):
695  """ Total width of the table """
696  width = 0
697  # the widths of each column
698  width += sum(self.widths)
699  # three characters between each two columns
700  width += (self.ncols - 1) * 3
701  # 2 characters at the very left and right
702  width += 2 * 2
703  return width
704 
705  def print_divider(self, char="="):
706  """ Print a divider made up from repeated chars """
707  print(char * self.tot_width)
708 
709  def print(self, cols):
710  """ Print one row """
711  assert(len(cols) == self.ncols)
712  out = []
713  for icol, col in enumerate(cols):
714  width = self.widths[icol]
715  if isinstance(col, int):
716  form = "{{:{}d}}".format(width)
717  out.append(form.format(col))
718  elif isinstance(col, float):
719  form = "{{:{}.{}f}}".format(width, width // 2)
720  out.append(form.format(col))
721  else:
722  # convert everything else to a string if it isn't already
723  col = str(col)
724  col = col[:width].rjust(width)
725  out.append(col)
726  print("| " + " | ".join(out) + " |")
727 
728 
729 def print_contents_and_errors(obj_a, obj_b):
730  """
731  Print contents, errors and chi2 deviation for each bin as well as
732  some other information about two TH1-like objects.
733  @param obj_a: First TH1-like object
734  @param obj_b: Second TH1-like object
735  @return: None
736  """
737  nbins = obj_a.GetNbinsX()
738 
739  total_a = sum([obj_a.GetBinContent(ibin) for ibin in range(0, nbins + 2)])
740  total_b = sum([obj_b.GetBinContent(ibin) for ibin in range(0, nbins + 2)])
741 
742  print(f"Total events/summed weights in object 1: {total_a:10.5f}")
743  print(f"Total events/summed weights in object 2: {total_b:10.5f}")
744 
745  chi2_tot = 0
746 
747  cp = TablePrinter(6)
748  print()
749  cp.print_divider()
750  cp.print(["ibin", "a", "err a", "b", "err b", "chi2"])
751  cp.print_divider()
752  for ibin in range(1, nbins + 1):
753  content_a = obj_a.GetBinContent(ibin)
754  content_b = obj_b.GetBinContent(ibin)
755  error_a = obj_a.GetBinError(ibin)
756  error_b = obj_b.GetBinError(ibin)
757  # This is implemented according to
758  # https://root.cern.ch/doc/master/classTH1.html
759  try:
760  chi2 = (total_b * content_a - total_a * content_b)**2 / \
761  (total_b**2 * error_a**2 + total_a**2 * error_b**2)
762  chi2_tot += chi2
763  except ZeroDivisionError:
764  chi2 = "nan"
765  cp.print([
766  ibin,
767  content_a,
768  error_a,
769  content_b,
770  error_b,
771  chi2
772  ])
773  cp.print_divider()
774  print()
775 
776  print(f"Total chi2: {chi2_tot:10.5f}")
777 
778 
779 # ==============================================================================
780 # Command Line Interface
781 # ==============================================================================
782 
783 def debug_cli():
784  """ A small command line interface for debugging purposes. """
785 
786  # 1. Get command line arguments
787  # =============================
788 
789  desc = "For testing purposes: Run the chi2 comparison with objects from " \
790  "two root files."
791  parser = argparse.ArgumentParser(desc)
792 
793  _ = "Rootfile to read the first object from"
794  parser.add_argument("rootfile_a", help=_)
795 
796  _ = "Name of object inside first rootfile."
797  parser.add_argument("name_a", help=_)
798 
799  _ = "Rootfile to read the second object from"
800  parser.add_argument("rootfile_b", help=_)
801 
802  _ = "Name of object inside second rootfile."
803  parser.add_argument("name_b", help=_)
804 
805  args = parser.parse_args()
806 
807  # 2. Open rootfiles and get objects
808  # =================================
809 
810  if not os.path.exists(args.rootfile_a):
811  raise ValueError(f"Could not find '{args.rootfile_a}'.")
812 
813  if not os.path.exists(args.rootfile_b):
814  raise ValueError(f"Could not find '{args.rootfile_b}'.")
815 
816  rootfile_a = ROOT.TFile(args.rootfile_a)
817  obj_a = rootfile_a.Get(args.name_a)
818  if not obj_a:
819  raise ValueError(
820  f"Could not find object '{args.name_a}' "
821  f"in file '{args.rootfile_a}'.")
822 
823  rootfile_b = ROOT.TFile(args.rootfile_b)
824  obj_b = rootfile_b.Get(args.name_b)
825  if not obj_b:
826  raise ValueError(
827  f"Could not find object '{args.name_b}' "
828  f"in file '{args.rootfile_b}'.")
829 
830  # 3. Performe testing with debug option
831  # =====================================
832 
833  test = Chi2Test(obj_a, obj_b, debug=True)
834  test.ensure_compute()
835 
836  print("If you see this message, then no exception was thrown.")
837 
838  # 4. Close files
839  # ==============
840 
841  rootfile_a.Close()
842  rootfile_b.Close()
843 
844 
845 if __name__ == "__main__":
846  # Run command line interface for testing purposes.
847  debug_cli()
848 
849 # End suppression of doxygen checks
850 # @endcond