17 from typing
import Dict, Any, List, Union, Optional
19 from multiprocessing
import Queue
30 from ROOT
import RooFit
36 from basf2
import B2ERROR
38 from validationplotuple
import Plotuple
39 from validationfunctions
import (
45 import validationfunctions
47 from validationrootobject
import RootObject
52 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
53 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
55 sys.exit(
"Error: No basf2 release set up!")
57 pp = pprint.PrettyPrinter(depth=6, indent=1, width=80)
65 def date_from_revision(
66 revision: str, work_folder: str
67 ) -> Optional[Union[int, float]]:
69 Takes the name of a revision and returns the 'last modified'-timestamp of
70 the corresponding directory, which holds the revision.
71 :param revision: A string containing the name of a revision
72 :return: The 'last modified'-timestamp of the folder which holds the
78 if revision ==
"reference":
84 if revision
in revisions:
85 return os.path.getmtime(
93 def merge_nested_list_dicts(a, b):
94 """ Given two nested dictionary with same depth that contain lists, return
95 'merged' dictionary that contains the joined lists.
96 :param a: Dict[Dict[...[Dict[List]]..]]
97 :param b: Dict[Dict[...[Dict[List]]..]] (same depth as a)
101 def _merge_nested_list_dicts(_a, _b):
102 """ Merge _b into _a, return _a. """
105 if isinstance(_a[key], dict)
and isinstance(_b[key], dict):
106 _merge_nested_list_dicts(_a[key], _b[key])
108 assert isinstance(_a[key], list)
109 assert isinstance(_b[key], list)
110 _a[key].extend(_b[key])
115 return _merge_nested_list_dicts(a.copy(), b.copy())
119 revisions: List[str], work_folder: str
120 ) -> Dict[str, Dict[str, List[str]]]:
122 Returns a list of all plot files as absolute paths. For this purpose,
123 it loops over all revisions in 'revisions', finds the
124 corresponding results folder and collects the plot ROOT files.
125 :param revisions: Name of the revisions.
126 :param work_folder: Folder that contains the results/ directory
127 :return: plot files, i.e. plot ROOT files from the
128 requested revisions as dictionary
129 {revision: {package: [root files]}}
132 results = collections.defaultdict(
lambda: collections.defaultdict(list))
138 for revision
in revisions:
140 if revision ==
"reference":
141 results[
"reference"] = collections.defaultdict(
142 list, get_tracked_reference_files()
146 rev_result_folder = os.path.join(results_foldername, revision)
147 if not os.path.isdir(rev_result_folder):
150 packages = os.listdir(rev_result_folder)
152 for package
in packages:
153 package_folder = os.path.join(rev_result_folder, package)
155 root_files = glob.glob(package_folder +
"/*.root")
157 results[revision][package].extend(
158 [os.path.abspath(rf)
for rf
in root_files]
164 def get_tracked_reference_files() -> Dict[str, List[str]]:
166 This function loops over the local and central release dir and collects
167 the .root-files from the validation-subfolders of the packages. These are
168 the files which we will use as references.
169 From the central release directory, we collect the files from the release
170 which is set up on the machine running this script.
171 :return: ROOT files that are located
172 in the same folder as the steering files of the package as
173 {package: [list of root files]}
178 "local": os.environ.get(
"BELLE2_LOCAL_DIR",
None),
179 "central": os.environ.get(
"BELLE2_RELEASE_DIR",
None),
184 "local": collections.defaultdict(list),
185 "central": collections.defaultdict(list),
190 validation_folder_name =
"validation"
191 validation_test_folder_name =
"validation-test"
194 for location
in [
"local",
"central"]:
198 if basepaths[location]
is None:
202 root = basepaths[location]
204 packages = os.listdir(root)
206 for package
in packages:
209 glob_search = os.path.join(
210 root, package, validation_folder_name,
"*.root"
212 results[location][package].extend(
215 for f
in glob.glob(glob_search)
221 if package ==
"validation":
222 glob_search = os.path.join(
223 root, package, validation_test_folder_name,
"*.root"
225 results[location][validation_test_folder_name].extend(
228 for f
in glob.glob(glob_search)
236 for package, local_files
in results[
"local"].items():
237 for local_file
in local_files:
239 local_path = local_file.replace(basepaths[
"local"],
"")
241 for central_file
in results[
"central"][package]:
244 central_path = central_file.replace(basepaths[
"central"],
"")
247 if local_path == central_path:
248 results[
"central"][package].remove(central_file)
256 package: results[
"central"][package] + results[
"local"][package]
257 for package
in list(results[
"central"].keys())
258 + list(results[
"central"].keys())
264 def generate_new_plots(
265 revisions: List[str],
267 process_queue: Optional[Queue] =
None,
268 root_error_ignore_level=ROOT.kWarning,
271 Creates the plots that contain the requested revisions. Each plot (or
272 n-tuple, for that matter) is stored in an object of class Plot.
274 @param work_folder: Folder containing results
275 @param process_queue: communication queue object, which is used in
276 multi-processing mode to report the progress of the plot creating.
277 @param root_error_ignore_level: Value for gErrorIgnoreLevel. Default:
278 ROOT.kWarning. If set to None, global level will be left unchanged.
279 @return: No return value
284 "Creating plots for the revision(s) " +
", ".join(revisions) +
"."
289 ROOT.gROOT.SetBatch()
290 ROOT.gStyle.SetOptStat(1110)
291 ROOT.gStyle.SetOptFit(101)
294 if root_error_ignore_level
is not None:
295 ROOT.gErrorIgnoreLevel = root_error_ignore_level
304 if len(revisions) == 0:
306 "No revisions selected for plotting. Returning without "
312 plot_files = get_plot_files(revisions[1:], work_folder)
313 reference_files = get_plot_files(revisions[:1], work_folder)
320 plot_packages = set()
321 only_tracked_reference = set(plot_files.keys()) | set(
322 reference_files.keys()
324 for results
in [plot_files, reference_files]:
326 if rev ==
"reference" and not only_tracked_reference:
328 for package
in results[rev]:
329 if results[rev][package]:
330 plot_packages.add(package)
333 plot_p2f2k2o = rootobjects_from_files(
334 plot_files, is_reference=
False, work_folder=work_folder
336 reference_p2f2k2o = rootobjects_from_files(
337 reference_files, is_reference=
True, work_folder=work_folder
341 for package
in set(plot_p2f2k2o.keys()) - plot_packages:
342 del plot_p2f2k2o[package]
343 for package
in set(reference_p2f2k2o.keys()) - plot_packages:
344 del reference_p2f2k2o[package]
346 all_p2f2k2o = merge_nested_list_dicts(plot_p2f2k2o, reference_p2f2k2o)
351 work_folder, revisions
354 work_folder, revisions
357 if not os.path.exists(content_dir):
358 os.makedirs(content_dir)
360 comparison_packages = []
366 for i, package
in enumerate(sorted(list(plot_packages))):
371 f
"Creating plots for package: {package}", level=1
379 for rootfile
in sorted(all_p2f2k2o[package].keys()):
380 file_name, file_ext = os.path.splitext(rootfile)
384 print(f
"Creating plots for file: {rootfile}")
392 process_queue.put_nowait(
394 "current_package": i,
395 "total_package": len(plot_packages),
397 "package_name": package,
398 "file_name": file_name,
410 compare_html_content = []
411 has_reference =
False
413 root_file_meta_data = collections.defaultdict(
lambda:
None)
415 for key
in all_p2f2k2o[package][rootfile].keys():
417 all_p2f2k2o[package][rootfile][key], revisions, work_folder
419 plotuple.create_plotuple()
420 plotuples.append(plotuple)
421 has_reference = plotuple.has_reference()
423 if plotuple.type ==
"TNtuple":
424 compare_ntuples.append(plotuple.create_json_object())
425 elif plotuple.type ==
"TNamed":
426 compare_html_content.append(plotuple.create_json_object())
427 elif plotuple.type ==
"meta":
428 meta_key, meta_value = plotuple.get_meta_information()
429 root_file_meta_data[meta_key] = meta_value
431 compare_plots.append(plotuple.create_json_object())
437 compared_revisions=revisions,
439 has_reference=has_reference,
440 ntuples=compare_ntuples,
441 html_content=compare_html_content,
442 description=root_file_meta_data[
"description"],
444 compare_files.append(compare_file)
446 all_plotuples.extend(plotuples)
448 comparison_packages.append(
450 name=package, plotfiles=compare_files
456 print(f
"Storing to {comparison_json_file}")
461 for i_revision, revision
in enumerate(revisions):
463 index = index_from_revision(revision, work_folder)
464 if index
is not None:
465 style = get_style(index)
466 line_color = ROOT.gROOT.GetColor(style.GetLineColor()).AsHexString()
468 line_color =
"#000000"
469 if line_color
is None:
471 f
"ERROR: line_color for revision f{revision} could not be set!"
472 f
" Choosing default color f{line_color}.",
479 comparison_revs.append(
486 comparison_json_file,
490 print_plotting_summary(all_plotuples)
493 def print_plotting_summary(
494 plotuples: List[Plotuple], warning_verbosity=1, chi2_verbosity=1
497 Print summary of all plotuples plotted, especially printing information
498 about failed comparisons.
499 :param plotuples: List of Plotuple objects
500 :param warning_verbosity: 0: no information about warnings, 1: write out
501 number of warnings per category, 2: report offending scripts
502 :param chi2_verbosity: As warning_verbosity but with the results of the
507 print(terminal_title_line(
"Summary of plotting", level=0))
509 print(
"Total number of plotuples considered: {}".format(len(plotuples)))
511 def pt_key(plotuple):
512 """ How we report on this plotuple """
515 key = key[:30] +
"..."
516 rf = os.path.basename(plotuple.rootfile)
519 return f
"{plotuple.package}/{key}/{rf}"
522 plotuple_no_warning = []
523 plotuple_by_warning = collections.defaultdict(list)
524 plotuples_by_comparison_result = collections.defaultdict(list)
525 for plotuple
in plotuples:
526 for warning
in plotuple.warnings:
528 plotuple_by_warning[warning].append(pt_key(plotuple))
529 if not plotuple.warnings:
530 plotuple_no_warning.append(pt_key(plotuple))
531 plotuples_by_comparison_result[plotuple.comparison_result].append(
535 if warning_verbosity:
538 print(f
"A total of {n_warnings} warnings were issued.")
539 for warning, perpetrators
in plotuple_by_warning.items():
541 f
"* '{warning}' was issued by {len(perpetrators)} "
544 if warning_verbosity >= 2:
545 for perpetrator
in perpetrators:
546 print(f
" - {perpetrator}")
548 print(
"No warnings were issued. ")
551 total=len(plotuples), success=len(plotuple_no_warning)
557 if not warning_verbosity:
559 print(
"Chi2 comparisons")
560 for result, perpetrators
in plotuples_by_comparison_result.items():
562 f
"* '{result}' was the result of {len(perpetrators)} "
565 if chi2_verbosity >= 2:
566 for perpetrator
in perpetrators:
567 print(f
" - {perpetrator}")
569 len(plotuples_by_comparison_result[
"equal"])
570 + 0.75 * len(plotuples_by_comparison_result[
"not_compared"])
571 + 0.5 * len(plotuples_by_comparison_result[
"warning"])
575 rate_name=
"Weighted score: ",
576 total=len(plotuples),
583 def rootobjects_from_files(
584 root_files_dict: Dict[str, Dict[str, List[str]]],
587 ) -> Dict[str, Dict[str, Dict[str, List[RootObject]]]]:
589 Takes a nested dictionary of root file paths for different revisions
590 and returns a (differently!) nested dictionary of root file objects.
592 :param root_files_dict: The dict of all *.root files which shall be
593 read in and for which the corresponding RootObjects shall be created:
594 {revision: {package: [root file]}}
595 :param is_reference: Boolean value indicating if the objects are
596 reference objects or not.
598 :return: {package: {file: {key: [list of root objects]}}}
602 return_dict = collections.defaultdict(
603 lambda: collections.defaultdict(
lambda: collections.defaultdict(list))
607 for revision, package2root_files
in root_files_dict.items():
608 for package, root_files
in package2root_files.items():
609 for root_file
in root_files:
610 key2objects = rootobjects_from_file(
611 root_file, package, revision, is_reference, work_folder
613 for key, objects
in key2objects.items():
614 return_dict[package][os.path.basename(root_file)][
621 def get_root_object_type(root_object: ROOT.TObject) -> str:
623 Get the type of the ROOT object as a string in a way that makes sense to us.
624 In particular, "" is returned if we have a ROOT object that is of no
626 :param root_object: ROOT TObject
627 :return: type as string if the ROOT object
629 if root_object.InheritsFrom(
"TNtuple"):
633 elif root_object.InheritsFrom(
"TH1"):
634 if root_object.InheritsFrom(
"TH2"):
639 elif root_object.InheritsFrom(
"TEfficiency"):
641 elif root_object.InheritsFrom(
"TGraph"):
643 elif root_object.ClassName() ==
"TNamed":
645 elif root_object.InheritsFrom(
"TASImage"):
651 def get_metadata(root_object: ROOT.TObject) -> Dict[str, Any]:
652 """ Extract metadata (description, checks etc.) from a ROOT object
653 :param root_object ROOT TObject
655 root_object_type = get_root_object_type(root_object)
658 "description":
"n/a",
666 def metaoption_str_to_list(metaoption_str):
667 return [opt.strip()
for opt
in metaoption_str.split(
",")
if opt.strip()]
669 if root_object_type
in [
"TH1",
"TH2",
"TEfficiency",
"TGraph"]:
671 e.GetName(): e.GetTitle()
for e
in root_object.GetListOfFunctions()
674 metadata[
"description"] = _metadata.get(
"Description",
"n/a")
675 metadata[
"check"] = _metadata.get(
"Check",
"n/a")
676 metadata[
"contact"] = _metadata.get(
"Contact",
"n/a")
678 metadata[
"metaoptions"] = metaoption_str_to_list(
679 _metadata.get(
"MetaOptions",
"")
682 elif root_object_type ==
"TNtuple":
683 _description = root_object.GetAlias(
"Description")
684 _check = root_object.GetAlias(
"Check")
685 _contact = root_object.GetAlias(
"Contact")
688 metadata[
"description"] = _description
690 metadata[
"check"] = _check
692 metadata[
"contact"] = _contact
694 _metaoptions_str = root_object.GetAlias(
"MetaOptions")
696 metadata[
"metaoptions"] = metaoption_str_to_list(_metaoptions_str)
703 def rootobjects_from_file(
709 ) -> Dict[str, List[RootObject]]:
711 Takes a root file, loops over its contents and creates the RootObjects
714 :param root_file: The *.root file which shall be read in and for which the
715 corresponding RootObjects shall be created
719 :param is_reference: Boolean value indicating if the object is a
720 reference object or not.
721 :return: package, {key: [list of root objects]}. Note: The list will
722 contain only one root object right now, because package + root file
723 basename key uniquely determine it, but later we will merge this list
724 with files from other revisions. In case of errors, it returns an
729 key2object = collections.defaultdict(list)
735 tfile = ROOT.TFile(root_file)
736 if not tfile
or not tfile.IsOpen():
737 B2ERROR(f
"The file {root_file} can not be opened. Skipping it.")
740 B2ERROR(f
"{e}. Skipping it.")
745 dir_date = date_from_revision(revision, work_folder)
748 for key
in tfile.GetListOfKeys():
753 if re.search(
".*dbstore.*root", root_file):
758 root_object = tfile.Get(name)
762 root_object_type = get_root_object_type(root_object)
763 if not root_object_type:
770 if root_object.InheritsFrom(
"TH1"):
771 root_object.SetDirectory(0)
773 metadata = get_metadata(root_object)
775 if root_object_type ==
"TNtuple":
777 root_object.GetEntry(0)
783 for leaf
in root_object.GetListOfLeaves():
784 ntuple_values[leaf.GetName()] = leaf.GetValue()
790 root_object = ntuple_values
792 key2object[name].append(
801 metadata[
"description"],
804 metadata[
"metaoptions"],
823 process_queue: Optional[Queue] =
None,
827 This function generates the plots and html
828 page for the requested revisions.
829 By default all available revisions are taken. New plots will ony be
830 created if they don't exist already for the given set of revisions,
831 unless the force option is used.
832 @param revisions: The revisions which should be taken into account.
833 @param force: If True, plots are created even if there already is a version
834 of them (which may me deprecated, though)
835 @param process_queue: communication Queue object, which is used in
836 multi-processing mode to report the progress of the plot creating.
837 @param work_folder: The work folder
845 for revision
in revisions:
852 revision
not in available_revisions(work_folder)
853 and not revision ==
"reference"
855 print(f
"Warning: Removing invalid revision '{revision}'.")
856 revisions.pop(revision)
862 revisions = [
"reference"] + available_revisions(work_folder)
869 work_folder, revisions
874 if os.path.exists(expected_path)
and not force:
876 "Plots for the revision(s) {} have already been created before "
877 "and will be served from the archive.".format(
", ".join(revisions))
881 generate_new_plots(revisions, work_folder, process_queue)
885 process_queue.put({
"status":
"complete"})
886 process_queue.close()
str terminal_title_line(title="", subtitle="", level=0)
str congratulator(Optional[Union[int, float]] success=None, Optional[Union[int, float]] failure=None, Optional[Union[int, float]] total=None, just_comment=False, rate_name="Success rate")
def get_results_tag_folder(output_base_dir, tag)
def get_html_plots_tag_comparison_json(output_base_dir, tags)
def get_html_plots_tag_comparison_folder(output_base_dir, tags)
def get_results_folder(output_base_dir)