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