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. """
16from abc
import ABC, abstractmethod
20from typing
import Optional, Any
26from metaoptions
import MetaOptionParser
36 The comparison failed for some reason. For example
37 because ROOT was not able to compute the Chi^2 properly
43 The type and/or combination of provided ROOT objects
44 is not supported for comparison
50 The two ROOT objects provided have a different bin count
51 and therefore, cannot be compared using the Chi2 test
57 Not sufficient bins to perform the Chi^2 test
67 object_1, object_2, mop: Optional[MetaOptionParser] =
None,
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
77 mop = MetaOptionParser()
78 if mop.has_option(
"kolmogorov"):
79 tester: Any = KolmogorovTest
80 elif mop.has_option(
"andersondarling"):
81 tester = AndersonDarlingTest
85 test = tester(object_1, object_2, mop=mop)
97 Base class for all comparison implementations.
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
106 2. The Comparison class saves the ROOT objects and the metaoptions
107 internally, but does not compute anything yet
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.
113 4. :meth:`_compute` ensures that all values, like chi2, p-value etc. are
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.
125 mop: Optional[MetaOptionParser] =
None,
129 Initialize ComparisonBase class
133 :param mop: MetaOptionParser
134 :param debug (bool): Debug mode enabled?
161 Ensure all required quantities get computed and are cached inside the
167 if self.
mop.has_option(
"nocompare"):
172 fail_message =
"Comparison failed: "
177 except ObjectsNotSupported
as e:
179 except DifferingBinCount
as e:
182 except TooFewBins
as e:
184 except ComparisonFailed
as e:
187 except Exception
as e:
190 "Unknown error occurred. Please "
191 "submit a bug report. " + str(e)
203 """ Used to format the value of :attr:`_comparison_result`. """
207 """ Used to format the value of :attr:`_comparison_result_long`. """
211 """ Comparison result, i.e. pass/warning/error """
217 """ Longer description of the comparison result """
223 """ This method performs the actual computations. """
227 @return: True if the two objects can be compared, False otherwise
229 return self._has_correct_types()
and self._has_compatible_bins()
233 @return: True if the two objects have a) a type supported for
234 comparison and b) can be compared with each other
241 supported_types = [
"TProfile",
"TH1D",
"TH1F",
"TEfficiency"]
244 if self.
object_a.ClassName()
not in supported_types:
247 if self.
object_a.ClassName() ==
"TEfficiency":
249 if self.
object_a.GetDimension() > 1:
256 Raise Exception if not the two objects have a) a type supported for
257 comparison and b) can be compared with each other
262 "Comparison of {} (Type {}) with {} (Type {}) not "
263 "supported.\nPlease open a GitLab issue (validation "
264 "label) if you need this supported. "
277 Check if both ROOT objects have the same amount of bins
278 @return: True if the bins are equal, otherwise False
285 nbins_a = self.
object_a.GetTotalHistogram().GetNbinsX()
286 nbins_b = self.
object_b.GetTotalHistogram().GetNbinsX()
291 return nbins_a == nbins_b
295 Raise Exception if not both ROOT objects have the same amount of bins
300 "The objects have differing x bin count: {} has {} vs. {} "
315 Convert the content of a TEfficiency plot to a histogram and set
316 the bin content and errors
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()
324 teff_a.GetName() +
"root_conversion",
332 for i
in range(1, xbin_count):
333 th1.SetBinContent(i, teff_a.GetEfficiency(i))
334 th1.SetBinError(i, teff_a.GetEfficiencyErrorLow(i))
340 """ Test with a pvalue """
344 _default_pvalue_warn = 0.99
348 _default_pvalue_error = 0.01
351 """ Initialize Pvalue test
354 *args: Positional arguments to ComparisonBase
355 **kwargs: Keyword arguments to ComparisonBase
399class Chi2Test(PvalueTest):
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.
411 :param args: See arguments of :class:`ComparisonBase`
412 :param kwargs: See arguments of :class:`ComparisonBase`
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
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:
436 a.SetBinContent(ibin, 0.0)
437 b.SetBinContent(ibin, 0.0)
440 f
"DEBUG: Warning: Setting bin content of bin {ibin} to zero for both histograms, because both " +
441 "histograms have vanishing errors there.")
445 Performs the actual Chi^2 test
455 if self.
object_a.ClassName() ==
"TEfficiency":
459 print(
"Converting TEfficiency objects to histograms.")
461 nbins = local_object_a.GetNbinsX()
465 f
"{nbins} bin(s) is too few to perform the Chi2 test."
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
474 first_obj = local_object_a.Clone()
475 second_obj = local_object_b.Clone()
477 if comp_weight_a
and not comp_weight_b:
480 first_obj, second_obj = second_obj, first_obj
483 "Debug: Warning: Switching the two objects, because "
484 "ROOT can only have the first one to be unweighted"
489 if comp_weight_a
and comp_weight_b:
491 elif comp_weight_a
or comp_weight_b:
496 if comp_weight_a
and comp_weight_b:
500 res_chi2 = numpy.array([1], numpy.float64)
501 res_igood = numpy.array([1], numpy.int32)
502 res_ndf = numpy.array([1], numpy.int32)
504 res_pvalue = first_obj.Chi2TestX(
505 second_obj, res_chi2, res_ndf, res_igood, comp_options
509 print(
"Performing our own chi2 test, with bin-by-bin results: ")
511 print_contents_and_errors(first_obj, second_obj)
514 f
"Here's what ROOT's Chi2Test gave us (comp_options: '{comp_options}'): "
520 tp.print([
"Key",
"Value",
"Comment"])
525 numpy.asscalar(res_chi2),
526 "Should roughly match above 'Total chi2'",
529 tp.print([
"ndf", numpy.asscalar(res_ndf),
"#Non-empty bins - 1"])
530 tp.print([
"chi2/ndf", numpy.asscalar(res_chi2 / res_ndf),
""])
534 numpy.asscalar(res_igood),
535 "a debug indicator, 0 if all good",
538 tp.print([
"pvalue", res_pvalue,
""])
542 "See https://root.cern.ch/doc/master/classTH1.html for more "
548 raise TooFewBins(
"res_ndf (<1) is too few to perform the Chi2 test.")
550 res_chi2ndf = res_chi2 / res_ndf
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."
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(
590 """ Kolmogorov-Smirnov Test """
594 Initialize Kolmogorov test.
595 @param args: See arguments of :class:`ComparisonBase`
596 @param kwargs: See arguments of :class:`ComparisonBase`
602 Perform the actual test
612 if self.
object_a.ClassName() ==
"TEfficiency":
616 print(
"Converting TEfficiency objects to histograms.")
622 self.
_pvalue = local_object_a.KolmogorovTest(local_object_b, option_str)
627 r"Could not perform Kolmogorov test between {{revision1}} "
628 r"and {{revision2}} due to an unknown error. Please submit "
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(
650 """ Anderson-Darling test"""
654 Initialize Anderson-Darling test.
655 @param args: See arguments of :class:`ComparisonBase`
656 @param kwargs: See arguments of :class:`ComparisonBase`
662 Perform the actual test
674 if self.
object_a.ClassName() ==
"TEfficiency":
678 print(
"Converting TEfficiency objects to histograms.")
684 self.
_pvalue = local_object_a.AndersonDarlingTest(
685 local_object_b, option_str
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."
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(
714 """ A tiny class to print columns of fixed width numbers. """
719 @param ncols: Number of columns
720 @param width: Width of each column. Either int or list.
726 if isinstance(width, int):
729 elif isinstance(width, list)
or isinstance(width, tuple):
735 """ Total width of the table """
740 width += (self.
ncols - 1) * 3
746 """ Print a divider made up from repeated chars """
752 """ Print one row """
753 assert len(cols) == self.
ncols
755 for icol, col
in enumerate(cols):
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))
766 col = col[:width].rjust(width)
768 print(
"| " +
" | ".join(out) +
" |")
771def print_contents_and_errors(obj_a, obj_b):
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
779 nbins = obj_a.GetNbinsX()
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)])
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}")
792 cp.print([
"ibin",
"a",
"err a",
"b",
"err b",
"chi2"])
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)
802 chi2 = (total_b * content_a - total_a * content_b) ** 2 / (
803 total_b ** 2 * error_a ** 2 + total_a ** 2 * error_b ** 2
806 except ZeroDivisionError:
808 cp.print([ibin, content_a, error_a, content_b, error_b, chi2])
812 print(f
"Total chi2: {chi2_tot:10.5f}")
821 """ A small command line interface for debugging purposes. """
827 "For testing purposes: Run the chi2 comparison with objects from "
830 parser = argparse.ArgumentParser(desc)
832 _ =
"Rootfile to read the first object from"
833 parser.add_argument(
"rootfile_a", help=_)
835 _ =
"Name of object inside first rootfile."
836 parser.add_argument(
"name_a", help=_)
838 _ =
"Rootfile to read the second object from"
839 parser.add_argument(
"rootfile_b", help=_)
841 _ =
"Name of object inside second rootfile."
842 parser.add_argument(
"name_b", help=_)
844 args = parser.parse_args()
849 if not os.path.exists(args.rootfile_a):
850 raise ValueError(f
"Could not find '{args.rootfile_a}'.")
852 if not os.path.exists(args.rootfile_b):
853 raise ValueError(f
"Could not find '{args.rootfile_b}'.")
855 rootfile_a = ROOT.TFile(args.rootfile_a)
856 obj_a = rootfile_a.Get(args.name_a)
859 f
"Could not find object '{args.name_a}' "
860 f
"in file '{args.rootfile_a}'."
863 rootfile_b = ROOT.TFile(args.rootfile_b)
864 obj_b = rootfile_b.Get(args.name_b)
867 f
"Could not find object '{args.name_b}' "
868 f
"in file '{args.rootfile_b}'."
874 test =
Chi2Test(obj_a, obj_b, debug=
True)
875 test.ensure_compute()
877 print(
"If you see this message, then no exception was thrown.")
886if __name__ ==
"__main__":
__init__(self, *args, **kwargs)
str _get_comparison_result_long(self)
_chi2ndf
chi2 / number of degrees of freedom
__init__(self, *args, **kwargs)
_ensure_zero_error_has_no_content(self, a, b)
str _get_comparison_result_long(self)
tuple _ndf
number of degrees of freedom
object_b
store the second object to compare
_convert_teff_to_hist(teff_a)
None _raise_has_compatible_bins(self)
object_a
store the first object to compare
str _get_comparison_result_long(self)
str _get_comparison_result(self)
str _comparison_result_long
Longer description of the comparison result (e.g.
str _comparison_result
Comparison result, i.e.
None _raise_has_correct_types(self)
bool _has_compatible_bins(self)
bool computed
used to store, whether the quantities have already been compared
bool _has_correct_types(self)
comparison_result_long(self)
__init__(self, object_a, object_b, Optional[MetaOptionParser] mop=None, debug=False)
__init__(self, *args, **kwargs)
str _get_comparison_result_long(self)
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)
__init__(self, *args, **kwargs)
str _get_comparison_result(self)
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)
_get_comparison_result_long(self)
ncols
the number of columns
__init__(self, ncols, width=None)
print_divider(self, char="=")
list widths
width of each column