Back to home page

EIC code displayed by LXR

 
 

    


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

0001 #!/usr/bin/env python3
0002 """
0003 generate_report.py
0004 
0005 Generate a run-level CSV report for sPHENIX production rules.
0006 
0007 For now this supports raw/event-combiner rules. Downstream rules are detected
0008 and rejected explicitly so the macro name and CLI can be reused later.
0009 """
0010 
0011 import cProfile
0012 import csv
0013 import math
0014 import pstats
0015 import sys
0016 from collections import defaultdict
0017 from pathlib import Path
0018 
0019 from argparsing import submission_args
0020 from simpleLogger import CHATTY, DEBUG, INFO, WARN, ERROR, CRITICAL  # noqa: F401
0021 from sphenixprodrules import RuleConfig
0022 from sphenixmatching import MatchConfig
0023 from sphenixdbutils import cnxn_string_map, dbQuery
0024 from sphenixmisc import human_event_count
0025 
0026 
0027 EXPECTED_SKIPPED_EVENTS_PER_DAQHOST = 2
0028 
0029 
0030 CSV_COLUMNS = [
0031     "rule_name",
0032     "runnumber",
0033     "possible_daqhosts",
0034     "total_daqhosts",
0035     "missing_daqhosts",
0036     "possible_segments",
0037     "total_segments",
0038     "missing_segments",
0039     "possible_events",
0040     "total_events",
0041     "missing_events",
0042     "error_codes",
0043     "incomplete_reasons",
0044     "status",
0045 ]
0046 
0047 
0048 def daqhost_to_dst_leaf(daqhost, match):
0049     if isinstance(match.input_stem, dict):
0050         for leaf, raw_daqhost in match.input_stem.items():
0051             if raw_daqhost == daqhost:
0052                 return leaf
0053     return str(daqhost).replace(":", "_")
0054 
0055 
0056 def sql_literal(value):
0057     return "'" + str(value).replace("'", "''") + "'"
0058 
0059 
0060 def csv_join(values):
0061     return ";".join(str(value) for value in sorted(values, key=str))
0062 
0063 
0064 
0065 
0066 def output_path(args):
0067     return Path(args.output) if args.output else Path(f"{args.rulename}.csv")
0068 
0069 
0070 def load_rule_and_match(args):
0071     param_overrides = {}
0072     param_overrides["runs"] = args.runs
0073     param_overrides["runlist"] = args.runlist
0074     param_overrides["nevents"] = args.nevents
0075     param_overrides["prodmode"] = "production"
0076     if args.physicsmode:
0077         param_overrides["physicsmode"] = args.physicsmode
0078 
0079     try:
0080         rule = RuleConfig.from_yaml_file(
0081             yaml_file=args.config,
0082             rule_name=args.rulename,
0083             param_overrides=param_overrides,
0084         )
0085     except (ValueError, FileNotFoundError) as e:
0086         ERROR(f"Error loading rule configuration: {e}")
0087         sys.exit(2)
0088 
0089     return rule, MatchConfig.from_rule_config(rule)
0090 
0091 
0092 
0093 def query_error_codes(args, match, run_condition):
0094     # what about ExitCode is Null?
0095     query = f"""
0096         SELECT runnumber, ExitCode
0097         FROM production_jobs
0098         WHERE rulename={sql_literal(args.rulename)}
0099           AND dataset={sql_literal(match.dataset)}
0100           AND tag={sql_literal(match.outtriplet)}
0101           AND dsttype like {sql_literal(match.dst_type_template)}
0102           AND {run_condition}
0103           AND ExitCode IS NOT NULL
0104           AND ExitCode != 0
0105         ORDER BY runnumber, ExitCode
0106     """
0107     rows = dbQuery(cnxn_string_map["statr"], query).fetchall()
0108     codes_by_run = defaultdict(set)
0109     for runnumber, exit_code in rows:
0110         codes_by_run[int(runnumber)].add(int(exit_code))
0111     INFO(f"{sum(len(codes) for codes in codes_by_run.values())} production job exit codes found.")
0112     return codes_by_run
0113 
0114 
0115 def add_reason(reasons_by_run, runnumber, reason):
0116     reasons_by_run[int(runnumber)].add(reason)
0117 
0118 
0119 def write_csv_report(
0120     path,
0121     args,
0122     runnumbers,
0123     possible_daqhosts_by_run,
0124     total_daqhosts_by_run,
0125     possible_segments_by_run,
0126     total_segments_by_run,
0127     possible_events_by_run,
0128     total_events_by_run,
0129     error_codes_by_run,
0130     reasons_by_run,
0131     expected_skipped_events_by_run=None,
0132 ):
0133     rows_written = 0
0134     expected_skipped_events_by_run = expected_skipped_events_by_run or {}
0135     path.parent.mkdir(parents=True, exist_ok=True)
0136     with path.open("w", newline="") as handle:
0137         writer = csv.DictWriter(handle, fieldnames=CSV_COLUMNS)
0138         writer.writeheader()
0139         for runnumber in sorted(runnumbers):
0140             possible_daqhosts = int(possible_daqhosts_by_run.get(runnumber, 0))
0141             total_daqhosts = int(total_daqhosts_by_run.get(runnumber, 0))
0142             missing_daqhosts = max(possible_daqhosts - total_daqhosts, 0)
0143             possible_segments = int(possible_segments_by_run.get(runnumber, 0))
0144             total_segments = int(total_segments_by_run.get(runnumber, 0))
0145             missing_segments = max(possible_segments - total_segments, 0)
0146             possible_events = int(possible_events_by_run.get(runnumber, 0))
0147             total_events = int(total_events_by_run.get(runnumber, 0))
0148             expected_skipped_events = int(expected_skipped_events_by_run.get(runnumber, 0))
0149             missing_events = max(possible_events - total_events - expected_skipped_events, 0)
0150             error_codes = error_codes_by_run.get(runnumber, set())
0151 
0152             reasons = set(reasons_by_run.get(runnumber, set()))
0153             if missing_daqhosts:
0154                 reasons.add("missing_daqhosts")
0155             if missing_segments:
0156                 reasons.add("missing_segments")
0157             if error_codes:
0158                 reasons.add("error_codes")
0159 
0160             writer.writerow({
0161                 "rule_name": args.rulename,
0162                 "runnumber": runnumber,
0163                 "possible_daqhosts": possible_daqhosts,
0164                 "total_daqhosts": total_daqhosts,
0165                 "missing_daqhosts": missing_daqhosts,
0166                 "possible_segments": possible_segments,
0167                 "total_segments": total_segments,
0168                 "missing_segments": missing_segments,
0169                 "possible_events": possible_events,
0170                 "total_events": total_events,
0171                 "missing_events": missing_events,
0172                 "error_codes": csv_join(error_codes),
0173                 "incomplete_reasons": csv_join(reasons),
0174                 "status": "incomplete" if missing_events else ("questionable" if reasons else "complete"),
0175             })
0176             rows_written += 1
0177 
0178     INFO(f"Wrote {rows_written} run-level rows to {path}")
0179 
0180 
0181 def normalized_neventsper(job_config):
0182     neventsper = getattr(job_config, "neventsper", None)
0183     try:
0184         return int(neventsper) if neventsper is not None else 0
0185     except (TypeError, ValueError):
0186         return 0
0187 
0188 
0189 
0190 def generate_eventcombiner_report(args, rule, match, report_path):
0191     daqhosts_dict, eventsinrun_by_run = match.daqhosts_for_combining()
0192     if not eventsinrun_by_run:
0193         INFO("No runs pass run quality cuts; no report written.")
0194         return False
0195 
0196     neventsper = normalized_neventsper(rule.job_config)
0197     if neventsper:
0198         total_expected_outputs = sum(
0199             math.ceil(eventsinrun / neventsper)
0200             for eventsinrun in eventsinrun_by_run.values()
0201             if eventsinrun
0202         )
0203         INFO(f"{total_expected_outputs} expected downstream output files from events/neventsper={neventsper}.")
0204 
0205     daqhost_types = [host for host in match.in_types if host != "gl1daq"]
0206     n_ideal = sum(sum(1 for host in hosts if host != "gl1daq") for hosts in daqhosts_dict.values())
0207     INFO(f"{n_ideal} (run, daqhost) combinations have all segments on lustre.")
0208 
0209     run_condition = match._run_condition(list(eventsinrun_by_run))
0210 
0211     total_query = f"""
0212         SELECT DISTINCT runnumber, daqhost FROM datasets
0213         WHERE {run_condition}
0214           AND daqhost IN {tuple(daqhost_types)}
0215         ORDER BY runnumber, daqhost
0216     """
0217     all_combos = dbQuery(cnxn_string_map["rawr"], total_query).fetchall()
0218     INFO(f"{len(all_combos)} (run, daqhost) combinations found in the raw DB.")
0219 
0220     not_on_lustre = [(int(r), h) for r, h in all_combos if h not in daqhosts_dict.get(int(r), set())]
0221     INFO(f"{len(not_on_lustre)} (run, daqhost) combinations are in the DB but not fully on lustre.")
0222     for run, daqhost in not_on_lustre[:args.example_limit]:
0223         DEBUG(f"  Not fully on lustre: Run {run} {daqhost}")
0224 
0225     runs_without_gl1daq = {run for run, hosts in daqhosts_dict.items() if "gl1daq" not in hosts}
0226     for run in sorted(runs_without_gl1daq):
0227         WARN(f"Run {run}: gl1daq not complete on lustre - run will not be submitted.")
0228 
0229     lustre_combos = [
0230         (run, daqhost)
0231         for run, hosts in daqhosts_dict.items()
0232         if "gl1daq" in hosts
0233         for daqhost in hosts
0234         if daqhost != "gl1daq"
0235     ]
0236 
0237     lastevent_query = f"""
0238         SELECT runnumber, dsttype, max(lastevent)
0239         FROM datasets
0240         WHERE dataset='{match.dataset}'
0241           AND tag='{match.outtriplet}'
0242           AND dsttype like '{match.dst_type_template}'
0243           AND {run_condition}
0244         GROUP BY runnumber, dsttype
0245         ORDER BY runnumber, dsttype
0246     """
0247     rows = dbQuery(cnxn_string_map["fcr"], lastevent_query).fetchall()
0248     INFO(f"{len(rows)} (run, dsttype) combinations have existing output in the FileCatalog.")
0249 
0250     fc_dsttypes_by_run = defaultdict(list)
0251     total_events_by_run = defaultdict(int)
0252     for runnumber, dsttype, lastevent in rows:
0253         runnumber = int(runnumber)
0254         fc_dsttypes_by_run[runnumber].append(dsttype)
0255         total_events_by_run[runnumber] += int(lastevent or 0)
0256 
0257     lustre_no_fc = [
0258         (run, daqhost)
0259         for run, daqhost in lustre_combos
0260         if not any(daqhost_to_dst_leaf(daqhost, match) in dsttype for dsttype in fc_dsttypes_by_run.get(run, []))
0261     ]
0262     INFO(f"{len(lustre_no_fc)} lustre combos have no FileCatalog entry.")
0263 
0264     all_no_fc = [
0265         (int(r), host)
0266         for r, host in all_combos
0267         if not any(daqhost_to_dst_leaf(host, match) in dsttype for dsttype in fc_dsttypes_by_run.get(int(r), []))
0268     ]
0269     if all_no_fc:
0270         WARN(f"{len(all_no_fc)} raw DB combos (lustre or not) have no FileCatalog entry. Check for corruption?")
0271         for run, daqhost in sorted(all_no_fc)[:args.example_limit]:
0272             DEBUG(f"  Run {run} {daqhost}")
0273 
0274     INFO(f"Checking for combinations flagged below ratio cut {args.ratio_cut}...")
0275     flagged = []
0276     for runnumber, dsttype, lastevent in rows:
0277         runnumber = int(runnumber)
0278         eventsinrun = eventsinrun_by_run.get(runnumber)
0279         if not eventsinrun:
0280             WARN(f"Run {runnumber} {dsttype}: eventsinrun=0, cannot compute ratio.")
0281             continue
0282 
0283         ratio = lastevent / eventsinrun
0284         msg = f"Run {runnumber} {dsttype}: lastevent={lastevent}, eventsinrun={eventsinrun}, ratio={ratio:.3f}"
0285         if ratio < args.ratio_cut:
0286             WARN(msg)
0287             flagged.append((runnumber, dsttype))
0288         elif ratio < 0.999:
0289             CHATTY(msg)
0290 
0291     flagged = sorted(set(flagged))
0292     INFO(f"{len(flagged)} (run, dsttype) combinations flagged below ratio cut {args.ratio_cut}.")
0293 
0294     possible_daqhost_sets_by_run = defaultdict(set)
0295     possible_daqhosts_by_run = defaultdict(int)
0296     total_daqhosts_by_run = defaultdict(int)
0297     possible_events_by_run = defaultdict(int)
0298     for runnumber, daqhost in all_combos:
0299         runnumber = int(runnumber)
0300         possible_daqhost_sets_by_run[runnumber].add(daqhost)
0301         possible_daqhosts_by_run[runnumber] += 1
0302         possible_events_by_run[runnumber] += int(eventsinrun_by_run.get(runnumber, 0))
0303 
0304     missing_daqhost_sets_by_run = defaultdict(set)
0305     for runnumber, daqhost in all_no_fc:
0306         missing_daqhost_sets_by_run[int(runnumber)].add(daqhost)
0307     for runnumber, possible_daqhosts in possible_daqhosts_by_run.items():
0308         total_daqhosts_by_run[runnumber] = max(
0309             possible_daqhosts - len(missing_daqhost_sets_by_run.get(runnumber, set())),
0310             0,
0311         )
0312 
0313     possible_segments_by_run = defaultdict(int)
0314     if neventsper:
0315         for runnumber, eventsinrun in eventsinrun_by_run.items():
0316             possible_segments_by_run[int(runnumber)] = math.ceil(int(eventsinrun or 0) / neventsper)
0317 
0318     output_events_by_run_host = defaultdict(dict)
0319     for runnumber, dsttype, lastevent in rows:
0320         runnumber = int(runnumber)
0321         for daqhost in possible_daqhost_sets_by_run.get(runnumber, set()):
0322             if daqhost_to_dst_leaf(daqhost, match) in dsttype:
0323                 output_events_by_run_host[runnumber][daqhost] = max(
0324                     output_events_by_run_host[runnumber].get(daqhost, 0),
0325                     int(lastevent or 0),
0326                 )
0327 
0328     total_segments_by_run = defaultdict(int)
0329     for runnumber, possible_hosts in possible_daqhost_sets_by_run.items():
0330         possible_segments = possible_segments_by_run.get(runnumber, 0)
0331         if not neventsper or not possible_segments or not possible_hosts:
0332             continue
0333         segment_depths = []
0334         for daqhost in possible_hosts:
0335             output_events = output_events_by_run_host.get(runnumber, {}).get(daqhost, 0)
0336             adjusted_events = output_events + EXPECTED_SKIPPED_EVENTS_PER_DAQHOST if output_events else 0
0337             segment_depths.append(min(possible_segments, math.ceil(adjusted_events / neventsper)))
0338         total_segments_by_run[runnumber] = min(segment_depths) if segment_depths else 0
0339 
0340     reasons_by_run = defaultdict(set)
0341     for runnumber, _ in flagged:
0342         add_reason(reasons_by_run, runnumber, "low_event_ratio")
0343     for runnumber, _ in not_on_lustre:
0344         add_reason(reasons_by_run, runnumber, "not_on_lustre")
0345     for runnumber in runs_without_gl1daq:
0346         add_reason(reasons_by_run, runnumber, "missing_gl1daq")
0347     for runnumber, possible_events in possible_events_by_run.items():
0348         total_events = total_events_by_run.get(runnumber, 0)
0349         expected_skipped = EXPECTED_SKIPPED_EVENTS_PER_DAQHOST * possible_daqhosts_by_run.get(runnumber, 0)
0350         if possible_events and (total_events + expected_skipped) / possible_events < args.ratio_cut:
0351             add_reason(reasons_by_run, runnumber, "low_run_event_ratio")
0352 
0353     expected_skipped_events_by_run = {
0354         runnumber: EXPECTED_SKIPPED_EVENTS_PER_DAQHOST * possible_daqhosts
0355         for runnumber, possible_daqhosts in possible_daqhosts_by_run.items()
0356     }
0357     error_codes_by_run = query_error_codes(args, match, run_condition)
0358 
0359     all_report_runs = set(eventsinrun_by_run) | set(possible_events_by_run) | set(total_events_by_run)
0360     write_csv_report(
0361         report_path,
0362         args,
0363         all_report_runs,
0364         possible_daqhosts_by_run,
0365         total_daqhosts_by_run,
0366         possible_segments_by_run,
0367         total_segments_by_run,
0368         possible_events_by_run,
0369         total_events_by_run,
0370         error_codes_by_run,
0371         reasons_by_run,
0372         expected_skipped_events_by_run,
0373     )
0374 
0375     files_db_events = sum(total_events_by_run.values())
0376     raw_combo_events = sum(possible_events_by_run.values())
0377     event_pct = 100.0 * files_db_events / raw_combo_events if raw_combo_events else 0.0
0378     INFO(
0379         f"Summary: FileCatalog has {human_event_count(files_db_events)}/"
0380         f"{human_event_count(raw_combo_events)} possible events from raw DB combos "
0381         f"({event_pct:.1f}%)."
0382     )
0383     INFO(f"Available: {raw_combo_events} \t Done {files_db_events}")
0384     return True
0385 
0386 
0387 def main():
0388     args = submission_args()
0389     args.example_limit = max(0, args.example_limit)
0390 
0391     from simpleLogger import slogger, set_log_timestamps_enabled
0392     import logging
0393     set_log_timestamps_enabled(False)
0394     slogger.setLevel(logging.getLevelName(args.loglevel))
0395 
0396     profiler = None
0397     if args.profile:
0398         DEBUG("Profiling is ENABLED.")
0399         profiler = cProfile.Profile()
0400         profiler.enable()
0401 
0402     rule, match = load_rule_and_match(args)
0403     report_path = output_path(args)
0404 
0405     if "raw" not in match.input_config.db:
0406         ERROR(
0407             f"Rule '{args.rulename}' is a downstream rule (db={match.input_config.db}). "
0408             "generate_report.py only supports event-combiner/raw rules for now."
0409         )
0410         sys.exit(2)
0411 
0412     wrote_report = generate_eventcombiner_report(args, rule, match, report_path)
0413 
0414     if args.report != "none":
0415         WARN("--report is accepted for argument compatibility but ignored by generate_report.py; CSV was written instead.")
0416     if args.delete:
0417         WARN("--delete is accepted for argument compatibility but ignored by generate_report.py.")
0418 
0419     if wrote_report:
0420         print(f"Result written to: {report_path}")
0421 
0422     if profiler:
0423         profiler.disable()
0424         DEBUG("Profiling finished. Printing stats...")
0425         stats = pstats.Stats(profiler)
0426         stats.strip_dirs().sort_stats("time").print_stats(20)
0427 
0428 
0429 if __name__ == "__main__":
0430     main()