17from typing
import Dict, Any, List, Union, Optional
19from multiprocessing
import Queue
30from ROOT
import RooFit
36from basf2
import B2ERROR
38from validationplotuple
import Plotuple
39from validationfunctions
import (
45import validationfunctions
47from 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!")
57pp = pprint.PrettyPrinter(depth=6, indent=1, width=80)
65def 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(
93def 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]
164def 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())
264def 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)
493def 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(f
"Total number of plotuples considered: {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),
583def 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)][
621def 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"):
651def 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)
704 root_object: ROOT.TObject,
705 key2object: Dict[str, List[RootObject]],
714 Add a root object with all its metadata to a dictionary
716 @param root_object root object that shall be added to the dictionary
717 @param key2object dictionary of all root objects
718 @param name name of root object
719 @param revision revision name
720 @param package package
721 @param root_file the *.root file
for which the corresponding RootObjects shall be created
722 @param dir_date time_stamp of the *.root file
723 @param is_reference boolean value indicating
if the object
is a reference object
or not.
725 root_object_type = get_root_object_type(root_object)
726 if not root_object_type:
733 if root_object.InheritsFrom(
"TH1"):
734 root_object.SetDirectory(0)
737 metadata = get_metadata(root_object)
739 if root_object_type ==
"TNtuple":
741 root_object.GetEntry(0)
747 for leaf
in root_object.GetListOfLeaves():
748 ntuple_values[leaf.GetName()] = leaf.GetValue()
754 root_object = ntuple_values
756 key2object[name].append(
765 metadata[
"description"],
768 metadata[
"metaoptions"],
774def rootobjects_from_file(
780) -> Dict[str, List[RootObject]]:
782 Takes a root file, loops over its contents and creates the RootObjects
785 :param root_file: The *.root file which shall be read
in and for which the
786 corresponding RootObjects shall be created
790 :param is_reference: Boolean value indicating
if the object
is a
791 reference object
or not.
792 :
return: package, {key: [list of root objects]}. Note: The list will
793 contain only one root object right now, because package + root file
794 basename key uniquely determine it, but later we will merge this list
795 with files
from other revisions. In case of errors, it returns an
800 key2object = collections.defaultdict(list)
806 tfile = ROOT.TFile(root_file)
807 if not tfile
or not tfile.IsOpen():
808 B2ERROR(f
"The file {root_file} can not be opened. Skipping it.")
811 B2ERROR(f
"{e}. Skipping it.")
816 dir_date = date_from_revision(revision, work_folder)
819 for key
in tfile.GetListOfKeys():
824 if re.search(
".*dbstore.*root", root_file):
829 root_object = tfile.Get(name)
834 if root_object.InheritsFrom(
"TDirectory"):
835 for sub_key
in root_object.GetListOfKeys():
837 sub_root_object = sub_key.ReadObj()
838 if not sub_root_object:
844 f
"{name}_{sub_key.GetName()}",
879 process_queue: Optional[Queue] =
None,
883 This function generates the plots and html
884 page
for the requested revisions.
885 By default all available revisions are taken. New plots will only be
886 created
if they don
't exist already for the given set of revisions, unless the force option is used.
887 @param revisions: The revisions which should be taken into account.
888 @param force: If
True, plots are created even
if there already
is a version
889 of them (which may me deprecated, though)
890 @param process_queue: communication Queue object, which
is used
in
891 multi-processing mode to report the progress of the plot creating.
892 @param work_folder: The work folder
900 for revision
in revisions:
907 revision
not in available_revisions(work_folder)
908 and not revision ==
"reference"
910 print(f
"Warning: Removing invalid revision '{revision}'.")
911 revisions.pop(revision)
917 revisions = [
"reference"] + available_revisions(work_folder)
924 work_folder, revisions
929 if os.path.exists(expected_path)
and not force:
931 f
"Plots for the revision(s) {', '.join(revisions)} have already been created before and will be served " +
936 generate_new_plots(revisions, work_folder, process_queue)
940 process_queue.put({
"status":
"complete"})
941 process_queue.close()
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")
str terminal_title_line(title="", subtitle="", level=0)
def get_html_plots_tag_comparison_json(output_base_dir, tags)
def get_results_folder(output_base_dir)
def get_html_plots_tag_comparison_folder(output_base_dir, tags)
def get_results_tag_folder(output_base_dir, tag)