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 msg = (
553 "Comparison failed, no Chi^2 could be computed. For "
554 "debugging, you can use the CLI of "
555 "'validation/scripts/validationcomparison.py' on your root "
556 "file and the reference. Run 'validationcomparison.py "
557 "--help' for info. If problem persists, please open "
558 "GitLab issue (validation label)."
559 )
560 raise ComparisonFailed(msg)
561
562 res_chi2ndf = res_chi2 / res_ndf
563
564 self._pvalue, self._chi2, self._chi2ndf, self._ndf = (
565 res_pvalue,
566 res_chi2[0],
567 res_chi2ndf[0],
568 res_ndf[0],
569 )
570
571 def _get_comparison_result_long(self) -> str:
572 if self._pvalue is None or self._chi2ndf is None or self._chi2 is None:
573 return (
574 r"Could not perform $\chi^2$-Test between {{revision1}} "
575 r"and {{revision2}} due to an unknown error. Please "
576 r"submit a bug report."
577 )
578
579 return (
580 r"Performed $\chi^2$-Test between {{revision1}} "
581 r"and {{revision2}} "
582 r"($\chi^2$ = {chi2:.4f}; NDF = {ndf}; "
583 r"$\chi^2/\text{{{{NDF}}}}$ = {chi2ndf:.4f})."
584 r" <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, "
585 r"p-value error: {pvalue_error})".format(
586 chi2=self._chi2,
587 ndf=self._ndf,
588 chi2ndf=self._chi2ndf,
589 pvalue=self._pvalue,
590 pvalue_warn=self._pvalue_warn,
591 pvalue_error=self._pvalue_error,
592 )
593 )
594
595
596# ------------------------------------------------------------------------------
597# Kolmogorov Test
598# ------------------------------------------------------------------------------
599
600
601class KolmogorovTest(PvalueTest):
602 """ Kolmogorov-Smirnov Test """
603
604 def __init__(self, *args, **kwargs):
605 """
606 Initialize Kolmogorov test.
607 @param args: See arguments of :class:`ComparisonBase`
608 @param kwargs: See arguments of :class:`ComparisonBase`
609 """
610 super().__init__(*args, **kwargs)
611
612 def _compute(self):
613 """
614 Perform the actual test
615 @return: None
616 """
617 self._raise_has_correct_types()
618 self._raise_has_compatible_bins()
619
620 local_object_a = self.object_a
621 local_object_b = self.object_b
622
623 # very special handling for TEfficiencies
624 if self.object_a.ClassName() == "TEfficiency":
625 local_object_a = self._convert_teff_to_hist(self.object_a)
626 local_object_b = self._convert_teff_to_hist(self.object_b)
627 if self.debug:
628 print("Converting TEfficiency objects to histograms.")
629
630 option_str = "UON"
631 if self.debug:
632 option_str += "D"
633
634 self._pvalue = local_object_a.KolmogorovTest(local_object_b, option_str)
635
636 def _get_comparison_result_long(self) -> str:
637 if self._pvalue is None:
638 return (
639 r"Could not perform Kolmogorov test between {{revision1}} "
640 r"and {{revision2}} due to an unknown error. Please submit "
641 r"a bug report."
642 )
643
644 return (
645 r"Performed Kolmogorov test between {{revision1}} "
646 r"and {{revision2}} "
647 r" <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, "
648 r"p-value error: {pvalue_error})".format(
649 pvalue=self._pvalue,
650 pvalue_warn=self._pvalue_warn,
651 pvalue_error=self._pvalue_error,
652 )
653 )
654
655
656# ------------------------------------------------------------------------------
657# Anderson-Darling Test
658# ------------------------------------------------------------------------------
659
660
661class AndersonDarlingTest(PvalueTest):
662 """ Anderson-Darling test"""
663
664 def __init__(self, *args, **kwargs):
665 """
666 Initialize Anderson-Darling test.
667 @param args: See arguments of :class:`ComparisonBase`
668 @param kwargs: See arguments of :class:`ComparisonBase`
669 """
670 super().__init__(*args, **kwargs)
671
672 def _compute(self):
673 """
674 Perform the actual test
675 @return: None
676 """
677 self._raise_has_correct_types()
678 # description on
679 # https://root.cern.ch/doc/master/classTH1.html#aa6b386786876dc304d73ab6b2606d4f6
680 # sounds like we don't have to have the same bins
681
682 local_object_a = self.object_a
683 local_object_b = self.object_b
684
685 # very special handling for TEfficiencies
686 if self.object_a.ClassName() == "TEfficiency":
687 local_object_a = self._convert_teff_to_hist(self.object_a)
688 local_object_b = self._convert_teff_to_hist(self.object_b)
689 if self.debug:
690 print("Converting TEfficiency objects to histograms.")
691
692 option_str = ""
693 if self.debug:
694 option_str += "D"
695
696 self._pvalue = local_object_a.AndersonDarlingTest(
697 local_object_b, option_str
698 )
699
700 def _get_comparison_result_long(self) -> str:
701 if self._pvalue is None:
702 return (
703 r"Could not perform-Anderson Darling test between "
704 r"{{revision1}} and {{revision2}} due to an unknown error."
705 r" Please support a bug report."
706 )
707
708 return (
709 r"Performed Anderson-Darling test between {{revision1}} "
710 r"and {{revision2}} "
711 r" <b>p-value: {pvalue:.6f}</b> (p-value warn: {pvalue_warn}, "
712 r"p-value error: {pvalue_error})".format(
713 pvalue=self._pvalue,
714 pvalue_warn=self._pvalue_warn,
715 pvalue_error=self._pvalue_error,
716 )
717 )
718
719
720# ==============================================================================
721# Helpers
722# ==============================================================================
723
724
725class TablePrinter:
726 """ A tiny class to print columns of fixed width numbers. """
727
728 def __init__(self, ncols, width=None):
729 """
730 Constructor.
731 @param ncols: Number of columns
732 @param width: Width of each column. Either int or list.
733 """
734
735 self.ncols = ncols
736 if not width:
737 width = 10
738 if isinstance(width, int):
739
740 self.widths = [width] * ncols
741 elif isinstance(width, list) or isinstance(width, tuple):
742 # let's hope this is a list then.
743 self.widths = width
744
745 @property
746 def tot_width(self):
747 """ Total width of the table """
748 width = 0
749 # the widths of each column
750 width += sum(self.widths)
751 # three characters between each two columns
752 width += (self.ncols - 1) * 3
753 # 2 characters at the very left and right
754 width += 2 * 2
755 return width
756
757 def print_divider(self, char="="):
758 """ Print a divider made up from repeated chars """
759 print(char * self.tot_width)
760
761 def print(self, cols):
762 """ Print one row """
763 assert len(cols) == self.ncols
764 out = []
765 for icol, col in enumerate(cols):
766 width = self.widths[icol]
767 if isinstance(col, int):
768 form = f"{{:{width}d}}"
769 out.append(form.format(col))
770 elif isinstance(col, float):
771 form = f"{{:{width}.{width // 2}f}}"
772 out.append(form.format(col))
773 else:
774 # convert everything else to a string if it isn't already
775 col = str(col)
776 col = col[:width].rjust(width)
777 out.append(col)
778 print("| " + " | ".join(out) + " |")
779
780
781def print_contents_and_errors(obj_a, obj_b):
782 """
783 Print contents, errors and chi2 deviation for each bin as well as
784 some other information about two TH1-like objects.
785 @param obj_a: First TH1-like object
786 @param obj_b: Second TH1-like object
787 @return: None
788 """
789 nbins = obj_a.GetNbinsX()
790
791 total_a = sum([obj_a.GetBinContent(ibin) for ibin in range(0, nbins + 2)])
792 total_b = sum([obj_b.GetBinContent(ibin) for ibin in range(0, nbins + 2)])
793
794 print(f"Total events/summed weights in object 1: {total_a:10.5f}")
795 print(f"Total events/summed weights in object 2: {total_b:10.5f}")
796
797 chi2_tot = 0
798
799 cp = TablePrinter(6)
800 print()
801 cp.print_divider()
802 cp.print(["ibin", "a", "err a", "b", "err b", "chi2"])
803 cp.print_divider()
804 for ibin in range(1, nbins + 1):
805 content_a = obj_a.GetBinContent(ibin)
806 content_b = obj_b.GetBinContent(ibin)
807 error_a = obj_a.GetBinError(ibin)
808 error_b = obj_b.GetBinError(ibin)
809 # This is implemented according to
810 # https://root.cern.ch/doc/master/classTH1.html
811 try:
812 chi2 = (total_b * content_a - total_a * content_b) ** 2 / (
813 total_b ** 2 * error_a ** 2 + total_a ** 2 * error_b ** 2
814 )
815 chi2_tot += chi2
816 except ZeroDivisionError:
817 chi2 = "nan"
818 cp.print([ibin, content_a, error_a, content_b, error_b, chi2])
819 cp.print_divider()
820 print()
821
822 print(f"Total chi2: {chi2_tot:10.5f}")
823
824
825# ==============================================================================
826# Command Line Interface
827# ==============================================================================
828
829
830def debug_cli():
831 """ A small command line interface for debugging purposes. """
832
833 # 1. Get command line arguments
834 # =============================
835
836 desc = (
837 "For testing purposes: Run the chi2 comparison with objects from "
838 "two root files."
839 )
840 parser = argparse.ArgumentParser(desc)
841
842 _ = "Rootfile to read the first object from"
843 parser.add_argument("rootfile_a", help=_)
844
845 _ = "Name of object inside first rootfile."
846 parser.add_argument("name_a", help=_)
847
848 _ = "Rootfile to read the second object from"
849 parser.add_argument("rootfile_b", help=_)
850
851 _ = "Name of object inside second rootfile."
852 parser.add_argument("name_b", help=_)
853
854 args = parser.parse_args()
855
856 # 2. Open rootfiles and get objects
857 # =================================
858
859 if not os.path.exists(args.rootfile_a):
860 raise ValueError(f"Could not find '{args.rootfile_a}'.")
861
862 if not os.path.exists(args.rootfile_b):
863 raise ValueError(f"Could not find '{args.rootfile_b}'.")
864
865 rootfile_a = ROOT.TFile(args.rootfile_a)
866 obj_a = rootfile_a.Get(args.name_a)
867 if not obj_a:
868 raise ValueError(
869 f"Could not find object '{args.name_a}' "
870 f"in file '{args.rootfile_a}'."
871 )
872
873 rootfile_b = ROOT.TFile(args.rootfile_b)
874 obj_b = rootfile_b.Get(args.name_b)
875 if not obj_b:
876 raise ValueError(
877 f"Could not find object '{args.name_b}' "
878 f"in file '{args.rootfile_b}'."
879 )
880
881 # 3. Perform testing with debug option
882 # ====================================
883
884 test = Chi2Test(obj_a, obj_b, debug=True)
885 test.ensure_compute()
886
887 print("If you see this message, then no exception was thrown.")
888
889 # 4. Close files
890 # ==============
891
892 rootfile_a.Close()
893 rootfile_b.Close()
894
895
896if __name__ == "__main__":
897 # Run command line interface for testing purposes.
898 debug_cli()
899
900# End suppression of doxygen checks
901# @endcond