Skip to content

Commit 538e4fa

Browse files
Benedikt Volkelchiarazampolli
authored andcommitted
Go to TH1::Chi2Test, small updates
* We are now using ROOT's TH1::Chi2Test, we don't have to re-invent the wheel. * Make test definition in Python script more dynamic and therefore easier (adds e.g. cmd flags automatically instead of hard-coded)
1 parent af7ae75 commit 538e4fa

2 files changed

Lines changed: 71 additions & 108 deletions

File tree

RelVal/ReleaseValidation.C

Lines changed: 37 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,36 @@ struct TestResult {
1717
};
1818

1919
// define the possible available tests
20-
enum options {
21-
CHI2 = 0x01,
22-
BINCONTNORM = 0x02,
23-
NENTRIES = 0x04,
20+
struct TestFlag {
21+
static constexpr int CHI2 = 0;
22+
static constexpr int BINCONTNORM = 1;
23+
static constexpr int NENTRIES = 2;
24+
static constexpr int LAST = NENTRIES;
2425
// ...
2526
};
2627

28+
bool shouldRunTest(int userTests, int flag)
29+
{
30+
return (userTests & (1 << flag)) > 0;
31+
}
32+
33+
int maxUserTests()
34+
{
35+
int maxTestNumber = 0;
36+
for (int i = 0; i <= TestFlag::LAST; i++) {
37+
maxTestNumber += (1 << i);
38+
}
39+
return maxTestNumber;
40+
}
41+
2742
// define a global epsilon
2843
double EPSILON = 0.00001;
2944

30-
void CompareHistos(TH1* hA, TH1* hB, int whichTest, bool firstComparison, bool finalComparison, std::unordered_map<std::string, std::vector<TestResult>>& allTests);
45+
void CompareHistos(TH1* hA, TH1* hB, int whichTests, bool firstComparison, bool finalComparison, std::unordered_map<std::string, std::vector<TestResult>>& allTests);
3146
void PlotOverlayAndRatio(TH1* hA, TH1* hB, TLegend& legend, TString& compLabel, int color);
3247
bool PotentiallySameHistograms(TH1*, TH1*);
3348
TestResult CompareChiSquare(TH1* hA, TH1* hB, bool areComparable);
49+
TestResult CompareChiSquareTH1(TH1* hA, TH1* hB, bool areComparable);
3450
TestResult CompareBinContent(TH1* hA, TH1* hB, bool areComparable);
3551
TestResult CompareNentr(TH1* hA, TH1* hB, bool areComparable);
3652
void DrawRatio(TH1* hR);
@@ -173,13 +189,13 @@ void PlotOverlayAndRatio(TH1* hA, TH1* hB, TLegend& legend, TString& compLabel,
173189
// 6) select if files have to be taken from the grid or not
174190
// 7) choose if specific critic plots have to be saved in a second .pdf file
175191

176-
void ReleaseValidation(std::string const& filename1, std::string const& filename2, int whichTest)
192+
void ReleaseValidation(std::string const& filename1, std::string const& filename2, int whichTests)
177193
{
178194
gROOT->SetBatch();
179195

180-
if (whichTest < 1 || whichTest > 7) {
181-
std::cerr << "ERROR: Please select which test you want to perform:\n"
182-
<< "1->Chi-square; 2--> ContBinDiff; 3 --> Chi-square+MeanDiff; 4->EntriesDiff; 5--> EntriesDiff + Chi2; 6 --> EntriesDiff + MeanDiff; 7 --> EntriesDiff + Chi2 + MeanDiff\n";
196+
auto maxTestNumber = maxUserTests();
197+
if (whichTests < 1 || whichTests > maxTestNumber) {
198+
std::cerr << "ERROR: Max test number is " << maxTestNumber << " to perform all tests. Otherwise please enable bits where the last possible bit is " << TestFlag::LAST << "\n";
183199
return;
184200
}
185201

@@ -188,16 +204,6 @@ void ReleaseValidation(std::string const& filename1, std::string const& filename
188204

189205
// prepare summary plots
190206
int nkeys = extractedFile1.GetNkeys();
191-
int nTests = 0;
192-
if ((whichTest & CHI2) == CHI2) {
193-
nTests++;
194-
}
195-
if ((whichTest & BINCONTNORM) == BINCONTNORM) {
196-
nTests++;
197-
}
198-
if ((whichTest & NENTRIES) == NENTRIES) {
199-
nTests++;
200-
}
201207

202208
// collect test results to store them as JSON later
203209
std::unordered_map<std::string, std::vector<TestResult>> allTestsMap;
@@ -237,7 +243,7 @@ void ReleaseValidation(std::string const& filename1, std::string const& filename
237243

238244
std::cout << "Comparing " << hA->GetName() << " and " << hB->GetName() << "\n";
239245

240-
CompareHistos(hA, hB, whichTest, isFirstComparison, isLastComparison, allTestsMap);
246+
CompareHistos(hA, hB, whichTests, isFirstComparison, isLastComparison, allTestsMap);
241247

242248
nComparisons++;
243249
if (nComparisons == 1)
@@ -351,7 +357,7 @@ bool CheckComparable(TH1* hA, TH1* hB)
351357
}
352358

353359
if (isEmptyA || isEmptyB) {
354-
std::cerr << "At least one of the histograms " << hA->GetName() << " is empty\n";
360+
std::cerr << "At least one of the histograms " << hA->GetName() << " is empty\n";
355361
return false;
356362
}
357363

@@ -370,7 +376,7 @@ void RegisterTestResult(std::unordered_map<std::string, std::vector<TestResult>>
370376
allTests[histogramName].push_back(testResult);
371377
}
372378

373-
void CompareHistos(TH1* hA, TH1* hB, int whichTest, bool firstComparison, bool finalComparison, std::unordered_map<std::string, std::vector<TestResult>>& allTests)
379+
void CompareHistos(TH1* hA, TH1* hB, int whichTests, bool firstComparison, bool finalComparison, std::unordered_map<std::string, std::vector<TestResult>>& allTests)
374380
{
375381

376382
double integralA = hA->Integral();
@@ -388,31 +394,28 @@ void CompareHistos(TH1* hA, TH1* hB, int whichTest, bool firstComparison, bool f
388394
TLegend legendOverlayPlot(0.6, 0.6, 0.9, 0.8);
389395
legendOverlayPlot.SetBorderSize(1);
390396

391-
// test if each of the 3 bits is turned on in subset ‘i = whichTest’?
397+
// test if each of the 3 bits is turned on in subset ‘i = whichTests’?
392398
// if yes, process the bit
393399

394-
if ((whichTest & CHI2) == CHI2) {
400+
if (shouldRunTest(whichTests, TestFlag::CHI2)) {
395401
auto testResult = CompareChiSquare(hA, hB, areComparable);
396402
RegisterTestResult(allTests, hA->GetName(), testResult);
397403
if (testResult.comparable) {
398-
legendOverlayPlot.AddEntry((TObject*)nullptr, Form("#chi^{2} / N_{bins} = %f", testResult.value), "");
404+
legendOverlayPlot.AddEntry((TObject*)nullptr, Form("#chi^{2} / N_{bins} = %f", testResult.value), "");
399405
}
400406
}
401407

402-
if ((whichTest & BINCONTNORM) == BINCONTNORM) {
408+
if (shouldRunTest(whichTests, TestFlag::BINCONTNORM)) {
403409
auto testResult = CompareBinContent(hA, hB, areComparable);
404410
RegisterTestResult(allTests, hA->GetName(), testResult);
405411
if (testResult.comparable) {
406412
legendOverlayPlot.AddEntry((TObject*)nullptr, Form("meandiff = %f", testResult.value), "");
407413
}
408414
}
409415

410-
if ((whichTest & NENTRIES) == NENTRIES) {
416+
if (shouldRunTest(whichTests, TestFlag::NENTRIES)) {
411417
auto testResult = CompareNentr(hA, hB, areComparable);
412418
RegisterTestResult(allTests, hA->GetName(), testResult);
413-
if (testResult.comparable) {
414-
legendOverlayPlot.AddEntry((TObject*)nullptr, Form("#chi^{2} / N_{bins} = %f", testResult.value), "");
415-
}
416419
if (testResult.comparable) {
417420
legendOverlayPlot.AddEntry((TObject*)nullptr, Form("entriesdiff = %f", testResult.value), "");
418421
}
@@ -429,67 +432,22 @@ void CompareHistos(TH1* hA, TH1* hB, int whichTest, bool firstComparison, bool f
429432
TestResult CompareChiSquare(TH1* hA, TH1* hB, bool areComparable)
430433
{
431434
TestResult res;
432-
res.testname = "test_chi2";
435+
res.testname = "chi2";
433436
if (!areComparable) {
434437
res.comparable = false;
435438
return res;
436439
}
437440

438-
double integralA = hA->Integral();
439-
double integralB = hB->Integral();
440-
double chi2 = 0;
441+
res.value = hA->Chi2Test(hB, "CHI2/NDF");
441442

442-
int nBins = 0;
443-
for (int ix = 1; ix <= hA->GetNbinsX(); ix++) {
444-
for (int iy = 1; iy <= hA->GetNbinsY(); iy++) {
445-
for (int iz = 1; iz <= hA->GetNbinsZ(); iz++) {
446-
double cA = hA->GetBinContent(ix, iy, iz);
447-
if (cA < 0) {
448-
std::cerr << "Negative counts!!! cA=" << cA << " in bin (" << ix << "," << iy << "," << iz << "\n";
449-
res.comparable = false;
450-
return res;
451-
}
452-
double cB = hB->GetBinContent(ix, iy, iz);
453-
if (cB < 0) {
454-
std::cerr << "Negative counts!!! cB=" << cB << " in bin (" << ix << "," << iy << "," << iz << "\n";
455-
res.comparable = false;
456-
return res;
457-
}
458-
double diff = cA * TMath::Sqrt(integralB / integralA) - cB * TMath::Sqrt(integralA / integralB);
459-
double correl = 0.;
460-
if (correlationCase == 1) {
461-
// estimate degree of correlation from number of events in histogram
462-
// assume that the histogram with less events is a subsample of that
463-
// with more events
464-
if ((cB > cA) && (cB > 0))
465-
correl = TMath::Sqrt(cA / cB);
466-
if ((cA > cB) && (cA > 0))
467-
correl = TMath::Sqrt(cB / cA);
468-
}
469-
double sigma2 = cA * cB - 2 * correl * TMath::Sqrt(cA) * TMath::Sqrt(cB); // maybe to be improved
470-
if (sigma2 > 0)
471-
chi2 += diff * diff / sigma2;
472-
if (cA > 0 || cB > 0) {
473-
nBins++;
474-
}
475-
}
476-
}
477-
}
478-
if (nBins > 0) {
479-
res.value = chi2 / nBins;
480-
std::cout << hA->GetName() << ": " << res.testname << " performed: chi2/nBins=" << res.value << "\n";
481-
return res;
482-
}
483-
484-
std::cerr << "Histogram with empty bins (" << hA->GetName() << ")\n";
485443
return res;
486444
}
487445

488446
// (normalized) difference of bin content
489447
TestResult CompareBinContent(TH1* hA, TH1* hB, bool areComparable)
490448
{
491449
TestResult res;
492-
res.testname = "test_bin_cont";
450+
res.testname = "bin_cont";
493451
if (!areComparable) {
494452
res.comparable = false;
495453
return res;
@@ -537,7 +495,7 @@ TestResult CompareBinContent(TH1* hA, TH1* hB, bool areComparable)
537495
TestResult CompareNentr(TH1* hA, TH1* hB, bool areComparable)
538496
{
539497
TestResult res;
540-
res.testname = "test_num_entries";
498+
res.testname = "num_entries";
541499
if (!areComparable) {
542500
res.comparable = false;
543501
return res;

RelVal/o2dpg_release_validation.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,12 @@
109109
REL_VAL_SEVERITIES_USE_SUMMARY = [True, False, False, True, True]
110110
REL_VAL_SEVERITY_MAP = {v: i for i, v in enumerate(REL_VAL_SEVERITIES)}
111111
REL_VAL_SEVERITY_COLOR_MAP = {"GOOD": "green", "WARNING": "orange", "NONCRIT_NC": "cornflowerblue", "CRIT_NC": "navy", "BAD": "red"}
112-
REL_VAL_TEST_NAMES = ["test_chi2", "test_bin_cont", "test_num_entries"]
112+
REL_VAL_TEST_NAMES = ["chi2", "bin_cont", "num_entries"]
113113
REL_VAL_TEST_NAMES_MAP = {v: i for i, v in enumerate(REL_VAL_TEST_NAMES)}
114114
REL_VAL_TEST_CRITICAL = [True, True, False]
115-
TEST_SUMMARY_NAME = "test_summary"
116-
REL_VAL_TEST_NAMES_SUMMARY = REL_VAL_TEST_NAMES + [TEST_SUMMARY_NAME]
115+
REL_VAL_TEST_DEFAULT_THRESHOLDS = [1.5, 1.5, 0.01]
116+
REL_VAL_TEST_SUMMARY_NAME = "summary"
117+
REL_VAL_TEST_NAMES_SUMMARY = REL_VAL_TEST_NAMES + [REL_VAL_TEST_SUMMARY_NAME]
117118
REL_VAL_TEST_NAMES_MAP_SUMMARY = {v: i for i, v in enumerate(REL_VAL_TEST_NAMES_SUMMARY)}
118119

119120
gROOT.SetBatch()
@@ -394,8 +395,8 @@ def plot_summary_grid(summary, flags, include_patterns, output_path):
394395
res = test["result"]
395396
ind = REL_VAL_TEST_NAMES_MAP_SUMMARY[test_name]
396397
if ind != len(REL_VAL_TEST_NAMES_MAP):
397-
value_annotaion = f"{test['value']:.2f}" if test["comparable"] else "---"
398-
collect_annotations_per_test[ind] = f"{test['threshold']:.2f}; {value_annotaion}"
398+
value_annotaion = f"{test['value']:.3f}" if test["comparable"] else "---"
399+
collect_annotations_per_test[ind] = f"{test['threshold']:.3f}; {value_annotaion}"
399400
collect_flags_per_test[ind] = REL_VAL_SEVERITY_MAP[res]
400401

401402
if not include_this:
@@ -433,6 +434,8 @@ def calc_thresholds(rel_val_dict, default_thresholds, margins_thresholds, args):
433434
this_histo_thresholds=[]
434435
for t in tests:
435436
test_name = t["test_name"]
437+
if test_name == REL_VAL_TEST_SUMMARY_NAME:
438+
continue
436439
these_thresholds={}
437440
these_thresholds["test_name"] = test_name
438441
these_thresholds["value"] = default_thresholds[test_name]
@@ -456,6 +459,8 @@ def calc_thresholds(rel_val_dict, default_thresholds, margins_thresholds, args):
456459
for t in tests:
457460
these_thresholds={}
458461
test_name = t["test_name"]
462+
if test_name == REL_VAL_TEST_SUMMARY_NAME:
463+
continue
459464
these_thresholds["test_name"] = test_name
460465
threshold_list = []
461466
for ut in user_thresholds:
@@ -493,19 +498,18 @@ def assign_result_flag(is_critical, comparable, passed):
493498
user_thresholds = {}
494499
this_summary = {}
495500

496-
default_thresholds = {"test_chi2": args.chi2_threshold,
497-
"test_bin_cont": args.rel_mean_diff_threshold,
498-
"test_num_entries": args.rel_entries_diff_threshold}
499-
margins_thresholds = {"test_chi2": args.chi2_threshold_margin,
500-
"test_bin_cont": args.rel_mean_diff_threshold_margin,
501-
"test_num_entries": args.rel_entries_diff_threshold_margin}
501+
default_thresholds = {t: getattr(args, f"{t}_threshold") for t in REL_VAL_TEST_NAMES}
502+
margins_thresholds = {t: getattr(args, f"{t}_threshold_margin") for t in REL_VAL_TEST_NAMES}
503+
test_enabled = {t: getattr(args, f"with_{t}") for t in REL_VAL_TEST_NAMES}
504+
if not any(test_enabled.values()):
505+
test_enabled = {t: True for t in test_enabled}
502506

503507
the_thresholds = calc_thresholds(rel_val_dict, default_thresholds, margins_thresholds, args)
504508
with open(join(output_dir, "used_thresholds.json"), "w") as f:
505509
json.dump(the_thresholds, f, indent=2)
506510

507511
for histo_name, tests in rel_val_dict.items():
508-
test_summary = {"test_name": TEST_SUMMARY_NAME,
512+
test_summary = {"test_name": REL_VAL_TEST_SUMMARY_NAME,
509513
"value": None,
510514
"threshold": None,
511515
"result": None}
@@ -514,7 +518,7 @@ def assign_result_flag(is_critical, comparable, passed):
514518
is_comparable_summary = True
515519
these_tests = []
516520
for t in tests:
517-
if t["test_name"] == TEST_SUMMARY_NAME:
521+
if t["test_name"] == REL_VAL_TEST_SUMMARY_NAME or not test_enabled[t["test_name"]]:
518522
continue
519523
test_name = t["test_name"]
520524
test_id = REL_VAL_TEST_NAMES_MAP[test_name]
@@ -525,7 +529,7 @@ def assign_result_flag(is_critical, comparable, passed):
525529
passed = True
526530
is_critical = REL_VAL_TEST_CRITICAL[test_id] or histo_name.find("_ratioFromTEfficiency") != -1
527531
if comparable:
528-
passed = t["value"] <= threshold
532+
passed = t["value"] <= threshold
529533

530534
t["result"] = assign_result_flag(is_critical, comparable, passed)
531535

@@ -634,7 +638,7 @@ def map_histos_to_severity(summary, include_patterns=None):
634638
# loop over tests done
635639
for test in tests:
636640
test_name = test["test_name"]
637-
if test_name != TEST_SUMMARY_NAME:
641+
if test_name != REL_VAL_TEST_SUMMARY_NAME:
638642
continue
639643
result = test["result"]
640644
test_n_hist_map[result].append(histo_name)
@@ -704,7 +708,7 @@ def rel_val_sim_dirs(args):
704708
if args.dir_config_enable:
705709
run_over_keys = [rok for rok in run_over_keys if rok in args.dir_config_enable]
706710
if args.dir_config_disable:
707-
run_over_keys = [rok for rok in run_over_keys if rok not in args.dir-dir_config_disable]
711+
run_over_keys = [rok for rok in run_over_keys if rok not in args.dir_config_disable]
708712
if not run_over_keys:
709713
print("WARNING: All keys in config disabled, nothing to do")
710714
return 0
@@ -734,9 +738,14 @@ def rel_val(args):
734738
print(f"NOTE: Extracted objects will be added to existing ones in case there was already a RelVal at {args.output}.\n")
735739
func = None
736740
# construct the bit mask
737-
args.test = 1 * args.with_test_chi2 + 2 * args.with_test_bincont + 4 * args.with_test_numentries
741+
args.test = 0
742+
default_sum = 0
743+
for i, t in enumerate(REL_VAL_TEST_NAMES):
744+
bit = 2**i
745+
args.test += bit * getattr(args, f"with_{t}")
746+
default_sum += bit
738747
if not args.test:
739-
args.test = 7
748+
args.test = default_sum
740749
if not exists(args.output):
741750
makedirs(args.output)
742751
if is_sim_dir(args.input1[0]) and is_sim_dir(args.input2[0]):
@@ -933,21 +942,17 @@ def main():
933942
common_file_parser.add_argument("-j", "--input2", nargs="*", help="EITHER second set of input files for comparison OR second input directory from simulation for comparison", required=True)
934943

935944
common_threshold_parser = argparse.ArgumentParser(add_help=False)
936-
common_threshold_parser.add_argument("--chi2-threshold", dest="chi2_threshold", type=float, help="Chi2 threshold", default=1.5)
937-
common_threshold_parser.add_argument("--rel-mean-diff-threshold", dest="rel_mean_diff_threshold", type=float, help="Threshold of relative difference in mean", default=1.5)
938-
common_threshold_parser.add_argument("--rel-entries-diff-threshold", dest="rel_entries_diff_threshold", type=float, help="Threshold of relative difference in number of entries", default=0.01)
939-
common_threshold_parser.add_argument("--use-values-as-thresholds", nargs="*", dest="use_values_as_thresholds", help="Use values from other runs as thresholds for this one")
940-
# The following only take effect for thresholds given via an input file
945+
common_threshold_parser.add_argument("--use-values-as-thresholds", nargs="*", dest="use_values_as_thresholds", help="Use values from another run as thresholds for this one")
941946
common_threshold_parser.add_argument("--combine-thresholds", dest="combine_thresholds", choices=["mean", "max"], help="Arithmetic mean or maximum is chosen as threshold value", default="mean")
942-
common_threshold_parser.add_argument("--chi2-threshold-margin", dest="chi2_threshold_margin", type=float, help="Margin to apply to the chi2 threshold extracted from file", default=1.0)
943-
common_threshold_parser.add_argument("--rel-mean-diff-threshold-margin", dest="rel_mean_diff_threshold_margin", type=float, help="Margin to apply to the rel_mean_diff threshold extracted from file", default=1.0)
944-
common_threshold_parser.add_argument("--rel-entries-diff-threshold-margin", dest="rel_entries_diff_threshold_margin", type=float, help="Margin to apply to the rel_entries_diff threshold extracted from file", default=1.0)
947+
for test, thresh in zip(REL_VAL_TEST_NAMES, REL_VAL_TEST_DEFAULT_THRESHOLDS):
948+
test_dahsed = test.replace("_", "-")
949+
common_threshold_parser.add_argument(f"--with-test-{test_dahsed}", dest=f"with_{test}", action="store_true", help=f"run {test} test")
950+
common_threshold_parser.add_argument(f"--test-{test_dahsed}-threshold", dest=f"{test}_threshold", type=float, help=f"{test} threshold", default=thresh)
951+
# The following only take effect for thresholds given via an input file
952+
common_threshold_parser.add_argument(f"--test-{test_dahsed}-threshold-margin", dest=f"{test}_threshold_margin", type=float, help=f"Margin to apply to the {test} threshold extracted from file", default=1.0)
945953

946954
sub_parsers = parser.add_subparsers(dest="command")
947955
rel_val_parser = sub_parsers.add_parser("rel-val", parents=[common_file_parser, common_threshold_parser])
948-
rel_val_parser.add_argument("--with-test-chi2", dest="with_test_chi2", action="store_true", help="run chi2 test")
949-
rel_val_parser.add_argument("--with-test-bincont", dest="with_test_bincont", action="store_true", help="run bin-content test")
950-
rel_val_parser.add_argument("--with-test-numentries", dest="with_test_numentries", action="store_true", help="run number-of-entries test")
951956
rel_val_parser.add_argument("--dir-config", dest="dir_config", help="What to take into account in a given directory")
952957
rel_val_parser.add_argument("--dir-config-enable", dest="dir_config_enable", nargs="*", help="only enable these top keys in your dir-config")
953958
rel_val_parser.add_argument("--dir-config-disable", dest="dir_config_disable", nargs="*", help="disable these top keys in your dir-config (precedence over dir-config-enable)")

0 commit comments

Comments
 (0)