Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-01 09:34:18

0001 #!/usr/bin/env python
0002 
0003 import os
0004 import sys
0005 from datetime import datetime
0006 import yaml
0007 
0008 import matplotlib.pyplot as plt
0009 import numpy as np
0010 from matplotlib.backends.backend_pdf import PdfPages
0011 from matplotlib.offsetbox import AnchoredOffsetbox, HPacker, TextArea, VPacker
0012 
0013 from argparsing import submission_args
0014 from sphenixdbutils import dbQuery, cnxn_string_map, list_to_condition
0015 from sphenixprodrules import RuleConfig
0016 from sphenixmisc import setup_rot_handler
0017 from simpleLogger import slogger, CustomFormatter, CHATTY, DEBUG, INFO, WARN, ERROR, CRITICAL  # noqa: F401
0018 
0019 
0020 def get_time_diffs_by_cpus(run_condition, dsttype, tag, dataset):
0021     query = f"""
0022     SELECT started, finished, request_cpus
0023     FROM production_jobs
0024     WHERE {run_condition}
0025       AND tag = '{tag}'
0026       AND dataset = '{dataset}'
0027       AND status = 'finished'
0028       AND dsttype LIKE '{dsttype}%'
0029       AND started IS NOT NULL
0030       AND finished IS NOT NULL
0031     """
0032 
0033     DEBUG(f"Executing query:\n{query}")
0034 
0035     cursor = dbQuery(cnxn_string_map['statr'], query)
0036     if not cursor:
0037         ERROR("Failed to query production database.")
0038         return None
0039 
0040     results = cursor.fetchall()
0041     if not results:
0042         return {}
0043 
0044     time_diffs_by_cpus = {}
0045     for started, finished, request_cpus in results:
0046         if isinstance(started, str):
0047             started = datetime.fromisoformat(started)
0048         if isinstance(finished, str):
0049             finished = datetime.fromisoformat(finished)
0050 
0051         cpu_key = int(request_cpus) if request_cpus is not None else None
0052         time_diffs_by_cpus.setdefault(cpu_key, []).append((finished - started).total_seconds())
0053 
0054     return time_diffs_by_cpus
0055 
0056 
0057 def flatten_time_diffs(time_diffs_by_cpus):
0058     return [diff for diffs in time_diffs_by_cpus.values() for diff in diffs]
0059 
0060 
0061 def cpu_sort_key(cpu_count):
0062     return (cpu_count is None, cpu_count if cpu_count is not None else 0)
0063 
0064 
0065 def cpu_label(cpu_count):
0066     if cpu_count is None:
0067         return "unknown CPUs"
0068     if cpu_count == 1:
0069         return "1 CPU"
0070     return f"{cpu_count} CPUs"
0071 
0072 
0073 def get_status_counts(run_condition, dsttype, tag, dataset):
0074     query = f"""
0075     SELECT
0076       SUM(CASE WHEN status = 'finished' THEN 1 ELSE 0 END) AS finished,
0077       SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
0078     FROM production_jobs
0079     WHERE {run_condition}
0080       AND tag = '{tag}'
0081       AND dataset = '{dataset}'
0082       AND status IN ('finished', 'failed')
0083       AND dsttype LIKE '{dsttype}%'
0084     """
0085 
0086     DEBUG(f"Executing query:\n{query}")
0087 
0088     cursor = dbQuery(cnxn_string_map['statr'], query)
0089     if not cursor:
0090         ERROR("Failed to query production database.")
0091         return None
0092 
0093     row = cursor.fetchone()
0094     finished = int(row.finished or 0)
0095     failed = int(row.failed or 0)
0096     return {
0097         "finished": finished,
0098         "failed": failed,
0099         "total": finished + failed,
0100     }
0101 
0102 
0103 def pick_unit(time_diffs_seconds):
0104     median = np.median(time_diffs_seconds)
0105     if median > 1800:
0106         return 'hours'
0107     if median > 30:
0108         return 'minutes'
0109     return 'seconds'
0110 
0111 
0112 def plot_histogram(ax, time_diffs_by_cpus, title, time_unit='hours'):
0113     """
0114     Plots wall-time histograms grouped by request_cpus with an overflow bin.
0115     """
0116     all_time_diffs_seconds = flatten_time_diffs(time_diffs_by_cpus)
0117     max_time_hours = np.max(all_time_diffs_seconds) / 3600 if all_time_diffs_seconds else 0
0118 
0119     config = {
0120         'hours': {'conv': 3600, 'label': 'hours', 'max_val': 30, 'bin_w': 0.25, 'tick_step': 2},
0121         'minutes': {'conv': 60, 'label': 'minutes', 'max_val': 600, 'bin_w': 10, 'tick_step': 60},
0122         'seconds': {'conv': 1, 'label': 'seconds', 'max_val': 1200, 'bin_w': 20, 'tick_step': 120},
0123     }
0124 
0125     if time_unit not in config:
0126         raise ValueError("Invalid time_unit. Must be 'hours', 'minutes', or 'seconds'.")
0127 
0128     cfg = config[time_unit]
0129     max_val = cfg['max_val']
0130     bin_width = cfg['bin_w']
0131     tick_step = cfg['tick_step']
0132 
0133     if time_unit == 'hours' and max_time_hours < 10:
0134         max_val = 10
0135         bin_width = 10 / 60
0136         tick_step = 1
0137 
0138     bins = np.arange(0, max_val + bin_width, bin_width)
0139 
0140     for cpu_count in sorted(time_diffs_by_cpus, key=cpu_sort_key):
0141         time_diffs = [t / cfg['conv'] for t in time_diffs_by_cpus[cpu_count]]
0142         plot_data = [min(diff, max_val) for diff in time_diffs]
0143         avg_time = np.mean(time_diffs)
0144         ax.hist(
0145             plot_data,
0146             bins=bins,
0147             histtype='step',
0148             linewidth=2,
0149             label=f'{cpu_label(cpu_count)} (n={len(time_diffs)}, avg={avg_time:.2f} {cfg["label"]})',
0150         )
0151 
0152     ax.set_title(title)
0153     ax.set_xlabel(f'Wall time (start to finish) ({cfg["label"]})')
0154     ax.set_ylabel('Number of Jobs')
0155     ax.legend(loc='upper left')
0156     ax.grid(True, which='both', linestyle='--', linewidth=0.5)
0157 
0158     ax.set_xlim(0, max_val)
0159     xticks = np.arange(0, max_val + bin_width, tick_step)
0160 
0161     if max_val not in xticks:
0162         xticks = np.append(xticks, max_val)
0163 
0164     xticklabels = [f'{t:g}' for t in xticks]
0165     xticklabels[-1] = f'{int(max_val)}+'
0166 
0167     ax.set_xticks(xticks)
0168     ax.set_xticklabels(xticklabels)
0169 
0170 
0171 def add_status_box(ax, status_counts):
0172     if status_counts is None:
0173         return
0174 
0175     rows = [
0176         ("finished:", status_counts["finished"], "black"),
0177         ("failed:", status_counts["failed"], "red"),
0178         ("total:", status_counts["total"], "black"),
0179     ]
0180 
0181     label_column = VPacker(
0182         children=[
0183             TextArea(label, textprops={"color": color, "ha": "left"})
0184             for label, _, color in rows
0185         ],
0186         align="left",
0187         pad=0,
0188         sep=2,
0189     )
0190     value_column = VPacker(
0191         children=[
0192             TextArea(f"{value}", textprops={"color": color, "ha": "right"})
0193             for _, value, color in rows
0194         ],
0195         align="right",
0196         pad=0,
0197         sep=2,
0198     )
0199     status_box = HPacker(
0200         children=[label_column, value_column],
0201         align="baseline",
0202         pad=0,
0203         sep=12,
0204     )
0205 
0206     anchored_box = AnchoredOffsetbox(
0207         loc="upper right",
0208         child=status_box,
0209         bbox_to_anchor=(0.98, 0.95),
0210         bbox_transform=ax.transAxes,
0211         frameon=True,
0212         borderpad=0,
0213         pad=0.35,
0214     )
0215     anchored_box.patch.set_boxstyle("round,pad=0.35")
0216     anchored_box.patch.set_facecolor("white")
0217     anchored_box.patch.set_edgecolor("0.5")
0218     anchored_box.patch.set_alpha(0.85)
0219     ax.add_artist(anchored_box)
0220 
0221 
0222 def main():
0223     """
0224     Main function to plot job time distribution.
0225     """
0226     args = submission_args()
0227 
0228     plt.rcParams.update({'font.size': 16})
0229 
0230     sublogdir = setup_rot_handler(args)
0231     slogger.setLevel(args.loglevel)
0232     INFO(f"Logging to {sublogdir}, level {args.loglevel}")
0233 
0234     param_overrides = {}
0235     param_overrides["runs"] = args.runs
0236     param_overrides["runlist"] = args.runlist
0237     param_overrides["nevents"] = 0
0238 
0239     if args.physicsmode is not None:
0240         param_overrides["physicsmode"] = args.physicsmode
0241 
0242     param_overrides["prodmode"] = "production"
0243     try:
0244         rule = RuleConfig.from_yaml_file(
0245             yaml_file=args.config,
0246             rule_name=args.rulename,
0247             param_overrides=param_overrides
0248         )
0249         INFO(f"Successfully loaded rule configuration: {args.rulename}")
0250     except (ValueError, FileNotFoundError) as e:
0251         ERROR(f"Error: {e}")
0252         sys.exit(1)
0253 
0254     if args.runs and 1 < len(rule.runlist_int) <= 5:
0255         output_pdf_path = f'job_time_distribution_{args.rulename}.pdf'
0256         with PdfPages(output_pdf_path) as pdf:
0257             for run in rule.runlist_int:
0258                 INFO(f"Processing run: {run}")
0259                 run_condition = list_to_condition([run], name="runnumber")
0260                 time_diffs_by_cpus = get_time_diffs_by_cpus(run_condition, rule.dsttype, rule.outtriplet, rule.dataset)
0261                 status_counts = get_status_counts(run_condition, rule.dsttype, rule.outtriplet, rule.dataset)
0262 
0263                 if time_diffs_by_cpus is None:
0264                     sys.exit(1)
0265                 all_time_diffs = flatten_time_diffs(time_diffs_by_cpus)
0266                 if not all_time_diffs:
0267                     INFO(f"No finished jobs found for run {run}.")
0268                     continue
0269 
0270                 fig, ax = plt.subplots(figsize=(12, 7))
0271                 plt.style.use('seaborn-v0_8-deep')
0272 
0273                 title = f'Job Time Distribution for {args.rulename} (Run: {run})'
0274                 plot_histogram(ax, time_diffs_by_cpus, title, time_unit=pick_unit(all_time_diffs))
0275                 add_status_box(ax, status_counts)
0276 
0277                 plt.tight_layout()
0278                 pdf.savefig(fig)
0279                 plt.close(fig)
0280 
0281             INFO(f"Saved multi-page PDF to {output_pdf_path}")
0282 
0283     else:
0284         run_condition = list_to_condition(rule.runlist_int, name="runnumber")
0285         time_diffs_by_cpus = get_time_diffs_by_cpus(run_condition, rule.dsttype, rule.outtriplet, rule.dataset)
0286         status_counts = get_status_counts(run_condition, rule.dsttype, rule.outtriplet, rule.dataset)
0287         if time_diffs_by_cpus is None:
0288             sys.exit(1)
0289         all_time_diffs = flatten_time_diffs(time_diffs_by_cpus)
0290         if not all_time_diffs:
0291             INFO("No finished jobs found for the specified runs.")
0292             sys.exit(0)
0293 
0294         run_str = f"Run(s): {rule.runlist_int}"
0295         if rule.runlist is not None:
0296             run_str = f"Runs from file: {os.path.basename(rule.runlist)}"
0297 
0298         base_title = f'Job Time Distribution for {args.rulename}\n{run_str}'
0299 
0300         unit = pick_unit(all_time_diffs)
0301         fig, ax = plt.subplots(figsize=(12, 7))
0302         plt.style.use('seaborn-v0_8-deep')
0303 
0304         plot_histogram(ax, time_diffs_by_cpus, base_title, time_unit=unit)
0305         add_status_box(ax, status_counts)
0306 
0307         plt.tight_layout()
0308         output_file = f'job_time_distribution_{args.rulename}.png'
0309         plt.savefig(output_file)
0310         INFO(f"Saved plot to {output_file}")
0311         plt.close(fig)
0312 
0313 
0314 if __name__ == '__main__':
0315     main()