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