10 from typing
import Dict, Any, List, Union, Optional
12 from multiprocessing
import Queue
22 from ROOT
import RooFit
29 from validationplotuple
import Plotuple
30 from validationfunctions
import index_from_revision, get_style, \
31 available_revisions, terminal_title_line
32 import validationfunctions
34 import simplejson
as json
38 from validationrootobject
import RootObject
42 if os.environ.get(
'BELLE2_RELEASE_DIR',
None)
is None and os.environ.get(
'BELLE2_LOCAL_DIR',
None)
is None:
43 sys.exit(
'Error: No basf2 release set up!')
45 pp = pprint.PrettyPrinter(depth=6, indent=1, width=80)
53 def date_from_revision(revision: str, work_folder: str) -> Optional[Union[int, float]]:
55 Takes the name of a revision and returns the 'last modified'-timestamp of
56 the corresponding directory, which holds the revision.
57 :param revision: A string containing the name of a revision
58 :return: The 'last modified'-timestamp of the folder which holds the
64 if revision ==
'reference':
70 if revision
in revisions:
71 return os.path.getmtime(
79 def merge_nested_list_dicts(a, b):
80 """ Given two nested dictionary with same depth that contain lists, return
81 'merged' dictionary that contains the joined lists.
82 :param a: Dict[Dict[...[Dict[List]]..]]
83 :param b: Dict[Dict[...[Dict[List]]..]] (same depth as a)
87 def _merge_nested_list_dicts(_a, _b):
88 """ Merge _b into _a, return _a. """
91 if isinstance(_a[key], dict)
and isinstance(_b[key], dict):
92 _merge_nested_list_dicts(_a[key], _b[key])
94 assert isinstance(_a[key], list)
95 assert isinstance(_b[key], list)
96 _a[key].extend(_b[key])
101 return _merge_nested_list_dicts(a.copy(), b.copy())
105 revisions: List[str],
107 ) -> Dict[str, Dict[str, List[str]]]:
109 Returns a list of all plot files as absolute paths. For this purpose,
110 it loops over all revisions in 'revisions', finds the
111 corresponding results folder and collects the plot ROOT files.
112 :param revisions: Name of the revisions.
113 :param work_folder: Folder that contains the results/ directory
114 :return: plot files, i.e. plot ROOT files from the
115 requested revisions as dictionary
116 {revision: {package: [root files]}}
119 results = collections.defaultdict(
lambda: collections.defaultdict(list))
125 for revision
in revisions:
127 if revision ==
"reference":
128 results[
"reference"] = collections.defaultdict(
129 list, get_tracked_reference_files()
133 rev_result_folder = os.path.join(results_foldername, revision)
134 if not os.path.isdir(rev_result_folder):
137 packages = os.listdir(rev_result_folder)
139 for package
in packages:
140 package_folder = os.path.join(rev_result_folder, package)
142 root_files = glob.glob(package_folder +
"/*.root")
144 results[revision][package].extend(
145 [os.path.abspath(rf)
for rf
in root_files]
151 def get_tracked_reference_files() -> Dict[str, List[str]]:
153 This function loops over the local and central release dir and collects
154 the .root-files from the validation-subfolders of the packages. These are
155 the files which we will use as references.
156 From the central release directory, we collect the files from the release
157 which is set up on the machine running this script.
158 :return: ROOT files that are located
159 in the same folder as the steering files of the package as
160 {package: [list of root files]}
164 basepaths = {
'local': os.environ.get(
'BELLE2_LOCAL_DIR',
None),
165 'central': os.environ.get(
'BELLE2_RELEASE_DIR',
None)}
169 'local': collections.defaultdict(list),
170 'central': collections.defaultdict(list)
175 validation_folder_name =
'validation'
176 validation_test_folder_name =
'validation-test'
179 for location
in [
'local',
'central']:
183 if basepaths[location]
is None:
187 root = basepaths[location]
189 packages = os.listdir(root)
191 for package
in packages:
194 glob_search = os.path.join(
195 root, package, validation_folder_name,
"*.root"
197 results[location][package].extend([
198 os.path.abspath(f)
for f
in glob.glob(glob_search)
203 if package ==
"validation":
204 glob_search = os.path.join(
207 validation_test_folder_name,
210 results[location][validation_test_folder_name].extend([
211 os.path.abspath(f)
for f
in glob.glob(glob_search)
218 for package, local_files
in results[
'local'].items():
219 for local_file
in local_files:
221 local_path = local_file.replace(basepaths[
'local'],
'')
223 for central_file
in results[
'central'][package]:
226 central_path = central_file.replace(basepaths[
'central'],
'')
229 if local_path == central_path:
230 results[
'central'][package].remove(central_file)
239 results[
'central'][package] + results[
'local'][package]
241 list(results[
'central'].keys()) + list(results[
'central'].keys())
247 def generate_new_plots(
248 revisions: List[str],
250 process_queue: Optional[Queue] =
None,
251 root_error_ignore_level=ROOT.kWarning
254 Creates the plots that contain the requested revisions. Each plot (or
255 n-tuple, for that matter) is stored in an object of class Plot.
257 @param work_folder: Folder containing results
258 @param process_queue: communication queue object, which is used in
259 multi-processing mode to report the progress of the plot creating.
260 @param root_error_ignore_level: Value for gErrorIgnoreLevel. Default:
261 ROOT.kWarning. If set to None, global level will be left unchanged.
262 @return: No return value
266 "Creating plots for the revision(s) " +
", ".join(revisions) +
"."
270 ROOT.gROOT.SetBatch()
271 ROOT.gStyle.SetOptStat(1110)
272 ROOT.gStyle.SetOptFit(101)
275 if root_error_ignore_level
is not None:
276 ROOT.gErrorIgnoreLevel = root_error_ignore_level
285 if len(revisions) == 0:
286 print(
"No revisions selected for plotting. Returning without "
287 "doing anything.", file=sys.stderr)
290 plot_files = get_plot_files(revisions[1:], work_folder)
291 reference_files = get_plot_files(revisions[:1], work_folder)
298 plot_packages = set()
299 only_tracked_reference = \
300 set(plot_files.keys()) | set(reference_files.keys()) == {
"reference"}
301 for results
in [plot_files, reference_files]:
303 if rev ==
"reference" and not only_tracked_reference:
305 for package
in results[rev]:
306 if results[rev][package]:
307 plot_packages.add(package)
310 plot_p2f2k2o = rootobjects_from_files(
313 work_folder=work_folder
315 reference_p2f2k2o = rootobjects_from_files(
318 work_folder=work_folder
322 for package
in set(plot_p2f2k2o.keys()) - plot_packages:
323 del plot_p2f2k2o[package]
324 for package
in set(reference_p2f2k2o.keys()) - plot_packages:
325 del reference_p2f2k2o[package]
327 all_p2f2k2o = merge_nested_list_dicts(plot_p2f2k2o, reference_p2f2k2o)
340 if not os.path.exists(content_dir):
341 os.makedirs(content_dir)
343 comparison_packages = []
349 for i, package
in enumerate(sorted(list(plot_packages))):
352 print(terminal_title_line(
353 f
'Creating plots for package: {package}',
361 for rootfile
in sorted(all_p2f2k2o[package].keys()):
362 file_name, file_ext = os.path.splitext(rootfile)
366 print(f
'Creating plots for file: {rootfile}')
374 process_queue.put_nowait(
376 "current_package": i,
377 "total_package": len(plot_packages),
379 "package_name": package,
380 "file_name": file_name
392 compare_html_content = []
393 has_reference =
False
395 root_file_meta_data = collections.defaultdict(
lambda:
None)
397 for key
in all_p2f2k2o[package][rootfile].keys():
399 all_p2f2k2o[package][rootfile][key],
403 plotuple.create_plotuple()
404 plotuples.append(plotuple)
405 has_reference = plotuple.has_reference()
407 if plotuple.type ==
'TNtuple':
408 compare_ntuples.append(plotuple.create_json_object())
409 elif plotuple.type ==
'TNamed':
410 compare_html_content.append(plotuple.create_json_object())
411 elif plotuple.type ==
"meta":
412 meta_key, meta_value = plotuple.get_meta_information()
413 root_file_meta_data[meta_key] = meta_value
415 compare_plots.append(plotuple.create_json_object())
421 compared_revisions=revisions,
423 has_reference=has_reference,
424 ntuples=compare_ntuples,
425 html_content=compare_html_content,
426 description=root_file_meta_data[
"description"]
428 compare_files.append(compare_file)
430 all_plotuples.extend(plotuples)
432 comparison_packages.append(
435 plotfiles=compare_files)
440 print(f
"Storing to {comparison_json_file}")
445 for i_revision, revision
in enumerate(revisions):
447 index = index_from_revision(revision, work_folder)
448 if index
is not None:
449 style = get_style(index)
450 line_color = ROOT.gROOT.GetColor(style.GetLineColor()).AsHexString()
452 line_color =
"#000000"
453 if line_color
is None:
455 f
"ERROR: line_color for revision f{revision} could not be set!"
456 f
" Choosing default color f{line_color}.",
471 comparison_json_file,
475 print_plotting_summary(all_plotuples)
478 def print_plotting_summary(
479 plotuples: List[Plotuple],
484 Print summary of all plotuples plotted, especially printing information
485 about failed comparisons.
486 :param plotuples: List of Plotuple objects
487 :param warning_verbosity: 0: no information about warnings, 1: write out
488 number of warnings per category, 2: report offending scripts
489 :param chi2_verbosity: As warning_verbosity but with the results of the
494 print(terminal_title_line(
495 "Summary of plotting",
499 print(
"Total number of plotuples considered: {}".format(len(plotuples)))
501 def pt_key(plotuple):
502 """ How we report on this plotuple """
505 key = key[:30] +
"..."
506 rf = os.path.basename(plotuple.rootfile)
509 return f
"{plotuple.package}/{key}/{rf}"
512 plotuple_no_warning = []
513 plotuple_by_warning = collections.defaultdict(list)
514 plotuples_by_comparison_result = collections.defaultdict(list)
515 for plotuple
in plotuples:
516 for warning
in plotuple.warnings:
518 plotuple_by_warning[warning].append(pt_key(plotuple))
519 if not plotuple.warnings:
520 plotuple_no_warning.append(pt_key(plotuple))
521 plotuples_by_comparison_result[plotuple.comparison_result].append(
525 if warning_verbosity:
528 print(f
"A total of {n_warnings} warnings were issued.")
529 for warning, perpetrators
in plotuple_by_warning.items():
530 print(f
"* '{warning}' was issued by {len(perpetrators)} "
532 if warning_verbosity >= 2:
533 for perpetrator
in perpetrators:
534 print(f
" - {perpetrator}")
536 print(
"No warnings were issued. ")
538 total=len(plotuples),
539 success=len(plotuple_no_warning)
544 if not warning_verbosity:
546 print(
"Chi2 comparisons")
547 for result, perpetrators
in plotuples_by_comparison_result.items():
548 print(f
"* '{result}' was the result of {len(perpetrators)} "
550 if chi2_verbosity >= 2:
551 for perpetrator
in perpetrators:
552 print(f
" - {perpetrator}")
553 score = len(plotuples_by_comparison_result[
"equal"]) + \
554 0.75 * len(plotuples_by_comparison_result[
"not_compared"]) + \
555 0.5 * len(plotuples_by_comparison_result[
"warning"])
557 rate_name=
"Weighted score: ",
558 total=len(plotuples),
564 def rootobjects_from_files(
565 root_files_dict: Dict[str, Dict[str, List[str]]],
568 ) -> Dict[str, Dict[str, Dict[str, List[RootObject]]]]:
570 Takes a nested dictionary of root file paths for different revisions
571 and returns a (differently!) nested dictionary of root file objects.
573 :param root_files_dict: The dict of all *.root files which shall be
574 read in and for which the corresponding RootObjects shall be created:
575 {revision: {package: [root file]}}
576 :param is_reference: Boolean value indicating if the objects are
577 reference objects or not.
579 :return: {package: {file: {key: [list of root objects]}}}
583 return_dict = collections.defaultdict(
584 lambda: collections.defaultdict(
585 lambda: collections.defaultdict(list)
590 for revision, package2root_files
in root_files_dict.items():
591 for package, root_files
in package2root_files.items():
592 for root_file
in root_files:
593 key2objects = rootobjects_from_file(
600 for key, objects
in key2objects.items():
601 return_dict[package][os.path.basename(root_file)][key].extend(objects)
606 def get_root_object_type(root_object: ROOT.TObject) -> str:
608 Get the type of the ROOT object as a string in a way that makes sense to us.
609 In particular, "" is returned if we have a ROOT object that is of no
611 :param root_object: ROOT TObject
612 :return: type as string if the ROOT object
614 if root_object.InheritsFrom(
'TNtuple'):
618 elif root_object.InheritsFrom(
'TH1'):
619 if root_object.InheritsFrom(
'TH2'):
624 elif root_object.InheritsFrom(
'TEfficiency'):
626 elif root_object.InheritsFrom(
'TGraph'):
628 elif root_object.ClassName() ==
'TNamed':
630 elif root_object.InheritsFrom(
'TASImage'):
636 def get_metadata(root_object: ROOT.TObject) -> Dict[str, Any]:
637 """ Extract metadata (description, checks etc.) from a ROOT object
638 :param root_object ROOT TObject
640 root_object_type = get_root_object_type(root_object)
643 "description":
"n/a",
651 def metaoption_str_to_list(metaoption_str):
653 opt.strip()
for opt
in metaoption_str.split(
',')
if opt.strip()
656 if root_object_type
in [
'TH1',
'TH2',
'TEfficiency',
'TGraph']:
658 e.GetName(): e.GetTitle()
659 for e
in root_object.GetListOfFunctions()
662 metadata[
"description"] = _metadata.get(
"Description",
"n/a")
663 metadata[
"check"] = _metadata.get(
"Check",
"n/a")
664 metadata[
"contact"] = _metadata.get(
"Contact",
"n/a")
666 metadata[
"metaoptions"] = metaoption_str_to_list(
667 _metadata.get(
"MetaOptions",
"")
670 elif root_object_type ==
'TNtuple':
671 _description = root_object.GetAlias(
'Description')
672 _check = root_object.GetAlias(
'Check')
673 _contact = root_object.GetAlias(
'Contact')
676 metadata[
"description"] = _description
678 metadata[
"check"] = _check
680 metadata[
"contact"] = _contact
682 _metaoptions_str = root_object.GetAlias(
'MetaOptions')
684 metadata[
"metaoptions"] = metaoption_str_to_list(_metaoptions_str)
691 def rootobjects_from_file(
697 ) -> Dict[str, List[RootObject]]:
699 Takes a root file, loops over its contents and creates the RootObjects
702 :param root_file: The *.root file which shall be read in and for which the
703 corresponding RootObjects shall be created
707 :param is_reference: Boolean value indicating if the object is a
708 reference object or not.
709 :return: package, {key: [list of root objects]}. Note: The list will
710 contain only one root object right now, because package + root file
711 basename key uniquely determine it, but later we will merge this list
712 with files from other revisions.
716 key2object = collections.defaultdict(list)
720 dir_date = date_from_revision(revision, work_folder)
723 tfile = ROOT.TFile(root_file)
726 for key
in tfile.GetListOfKeys():
731 if re.search(
".*dbstore.*root", root_file):
736 root_object = tfile.Get(name)
740 root_object_type = get_root_object_type(root_object)
741 if not root_object_type:
748 if root_object.InheritsFrom(
"TH1"):
749 root_object.SetDirectory(0)
751 metadata = get_metadata(root_object)
753 if root_object_type ==
"TNtuple":
755 root_object.GetEntry(0)
761 for leaf
in root_object.GetListOfLeaves():
762 ntuple_values[leaf.GetName()] = leaf.GetValue()
768 root_object = ntuple_values
770 key2object[name].append(
779 metadata[
"description"],
782 metadata[
"metaoptions"],
801 process_queue: Optional[Queue] =
None,
805 This function generates the plots and html
806 page for the requested revisions.
807 By default all available revisions are taken. New plots will ony be
808 created if they don't exist already for the given set of revisions,
809 unless the force option is used.
810 @param revisions: The revisions which should be taken into account.
811 @param force: If True, plots are created even if there already is a version
812 of them (which may me deprecated, though)
813 @param process_queue: communication Queue object, which is used in
814 multi-processing mode to report the progress of the plot creating.
815 @param work_folder: The work folder
823 for revision
in revisions:
829 if revision
not in available_revisions(work_folder) \
830 and not revision ==
'reference':
831 print(f
"Warning: Removing invalid revision '{revision}'.")
832 revisions.pop(revision)
838 revisions = [
'reference'] + available_revisions(work_folder)
851 if os.path.exists(expected_path)
and not force:
853 "Plots for the revision(s) {} have already been created before "
854 "and will be served from the archive.".format(
855 ", ".join(revisions))
859 generate_new_plots(revisions, work_folder, process_queue)
863 process_queue.put({
"status":
"complete"})
864 process_queue.close()