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